基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发1-通用框架

Avalonia简介:

Avalonia是.NET的一个跨平台UI框架,提供了一个灵活的样式系统,支持广泛的操作系统,如Windows、Linux、macOS,并对Android、iOS和WebAssembly提供了实验性支持。

为什么使用Avalonia:

之前已经了解了基于Avalonia的项目在国产麒麟系统中运行的案例。正是Avalonia在跨平台的出色表现,学习和了解Avalonia这个UI框架显得十分有必要。本项目采用的是最新稳定版本11.0.0-rc1.1。希望通过该项目了解和学习Avalonia开发的朋友可以在我的github上拉取代码,同时希望大家多多点点star。

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

项目需求:

Web端的AI应用我已经开发了一个AI聊天平台了。希望开发一个跨平台的AI聊天和其他功能的客户端平台。有个面试邀请是要求avalonia应用经验要求。写这样一个项目来学习和了解Avalonia。同时麒麟系统的开放版本也发布了也想后面部署一个将这个项目部署在openKylin 1.0 的系统上。

开发环境:

.net 7

篇幅有限,创建项目的部分跳过,需要了解的可以看我之前的基础博客:https://www.raokun.top/archives/chuang-jian-avalonia-mo-ban-xiang-mu—ji-chu

下面我会直接以TerraMours.Chat.Ava项目为例子 来介绍项目开发过程和各技术的应用

1.nuget包引用

image-20230717150959484

引用包介绍:

  • Avalonia 版本11.0.0-rc1.1,稳定版本,其他基于avalonia的包要选用支持11.0.0-rc1.1的版本

  • Avalonia.ReactiveUI MVVM 架构模式的工具库,创建avalonia项目时会提示选择。

  • DialogHost.Avalonia 它提供了一种简单的方式来显示带有信息的对话框或在需要信息时提示用户。

  • FluentAvaloniaUI UI库,并将更多WinUI控件引入Avalonia

  • System.Data.SQLite 本地数据库SQLite

  • CsvHelper Csv导入导出工具库

  • Markdown.Avalonia 用于显示markdown文本的工具,用于展示聊天结果的渲染

  • Betalgo.OpenAI 调用ChatGpt的扩展库

<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0-rc1.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Xaml.Interactivity" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.0-rc1.1" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0-rc1.1" />
<PackageReference Include="DialogHost.Avalonia" Version="0.7.4" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.0-rc1" />
<PackageReference Include="System.Data.SQLite" Version="1.0.117" />
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.0-d1" />
<PackageReference Include="Markdown.Avalonia.SyntaxHigh" Version="11.0.0-d1" />
<PackageReference Include="Markdown.Avalonia.Tight" Version="11.0.0-d1" />
<PackageReference Include="Betalgo.OpenAI" Version="7.1.2-beta" />

2.功能介绍

项目开发的功能分为如下:

1.通用框架:

  • VMLocator: ViewModel 定位器。方便地获取和管理 ViewModel 实例,从而实现界面和数据的解耦和模块化,提高代码的可维护性和可测试性。
  • 国际化: 使用 CultureInfo.CurrentCulture 来实现多语言支持和本地化
  • 本地化数据:通过SQLite实现数据本地化
  • CSV导入导出:实现数据的迁移和补充
  • 自定义快捷键: 自定义快捷键,方便操作。发挥客户端的按键优势。
  • 自定义字体
  • 全局样式

2.界面交互

  • LoadView.axaml 加载界面:系统打开时候的加载界面,用于首页替换的技术实践。可改造成登陆界面。
  • MainWindow.axaml 首页
  • MainView.axaml 主界面
  • DataGridView.axaml 会话列表
  • ChatView.axaml 聊天界面
  • ApiSettingsView.axaml API配置

3.功能开发

1.通用功能开发

1.VMLocator ViewModel 定位器

VMLocator 类在 Avalonia 项目中的作用是作为一个静态的 ViewModel 定位器,用于管理和提供各个 ViewModel 类的实例。ViewModel 是 MVVM 模式中用于处理界面逻辑和数据的类,而 ViewModel 定位器则负责管理 ViewModel 的实例,供界面进行数据绑定和交互。

