基於Avalonia 11.0.0+ReactiveUI 的跨平臺項目開發2-功能開發

来源:https://www.cnblogs.com/raok/archive/2023/07/19/17566752.html
-Advertisement-
Play Games

# 基於Avalonia 11.0.0+ReactiveUI 的跨平臺項目開發2-功能開發 ![image-20230718225201652](https://www.raokun.top/upload/2023/07/image-20230718225201652.png) **項目簡介**:目 ...


基於Avalonia 11.0.0+ReactiveUI 的跨平臺項目開發2-功能開發

image-20230718225201652

項目簡介:目標是開發一個跨平臺的AI聊天和其他功能的客戶端平臺。目的來學習和瞭解Avalonia。將這個項目部署在openKylin 1.0 的系統上。

為什麼使用Avalonia:之前已經瞭解了基於Avalonia的項目在國產麒麟系統中運行的案例。正是Avalonia在跨平臺的出色表現,學習和瞭解Avalonia這個UI框架顯得十分有必要。本項目採用的是最新穩定版本11.0.0-rc1.1。希望通過該項目瞭解和學習Avalonia開發的朋友可以在我的github上拉取代碼,同時希望大家多多點點star。

https://github.com/raokun/TerraMours.Chat.Ava

項目的基礎框架和通用功能在上一篇博客中介紹過了,想瞭解的同學跳轉學習:

基於Avalonia 11.0.0+ReactiveUI 的跨平臺項目開發1-通用框架

瞭解Avalonia創建模板項目-基礎可跳轉:

創建Avalonia 模板項目-基礎

本次我主要分享的內容是項目中具體功能開發實現的過程和各技術的應用

1.功能介紹

1.界面交互

第一版的內容主要分為以下幾個模塊:

  • LoadView.axaml 載入界面:系統打開時候的載入界面,用於首頁替換的技術實踐。可改造成登陸界面。
  • MainWindow.axaml 首頁
  • MainView.axaml 主界面
  • DataGridView.axaml 會話列表
  • ChatView.axaml 聊天界面
  • ApiSettingsView.axaml API配置

2.功能實現

下麵我會按照各個模塊來介紹對應的功能和實現方法。

1.載入界面

載入界面 是系統的首個載入界面,界面樣式如下:

image-20230718225041733

1.作用和功能:

載入界面是系統在運行前的準備界面,目前並沒有做什麼操作,只是做了個進度條,到100%時跳轉首頁。不過這是一個可擴展的實踐。

載入界面完成了首頁的切換的實踐,為後期登錄頁面做好了準備。同時,載入界面的內容,改寫成蒙版,在需要長時間數據處理用於限制用戶操作也是不錯的選擇。

2.設置載入界面為項目運行時首個載入界面

設置首個載入界面,需要在App.axaml.cs中的OnFrameworkInitializationCompleted方法中設置 desktop.MainWindow

image-20230719000542962

OnFrameworkInitializationCompleted代碼如下:

public override void OnFrameworkInitializationCompleted() {
    //添加共用資源
    var VMLocator = new VMLocator();
    Resources.Add("VMLocator", VMLocator);

    if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
        var load= new LoadView {
            DataContext = new LoadViewModel(),
        };
        desktop.MainWindow = load;
        VMLocator.LoadViewModel.ToMainAction = () =>
        {
            desktop.MainWindow = new MainWindow();
            desktop.MainWindow.Show();
            load.Close();
        };

    }

    base.OnFrameworkInitializationCompleted();
}

3.隱藏視窗的關閉按鈕,並設置視窗居中顯示

載入界面不應該有關閉等按鈕,我們用 SystemDecorations="None"。將 SystemDecorations 屬性設置為 "None" 可以隱藏視窗的系統裝飾。系統裝飾包括標題欄、最小化、最大化和關閉按鈕等。通過設置 SystemDecorations"None",可以使視窗更加定製化和個性化,同時減少了不必要的系統裝飾。

界面應該顯示在屏幕正中間。我們用 WindowStartupLocation="CenterScreen"。設置 WindowStartupLocation"CenterScreen" 可以使視窗在屏幕上居中顯示。

image-20230718231841408

