Bootstrap

AvaloniaUI 快捷制作精美的多端客户端

AvaloniaUI 快捷制作精美的多端客户端(Windows、Linux、Mac)示例,暂无手机平板端

前言

经过一段时间的沉淀,桌面客户端的复杂度上升,此前发布的Tkinter 开发可以满足功能,但是在美观上,客户一直不满意,于是在Github上找了N久的时间,找到了使用C#或F#开发的 AvaloniaUI 开源架构。

优缺点

  1. 优点:
    1. 界面较为美观,文档清晰,不需要过多的设置样式就可以得到好看的界面
    2. 代码架构清晰,界面跳转简便
    3. 打包部署简单,基本上只需要鼠标点点点就行了
    4. 开发工具的提示较全,推荐使用Rider,也可以使用:Visual Studio(目前仅发布打包使用)
  2. 缺点:
    1. 对不熟悉C#或F#的人员不太友好,需要熟悉相关代码
    2. 需要有一定的Html界面样式知识,不然就得一直百度样式和排版
    3. 每一个交互的方法需要处理异常,不然会导致程序直接崩溃(C#开发为例,F#没有使用不太清楚)

正文

  1. 代码架构
    在这里插入图片描述
    介绍:以我当前在做的项目为例:
    1. Assets: 为静态资源如图片,图标,视频等存放位置,打包时会一起打包进去
    2. Helpers: 存放全局帮助的插件(自己建的不是默认的),此处主要存放了图片的处理,包括远程url图片和本地绝对或相对路径图片的处理,以及一些样式方法的继承IValueConverter类
    3. Models:存放数据解析的实体类
    4. Package:存放的是自己写的打包脚本,目前使用的是Inno Setup 打包脚本
    5. Plugins:存放全局插件,减少代码的书写
    6. Utils: 工具包,存放各种数据处理的工具类
    7. ViewModels:存放视图绑定数据的模型
    8. Views:存放界面样式以及界面功能的交互方法等
    9. App.axaml:应用的主类,主要设置全局的样式,数据模板等
    10. app.manifest:应用程序清单文件:指定应用程序需要的操作系统权限、指定应用程序的DPI感知、指定应用程序使用的Windows主题和视觉样式、兼容性设置、指定依赖库的版本等
    11. Program.cs:应用的入口定义,及启动时执行的操作
    12. publish.xml:发布的配置文件
    13. ViewLocator:ViewLocator是一个用于解析与特定视图模型(ViewModel)对应的视图(View)的机制。它是MVVM(模型-视图-视图模型)模式的核心部分,该模式将图形用户界面的开发与业务逻辑或后端逻辑的开发分离
  2. 实操
    废话不多说,贴代码和样式图喽!
    登录界面:
    在这里插入图片描述

主页:
在这里插入图片描述
介绍: 上方界面没有做过多的样式设计,除去颜色外,仅设置了间距,界面相较于Tkinter美观了许多。
代码:

  1. MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:YuJian.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
        x:Class="YuJian.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/login/favicon.ico"
        Title="遇见" Loaded="Control_OnLoaded">
    <Design.DataContext>
        <vm:MainWindowViewModel />
    </Design.DataContext>

    <Grid RowDefinitions="*" ColumnDefinitions="Auto,*">
        <Image Grid.Column="0" Source="/Assets/login/yujian.jpg" Stretch="UniformToFill" />
        <StackPanel Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" VerticalAlignment="Center">
            <TextBox Name="Username" AcceptsReturn="False" Text="{Binding Description}" Watermark="请输入登录账号"
                     KeyDown="Username_KeyDown" Width="200" Margin="0,0,30,0" />
            <TextBox Name="Password" AcceptsReturn="False" PasswordChar="*" Text="{Binding DescriptionPassword}"
                     Watermark="请输入登录密码" KeyUp="Password_KeyDown" Width="200" Margin="0,8,30,0" />
            <Button Content="立即登录" Click="Button_OnClick" HorizontalAlignment="Center"
                    VerticalAlignment="Center" HorizontalContentAlignment="Center"
                    Width="200" Margin="0,8,30,0" Background="HotPink">
            </Button>
            <TextBlock x:Name="RegisterTextBlock" Text="还没有账号?立即注册" HorizontalAlignment="Right"
                       VerticalAlignment="Center" Margin="0,5,30,0" />
        </StackPanel>
    </Grid>
</Window>
``
2. MainWindow.cs
```csharp
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YuJian.Models;
using YuJian.Plugins;
using YuJian.Utils;
using YuJian.ViewModels;
using YuJian.Views.home;

namespace YuJian.Views;

public partial class MainWindow : Window
{
    private readonly MainWindowViewModel _viewModel;

    public MainWindow()
    {
        InitializeComponent();
        _viewModel = new MainWindowViewModel();
        // DataContext不等于模型无法变更界面信息
        DataContext = _viewModel;
        RegisterTextBlock.PointerPressed += RegisterTextBlock_PointerPressed;
        Width = 520;
        Height = 300;
        WindowStartupLocation = WindowStartupLocation.CenterScreen;
        CanResize = false;
    }

    private void RegisterTextBlock_PointerPressed(object? sender, PointerPressedEventArgs e)
    {
        AlertTaps.ShowAlert(this, "非常抱歉,暂时未开通用户注册功能,敬请期待!", width: 350);
    }

    private void Username_KeyDown(object sender, KeyEventArgs e)
    {
        try
        {
            // 检查焦点是否在Username文本框
            if (!((TextBox)sender).IsFocused)
            {
                Console.WriteLine("Username textbox is not focused");
                return;
            }

            if (e.Key != Key.Enter) return;
            var password = this.FindControl<TextBox>("Password");
            if (password == null) return;
            password.Focus();
            e.Handled = true;
        }
        catch (Exception exception)
        {
            AlertTaps.ShowAlert(this, $"登录账号文本框焦点切换出现错误:{exception.Message}");
        }
    }

    public void Password_KeyDown(object? sender, KeyEventArgs e)
    {
        try
        {
            if (e.Key != Key.Enter || string.IsNullOrEmpty(_viewModel.DescriptionPassword)) return;
            Button_OnClick(null, null);
            e.Handled = true;
        }
        catch (Exception exception)
        {
            AlertTaps.ShowAlert(this, $"登录密码文本框焦点切换出现错误:{exception.Message}");
        }
    }

    private async void Button_OnClick(object? sender, RoutedEventArgs? e)
    {
        try
        {
            var username = _viewModel.Description;
            var password = _viewModel.DescriptionPassword;

            if (string.IsNullOrWhiteSpace(username))
            {
                AlertTaps.ShowAlert(this, "登录账号不可为空哦!");
                Console.WriteLine("Username or password cannot be empty.");
                return;
            }

            if (string.IsNullOrWhiteSpace(password))
            {
                AlertTaps.ShowAlert(this, "登录密码不可为空哦!");
                Console.WriteLine("Username or password cannot be empty.");
                return;
            }

            try
            {
                using var client = new HttpClient();
                var authBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
                var encodedAuth = Convert.ToBase64String(authBytes).Trim();
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", encodedAuth);
                var response = await client.GetAsync(HttpUrls.LoginTokenUrl);
                if (response.IsSuccessStatusCode)
                {
                    var content = await response.Content.ReadAsStringAsync();
                    CacheUtils.SaveUsernameEncrypted(username, password);
                    var jsonObject = JsonConvert.DeserializeObject(content) as JObject;
                    if (response.Headers.TryGetValues("Set-Cookie", out var cookies) && jsonObject != null)
                    {
                        foreach (var cookie in cookies)
                        {
                            jsonObject.Add("SESSION_ID", cookie);
                        }
                    }

                    CacheUtils.SaveLoginEncrypted(jsonObject);
                    Hide();
                    var homeView = new HomeView();
                    homeView.Show();
                    Close();
                }
                else
                {
                    Console.WriteLine($"Error: {response.StatusCode}");
                    if (response.StatusCode != System.Net.HttpStatusCode.Unauthorized) return;
                    Console.WriteLine("Invalid username or password.");
                    AlertTaps.ShowAlert(this, "登录失败啦,账号或密码不正确哦!");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
                AlertTaps.ShowAlert(this, "登陆失败啦,请检查网络情况,或联系管理员!");
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception.Message);
        }
    }

    public void Control_OnLoaded(object sender, RoutedEventArgs e)
    {
        try
        {
            var loadedInfo = CacheUtils.LoadUsernameEncrypted();
            if (loadedInfo == null) return;
            _viewModel.Description = loadedInfo.Item1;
            _viewModel.DescriptionPassword = loadedInfo.Item2;
        }
        catch (Exception exception)
        {
            Console.WriteLine($"回写密码失败:{exception.Message}");
        }
    }
}

上方代码为一组,再加上Model做数据绑定界面就好了,开发起来很便捷,好啦,再发就不太好啦,各位可以多动手试一试,不清楚的可以私信或留言!

;