优点:
  1. 提供了一种集中管理 ViewModel 的机制,使得整个应用程序可以方便地访问和使用各个 ViewModel 实例。
  2. 通过使用静态属性和延迟初始化的方式,避免了频繁创建 ViewModel 实例的开销,提高了程序的性能和效率。
  3. 可以在需要的时候动态获取 ViewModel 实例,而不需要手动实例化 ViewModel 类。
  4. 可以在不同的界面和组件之间共享同一个 ViewModel 实例,实现数据的共享和一致性
1.创建资源类VMLocator

image-20230717153901312

2.注入资源

app.axaml.cs中OnFrameworkInitializationCompleted方法加入

var VMLocator = new VMLocator();
Resources.Add("VMLocator", VMLocator);

image-20230717153945682

2.国际化

1.添加不同语言的资源文件
1.创建个语言的资源文件

image-20230717160806336

2.文件内容

image-20230717160909481

代码如下:

<ResourceDictionary xmlns="https://github.com/avaloniaui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
					xmlns:system="clr-namespace:System;assembly=System.Runtime">
    <!-- Add Resources Here -->
	<system:String x:Key="My.Strings.ConversationHitoryInfo">
		"conversation history limit" is a client option, not an API parameter.
		When the set value (tokens) is exceeded,
		the conversation history will be automatically summarized.
	</system:String>
</ResourceDictionary>

资源文件中定义了各个配置的说明文本的内容,通过使用 CultureInfo.CurrentCulture 来实现多语言支持和本地化

2.添加Application.Resources

App.axaml中添加资源文件配置

image-20230717161135925

<ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
        <ResourceInclude Source="/Assets/lang/zh-CN.axaml" />
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
3.添加AvaloniaXaml

在csproj中添加AvaloniaXaml

image-20230717161801378

<AvaloniaXaml Update="Assets\lang\zh-CN.axaml">
    <SubType>Designer</SubType>
</AvaloniaXaml>
什么是AvaloniaXaml:

AvaloniaXaml 是 Avalonia 框架中用于定义用户界面的 XAML 格式。它具有以下作用和特点:

作用:

  1. 声明式定义用户界面:AvaloniaXaml 允许开发人员使用 XAML 标记语言来声明用户界面的结构和外观。它提供了一种简洁、可读性强的方式来描述应用程序的用户界面。
  2. 分离视图与逻辑:通过使用 AvaloniaXaml,开发人员可以将用户界面的定义与应用程序的逻辑代码进行分离。这种分离可以提高代码的组织性、可维护性和可测试性。
  3. 支持样式和模板:AvaloniaXaml 提供了强大的样式和模板机制,使开发人员能够轻松地定制和重用用户界面的外观和行为。这样,可以实现更灵活和可扩展的界面设计。
4.自动判断语言

通过CultureInfo.CurrentCulture 实现系统随机器的语言系统自动切换显示语言实现国际化配置。

CultureInfo.CurrentCulture 是一个表示当前线程所使用的区域设置(Culture)的属性。它提供了关于当前文化信息(如语言、国家/地区、日期和时间格式等)的访问和管理。

在MainWindow的构造函数中添加

image-20230717162806170

代码如下:

 var cultureInfo = CultureInfo.CurrentCulture;
if (cultureInfo.Name == "zh-CN") {
    Translate("zh-CN");
}
Translate 方法

image-20230717162923588

代码如下:

#region 国际化
    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")
        )
    );
}
#endregion

3.本地化数据

本地化数据采用了SQLite本地数据库,直接使用SQLite,用sql查询,简单高效,但是需要开发人员对sql的了解程度高。当然,也可以其他ORM框架调用SQLite。

1.SQLite特点