4.實現進度條

image-20230718234226513

代碼如下:

<StackPanel
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Orientation="Vertical"
            Spacing="10">
    <TextBlock
               Text="Loading..."
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Foreground="White"
               Background="Transparent"/>
    <ProgressBar
                 x:Name="progressBar"
                 HorizontalAlignment="Center"
                 Minimum="0"
                 Maximum="100"
                 Value="{Binding Progress}"
                 Width="200"
                 Height="20"
                 Background="Transparent">
        <ProgressBar.Foreground>
            <SolidColorBrush Color="White"/>
        </ProgressBar.Foreground>
    </ProgressBar>
</StackPanel>

進度條用到了ProgressBar 的控制項,對應的官方文檔地址:ProgressBar

控制項的屬性:

Property Description
Minimum 最大值
Maximum 最小值
Value 當前值
Foreground 進度條顏色
ShowProgressText 顯示進度數值

Value 值通過Binding綁定了ViewModel中的屬性欄位Progress。通過UpdateProgress()方法,讓Progress的值由0變化到100,模擬載入過程。

image-20230718234825365

代碼如下:

private async void UpdateProgress() {
    // 模擬登錄載入過程
    for (int i = 0; i <= 100; i++) {
        Progress = i;
        await Task.Delay(100); // 延遲一段時間,以模擬載入過程
    }
    ToMainAction?.Invoke();
}

5.載入完成後跳轉首頁

image-20230718235630454

界面的跳轉,通過Action委托來完成,首先在LoadViewModel中定義 ToMainAction,在上面的UpdateProgress方法完成時執行Invoke,而ToMainAction的實現方法,寫在OnFrameworkInitializationCompleted方法中。

image-20230718235414637

ToMainAction的實現方法中,將desktop.MainWindow變更成MainWindowloadView隱藏,MainWindow顯示。

2.首頁+API配置

載入界面 是承載程式的界面,界面樣式如下:

image-20230719144805612

1.作用和功能:

首頁 主要作用是承載程式的界面,每一個Avalonia項目在創建時會自動創建MainWindow.axaml 在界面axaml中很簡單。承載了MainView 的用戶控制項,和API設置界面。

首頁 包括控制API設置的數據交互、鍵盤的監聽事件、系統語言的判斷。

API配置 包括用於OpenAI介面調用參數的全部設置。

2.界面設計-設置彈框

image-20230719143401064

代碼如下:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:TerraMours.Chat.Ava.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="TerraMours.Chat.Ava.Views.MainWindow"
		x:DataType="vm:MainWindowViewModel"
		xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
		RenderOptions.BitmapInterpolationMode="HighQuality"
		xmlns:sty="using:FluentAvalonia.Styling"
		xmlns:ui="using:FluentAvalonia.UI.Controls"
		xmlns:local="using:TerraMours.Chat.Ava.Views"
        Icon="/Assets/terramours.ico"
        Title="TerraMours.Chat.Ava">
	<dialogHost:DialogHost IsOpen="{Binding	ApiSettingIsOpened}"
						   DialogMargin="16"
							DisableOpeningAnimation="True"
						   dialogHost:DialogHostStyle.CornerRadius="8"
						   Background="rgb(52, 53, 65)">
		<dialogHost:DialogHost.DialogContent>
			<local:ApiSettingsView />
		</dialogHost:DialogHost.DialogContent>

		<Panel>
			<local:MainView />
		</Panel>

	</dialogHost:DialogHost>

</Window>

界面中使用到了 DialogHost.Avalonia,做彈框的簡單實現。IsOpen 控制彈窗的顯隱性。DialogHost.DialogContent 中填入彈框的顯示內容。顯示內容為

ApiSettingsView

而主體部分只有一個Panel,包含著MainView,這使得MainView的界面會占滿整個程式界面。至此,首頁的界面設計就完成了。

Icon="/Assets/terramours.ico" 設置了程式的logo,如下:

image-20230719145303993

3.初始化

MainWindow 在界面載入時要做很多工作。MainWindow的構造函數如下:

image-20230719150604374

代碼如下:

