c# Avalonia 架构开发跨平台应用

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/4otvKicon-default.png?t=N7T8http://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)icon-default.png?t=N7T8https://gitee.com/sunyuzhe114/AvaloniaCalculatorApp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值