SQLite 是一种嵌入式关系型数据库管理系统(RDBMS),有以下优点,这些优点也是为什么客户端广泛使用它的原因:

  1. 简单易用:SQLite 的设计目标之一是简化数据库系统的使用和管理。它有一个简洁的操作接口,易于学习和使用,无需繁琐的配置和管理步骤。因此,开发人员可以很快上手并快速开发出功能丰富的应用程序。
  2. 无服务器架构:与大多数数据库管理系统不同的是,SQLite 是一个无服务器的数据库引擎,数据库以一个文件的形式存储在磁盘上。这种架构使得 SQLite 的部署和维护变得非常简单,不需要安装和配置额外的服务器。
  3. 轻量级和高性能:由于 SQLite 的设计目标是资源消耗低、性能高效,它被认为是一个轻量级的数据库引擎。SQLite 数据库文件通常较小,并且可以在资源受限的环境中快速运行。同时,SQLite 使用了精简的 SQL 语法和优化技术,提供了快速的查询和高效的数据读写性能。
  4. 跨平台支持:SQLite 是一个跨平台数据库引擎,可以在多种操作系统上运行,包括 Windows、Linux、macOS 和嵌入式系统等。这种跨平台的特性使得开发人员可以方便地在不同的环境中使用 SQLite,并轻松共享和迁移数据库文件。
  5. 高度可移植:SQLite 数据库文件是独立于操作系统和硬件的二进制文件,可以在不同的平台之间自由传输和共享。这种可移植性使得 SQLite 成为一个理想的选择,特别是在需要在不同系统之间进行数据共享或迁移的情况下。
  6. ACID 事务支持:SQLite 支持事务处理和完整性约束,遵循 ACID(原子性、一致性、隔离性和持久性)原则。这意味着开发人员可以使用 SQLite 进行可靠的数据处理,并确保数据的正确性和一致性。
2.创建数据库操作类 DatabaseProcess
1.数据库连接
using var connection = new SQLiteConnection($"Data Source={AppSettings.Instance.DbPath}");
2.创建表和索引

image-20230717164117157

代码如下:

public void CreateDatabase() {
            using var connection = new SQLiteConnection($"Data Source={AppSettings.Instance.DbPath}");
            string sql = "CREATE TABLE phrase (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL DEFAULT '', phrase TEXT NOT NULL DEFAULT '');";

            using var command = new SQLiteCommand(sql, connection);
            // phrase创建表格
            connection.Open();
            command.ExecuteNonQuery();

            // phrase索引
            sql = "CREATE INDEX idx_text ON phrase (phrase);";
            command.CommandText = sql;
            command.ExecuteNonQuery();
}
3.查询列表(例)

image-20230717164249204

代码如下:

public async Task<List<string>> GetPhrasesAsync() {
    List<string> phrases = new List<string>();
    string sql = "SELECT name FROM phrase ORDER BY name COLLATE NOCASE";

    using (var command = new SQLiteCommand(sql, memoryConnection)) {
        using var reader = await command.ExecuteReaderAsync();
        while (await reader.ReadAsync()) {
            phrases.Add(reader.GetString(0));
        }
    }
    return phrases;
}
4.新增(例)

image-20230717164637454

public async Task SavePhrasesAsync(string name, string phrasesText) {
    using var connection = new SQLiteConnection($"Data Source={AppSettings.Instance.DbPath}");
    await connection.OpenAsync();

    using var transaction = connection.BeginTransaction();
    try {
        string sql = $"INSERT INTO phrase (name, phrase) VALUES (@name, @phrase)";

        using var command = new SQLiteCommand(sql, connection);
        command.Parameters.AddWithValue("@name", name);
        command.Parameters.AddWithValue("@phrase", phrasesText);

        await command.ExecuteNonQueryAsync();

        transaction.Commit();
    }
    catch (Exception) {
        transaction.Rollback();
        throw;
    }
    await memoryConnection.CloseAsync();
    await DbLoadToMemoryAsync();
}

4.CSV导入导出

通过CsvHelper实现数据的导入导出

1.选择文件

弹出选择文件框,限制文件类型为CSV

image-20230717165356117

代码如下:

private async Task ImportChatLogAsync() {
    var dialog = new FilePickerOpenOptions {
        AllowMultiple = false,
        Title = "Select CSV file",
        FileTypeFilter = new List<FilePickerFileType>
        {new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } },
         new("All files (*.*)") { Patterns = new[] { "*" } }}
    };

    var result = await (Application.Current!.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)!.MainWindow!.StorageProvider.OpenFilePickerAsync(dialog);

    if (result.Count > 0) {
        var selectedFilePath = result[0].Path.LocalPath;
        try {
            var msg = await _dbProcess.ImportCsvToTableAsync(selectedFilePath);
            var cdialog = new ContentDialog() { Title = msg, PrimaryButtonText = "OK" };
            await ContentDialogShowAsync(cdialog);
        }
        catch (Exception ex) {
            var cdialog = new ContentDialog() { Title = "Failed to import log." + Environment.NewLine + "Error: " + ex.Message, PrimaryButtonText = "OK" };
            await ContentDialogShowAsync(cdialog);
        }
        VMLocator.DataGridViewModel.ChatList = await _dbProcess.SearchChatDatabaseAsync();
    }
}
2.导入到数据库