public MainWindow() {
            InitializeComponent();
            this.Closing += (sender, e) => SaveWindowSizeAndPosition();

            this.Loaded += MainWindow_Loaded;
            MainWindowViewModel = new MainWindowViewModel();
            VMLocator.MainWindowViewModel = MainWindowViewModel;
            DataContext = MainWindowViewModel;
            var cultureInfo = CultureInfo.CurrentCulture;
            if (cultureInfo.Name == "zh-CN") {
                Translate("zh-CN");
            }

            this.KeyDown += MainWindow_KeyDown;

        }

MainWindow的構造函數綁定了多個事件的實現方法:

  1. this.Loaded 界面載入時觸發MainWindow_Loaded 方法,作用是載入本地數據和本地配置文件。
  2. this.Closing 程式關閉時觸發SaveWindowSizeAndPosition方法,作用是保存當前的系統設置,包括用戶調整後的界面的長寬和在屏幕的位置,用戶下次打開時程式還會出現在之前的位置。比如,我把系統拉到桌面左上角,把視窗縮小到最小尺寸時候退出程式,下次打開,程式界面還會在之前退出的位置,在桌面左上角以最小尺寸出現。
  3. this.KeyDown 監聽鍵盤的輸入事件,當按鍵按下時,會觸發MainWindow_KeyDown方法,用於綁定自定義的快捷鍵。

MainWindow構造函數中,通過判斷CultureInfo.CurrentCulture,獲取當前 操作系統的語言系統,判斷程式應該顯示哪個國家的語言。從而確定程式顯示的語言,通過Translate 修改語言相關配置。是系統國際化的實踐。

4.系統配置-本地保存和載入

系統配置 對應AppSettings類,記錄了應用程式設置ChatGPT API參數

image-20230719150155309

系統設置參數通過保存到文件settings.json中實現配置的本地化和持久化。

MainWindow_Loaded 的方法實現系統配置載入:

image-20230719152508260

代碼如下:

private async void MainWindow_Loaded(object sender,RoutedEventArgs e) {
    var settings = await LoadAppSettingsAsync();

    if (File.Exists(Path.Combine(settings.AppDataPath, "settings.json"))) {
        this.Width = settings.Width - 1;
        this.Position = new PixelPoint(settings.X, settings.Y);
        this.Height = settings.Height;
        this.Width = settings.Width;
        this.WindowState = settings.IsMaximized ? WindowState.Maximized : WindowState.Normal;
    }
    else {
        var screen = Screens.Primary;
        var workingArea = screen.WorkingArea;

        double dpiScaling = screen.PixelDensity;
        this.Width = 1300 * dpiScaling;
        this.Height = 840 * dpiScaling;

        this.Position = new PixelPoint(5, 0);
    }


    if (!File.Exists(settings.DbPath)) {
        _dbProcess.CreateDatabase();
    }

    await _dbProcess.DbLoadToMemoryAsync();
    await VMLocator.MainViewModel.LoadPhraseItemsAsync();

    VMLocator.MainViewModel.SelectedPhraseItem = settings.PhrasePreset;

    VMLocator.MainViewModel.SelectedLogPain = "Chat List";

    await Dispatcher.UIThread.InvokeAsync(() => { VMLocator.MainViewModel.LogPainIsOpened = false; });
    if (this.Width > 1295) {
        //await Task.Delay(1000);
        await Dispatcher.UIThread.InvokeAsync(() => { VMLocator.MainViewModel.LogPainIsOpened = true; });
    }

    this.GetObservable(ClientSizeProperty).Subscribe(size => OnSizeChanged(size));
    _previousWidth = ClientSize.Width;

    await _dbProcess.UpdateChatLogDatabaseAsync();


    await _dbProcess.CleanUpEditorLogDatabaseAsync();

    if (string.IsNullOrWhiteSpace(VMLocator.MainWindowViewModel.ApiKey)) {
        var dialog = new ContentDialog() { Title = $"Please enter your API key.", PrimaryButtonText = "OK" };
        await VMLocator.MainViewModel.ContentDialogShowAsync(dialog);
        VMLocator.ChatViewModel.OpenApiSettings();
    }
}

MainWindow_Loaded 的方法中,通過解析settings.json,載入系統配置。

