AvaloniaUI 快捷制作精美的多端客户端(Windows、Linux、Mac)示例,暂无手机平板端
前言
经过一段时间的沉淀,桌面客户端的复杂度上升,此前发布的Tkinter 开发可以满足功能,但是在美观上,客户一直不满意,于是在Github上找了N久的时间,找到了使用C#或F#开发的 AvaloniaUI 开源架构。
优缺点
- 优点:
- 界面较为美观,文档清晰,不需要过多的设置样式就可以得到好看的界面
- 代码架构清晰,界面跳转简便
- 打包部署简单,基本上只需要鼠标点点点就行了
- 开发工具的提示较全,推荐使用Rider,也可以使用:Visual Studio(目前仅发布打包使用)
- 缺点:
- 对不熟悉C#或F#的人员不太友好,需要熟悉相关代码
- 需要有一定的Html界面样式知识,不然就得一直百度样式和排版
- 每一个交互的方法需要处理异常,不然会导致程序直接崩溃(C#开发为例,F#没有使用不太清楚)
正文
- 代码架构
介绍:以我当前在做的项目为例:
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(模型-视图-视图模型)模式的核心部分,该模式将图形用户界面的开发与业务逻辑或后端逻辑的开发分离 - 实操
废话不多说,贴代码和样式图喽!
登录界面:
主页:
介绍: 上方界面没有做过多的样式设计,除去颜色外,仅设置了间距,界面相较于Tkinter美观了许多。
代码:
- 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做数据绑定界面就好了,开发起来很便捷,好啦,再发就不太好啦,各位可以多动手试一试,不清楚的可以私信或留言!