国产化即时通信系统开发 -- 使用Avalonia实现GGTalk的UI界面(Linux、Ubuntu、UOS、中标麒麟)
距离 2013 年开源 GGTalk 以来,7 年已经过去了,GGTalk 现在有了完整的 PC 版、安卓版、iOS 版(即将发布),以及 Xamarin 版本。
然而,时代一直在变化,在今天,有个趋势越来越明显,那就是政府系统将逐渐迁移到 Linux 以及国产化的操作系统和数据库上面来。
所以,GGTalk 也将随顺这一必然的趋势,服务端将推出 Linux(CentOS)版本,客户端也将支持 Linux/Ubuntu、中标麒麟、UOS 等国产操作系统。
基于.NET Core,服务端的迁移相对容易;而客户端的迁移则繁琐很多,其主要在于界面 UI 部分需要完全重写。
在考察了众多的 Linux 上的 UI 技术之后,我们选定了现在很主流的 Avalonia 框架作为 GGTalk 的客户端版本的 UI 技术。
在使用 Avalonia 开发 GGTalk 客户端 linux 版本的过程中,我们遇到了很多坑,也积累了很多经验,接下来我们将通过“Avalonia 跨平台 UI 开发”这个系列,将这些过坑的经验分享出来,为后来者提供参考。
那下面先从 Avalonia 简介开始吧。
一. Avalonia 简介
Avalonia 是一个基于 WPF XAML 的跨平台 UI 框架,并支持多种操作系统:Windows(.NET Framework,.NET Core),Linux(GTK),MacOS,Android 和 iOS。
通过 Avalonia,可以使用 XAML 标记来实现应用程序的外观,同时使用代码来实现其行为。
Avalonia 官网:https://avaloniaui.net/
Avalonia 开源地址:https://github.com/AvaloniaUI/Avalonia
1. 开发准备
VS 2019 安装扩展 https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio
安装之后就有了 Avalonia 开发模板。
选择 Avalonia MVVM Application ,就可以创建一个项目。
2. GGTalk 登录界面示例
新建一个基础项目 GGTalk,在 MainWindow.xaml 添加图片、按钮、输入框等控件,就是 WPF XAML 的语法,直接可以实现对应布局。
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:myoneavalonia.ViewModels;assembly=myoneavalonia" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="430" d:DesignHeight="330" x:Class="myoneavalonia.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="GGTalk" Width="430" Height="340" CanResize="False" WindowStartupLocation="CenterScreen"><Design.DataContext> <vm:MainWindowViewModel/> </Design.DataContext>
<Window.Styles >
<Style Selector="TextBox.tb1">
<Setter Property="Margin" Value="0,-40,0,0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Width" Value="250"/>
<Setter Property="Watermark" Value="账号"/>
<Setter Property="BorderBrush" Value="#80c0ff"/>
</Style>
<Style Selector="TextBox.tb2">
<Setter Property="Margin" Value="0,35,0,0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Width" Value="250"/>
<Setter Property="Watermark" Value="密码"/>
<Setter Property="BorderBrush" Value="#80c0ff"/>
<Setter Property="PasswordChar" Value="*"/>
</Style>
<!--######<TextBox 标签 >######--><Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Image.img1</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0,-250,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Width</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">430</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Image.img2</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0,-190,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Width</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">73</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Height</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">73</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <!--######<Image标签>######--> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">TextBlock.tbk1</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">5,5,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Foreground</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">White</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">TextBlock.tbk2</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">292,213,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Foreground</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#0c7ab9</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Cursor</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Hand</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">TextBlock.tbk3</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">275,305,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Foreground</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#696969</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <!--######<TextBlock标签>######--> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Button.bt1</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0,195,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Width</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">250</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Height</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">40</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Background</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#407cff</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Foreground</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">White</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">FontSize</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">17</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <!--######<Button标签>######--> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">CheckBox.cbx1</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">89,105,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span> /> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">BorderBrush</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#3c9fc5</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <Style Selector=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">CheckBox.cbx2</span><span style="color: rgba(128, 0, 0, 1)">"</span>> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Margin</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">190,105,0,0</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> <Setter Property=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">BorderBrush</span><span style="color: rgba(128, 0, 0, 1)">"</span> Value=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#3c9fc5</span><span style="color: rgba(128, 0, 0, 1)">"</span>/> </Style> <!--######<CheckBox标签>######--> </Window.Styles>
<StackPanel>
<Border Background="White"
BorderBrush="Gray"
BorderThickness="1"
Padding="0"
Width="430"
Height="340">
<Grid>
<Image Classes="img1" Source="D:\yzy\avaloniafiles\myoneavalonia\Resources\image_sign.png" ></Image>
<Image Classes="img2" Source="D:\yzy\avaloniafiles\myoneavalonia\Resources\8.png" />
<TextBlock Classes="tbk1">GGTalk 2020</TextBlock>
<TextBlock Classes="tbk2"> 注册登录 </TextBlock>
<TextBlock Classes="tbk3">GGTalk 企业即时通讯系统 </TextBlock>
<TextBox Classes="tb1"/>
<TextBox Classes="tb2"/>
<CheckBox Classes="cbx2" > 自动登录 </CheckBox>
<CheckBox Classes="cbx1" > 记住密码 </CheckBox>
<Button Classes="bt1"> 登录 </Button>
</Grid>
</Border>
</StackPanel></Window>
如果熟悉 WPF XAML,那么上面的代码理解起来就非常容易了。
二. 运行程序
我们在 Ubuntu 和中标麒麟上运行这个程序,效果如下所示(还不错吧):
(在 Ubuntu 上运行的效果)
(在中标麒麟上运行的效果)
三. 在使用 Avalonia 时遇到的坑
在开发这个登录界面的时候,遇到了三个坑。
(1)当将 <Textbox> 输入框标签的 height 属性设置为小于或等于 25 时,在输入框的右边,会多出一个下拉框。
研究了半天才发现跟输入框高度有关,瞬间泪崩......
(2)官方文档中有一些控件无法正常使用,提示无法解析该类型,在运行时会报错,比如:
解决方案:在官网(https://avaloniaui.net/)上下载对应的控件,然后引入项目中就不会报错了。
(3)另外,在 Ubuntu 上运行 Avalonia 项目后如果没有显示界面,这时就需要去修改 /etc/apt/sources.list 文件,更新软件源后就能够显示界面了。
更换软件源的步骤:
Ubuntu 的源存放在在 /etc/apt/ 目录下的 sources.list 文件中,修改前我们先备份,在终端中执行以下命令:
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bcakup
然后执行下面的命令打开 sources.list 文件,清空里面的内容
sudo gedit /etc/apt/sources.list
把下面阿里云与清华大学的 Ubuntu 源复制进去,保存后退出
# 阿里云源 deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse ## 測試版源 deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse # 源碼 deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse ## 測試版源 deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse清华大学源
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
## 測試版源
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
# 源碼
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
## 測試版源
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
接着在终端上执行以下命令更新软件列表,检测出可以更新的软件:
sudo apt-get update
最后在终端上执行以下命令进行软件更新:
sudo apt-get upgrade
到这里,GGTalk 的登录界面就实现完成了。同样的,我们会将 GGTalk 的 Linux/ 国产化操作系统的版本的源码全部放出来给大家参考,敬请期待。