settings.json 的解析和保存

相關方法如下:

image-20230719153049144

至此,系統配置的開發就基本完成了。對於這些不需要遠程同步的基礎配置,保存在本地文件中。

5.國際化

通過Translate方法,根據當前系統語言,改變控制文字顯示的資源文件,實現語言的切換。

image-20230719155428155

代碼如下:

public void Translate(string targetLanguage) {
    var translations = App.Current.Resources.MergedDictionaries.OfType<ResourceInclude>().FirstOrDefault(x => x.Source?.OriginalString?.Contains("/Lang/") ?? false);

    if (translations != null)
        App.Current.Resources.MergedDictionaries.Remove(translations);

    App.Current.Resources.MergedDictionaries.Add(
        (ResourceDictionary)AvaloniaXamlLoader.Load(
            new Uri($"avares://TerraMours.Chat.Ava/Assets/lang/{targetLanguage}.axaml")
        )
    );
}

關於國際化的資源文件的創建請看前篇內容:基於Avalonia 11.0.0+ReactiveUI 的跨平臺項目開發1-通用框架

3.主界面

主界面 是項目的核心,包括了以下圖片所有內容的佈局,它勾勒出了整個程式。中間包括左上角的圖標,會話列表,聊天區域,查詢,配置等等。

image-20230718225201652

1.作用和功能

主界面 的作用,是顯示和完成整個業務功能的展示和交互。主界面 將會話列表和聊天視窗左右分開。控制了整個程式的排版和佈局。

image-20230719173323402

主要分三塊:

  1. 程式標題logo
  2. 會話列表
  3. 聊天視窗

2.界面設計

image-20230719173721437

具體代碼不貼出來了,需要瞭解的同學可以fork項目代碼查看,功能區已經標註註釋了,方便查看。

3.SplitView控制會話列表顯示

會話列表聊天視窗 通過SplitView 實現,會話列表在視窗縮小時自動隱藏。通過IsPaneOpen屬性控制。

隱藏效果:

image-20230719174600833

實現方法為OnSizeChanged方法:

image-20230719174645433

代碼如下:

 private void OnSizeChanged(Size newSize) {
     if (_previousWidth != newSize.Width) {
         if (newSize.Width <= 1295) {
             VMLocator.MainViewModel.LogPainIsOpened = false;
             VMLocator.MainViewModel.LogPainButtonIsVisible = false;
         }
         else {
             if (VMLocator.MainViewModel.LogPainButtonIsVisible == false) {
                 VMLocator.MainViewModel.LogPainButtonIsVisible = true;
             }
             if (newSize.Width > _previousWidth) {
                 VMLocator.MainViewModel.LogPainIsOpened = true;
             }
         }
         _previousWidth = newSize.Width;
     }
 }

當視窗寬度小於1295,會修改VMLocator.MainViewModel.LogPainButtonIsVisible為false,實現會話列表隱藏的效果。

4.初始化

MainViewModel控制了程式大部分的按鍵的事件實現,MainViewModel的構造函數如下:

image-20230719175328066

代碼如下:

 public MainViewModel() {
     PostButtonText = "Post";

     LoadChatListCommand = ReactiveCommand.CreateFromTask<string>(async (keyword) => await LoadChatListAsync(keyword));
     PhrasePresetsItems = new ObservableCollection<string>();

     //會話
     ImportChatLogCommand = ReactiveCommand.CreateFromTask(ImportChatLogAsync);
     ExportChatLogCommand = ReactiveCommand.CreateFromTask(ExportChatLogAsync);
     DeleteChatLogCommand = ReactiveCommand.CreateFromTask(DeleteChatLogAsync);
     //配置
     SystemMessageCommand = ReactiveCommand.Create(InsertSystemMessage);
     HotKeyDisplayCommand = ReactiveCommand.CreateFromTask(HotKeyDisplayAsync);
     OpenApiSettingsCommand = ReactiveCommand.Create(OpenApiSettings);
     ShowDatabaseSettingsCommand = ReactiveCommand.CreateFromTask(ShowDatabaseSettingsAsync);
     //聊天
     PostCommand = ReactiveCommand.CreateFromTask(PostChatAsync);
 }