CSV从文件导入数据,跳过标题行,取得数据,将数据插入数据库

image-20230717165224366

代码如下:

 // CSV从文件导入数据
using var reader = new StreamReader(fileName, System.Text.Encoding.UTF8);
var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
    HasHeaderRecord = true,
    Delimiter = ","
};
using var csvReader = new CsvReader(reader, config);
csvReader.Read(); // 跳过标题行

using var con = new SQLiteConnection($"Data Source={AppSettings.Instance.DbPath}");
await con.OpenAsync();
using (var transaction = con.BeginTransaction()) {
    try {
        while (await csvReader.ReadAsync()) // 导入数据行
        {

            // 数据取得
            var rowData = new List<string>();
            for (int i = 1, loopTo = columnEnd; i <= loopTo; i++) // 从第二排到第八排
                rowData.Add(csvReader.GetField(i));
            // 创建插入语句
            string values = string.Join(", ", Enumerable.Range(0, rowData.Count).Select(i => $"@value{i}"));

            string insertQuery = $"INSERT INTO {tableName} ({columnNames}) VALUES ({values});";

            // 将数据插入数据库
            using (var command = new SQLiteCommand(insertQuery, con)) {
                for (int i = 0, loopTo1 = rowData.Count - 1; i <= loopTo1; i++)
                    command.Parameters.AddWithValue($"@value{i}", rowData[i]);
                await command.ExecuteNonQueryAsync();
            }
            processedCount += 1;
        }

        transaction.Commit();
        msg = $"Successfully imported log. ({processedCount} Records)";
    }
    catch (Exception) {
        transaction.Rollback();
        throw;
    }
}
await con.CloseAsync();

5.自定义快捷键

1.CustomNumericUpDown
  1. 提供了一种自定义的 NumericUpDown 控件,可以用于在用户界面中显示和编辑数字值。NumericUpDown 控件通常用于允许用户输入数值或选择数值的场景。
  2. 通过重写 OnApplyTemplate 方法,该类可以在控件的模板应用后,获取到控件内部的 TextBox 控件,并订阅其 KeyDown 事件。
  3. 在 TextBox_KeyDown 事件处理程序中,实现了自定义的键盘输入逻辑,只允许在 TextBox 中输入数字、小数点以及一些控制键。
  4. 在 DetachedFromVisualTree 事件中,清理了事件的订阅,以避免内存泄漏。
  5. 通过实现 IStyleable 接口,可以定义该控件的样式(StyleKey)。

image-20230717170706649

6.自定义字体

1.添加 AvaloniaResource

在csproj中添加

<AvaloniaResource Include="Assets\Lato-Regular.ttf" /> 
<AvaloniaResource Include="Assets\migu-1m-regular.ttf" />
2.设置控件对应的字体

image-20230717171046642

<Style Selector="TextBlock">
    <Setter Property="FontFamily" Value="avares://TmCGPTD/Assets/Lato-Regular.ttf#Lato" />
    <Setter Property="Foreground" Value="rgb(220, 220, 220)" />
</Style>

7.全局样式

1.添加Styles.axaml

Styles.axaml的作用是全局定义样式,控制整个系统的样式风格

image-20230717172307513

2.Application.Styles

app.axaml中添加

image-20230717172420489

代码如下:

<Application.Styles>
    <StyleInclude Source="avares://TerraMours.Chat.Ava/Assets/Styles.axaml" />
</Application.Styles>
3.添加AvaloniaResource

csproj添加AvaloniaResource

  <ItemGroup>
	<AvaloniaResource Include="Assets\Styles.axaml" />
  </ItemGroup>

项目截图:

image-20230717163231985

篇幅有限,功能开发部分请跳转下篇

基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发2-功能开发

阅读如遇样式问题,请前往个人博客浏览: https://www.raokun.top
拥抱ChatGPT:https://ai.terramours.site
开源项目地址:https://github.com/raokun/TerraMours.Chat.Ava