Avalonia,读:阿瓦隆尼亚
由于以前的c#开发的windows平台项目想移植到信创平台(UOS,Kylin)上,起初想用python重写,后来发现了这个Avalonia,用这个改动起来工作相对较少于是就先了解一下。
官网Avalonia Docs | Avalonia Docs (avaloniaui.net)
实现了一个计算器的应用,先看在不同平台的效果
windows11上
ubuntu上
统信UOS 上
麒麟 kylin v10
好了,先说一下问题,如果想一套代码在不同平台同时运行,里面调用的逻辑还是要分系统的,先分linux系统和windows系统,macOS等,然后不同系统里面还要再细分,如linux下的ubuntu,kylin,uos等,这里主要说一下在linux系统上,经常出错的一个问题就是下面的:
Could not create glyphTypeface.
Unhandled exception. System.InvalidOperationException: Could not create glyphTypeface. Font family: $Default (key: ). Style: Normal. Weight: Normal. Stretch: Normal
at Avalonia.Media.Typeface.get_GlyphTypeface()
at Avalonia.Rendering.Composition.Compositor.get_DiagnosticTextRenderer()
at Avalonia.Rendering.Composition.Compositor.CreateCompositionTarget(Func`1 surfaces)
at Avalonia.Rendering.Composition.CompositingRenderer..ctor(IRenderRoot root, Compositor compositor, Func`1 surfaces)
at Avalonia.Controls.TopLevel..ctor(ITopLevelImpl impl, IAvaloniaDependencyResolver dependencyResolver)
at Avalonia.Controls.WindowBase..ctor(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver)
at Avalonia.Controls.WindowBase..ctor(IWindowBaseImpl impl)
at Avalonia.Controls.Window..ctor(IWindowImpl impl)
at Avalonia.Controls.Window..ctor()
at GetStartedApp.MainWindow..ctor() in D:\working\c#_test\GetStartedApp\MainWindow.axaml.cs:line 10
at GetStartedApp.App.OnFrameworkInitializationCompleted() in D:\working\c#_test\GetStartedApp\App.axaml.cs:line 18
at Avalonia.AppBuilder.SetupUnsafe()
at Avalonia.AppBuilder.Setup()
at Avalonia.AppBuilder.SetupWithLifetime(IApplicationLifetime lifetime)
at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder builder, String[] args, Action`1 lifetimeBuilder)
at GetStartedApp.Program.Main(String[] args) in D:\working\c#_test\GetStartedApp\Program.cs:line 12
已放弃 (核心已转储)
主要原因是加载的字体不存在就报bug了。刚上也有自己安装字体的解决方法,参见
http://t.csdnimg.cn/4otvKhttp://t.csdnimg.cn/4otvK但是上文没有我试过这么多系统
最好的方法是在每个系统中使用已经自带的字体。改代码Program.cs如下
using Avalonia;
using Avalonia.Media;
using System;
using System.Drawing.Text;
using System.Linq;
using System.Runtime.InteropServices;
namespace CalculatorApp;
class Program
{
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// BuildAvaloniaApp 是应用程序的启动方法,负责配置并启动 Avalonia 应用程序。
public static AppBuilder BuildAvaloniaApp()
{
// 初始化默认的字体名称为空字符串
string defaultFont = "";
// 检查当前操作系统是否是 Linux 系统
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// 获取当前操作系统的描述信息,通常包含操作系统的名称和版本
string osDescription = RuntimeInformation.OSDescription.ToLower();
Console.WriteLine("osDescription: " + osDescription);
// 检查操作系统描述是否包含 "uniontech",如果是,表示是 UOS 系统
if (osDescription.Contains("uniontech"))
{
// 设置默认字体为 "Noto Sans"(这是 UOS 系统中常见的字体)
defaultFont = "Noto Sans";
}
// 检查操作系统描述是否包含 "kylin",如果是,表示是麒麟 v10 系统
else if (osDescription.Contains("kylin"))
{
// 设置默认字体为 "Arial"(麒麟系统中常用的字体)
defaultFont = "Arial";
}
// 检查操作系统描述是否包含 "ubuntu",如果是,表示是 Ubuntu 系统
else if (osDescription.Contains("ubuntu"))
{
// 设置默认字体为 "Noto Mono"(Ubuntu 中常用的等宽字体)
defaultFont = "Noto Mono";
}
}
// 创建 FontManagerOptions 对象,并设置默认的字体名称
FontManagerOptions options = new FontManagerOptions
{
DefaultFamilyName = defaultFont // 通过操作系统检测结果设置的默认字体
};
// 输出当前使用的字体信息到控制台,以便调试和验证
Console.WriteLine("Using font: " + options.DefaultFamilyName);
// 返回一个 AppBuilder 实例,配置并启动 Avalonia 应用程序
return AppBuilder.Configure<App>() // 配置应用程序
.UsePlatformDetect() // 自动检测运行平台(Windows, Linux, macOS)
.WithInterFont() // 配置默认字体为 InterFont,作为默认字体集
.LogToTrace() // 允许日志信息输出到控制台,方便调试
.With(options); // 应用字体选项
}
}
只有上面这个代码配好了,后面的逻辑在各个linux平台上才没有问题。
下面写出界面代码 ,文件名 MainWindow.axaml,其后缀为axaml与wpf中常用的xaml多个a表示使用的是Avalonia架构。这个代码定义了一个简洁的计算器界面,使用 Avalonia 框架构建。通过数据绑定和命令模式,实现了将界面操作与业务逻辑分离的设计。
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="CalculatorApp.MainWindow"
Title="手搓计算器"
Width="350" Height="650"
MinWidth="300" MinHeight="580"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CalculatorApp"
mc:Ignorable="d"
x:DataType="local:MainWindowViewModel"
Background="#F0F0F0"
WindowStartupLocation="CenterScreen"
KeyDown="Window_KeyDown">
<!-- 设置窗体居中 -->
<!-- 主布局容器 -->
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<!-- 设置显示屏的行高度 -->
<RowDefinition Height="40"/>
<!-- 设置结果显示行的高度 -->
<RowDefinition Height="100"/>
<!-- 预留的一行(可以放其他功能) -->
<RowDefinition Height="*"/>
<!-- 按钮行,占用剩余空间 -->
</Grid.RowDefinitions>
<!-- 显示屏,显示输入的数字和操作 -->
<Grid Grid.Row="0">
<!-- 显示输入的数字,右对齐,字体大小为 40 -->
<Label Name="DisplayAll" FontSize="40" Content="{Binding DisplayAll}" HorizontalContentAlignment="Right" VerticalContentAlignment="Center"/>
</Grid>
<!-- 显示当前计算结果 -->
<Grid Grid.Row="1">
<!-- 显示计算结果,右对齐,字体大小为 30 -->
<Label Name="DisplayResult" FontSize="30" Content="{Binding DisplayResult}" HorizontalContentAlignment="Right" VerticalContentAlignment="Center"/>
</Grid>
<!-- 按钮区域,设置按钮布局 -->
<Grid Grid.Row="3">
<!-- 定义按钮的行 -->
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<!-- 每行高度相等,使用比例分配 -->
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<!-- 定义按钮的列 -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<!-- 每列宽度相等,使用比例分配 -->
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 第一行按钮 -->
<Button Content="C" Grid.Row="0" Grid.Column="0" Classes="clear" Command="{Binding ClearCommand}"/>
<!-- 清除按钮,绑定清除命令 -->
<!-- 暂时未添加的功能按钮 -->
<!-- <Button Content="±" Grid.Row="0" Grid.Column="1" Command="{Binding NegateCommand}"/>-->
<!--<Button Content="%" Grid.Row="0" Grid.Column="2" Command="{Binding PercentCommand}"/>-->
<Button Content="÷" Grid.Row="0" Grid.Column="3" Classes="operator" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="/"/>
<!-- 除法按钮,绑定操作命令 -->
<!-- 第二行按钮 -->
<Button Content="7" Grid.Row="1" Grid.Column="0" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="7"/>
<!-- 数字 7 -->
<Button Content="8" Grid.Row="1" Grid.Column="1" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="8"/>
<!-- 数字 8 -->
<Button Content="9" Grid.Row="1" Grid.Column="2" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="9"/>
<!-- 数字 9 -->
<Button Content="×" Grid.Row="1" Grid.Column="3" Classes="operator" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="*"/>
<!-- 乘法按钮 -->
<!-- 第三行按钮 -->
<Button Content="4" Grid.Row="2" Grid.Column="0" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="4"/>
<!-- 数字 4 -->
<Button Content="5" Grid.Row="2" Grid.Column="1" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="5"/>
<!-- 数字 5 -->
<Button Content="6" Grid.Row="2" Grid.Column="2" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="6"/>
<!-- 数字 6 -->
<Button Content="−" Grid.Row="2" Grid.Column="3" Classes="operator" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="-"/>
<!-- 减法按钮 -->
<!-- 第四行按钮 -->
<Button Content="1" Grid.Row="3" Grid.Column="0" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="1"/>
<!-- 数字 1 -->
<Button Content="2" Grid.Row="3" Grid.Column="1" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="2"/>
<!-- 数字 2 -->
<Button Content="3" Grid.Row="3" Grid.Column="2" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="3"/>
<!-- 数字 3 -->
<Button Content="+" Grid.Row="3" Grid.Column="3" Classes="operator" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="+"/>
<!-- 加法按钮 -->
<!-- 第五行按钮 -->
<Button Content="0" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="0" HorizontalAlignment="Stretch"/>
<!-- 数字 0,占两列 -->
<Button Content="." Grid.Row="4" Grid.Column="2" Command="{Binding AddDigitOrOperationCommand}" CommandParameter="."/>
<!-- 小数点按钮 -->
<Button Content="=" Grid.Row="4" Grid.Column="3" Classes="equals" Command="{Binding EqualsCommand}"/>
<!-- 等于按钮,绑定等于命令 -->
</Grid>
</Grid>
</Window>
MainWindow.axaml.cs的代码
这个代码在 Avalonia UI 框架下,实现了按键输入处理功能。通过判断用户按下的键来触发计算器的相应操作,例如输入数字、执行加减乘除等操作,并通过 ViewModel 中的命令绑定来更新界面和执行计算逻辑。
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Remote.Protocol.Input;
using System;
using Key = Avalonia.Input.Key; // 为避免与其他命名空间中的 Key 冲突,显式定义 Key
namespace CalculatorApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent(); // 初始化组件(加载 XAML UI)
DataContext = new MainWindowViewModel(); // 设置数据上下文为 MainWindowViewModel,进行数据绑定
}
// 处理按键事件的方法,当用户按下键盘时触发
private void Window_KeyDown(object sender, KeyEventArgs e)
{
// 获取当前的 ViewModel
var viewModel = DataContext as MainWindowViewModel;
// 如果 ViewModel 为空,则直接返回,不进行任何操作
if (viewModel == null)
return;
// 根据按下的键执行相应的操作
// 检查是否按下了数字键 0-9,并且没有按下 Shift 键
if (e.Key >= Key.D0 && e.Key <= Key.D9 && e.KeyModifiers.HasFlag(KeyModifiers.Shift) == false)
{
// 将数字转换为字符串并传递给命令
string digit = (e.Key - Key.D0).ToString();
viewModel.AddDigitOrOperationCommand.Execute(digit);
}
// 检查是否按下了加号键(+),支持数字键盘的加号以及 Shift + OemPlus(通常是 + 键)
else if (e.Key == Key.Add || (e.Key == Key.OemPlus && e.KeyModifiers.HasFlag(KeyModifiers.Shift)))
{
viewModel.AddDigitOrOperationCommand.Execute("+");
}
// 检查是否按下了减号键(-)
else if (e.Key == Key.Subtract)
{
viewModel.AddDigitOrOperationCommand.Execute("-");
}
// 检查是否按下了乘号键(*),支持数字键盘的乘号以及 Shift + 8(通常是 * 键)
else if (e.Key == Key.Multiply || (e.Key == Key.D8 && e.KeyModifiers.HasFlag(KeyModifiers.Shift)))
{
viewModel.AddDigitOrOperationCommand.Execute("*");
}
// 检查是否按下了除号键(/),支持数字键盘的除号以及 Oem2(通常是 / 键)
else if (e.Key == Key.Divide || e.Key == Key.Oem2)
{
viewModel.AddDigitOrOperationCommand.Execute("/");
}
// 检查是否按下了等于号键(=)或回车键(Enter)
else if (e.Key == Key.OemPlus || e.Key == Key.Enter)
{
viewModel.AddDigitOrOperationCommand.Execute("=");
}
// 检查是否按下了删除键(Backspace)或删除键(Delete)
else if (e.Key == Key.Back || e.Key == Key.Delete)
{
viewModel.ClearCommand.Execute(null); // 执行清除命令
}
}
}
}
MainWindow.axaml.cs代码
using Avalonia.Input;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace CalculatorApp
{
public class MainWindowViewModel : INotifyPropertyChanged
{
// 私有字段,用于存储显示的文本、当前操作符、结果和其他操作状态
private string _displayText = "0"; // 显示屏的默认值为 "0"
private string _displaySymbol = ""; // 用于显示当前的运算符号
private string _displayResult = ""; // 显示最终的运算结果
private string _currentOperation; // 当前正在进行的操作符
private double _firstNumber; // 存储第一个操作数
private bool _isOperationPending; // 标志是否有未完成的操作
private bool _isNewEntry = true; // 标志是否是新的输入
private bool _isResultDisplayed = false; // 标志是否刚刚显示了计算结果
private string _displayAll; // 用于显示所有操作记录
// 实现 INotifyPropertyChanged 接口,用于通知 UI 属性的变化
public event PropertyChangedEventHandler PropertyChanged;
public MainWindowViewModel()
{
// 初始化命令,将按钮绑定到相应的命令
AddDigitOrOperationCommand = new RelayCommand<string>(AddDigitOrOperation); // 处理数字和操作符的输入
ClearCommand = new RelayCommand(Clear); // 处理清除操作
EqualsCommand = new RelayCommand(Calculate); // 处理等于操作
}
// 公有属性,用于绑定 UI
public string DisplayAll
{
get => _displayAll;
set
{
_displayAll = value;
OnPropertyChanged(nameof(DisplayAll)); // 通知 UI 属性已更改
}
}
public string DisplayText
{
get => _displayText;
set
{
_displayText = value;
OnPropertyChanged(); // 使用 CallerMemberName 自动获取属性名称
}
}
public string DisplaySymbol
{
get => _displaySymbol;
set
{
_displaySymbol = value;
OnPropertyChanged();
}
}
public string DisplayResult
{
get => _displayResult;
set
{
_displayResult = value;
OnPropertyChanged();
}
}
// 命令用于处理各种用户操作
public ICommand AddDigitOrOperationCommand { get; } // 处理数字或操作符
public ICommand ClearCommand { get; } // 清除输入和结果
public ICommand EqualsCommand { get; } // 执行等于运算
public ICommand NegateCommand { get; } // 处理取反操作(可扩展)
public ICommand PercentCommand { get; } // 处理百分比操作(可扩展)
// 根据输入值判断是数字还是操作符
private void AddDigitOrOperation(string input)
{
if (IsOperation(input))
{
SetOperation(input); // 如果是操作符,则调用 SetOperation
}
else
{
AddDigit(input); // 如果是数字,则调用 AddDigit
}
}
// 判断输入是否是操作符
private bool IsOperation(string input)
{
return input == "+" || input == "-" || input == "*" || input == "/" || input == "=";
}
// 添加数字到显示屏
private void AddDigit(string digit)
{
// 如果刚刚显示过结果,清空显示
if (_isResultDisplayed)
{
DisplayAll = "";
DisplayResult = "";
_isResultDisplayed = false;
}
// 如果是新输入,则替换显示内容,否则追加
if (_isNewEntry)
{
DisplayText = digit == "." ? "0." : digit;
_isNewEntry = false;
DisplayAll += digit;
}
else
{
// 避免重复输入小数点
if (digit == "." && DisplayText.Contains("."))
return;
DisplayText += digit;
DisplayAll += digit;
}
}
// 设置操作符并处理运算
private void SetOperation(string operation)
{
if (!_isOperationPending) // 如果当前没有进行中的操作
{
_firstNumber = double.Parse(DisplayText); // 解析输入的第一个数字
_currentOperation = operation; // 保存当前操作符
_isOperationPending = true; // 标记操作进行中
_isNewEntry = true; // 准备接受新的输入
// 显示操作符
switch (operation)
{
case "+":
case "-":
case "*":
case "/":
DisplaySymbol = operation;
break;
case "=":
Calculate(); // 如果操作符是 "=", 则执行计算
return;
}
DisplayAll += DisplaySymbol;
}
else
{
// 如果有进行中的操作,执行计算后再设置新的操作符
Calculate();
_currentOperation = operation;
}
}
// 执行计算逻辑
private void Calculate()
{
if (!_isOperationPending)
return;
double secondNumber = double.Parse(DisplayText); // 获取输入的第二个数字
double result = 0;
try
{
// 根据操作符计算结果
switch (_currentOperation)
{
case "+":
result = _firstNumber + secondNumber;
break;
case "-":
result = _firstNumber - secondNumber;
break;
case "*":
result = _firstNumber * secondNumber;
break;
case "/":
if (secondNumber == 0)
throw new DivideByZeroException();
result = _firstNumber / secondNumber;
break;
}
// 更新显示结果
DisplayAll += "=";
DisplayResult = result.ToString();
_firstNumber = result; // 将结果作为下一次操作的第一个数字
_isNewEntry = true;
_isOperationPending = false;
_isResultDisplayed = true; // 标记刚刚显示了结果
}
catch (Exception)
{
DisplayText = "Error"; // 捕获错误,例如除零错误
_isNewEntry = true;
_isOperationPending = false;
DisplaySymbol = "";
}
}
// 清除输入和结果
private void Clear()
{
DisplayText = "0"; // 重置显示屏
DisplaySymbol = ""; // 清除操作符
DisplayAll = ""; // 清空所有显示
DisplayResult = ""; // 清空结果
_firstNumber = 0; // 重置第一个数字
_currentOperation = null; // 清除当前操作符
_isOperationPending = false; // 重置操作状态
_isNewEntry = true; // 重置为新输入状态
_isResultDisplayed = false; // 重置结果显示状态
}
// 通知属性已更改,刷新 UI
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
// RelayCommand 类:用于绑定命令和操作
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute; // 执行操作的委托
private readonly Func<T, bool> _canExecute; // 判断操作是否可以执行的委托
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
// 判断命令是否可以执行
public bool CanExecute(object parameter) => _canExecute == null || _canExecute((T)parameter);
// 执行命令
public void Execute(object parameter) => _execute((T)parameter);
// 事件,当命令的可执行状态发生变化时触发
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
}
// 不带参数的 RelayCommand 类,用于绑定简单操作
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
// 判断命令是否可以执行
public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
// 执行命令
public void Execute(object parameter) => _execute();
// 事件,当命令的可执行状态发生变化时触发
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
}
}
另外就是发布成各种平台的方法了:
右击工程点击发布-》选择文件夹,相当于支持了win,linux,osx,以及x86,x64,arm多种架构。
下载代码如下:
AvaloniaCalculatorApp: 一个基于Avalonia架构的c#简单的计算器应用,支持windows ,linux(ubuntu,kylin,uos),visual 2022开发 (gitee.com)https://gitee.com/sunyuzhe114/AvaloniaCalculatorApp