其中,綁定了會話、配置、聊天等功能的按鈕事件。實現業務的交互。

5.調用ChatGpt介面

通過Betalgo.OpenAI 完成介面調用,是一個開源的nuget包,集成了OpenAI的介面,簡化了調用邏輯。

本來更傾向於Senmantic Kernel的,是微軟開發的LLM訓練框架,但是代理方面我還沒有很好的解決辦法,後面再替換。

介面調用方法寫在PostChatAsync方法里,通過post按鈕發起調用:

image-20230719200445744

代碼如下:

/// <summary>
/// OpenAI 調用方法
/// </summary>
/// <returns></returns>
private async Task PostChatAsync()
{
    try
    {
        string message = PostMessage;
        int conversationId = 1;
        //創建會話
        if(VMLocator.DataGridViewModel.ChatList == null)
        {
            VMLocator.DataGridViewModel.ChatList=new ObservableCollection<ChatList> ();
            VMLocator.DataGridViewModel.ChatList.Add(new ChatList() { Id=1,Title=(message.Length< 5?message:$"{message.Substring(0,5)}..."), Category = (message.Length < 5 ? message : $"{message.Substring(0, 5)}...") ,Date=DateTime.Now});
        }
        if (VMLocator.ChatViewModel.ChatHistory == null)
            VMLocator.ChatViewModel.ChatHistory = new ObservableCollection<Models.ChatMessage>();
        VMLocator.ChatViewModel.ChatHistory.Add(new Models.ChatMessage() { ChatRecordId = 1, ConversationId = conversationId, Message = message, Role = "User", CreateDate = DateTime.Now });

        //根據配置中的CONTEXT_COUNT 查詢上下文
        var messages = new List<OpenAI.ObjectModels.RequestModels.ChatMessage>();
        messages.Add(OpenAI.ObjectModels.RequestModels.ChatMessage.FromUser(message));
        var openAiOpetions = new OpenAI.OpenAiOptions()
        {
            ApiKey = AppSettings.Instance.ApiKey,
            BaseDomain = AppSettings.Instance.ApiUrl
        };
        var openAiService = new OpenAIService(openAiOpetions);
        //調用SDK
        var response = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest
                                                                           {
                                                                               Messages = messages,
                                                                               Model = AppSettings.Instance.ApiModel,
                                                                               MaxTokens = AppSettings.Instance.ApiMaxTokens,
                                                                           });
        if (response == null)
        {
            var dialog = new ContentDialog()
            {
                Title = "介面調用失敗",
                PrimaryButtonText = "Ok"
            };
            await VMLocator.MainViewModel.ContentDialogShowAsync(dialog);
        }
        if (!response.Successful)
        {
            var dialog = new ContentDialog()
            {
                Title = $"介面調用失敗,報錯內容: {response.Error.Message}",
                PrimaryButtonText = "Ok"
            };
            await VMLocator.MainViewModel.ContentDialogShowAsync(dialog);
        }
        VMLocator.ChatViewModel.ChatHistory.Add(new Models.ChatMessage() { ChatRecordId = 2, ConversationId = conversationId, Message = response.Choices.FirstOrDefault().Message.Content, Role = "Assistant", CreateDate = DateTime.Now });
        VMLocator.MainViewModel.PostMessage = "";
    }
    catch (Exception e)
    {
    }
}

通過創建OpenAIService初始化,Completion介面調用時使用openAiService.ChatCompletion.CreateCompletion方法。

ChatMessage是上下文的模型,通過創建messages完成上下文的創建,請求參數都寫在ChatCompletionCreateRequest之中。

目前的第一版使用的CreateCompletion是直接返回的結果。後面我會優化調用,使用Stream流式輸出。

4.會話列表

會話列表是模擬chatgpt官網的樣式,將聊天按會話的形式歸類。chatgpt官網截圖如下:

image-20230719201304205

1.作用和功能

會話列表將聊天按會話的形式歸類,更好的管理聊天內容。

2.界面設計

因為考慮到後面會有其他類型的AI 類型,決定通過DataGrid實現會話列表,DataGrid的表格類型也能更多的展示數據。

