.Net-Avalonia学习笔记(八)-音乐商店应用(MVVM+IOC/ID)
一、音乐商店应用
该应用程序具有高度图形化的界面,显示专辑封面图像,并使用半透明的“亚克力”效果模糊窗口背景,使其具有非常时尚的外观。在本教程结束时,您将能够搜索 iTunes 在线专辑列表,并选择专辑添加到您自己的列表中。
官方项目地址:https://docs.avaloniaui.net/zh-Hans/docs/tutorials/music-store-app/
1、创建项目
(1)新建解决方案
(2)选择Avalonia .NET Core MVVM App
(3)解决方案名为 “Avalonia_MusicStore”,下一步然后点击“创建”
(4)新项目大体目录如下:
2、实现亚克力风格
(1)改为暗色模式(可选)
找到App.axaml,将 <Application>
元素中的 RequestedThemeVariant
属性从 Default
更改为 Dark
;
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia_MusicStore.App"
xmlns:local="using:Avalonia_MusicStore"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
...
(2)改为亚克力模糊
找到 /Views/MainWindow.axaml 文件,在<Window>中添加新属性TransparencyLevelHint="AcrylicBlur"
Background="Transparent"
;效果如下:
(3)将毛玻璃效果拓展到标题栏
在 /Views/MainWindow.axaml 文件的<Window>中添加属性ExtendClientAreaToDecorationsHint="True"
即可;效果如下:
3、实现专辑列表
(1)主页跳转按钮样式
实现专辑列表前,要现在窗体主页中添加一个跳转到‘专辑列表’的按钮,样式如下:
代码如下:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Avalonia_MusicStore.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia_MusicStore.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="Avalonia_音乐商店"
TransparencyLevelHint="AcrylicBlur" Background="Transparent"
ExtendClientAreaToDecorationsHint="True">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<Panel Margin="40">
<Button Content="BuyMusic"
HorizontalAlignment="Right" VerticalAlignment="Top" >
<PathIcon Data="{StaticResource store_microsoft_regular}" />
</Button>
</Panel>
</Window>
(2)跳转按钮图标
Microsoft Store 图标取自:https://avaloniaui.github.io/icons.html;代码为
<StreamGeometry x:Key="store_microsoft_regular">M11.5 9.5V13H8V9.5H11.5Z M11.5 17.5V14H8V17.5H11.5Z M16 9.5V13H12.5V9.5H16Z M16 17.5V14H12.5V17.5H16Z M8 6V3.75C8 2.7835 8.7835 2 9.75 2H14.25C15.2165 2 16 2.7835 16 3.75V6H21.25C21.6642 6 22 6.33579 22 6.75V18.25C22 19.7688 20.7688 21 19.25 21H4.75C3.23122 21 2 19.7688 2 18.25V6.75C2 6.33579 2.33579 6 2.75 6H8ZM9.5 3.75V6H14.5V3.75C14.5 3.61193 14.3881 3.5 14.25 3.5H9.75C9.61193 3.5 9.5 3.61193 9.5 3.75ZM3.5 18.25C3.5 18.9404 4.05964 19.5 4.75 19.5H19.25C19.9404 19.5 20.5 18.9404 20.5 18.25V7.5H3.5V18.25Z</StreamGeometry>
a. 创建图标样式文件
工程->新建类->创建AvaloniaStyle文件,名称为IconsStyles.axaml
,代码如下:
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
<Style>
<Style.Resources>
<StreamGeometry x:Key="store_microsoft_regular">M11.5 9.5V13H8V9.5H11.5Z M11.5 17.5V14H8V17.5H11.5Z M16 9.5V13H12.5V9.5H16Z M16 17.5V14H12.5V17.5H16Z M8 6V3.75C8 2.7835 8.7835 2 9.75 2H14.25C15.2165 2 16 2.7835 16 3.75V6H21.25C21.6642 6 22 6.33579 22 6.75V18.25C22 19.7688 20.7688 21 19.25 21H4.75C3.23122 21 2 19.7688 2 18.25V6.75C2 6.33579 2.33579 6 2.75 6H8ZM9.5 3.75V6H14.5V3.75C14.5 3.61193 14.3881 3.5 14.25 3.5H9.75C9.61193 3.5 9.5 3.61193 9.5 3.75ZM3.5 18.25C3.5 18.9404 4.05964 19.5 4.75 19.5H19.25C19.9404 19.5 20.5 18.9404 20.5 18.25V7.5H3.5V18.25Z</StreamGeometry>
</Style.Resources>
</Style>
</Styles>
b. 将图标资源添加到工程资源中
打开 App.axaml 文件,添加一个 <StyleInclude>
元素,如下所示:
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia_MusicStore/IconsStyles.axaml" />
</Application.Styles>
(3)按钮事件(MVVM;CommunityToolkit.Mvvm)
这里使用CommunityToolkit.Mvvm+依赖注入,官方示例使用的ReactiveUI。
a. 搜索App.cs
,添加依赖注入的方法,先阶段完整代码如下:
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia_MusicStore.ViewModels;
using Avalonia_MusicStore.Views;
using Microsoft.Extensions.DependencyInjection; // .NET Core内置依赖注入模块。
using CommunityToolkit.Mvvm.DependencyInjection; // mvvm框架的内置的依赖注入模块。
using Avalonia_MusicStore.Models;
namespace Avalonia_MusicStore
{
public partial class App : Application
{
#region
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; set; }
#endregion
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
Services = ServiceCollection();
// Line below is needed to remove Avalonia data validation. Without this line you will get duplicate validations from both Avalonia and CT
BindingPlugins.DataValidators.RemoveAt(0);
desktop.MainWindow = Services.GetService<MainWindowView>();
{
DataContext = Services.GetService<MainWindowViewModel>();
};
}
base.OnFrameworkInitializationCompleted();
}
/// <summary>
/// 注册服务
/// </summary>
/// <returns></returns>
private static IServiceProvider ServiceCollection()
{
var service = new ServiceCollection();
//service.AddSingleton<Album>();
service.AddSingleton<MainWindowViewModel>();
//service.AddSingleton<MusicStoreWindowViewModel>();
//service.AddSingleton<MusicStoreViewModel>();
//service.AddSingleton<AlbumViewModel>();
service.AddSingleton<MainWindowView>();
//service.AddSingleton<MusicStoreWindowView>();
return service.BuildServiceProvider();
}
}
}
b. 搜索MainWindowViewModel.cs
,添加BuyMusicCommand方法
,完整代码如下:
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Avalonia_MusicStore.ViewModels
{
public partial class MainWindowViewModel : ViewModelBase
{
/// <summary>
/// 购买音乐 按钮
/// </summary>
/// <returns></returns>
[RelayCommand]
public void BuyMusic()
{
Debug.WriteLine("购买音乐");
MusicStoreWindowViewModel musicStoreWindowVM = App.Current.Services.GetService<MusicStoreWindowViewModel>();
//musicStoreWindowVM.Show();
MainWindowView mainWindow = App.Current.Services.GetService<MainWindowView>();
musicStoreWindowVM.ShowDialog(mainWindow);
}
}
}
c. 在MainWindow.axaml
中的Button按钮中添加属性Command="{Binding BuyMusicCommand}"
;代码如下:
<Panel Margin="40">
<Button Content="BuyMusic"
HorizontalAlignment="Right" VerticalAlignment="Top" Command="{Binding BuyMusicCommand}">
<PathIcon Data="{StaticResource store_microsoft_regular}" />
</Button>
</Panel>
4、音乐商店对话框(MusicStoreWindow)
(1)添加窗体
- 在解决方案资源管理器中,右键单击 /Views 文件夹;
- 点击 添加 Avalonia Window;
- 输入名称 'MusicStoreWindow';
- 添加 亚克力模糊背景,并将其延伸到标题栏(与之前相同);
- 在 /ViewModels 文件夹添加 MusicStoreViewModel.cs,代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia_MusicStore.ViewModels
{
/// <summary>
/// 音乐商店窗体ViewModel
/// </summary>
public partial class MusicStoreViewModel : ViewModelBase
{
}
}
(2)搜索框(MusicStoreView.axaml)
这是一个UserControl,放在‘音乐商店对话框(MusicStoreWindow)’中,界面效果(Dark主题)如下:
a、添加窗体
- 在解决方案资源管理器中,右键单击 /Views 文件夹;
- 点击 添加 Avalonia UserControl;
- 输入 名称 MusicStoreView;
- 在 /ViewModels 文件夹添加 MusicStoreViewModel.cs,代码如下:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Avalonia_MusicStore.ViewModels"
x:DataType="vm:MusicStoreViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia_MusicStore.Views.MusicStoreView">
<!--<Design.DataContext>
<vm:MusicStoreViewModel/>
</Design.DataContext>-->
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<TextBox Text="{Binding SearchText}" Watermark="搜索音乐专辑....">
<!-- 再内部添加一个按钮 -->
<TextBox.InnerRightContent>
<Button Content="搜索" Command="{Binding SearchTextBtnCommand}"/>
</TextBox.InnerRightContent>
<!-- 绑定回车键 -->
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SearchTextBtnCommand}" />
</TextBox.KeyBindings>
</TextBox>
<ProgressBar IsVisible="{Binding IsBusy}" IsIndeterminate="True" />
</StackPanel>
<Button Content="购买专辑"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center" Command="{Binding BuyAlbumBtnCommand}" />
<ListBox ItemsSource="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" Background="Transparent" Margin="0 20">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</DockPanel>
</UserControl>
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Album = Avalonia_MusicStore.Models.Album;
namespace Avalonia_MusicStore.ViewModels
{
/// <summary>
/// 音乐商店窗体ViewModel
/// </summary>
public partial class MusicStoreViewModel : ViewModelBase
{
private readonly MainWindowViewModel _MainWindowVM;
private readonly Album _Album;
#region 控件 Binding
/// <summary>
/// 购买音乐专辑 按钮展示字
/// </summary>
[ObservableProperty]
private string _btnText = "购买音乐专辑";
/// <summary>
/// 搜索条件
/// </summary>
[ObservableProperty]
private string _searchText = string.Empty;
/// <summary>
/// 搜索是否繁忙
/// </summary>
[ObservableProperty]
private bool _isBusy = false;
/// <summary>
/// 被选中的专辑信息
/// </summary>
[ObservableProperty]
private AlbumViewModel? _selectedAlbum;
/// <summary>
/// 专辑列表
/// </summary>
public ObservableCollection<AlbumViewModel> SearchResults { get; } = new ObservableCollection<AlbumViewModel>();
#endregion 控件 Binding
public MusicStoreViewModel(MainWindowViewModel mainWindowVM,Album album)
{
_MainWindowVM = mainWindowVM;
_Album = album;
}
#region 事件Binding
/// <summary>
/// 搜索按钮
/// </summary>
[RelayCommand]
// SearchTextEnter()
public void SearchTextBtn()
{
try
{
Debug.WriteLine("搜索音乐专辑");
DoSearch(SearchText);
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
/// <summary>
/// 购买按钮
/// </summary>
[RelayCommand]
public void BuyAlbumBtn()
{
try
{
Debug.WriteLine("购买音乐专辑");
if (_selectedAlbum != null)
{
_MainWindowVM.Albums.Add(_selectedAlbum);
// 保存或更新 _MainWindowVM.Albums 数据文件
//List<Album> albums=_MainWindowVM.Albums.Select(x=>new Album()
//{
// Artist= x.Artist,
// Title=x.Title,
//}).ToList();
//_MainWindowVM.SaveAlbums(albums);
// 保存 专辑封面文件
//_MainWindowVM.SaveAlbumPic(_selectedAlbum);
}
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
#endregion 事件Binding
/// <summary>
/// 搜索音乐专辑
/// </summary>
/// <param name="s"></param>
private async void DoSearch(string name)
{
IsBusy = true;
SearchResults.Clear();
if (!string.IsNullOrWhiteSpace(name))
{
var albums = await _Album.SearchAsync(name);
foreach (var album in albums)
{
var albumVM = new AlbumViewModel(album);
SearchResults.Add(albumVM);
}
}
IsBusy = false;
}
}
}
(3)专辑搜索类(Album.cs)
这里使用的是Neget包 iTunesSearch,主要获取专辑的标题、艺术家、封面信息;完整代码如下:
using iTunesSearch.Library;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia_MusicStore.Models
{
/// <summary>
/// 专辑信息
/// </summary>
public class Album
{
#region 私有变量
/// <summary>
/// 提供者
/// </summary>
private static iTunesSearchManager s_SearchManager = new();
/// <summary>
/// HttpClient
/// </summary>
private static HttpClient s_httpClient = new();
/// <summary>
/// 图片缓存路径
/// </summary>
public string CachePath => $"./Cache/{Artist}_{Title}";
#endregion 私有变量
#region 公有变量
/// <summary>
/// 艺术家
/// </summary>
public string Artist { get; set; }=string.Empty;
/// <summary>
/// 专辑名
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 封面链接地址
/// </summary>
public string CoverUrl { get; set; } = string.Empty;
#endregion 公有变量
/// <summary>
/// 搜索
/// </summary>
/// <param name="searchTerm"></param>
/// <returns></returns>
public async Task<IEnumerable<Album>> SearchAsync(string searchTerm)
{
var query = await s_SearchManager.GetAlbumsAsync(searchTerm).ConfigureAwait(false);
return query.Albums.Select(x =>
new Album()
{
Artist = x.ArtistName,
Title = x.CollectionName, // .Replace("-","_").Replace(" ","")
CoverUrl = x.ArtworkUrl100.Replace("100x100bb", "600x600bb"),
});
}
/// <summary>
/// 获取图片
/// </summary>
/// <returns></returns>
public async Task<Stream> LoadCoverBitmapAsync()
{
if (File.Exists(CachePath + ".bmp"))
{
return File.OpenRead(CachePath + ".bmp");
}
else
{
var data = await s_httpClient.GetByteArrayAsync(CoverUrl);
return new MemoryStream(data);
}
}
}
}
(4)专辑列表(AlbumView.axaml + ViewLocator技术)
这里使用的 视图定位器ViewLocator 技术,功能概况就是先注册一个模版(在App.axaml中注册),然后在MusicStoreView.axaml中的ListBox控件上绑定List<ViewModel>时程序会自动找到对应的View,替换ListBox控件中的模版区域(<ItemsPanelTemplate>)。
a、App.axaml添加对 ViewLocator的引用
<Application ...
>
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
...
b、AlbumView.axaml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Avalonia_MusicStore.ViewModels"
x:DataType="vm:MusicStoreViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia_MusicStore.Views.MusicStoreView">
<!--<Design.DataContext>
<vm:MusicStoreViewModel/>
</Design.DataContext>-->
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<TextBox Text="{Binding SearchText}" Watermark="搜索音乐专辑....">
<!-- 再内部添加一个按钮 -->
<TextBox.InnerRightContent>
<Button Content="搜索" Command="{Binding SearchTextBtnCommand}"/>
</TextBox.InnerRightContent>
<!-- 绑定回车键 -->
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SearchTextBtnCommand}" />
</TextBox.KeyBindings>
</TextBox>
<ProgressBar IsVisible="{Binding IsBusy}" IsIndeterminate="True" />
</StackPanel>
<Button Content="购买专辑"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center" Command="{Binding BuyAlbumBtnCommand}" />
<ListBox ItemsSource="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" Background="Transparent" Margin="0 20">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</DockPanel>
</UserControl>
c、AlbumViewModel.cs
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia_MusicStore.ViewModels
{
/// <summary>
/// 专辑信息VModel
/// </summary>
public partial class AlbumViewModel : ViewModelBase
{
#region 成员
/// <summary>
/// 艺术家
/// </summary>
[ObservableProperty]
private string artist;
/// <summary>
/// 专辑名
/// </summary>
[ObservableProperty]
private string title;
/// <summary>
/// 专辑封面
/// </summary>
[ObservableProperty]
private Bitmap? _cover = null;
#endregion 成员
public AlbumViewModel() { }
// DI Album
public AlbumViewModel(Models.Album album)
{
Artist = album.Artist;
Title = album.Title;
Task.Run(() =>
{
using (var imageStream = album.LoadCoverBitmapAsync().Result)
{
Cover = Bitmap.DecodeToWidth(imageStream, 400);
}
});
}
}
}
(5)对话框返回数据
这里对官方代码进行了修改,如上面(3)中代码所看的 MusicStoreViewModel 中的‘购买按钮’事件写法如下;即直接直接DI MainWindowViewModel,操作其中的Albums。
private readonly MainWindowViewModel _MainWindowVM;
private readonly Album _Album;
public MusicStoreViewModel(MainWindowViewModel mainWindowVM,Album album)
{
_MainWindowVM = mainWindowVM;
_Album = album;
}
/// <summary>
/// 购买按钮
/// </summary>
[RelayCommand]
public void BuyAlbumBtn()
{
try
{
Debug.WriteLine("购买音乐专辑");
if (_selectedAlbum != null)
{
_MainWindowVM.Albums.Add(_selectedAlbum);
// 保存或更新 _MainWindowVM.Albums 数据文件
//List<Album> albums=_MainWindowVM.Albums.Select(x=>new Album()
//{
// Artist= x.Artist,
// Title=x.Title,
//}).ToList();
//_MainWindowVM.SaveAlbums(albums);
// 保存 专辑封面文件
//_MainWindowVM.SaveAlbumPic(_selectedAlbum);
}
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
5、保存用户数据
我将保存数据的逻辑放到了‘购买按钮’中,BuyAlbumBtn代码修改如下:
/// <summary>
/// 购买按钮
/// </summary>
[RelayCommand]
public void BuyAlbumBtn()
{
try
{
Debug.WriteLine("购买音乐专辑");
if (_selectedAlbum != null)
{
_MainWindowVM.Albums.Add(_selectedAlbum);
// 保存或更新 _MainWindowVM.Albums 数据文件
List<Album> albums=_MainWindowVM.Albums.Select(x=>new Album()
{
Artist= x.Artist,
Title=x.Title,
}).ToList();
_MainWindowVM.SaveAlbums(albums);
// 保存 专辑封面文件
_MainWindowVM.SaveAlbumPic(_selectedAlbum);
}
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
(1)保存专辑数据(SaveAlbums)
/// <summary>
/// 保存专辑列表文件
/// </summary>
/// <param name="albumVMs">专辑列表</param>
public void SaveAlbums(List<Album> albumVMs)
{
string jsonStr = JsonConvert.SerializeObject(albumVMs);
File.WriteAllText(jsonFilePath, jsonStr,System.Text.Encoding.UTF8);
}
(2)保存专辑封面图(SaveAlbumPic)
/// <summary>
/// 保存专辑封面
/// </summary>
/// <param name="albumVM">专辑</param>
public void SaveAlbumPic(AlbumViewModel albumVM)
{
if (!Directory.Exists($"./Cache"))
{
Directory.CreateDirectory(dirStr);
}
string filePath = dirStr + $"/{albumVM.Artist}_{albumVM.Title}" + ".bmp";
if (albumVM != null && albumVM.Cover != null)
{
albumVM.Cover.Save(filePath);
}
}
6、软件启动时加载数据
可将加载数据的方法放到主窗体VM的构造函数中,详细代码如下:
public MainWindowViewModel()
{
LoadAlbums();
}
/// <summary>
/// 加载专辑数据
/// </summary>
public async void LoadAlbums()
{
try
{
if (File.Exists(jsonFilePath) && Directory.Exists(dirStr))
{
Albums.Clear();
string json = File.ReadAllText(jsonFilePath);
List<Album> albums = JsonConvert.DeserializeObject<List<Album>>(json);
foreach (Album album in albums)
{
Albums.Add(new AlbumViewModel()
{
Artist = album.Artist,
Title = album.Title,
Cover = new Avalonia.Media.Imaging.Bitmap(album.CachePath + ".bmp"),
});
}
}
}
catch (Exception ex)
{
}
}
7、效果
二、其他案例
其他案例见:https://github.com/AvaloniaUI/AvaloniaUI.QuickGuides
本文来自博客园,作者:꧁执笔小白꧂,转载请注明原文链接:https://www.cnblogs.com/qq2806933146xiaobai/p/18321002