image-20230719201810771

代碼如下:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
			 xmlns:vm="using:TerraMours.Chat.Ava.ViewModels"
		     x:DataType="vm:DataGridViewModel"
			 xmlns:local="using:TerraMours.Chat.Ava"
             x:Class="TerraMours.Chat.Ava.Views.DataGridView">
	<UserControl.Resources>
		<local:CustomDateConverter x:Key="CustomDateConverter" />
	</UserControl.Resources>
	<Grid>
		<DataGrid Name="ChatListDataGrid"
				  ItemsSource="{Binding ChatList}"
				  AutoGenerateColumns="False"
				  HeadersVisibility="None"
				  SelectionMode="Single"
				  SelectedItem="{Binding SelectedItem}"
				  SelectedIndex="{Binding SelectedItemIndex}">
			<DataGrid.Columns>
				<DataGridTextColumn Binding="{Binding Id}" IsVisible="False"/>
				<DataGridTextColumn Foreground="rgb(155,155,155)"
								  FontSize="12"
									Binding="{Binding Date,Converter={StaticResource CustomDateConverter},Mode=OneWay}"
									IsReadOnly="True"/>
				<DataGridTextColumn Binding="{Binding Title}" IsReadOnly="True"/>
			</DataGrid.Columns>
		</DataGrid>
	</Grid>
</UserControl>

會話列表的數據共有三個:Id,創建時間,會話標題。通過DataGridTextColumn 通過綁定的形式實現。

其中,使用了CustomDateConverter 時間轉換器,將Date的顯示格式做轉換。

3.數據交互

會話列表目前還在優化,第一版是通過第一次調用PostChatAsync時創建的。目前的數據存在本地SQLite資料庫中。

5.聊天視窗

聊天視窗是程式的工作重心,是展示聊天成果的重要界面。其中用到Markdown.Avalonia的擴展包實現Markdown內容的展示。

image-20230719202912320

1.作用和功能

數據是核心,聊天視窗是數據的展示平臺,作用不容小噓。通過編寫數據模板DataTemplate來控制內容的展示。呈現chat問答式的結果。

2.Markdown 風格樣式

通過DataTemplate來設置Markdown 風格樣式。代碼如下:

<DataTemplate>
    <Border
    Name="MessageBorder"
    Background="{Binding Role, Converter={StaticResource ChatBackgroundConverter}}"
    HorizontalAlignment="Left"
    Padding="5"
    Margin="20,5,20,20"
    CornerRadius="8,8,8,0">
    <md:MarkdownScrollViewer
        VerticalAlignment="Stretch"
        MarkdownStyleName="Standard"
        SaveScrollValueWhenContentUpdated="True"
        TextElement.FontSize="16"
        TextElement.Foreground="White"
        Markdown="{Binding Message}">
        <md:MarkdownScrollViewer.Styles>
            <Style Selector="ctxt|CCode">
            <Style.Setters>
            <Setter Property="BorderBrush"         Value="Green"/>
            <Setter Property="BorderThickness"     Value="2"/>
            <Setter Property="Padding"             Value="2"/>
            <Setter Property="MonospaceFontFamily" Value="Meiryo" />
            <Setter Property="Foreground"          Value="DarkGreen" />
            <Setter Property="Background"          Value="LightGreen" />
            </Style.Setters>
            </Style>

            <Style Selector="Border.CodeBlock">
            <Style.Setters>
            <Setter Property="BorderBrush" Value="#E2E6EA" />
            <Setter Property="BorderThickness" Value="0,30,0,0" />
            <Setter Property="Margin" Value="5,0,5,0" />
            <Setter Property="Background" Value="Black" />
            </Style.Setters>
            </Style>

            <Style Selector="TextBlock.CodeBlock">
            <Style.Setters>
            <Setter Property="Background" Value="Black" />
            </Style.Setters>
            </Style>

            <Style Selector="avedit|TextEditor">
            <Style.Setters>
            <Setter Property="BorderBrush" Value="#E2E6EA" />
            <Setter Property="Background" Value="Black" />
            <Setter Property="Padding" Value="5"></Setter>
            </Style.Setters>
            </Style>

            </md:MarkdownScrollViewer.Styles>
                <md:MarkdownScrollViewer.ContextMenu>
                    <ContextMenu Padding="3">
                    <MenuItem>
                    <MenuItem.Header>
                    <TextBlock>編輯</TextBlock>
                    </MenuItem.Header>
                    </MenuItem>
                    <!--<MenuItem Tag="{Binding ChatRecordId}" Click="DeleteClick">
                    <MenuItem.Header>
                    <TextBlock>刪除</TextBlock>
                    </MenuItem.Header>
                    </MenuItem>
                    <MenuItem Tag="{Binding Message}" Click="CopyClick">
                    <MenuItem.Header>
                    <TextBlock>複製</TextBlock>
                    </MenuItem.Header>
                    </MenuItem>-->
                    </ContextMenu>
                    </md:MarkdownScrollViewer.ContextMenu>
                        </md:MarkdownScrollViewer>
                            </Border>
                            </DataTemplate>

MarkdownScrollViewer.Styles 根據不同的內容設置不同的樣式。

MarkdownScrollViewer.ContextMenu設置右鍵菜單。

其中通過ChatBackgroundConverter轉換器根據角色控制背景,ChatBackgroundConverter代碼如下:

image-20230719203600760

3.總結和待辦事項

avalonia開發目前網上,特別是國內的網站的教程和文章很少,希望能給大家一點學習使用avalonia開發客戶端項目的朋友一點幫助。寫的不對的地方也懇請大家多多留言,我會及時更正,多多交流心得體會。

Todo:

  1. 項目發佈,在多平臺下的運行
  2. 搭建國產系統虛擬機測試avalonia項目
  3. 程式改造成雲同步版本,跟我做的web項目互通。
  4. 優化UI界面
  5. 優化語言國際化內容

**目前程式還沒有完全開發完成。後續的開發我會及時跟進。閱讀如遇樣式問題,請前往個人博客瀏覽:https://www.raokun.top

目前web端ChatGPT:https://ai.terramours.site

當前開源項目地址:https://github.com/raokun/TerraMours.Chat.Ava


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 下麵這段優秀的代碼節選自hutool-DateUtil(hutool-all-4.5.18.jar ,maven坐標:cn.hutool:hutool-all:4.5.18),香香的,甜甜的! ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • **本文詳細解析了Python的logging模塊,從基本介紹到實際應用和最佳實踐。我們通過具體的代碼示例解釋瞭如何高效地使用這個模塊進行日誌記錄,以及如何避免常見的陷阱,旨在幫助讀者更好地掌握這個強大的工具。** ![file](https://img2023.cnblogs.com/other/ ...
  • # Spring6 初始 @[toc] ## 每博一文案: ```tex 人生的態度是:抱有最大的希望。 盡最大的努力,做最壞的打算。 —————— 柏拉圖《理想國》 ``` ## 1. 初始 Spring6 閱讀以下代碼: ```java package com.powernode.oa.cont ...
  • 這篇文章主要記錄了本次遇到的問題:即maven在面對不同版本的jar包在pom文件中同時聲明會存在載入覆蓋的問題,於是通過查詢網上相關資料對maven包的載入規則介紹,並通過實際場景對其進行分析驗證 ...
  • # 1.模塊 ``` import 模塊名 ``` ## 1.1 模塊就是程式 任何python程式都可以作為模塊導入,並標明程式(模塊)的位置 ``` import sys sys.path.append('路徑') ``` ``` import hello // 在同一文件夾下 ``` 會在該文 ...
  • [toc] # 引入 - Microsoft.EntityFrameworkCore - Microsoft.EntityFrameworkCore.Design - Microsoft.EntityFrameworkCore.SqlServer - Microsoft.EntityFramewor ...
  • # 一、日誌記錄 日誌記錄是什麼?簡單而言,就是通過一些方式記錄應用程式運行中的某一時刻的狀態,保留應用程式當時的信息。這對於我們進行應用程式的分析、審計以及維護有很大的作用。 作為程式員,我們恐怕誰也不敢保證我們開發的軟體應用一定不存在BUG,一定不會出現故障,而當故障出現的時候,日誌就是我們排查 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...