原文链接:Avalonia 学习笔记04. Page Navigation(页面导航) - simonoct - 博客园
本节课的目标是实现应用内的页面切换功能。我们将创建一个核心的 ViewLocator 类,它能根据当前需要显示的 ViewModel 自动查找并加载对应的 View。同时,我们会为侧边栏的按钮添加命令绑定,实现点击按钮切换页面的功能,并为当前选中的页面按钮添加高亮样式,以提供清晰的视觉反馈。
4.1 ViewLocator.cs
在项目根目录下新建一个 ViewLocator.cs 文件。这个类是本节课实现页面导航的核心。
它的作用是充当一个“视图定位器”。当你告诉应用“显示这个 ViewModel”时,ViewLocator 会自动找到并实例化与之对应的 View,并将两者关联起来。
这遵循了 MVVM 模式中一个重要的思想:“约定优于配置”(Convention over Configuration)。我们只需要遵循 HomePageViewModel / HomePageView 这样的命名约定,ViewLocator 就能自动完成工作,而无需我们手动编写大量的 if-else 或 switch 语句来指定哪个 ViewModel 对应哪个 View。
using System; using Avalonia.Controls; using Avalonia.Controls.Templates; using AvaloniaApplication2.ViewModels; // 确保 using 了 ViewModels 命名空间 // using AvaloniaApplication2.Views; // 这个 using 在当前代码中不是必需的,但保留也无妨namespace AvaloniaApplication2;// IDataTemplate 是一个接口,它定义了一种根据数据(Data)创建 UI 元素(控件)的规范。 // 我们的 ViewLocator 实现了这个接口,意味着它能将一个数据对象(在这里是 ViewModel)转换成一个视图控件(View)。 public class ViewLocator : IDataTemplate {// Build 方法是 IDataTemplate 接口的核心。// 当 Match 方法返回 true 时,Avalonia 框架会调用 Build 方法,// 并将数据对象(data)传递进来,期望返回一个可以显示的控件(Control)。public Control? Build(object? data){// 如果传入的数据是 null,直接返回 null,不做任何处理。if (data is null)return null;// 这是 ViewLocator 的核心魔法:// 1. data.GetType().FullName! 获取 ViewModel 的完整类名,例如 "AvaloniaApplication2.ViewModels.HomePageViewModel"。// 2. .Replace("ViewModel", "View", ...) 将类名中的 "ViewModel" 替换为 "View"。// 结果就变成了 "AvaloniaApplication2.Views.HomePageView"。// 这就是我们的“约定”:View 和 ViewModel 的命名必须遵循这个模式。var viewName = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.InvariantCulture);// 使用 C# 的反射(Reflection)功能,根据上面生成的字符串类名,查找对应的实际类型(Type)。var type = Type.GetType(viewName);// 如果没有找到对应的 View 类型(可能你忘了创建 View 文件或者命名不匹配),就返回 null。// 在视频中,作者返回了一个 TextBlock 来显示错误,返回 null 也是一种处理方式。if (type is null)return null;// 如果找到了类型,就使用 Activator.CreateInstance(type) 创建该 View 的一个新实例。// 这行代码的效果等同于 new HomePageView(),但是它是动态执行的。var control = (Control)Activator.CreateInstance(type);// 这是非常关键的一步:将新创建的 View 的 DataContext(数据上下文)设置为传入的 ViewModel (data)。// 这样,View 和 ViewModel 就被绑定在了一起,View 内部的 {Binding ...} 才能正确地找到 ViewModel 中的属性。control.DataContext = data;// 返回创建并设置好数据上下文的 View 控件。return control;}// Match 方法用于判断此 DataTemplate 是否适用于给定的数据(data)。// 在这里,我们判断传入的数据是否是一个 ViewModelBase 或其派生类的实例。// 如果是,就返回 true,告诉 Avalonia:“这个数据我能处理,请调用我的 Build 方法吧!”public bool Match(object? data) => data is ViewModelBase; }
4.2 ViewModels\MainViewModel.cs
修改 MainViewModel.cs,为它添加页面状态管理和导航的逻辑。
using Avalonia.Svg.Skia; // using AvaloniaApplication2.Views; // 这个 using 在 ViewModel 中通常是不需要的,因为 ViewModel 不应该直接了解 View。 // 这是 MVVM 模式的一个核心原则:ViewModel 负责提供数据和逻辑,它不应该“知道”任何关于 View(视图/UI)的具体实现细节。 // View 和 ViewModel 之间的解耦是由 ViewLocator 和数据绑定机制来完成的。 using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input;namespace AvaloniaApplication2.ViewModels;public partial class MainViewModel : ViewModelBase {// 这个常量在视频中被定义,但后来切换到了另一种实现方式,所以它没有被使用。// 但后来采用了 Avalonia 更优雅的 `Classes.active="{Binding ...}"` 绑定布尔值的方式,// 我们可以安全地忽略或删除它。private const string buttonActiveClass = "active";[ObservableProperty]private bool _sideMenuExpanded = true;// 定义一个属性来持有当前正在显示的页面的 ViewModel。 [ObservableProperty]// 这是 MVVM Toolkit 的一个强大功能。它告诉编译器:// 当 _currentPage 属性发生变化时,也需要发出 HomePageIsActive 和 ProcessPageIsActive 属性已更改的通知。// 这样,UI 就会自动更新绑定到这几个属性的任何元素。 [NotifyPropertyChangedFor(nameof(HomePageIsActive))][NotifyPropertyChangedFor(nameof(ProcessPageIsActive))][NotifyPropertyChangedFor(nameof(ActionsPageIsActive))][NotifyPropertyChangedFor(nameof(MacrosPageIsActive))][NotifyPropertyChangedFor(nameof(ReporterPageIsActive))][NotifyPropertyChangedFor(nameof(HistoryPageIsActive))]private ViewModelBase _currentPage;// 这几个是只读的计算属性,用于判断当前页面是否是主页或流程页。// UI 上的按钮会绑定到这些属性,以决定是否应用 "active" 样式。// => 是 "lambda" 表达式的简写,表示这个属性的值是通过后面的表达式计算得出的。public bool HomePageIsActive => CurrentPage == _homePage;public bool ProcessPageIsActive => CurrentPage == _processPage;public bool ActionsPageIsActive => CurrentPage == _actionsPage;public bool MacrosPageIsActive => CurrentPage == _macrosPage;public bool ReporterPageIsActive => CurrentPage == _reporterPage;public bool HistoryPageIsActive => CurrentPage == _historyPage;// 为每个页面创建一个私有的、只读的 ViewModel 实例。// 在应用的生命周期内,我们只使用这几个实例,而不是每次切换页面都创建新的。private readonly HomePageViewModel _homePage = new HomePageViewModel();private readonly ProcessPageViewModel _processPage = new ProcessPageViewModel();private readonly ActionsPageViewModel _actionsPage = new ActionsPageViewModel();private readonly MacrosPageViewModel _macrosPage = new MacrosPageViewModel();private readonly ReporterPageViewModel _reporterPage = new ReporterPageViewModel();private readonly HistoryPageViewModel _historyPage = new HistoryPageViewModel();// MainViewModel 的构造函数。// 当 MainViewModel 被创建时(通常是应用启动时),这个方法会被调用。public MainViewModel(){// 在这里设置应用的默认显示页面。视频中设置的是 ProcessPage。CurrentPage = _homePage;}[RelayCommand]private void SideMenuResize(){SideMenuExpanded = !SideMenuExpanded;}// 定义一个命令,用于导航到主页。 [RelayCommand]private void GoToHome(){// 当命令被执行时(例如,点击了主页按钮),将当前页面设置为 _homePage 实例。// 因为 CurrentPage 属性的 set 访问器会触发属性变更通知,UI 会自动更新。CurrentPage = _homePage;}// 定义一个命令,用于导航到流程页。 [RelayCommand]private void GoToProcess(){CurrentPage = _processPage;}[RelayCommand]private void GoToMacros(){CurrentPage = _macrosPage;}[RelayCommand]private void GoToActions(){CurrentPage = _actionsPage;}[RelayCommand]private void GoToReporter(){CurrentPage = _reporterPage;}[RelayCommand]private void GoToHistory(){CurrentPage = _historyPage;} }
4.3 [ObservableProperty]、[RelayCommand]和[NotifyPropertyChangedFor]
突然发现第三章的MVVM又记得不太清晰,还是在这里重复加强记忆。
CommunityToolkit.Mvvm (也常被称为 MVVM Toolkit) 库,这个库的核心功能之一就是利用 C# 的 Source Generator (源代码生成器) 技术,来自动写那些繁琐又重复的代码。
写一个简单的“指令”,编译器就会在后台帮你生成完整的、符合 MVVM 规范的代码。
4.3.1 [ObservableProperty]
属性的“自动生成器”,这个是最基础,也是最常用的。
[ObservableProperty] private bool _sideMenuExpanded;
你只写了一行私有字段_sideMenuExpanded
。当你给它贴上[ObservableProperty]
这个标签后,MVVM Toolkit 在编译时会自动在后台为你生成一个完整的、公开的、带有通知功能的属性 SideMenuExpanded
。
它在后台帮你生成的代码,大致是这个样子:
public bool SideMenuExpanded {get => _sideMenuExpanded;set{// SetProperty 是一个核心方法,它会做两件事:// 1. 检查新传入的 value 和旧的值 _sideMenuExpanded 是否真的不同。// 2. 如果真的不同,它会更新 _sideMenuExpanded 的值,然后发出一个“通知”,告诉UI:“嘿,SideMenuExpanded这个属性的值变了,所有绑定了它的地方都快来更新一下!”SetProperty(ref _sideMenuExpanded, value);} }
[ObservableProperty] 帮你把一个简单的私有字段,包装成一个能和UI顺畅沟通的公开属性,让你不用每次都手动去写 get、set 和那一长串的通知逻辑。
- 必须用
[ObservableProperty]
标记字段(带_
的变量) - 自动生成的属性名 = 去掉下划线 + 首字母大写(
_sideMenu
→SideMenu
) - 自动实现
INotifyPropertyChanged
,修改属性值会触发UI更新
4.3.2 [RelayCommand]
方法的“命令转换器”,在MVVM模式里,界面上的按钮点击不能直接调用ViewModel里的一个方法,而是需要通过一个叫“命令(Command)”的东西来做中间人。[RelayCommand]
就是帮你创建这个中间人的工具。
[RelayCommand] private void GoToProcess() {CurrentPage = _processPage; }
上面代码只写了一个普通、私有的方法 GoToProcess。当你给它贴上[RelayCommand]
这个标签后,MVVM Toolkit 会自动在后台为你创建一个公开的、符合WPF/Avalonia绑定规范的命令属性。这个新属性的名字默认是在你的方法名后面加上 Command。
它在后台帮你生成的代码,大致是这个样子:
// 它创建了一个公开的、只读的命令属性 public IRelayCommand GoToProcessCommand { get; }// 同时,它在构造函数里初始化了这个命令, // 告诉这个命令:“当UI执行你的时候,你就去调用那个私有的 GoToProcess 方法。” // new RelayCommand(GoToProcess);
[RelayCommand] 帮你把一个普通的业务逻辑方法,包装成一个可以被XAML里按钮的 Command="{Binding ...}" 语法所识别和绑定的“命令对象”。
-
方法必须标记
[RelayCommand]
-
生成的命令名 = 方法名 +
Command
-
在XAML中绑定时要使用
<Button Command="{Binding GoToProcessCommand}"/>
4.3.3 [NotifyPropertyChangedFor]
属性之间的“关联通知器”,在上面的代码里,有好几个用于判断按钮是否高亮的属性,比如:public bool HomePageIsActive => CurrentPage == _homePage;
这个 HomePageIsActive
属性的值,完全依赖于 CurrentPage
属性。当 CurrentPage
改变时,HomePageIsActive
的值也应该随之改变。
但是,计算机没那么智能。 当你执行CurrentPage = _homePage;
这句代码时,系统只知道CurrentPage
变了,它会去通知UI更新绑定了CurrentPage
的地方(比如那个 ContentControl
)。但它不知道 HomePageIsActive
、ProcessPageIsActive
这些依赖它的属性也需要更新。所以,按钮的高亮状态不会自动变化。
[NotifyPropertyChangedFor] 的作用:
它就像一个“信使”或者“传话筒”。你把它贴在“源头”属性上,告诉它:“当你自己变化的时候,请顺便帮我通知一下其他几个相关的属性也变化了。”
[ObservableProperty] // 当 CurrentPage 变化时,请顺便通知 HomePageIsActive 也变了 [NotifyPropertyChangedFor(nameof(HomePageIsActive))] // 当 CurrentPage 变化时,也请通知 ProcessPageIsActive 也变了 [NotifyPropertyChangedFor(nameof(ProcessPageIsActive))] // ... 其他按钮的IsActive属性也一样 private ViewModelBase _currentPage;
它的工作流程:
- 你点击了“Process”按钮,触发
GoToProcessCommand
命令。 - 命令执行
GoToProcess()
方法,这句代码CurrentPage = _processPage;
被调用。 [ObservableProperty]
的底层机制检测到_currentPage
的值变了,于是它准备发出CurrentPage
属性已更改的通知。- 在发出通知前,它看到了你贴的
[NotifyPropertyChangedFor]
标签。 - 于是,它不仅发出了
CurrentPage
的通知,还一并发出了HomePageIsActive
、ProcessPageIsActive
等所有你在标签里指定的属性的“已更改”通知。 - UI收到了这些通知,于是它去重新获取
HomePageIsActive
的值(此时是false),也去获取ProcessPageIsActive
的值(此时是true),然后正确地更新了所有按钮的高亮样式。
[NotifyPropertyChangedFor]
解决了一个属性的变化如何触发其他依赖它的属性进行UI更新的问题,它在多个属性之间建立了一条“通知链”。
4.3.4 三者的关系图示:
[ObservableProperty] private bool _sideMenuExpanded; │ └─► 自动生成公共属性 SideMenuExpanded│└─► 当值变化时自动通知UI[RelayCommand] private void GoToProcess() { ... } │ └─► 自动生成 GoToProcessCommand[NotifyPropertyChangedFor] │ └─► 当前属性变化时,强制刷新其他依赖属性
4.4 ViewModels\ProcessPageViewModel.cs和ViewModels\HomePageViewModel.cs
新建ProcessPageViewModel.cs、HomePageViewModel.cs。
其他的ActionsPageViewModel.cs、HistoryPageViewModel.cs、MacrosPageViewModel.cs、ReporterPageViewModel.cs、SettingsPageViewModel.cs则仿造下面两个自行创建,由于内容雷同就不重复展示了。
ProcessPageViewModel.cs
namespace AvaloniaApplication2.ViewModels;public partial class ProcessPageViewModel : ViewModelBase {// 定义一个简单的字符串属性,用于在页面上显示,以验证数据绑定是否成功。public string Test { get; set; } = "Process"; }
HomePageViewModel.cs
namespace AvaloniaApplication2.ViewModels;public partial class HomePageViewModel : ViewModelBase {public string Test { get; set; } = "Home"; }
4.5 Views\MainView.axaml
在项目下新建一个 Views 文件夹,然后把 MainView.axaml 移动到 Views 内。修改其 XAML 代码以支持页面导航。
<Window 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"mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="600"Width="1024" Height="600"x:Class="AvaloniaApplication2.MainView"xmlns:vm="clr-namespace:AvaloniaApplication2.ViewModels"xmlns:view="clr-namespace:AvaloniaApplication2.Views"x:DataType="vm:MainViewModel"Title="AvaloniaApplication2"><Design.DataContext><vm:MainViewModel></vm:MainViewModel></Design.DataContext><Grid Background="{DynamicResource PrimaryBackground}" ColumnDefinitions="Auto, *"><!-- 这是显示页面的关键控件。 --><!-- ContentControl 是一个占位符,可以显示任何内容。 --><!-- 我们将其 Content 属性绑定到 ViewModel 中的 CurrentPage 属性。 --><!-- 当 CurrentPage 的值是一个 ViewModel 实例时,我们注册的 ViewLocator 就会介入, --><!-- 找到对应的 View,并将其显示在这里。 --><ContentControl Grid.Column="1" Content="{Binding CurrentPage}" /><Border Padding="20" Background="{DynamicResource PrimaryBackgroundGradient}"><Grid RowDefinitions="*, Auto"><StackPanel Spacing="12"><Image PointerPressed="InputElement_OnPointerPressed" Source="{SvgImage /Assets/Images/logo.svg}" Width="220" IsVisible="{Binding SideMenuExpanded}"></Image><Image PointerPressed="InputElement_OnPointerPressed" Source="{SvgImage /Assets/Images/icon.svg}" Width="22" IsVisible="{Binding !SideMenuExpanded}"></Image><!-- 主页按钮 --><!-- Command="{Binding GoToHomeCommand}" 将按钮的点击操作绑定到 ViewModel 中的 GoToHomeCommand。 --><!-- MVVM Toolkit 会自动将 GoToHome 方法生成为 GoToHomeCommand。 --><!-- Classes.active="{Binding HomePageIsActive}" 是 Avalonia 的一个特性。 --><!-- 当 HomePageIsActive 属性为 true 时,此按钮会获得一个名为 "active" 的样式类。 --><!-- Classes.active="{Binding HomePageIsActive}" 是 Avalonia 的一个强大特性,非常类似于网页开发中的 CSS 类绑定。 --><!-- 当 ViewModel 中的 HomePageIsActive 属性为 true 时,此按钮会自动获得一个名为 "active" 的样式类。 --><!-- 当它变为 false 时,这个类会被自动移除。我们可以在样式文件中定义 .active 类的外观。 --><Button HorizontalAlignment="Stretch" Classes.active="{Binding HomePageIsActive}" Command="{Binding GoToHomeCommand}"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Home" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch" Classes.active="{Binding ProcessPageIsActive}" Command="{Binding GoToProcessCommand}"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Process" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch" Classes.active="{Binding ActionsPageIsActive}" Command="{Binding GoToActionsCommand}"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Actions" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch" Classes.active="{Binding MacrosPageIsActive}" Command="{Binding GoToMacrosCommand}"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Macros" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch" Classes.active="{Binding ReporterPageIsActive}" Command="{Binding GoToReporterCommand}"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Reporter" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch" Classes.active="{Binding HistoryPageIsActive}" Command="{Binding GoToHistoryCommand}"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="History" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button></StackPanel><Button Classes="transparent" Grid.Row="1"><Label Classes="icon-only" Content=""></Label></Button></Grid></Border></Grid></Window>
4.6 Views\HomePageView.axaml和Views\ProcessPageView.axaml
在Rider里,add里面选择Avalonia User Control功能新建HomePageView.axaml、ProcessPageView.axaml。这些是构成页面的用户控件。
其他的ActionsPageView.axaml、HistoryPageView.axaml、MacrosPageView.axaml、ReporterPageView.axaml、SettingsPageView.axaml则仿造下面两个自行创建,由于内容雷同就不重复展示了。
HomePageView.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"mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"x:Class="AvaloniaApplication2.Views.HomePageView"><!-- 为了在设计器中获得更好的预览体验和编译时类型检查, --><!-- 建议像 ProcessPageView 一样,也为 HomePageView 添加 x:DataType 和 Design.DataContext。 --><!-- 不过这里为了和视频教程的代码保持一致,方便接下来的学习,就不修改了。 --><!-- 为了演示,这里只放了一段纯文本。 --><!-- 之后可以像 ProcessPageView 一样添加绑定和更复杂的布局。 -->Welcome to HomePage! </UserControl>
ProcessPageView.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"mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"Foreground="White"xmlns:vm="clr-namespace:AvaloniaApplication2.ViewModels"x:DataType="vm:ProcessPageViewModel"x:Class="AvaloniaApplication2.Views.ProcessPageView"><!-- Design.DataContext 用于在设计器(预览窗口)中提供一个数据样本, --><!-- 这样预览器就能正确显示绑定的数据。它在程序运行时不起作用。 --><Design.DataContext><vm:ProcessPageViewModel></vm:ProcessPageViewModel></Design.DataContext><!-- 将 Label 的内容绑定到 ViewModel 的 Test 属性。 --><!-- 因为 ViewLocator 已经将此 View 的 DataContext 设置为了 ProcessPageViewModel 的实例, --><!-- 所以这里的绑定能够成功。 --><Label Content="{Binding Test}"></Label> </UserControl>
4.7 App.axaml
修改App.axaml,在整个应用程序层面注册我们的 ViewLocator 并添加高亮样式所需的颜色资源。
<Application xmlns="https://github.com/avaloniaui"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"x:Class="AvaloniaApplication2.App"xmlns:local="clr-namespace:AvaloniaApplication2"RequestedThemeVariant="Default"><!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --><!-- Application.DataTemplates 是一个全局的数据模板集合。 --><Application.DataTemplates><!-- 在这里实例化并注册我们的 ViewLocator。 --><!-- 这使得它对整个应用程序都生效。任何地方只要把一个 ViewModel 赋给 Content 属性, --><!-- ViewLocator 就会尝试去匹配和构建对应的 View。 --><local:ViewLocator></local:ViewLocator></Application.DataTemplates><Application.Styles><FluentTheme /><StyleInclude Source="Styles/AppDefaultStyles.axaml"></StyleInclude></Application.Styles><Application.Resources><SolidColorBrush x:Key="PrimaryForeground">#CFCFCF</SolidColorBrush><SolidColorBrush x:Key="PrimaryBackground">#14172D</SolidColorBrush><LinearGradientBrush x:Key="PrimaryBackgroundGradient" StartPoint="0%, 0%" EndPoint="100%, 0%"><GradientStop Offset="0" Color="#111214"></GradientStop><GradientStop Offset="1" Color="#151E3E"></GradientStop></LinearGradientBrush><SolidColorBrush x:Key="PrimaryHoverBackground">#333B5A</SolidColorBrush><!-- 新增的颜色资源,用于激活状态按钮的背景色。 --><SolidColorBrush x:Key="PrimaryActiveBackground">#6633dd</SolidColorBrush><SolidColorBrush x:Key="PrimaryHoverForeground">White</SolidColorBrush><FontFamily x:Key="AkkoPro">/Assets/Fonts/AkkoPro-Regular.ttf#Akko Pro</FontFamily><FontFamily x:Key="AkkoProBold">/Assets/Fonts/AkkoPro-Bold.ttf#Akko Pro</FontFamily><FontFamily x:Key="Phosphor">/Assets/Fonts/Phosphor.ttf#Phosphor</FontFamily><FontFamily x:Key="Phosphor-Fill">/Assets/Fonts/Phosphor-Fill.ttf#Phosphor</FontFamily></Application.Resources> </Application>
4.8 Styles\AppDefaultStyles.axaml
修改AppDefaultStyles.axaml,添加当按钮拥有 active 类时的样式。
<Styles xmlns="https://github.com/avaloniaui"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><Design.PreviewWith><Border Padding="20" Background="{DynamicResource PrimaryBackgroundGradient}" Width="200"><!-- Add Controls for Previewer Here --><StackPanel Spacing="12"><Image Source="{SvgImage /Assets/Images/logo.svg}" Width="200"></Image><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Home"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Process"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Actions"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Macros"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="Reporter"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content=""></Label><Label Classes="akko" Content="History"></Label></StackPanel></Button><Button><Label Classes="icon-only" Content=""></Label></Button><Button Classes="transparent" Grid.Row="1"><Label Classes="icon-only" Content=""></Label></Button></StackPanel></Border></Design.PreviewWith><!-- Add Styles Here --><Style Selector="Window"><!-- <Setter Property="FontFamily" Value="{DynamicResource AkkoPro}"></Setter> --></Style><Style Selector="Border"><Setter Property="Transitions"><Transitions><DoubleTransition Property="Width" Duration="0:0:1"></DoubleTransition></Transitions></Setter></Style><Style Selector="Label.icon, Label.icon-only"><Setter Property="FontFamily" Value="{DynamicResource Phosphor-Fill}"></Setter><Setter Property="Margin" Value="0 2 5 0"></Setter><Setter Property="FontSize" Value="19"></Setter></Style><Style Selector="Label.icon-only"><Setter Property="Margin" Value="0"></Setter></Style><Style Selector="Button, Label.akko"><Setter Property="FontFamily" Value="{DynamicResource AkkoPro}"></Setter></Style><Style Selector="Button"><Setter Property="FontSize" Value="20"></Setter><Setter Property="CornerRadius" Value="10"></Setter><Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}"></Setter><Setter Property="Background" Value="{DynamicResource PrimaryBackground}"></Setter></Style><Style Selector="Button /template/ ContentPresenter"><Setter Property="RenderTransform" Value="scale(1)"></Setter><Setter Property="Transitions"><Transitions><BrushTransition Property="Foreground" Duration="0:0:0.1"></BrushTransition><BrushTransition Property="Background" Duration="0:0:0.1"></BrushTransition><TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.1"></TransformOperationsTransition></Transitions></Setter></Style><Style Selector="Button.transparent:pointerover Label"><Setter Property="RenderTransform" Value="scale(1.2)"></Setter></Style><Style Selector="Button:pointerover /template/ ContentPresenter"><Setter Property="Foreground" Value="{DynamicResource PrimaryHoverForeground}"></Setter><Setter Property="Background" Value="{DynamicResource PrimaryHoverBackground}"></Setter></Style><!-- 这是为激活按钮新增的样式。 --><!-- 选择器 "Button.active" 意味着它会应用在同时是 Button 并且拥有 "active" 类的控件上。 --><!-- `/template/ ContentPresenter` 这个语法是 Avalonia 样式系统的一部分,它的意思是“深入到按钮的控件模板(template)内部,找到名为 ContentPresenter 的部分并对它应用样式”。 --><!-- 这允许我们修改控件的内部视觉元素,而不仅仅是控件本身。 --><!-- 视频中提到,为了让 active 状态的样式优先级高于 pointerover (鼠标悬浮) 状态,--><!-- 需要将 active 样式的定义放在 pointerover 样式的后面。 --><!-- 所以当一个按钮是 active 状态时, --><!-- 鼠标再悬浮上去,背景色不会再变为悬浮的颜色,这符合预期。 --><Style Selector="Button.active /template/ ContentPresenter"><Setter Property="Background" Value="{DynamicResource PrimaryActiveBackground}"></Setter></Style><Style Selector="Button.transparent"><Setter Property="Background" Value="Transparent"></Setter></Style><Style Selector="Button.transparent Label.icon-only"><Setter Property="FontFamily" Value="{DynamicResource Phosphor}"></Setter></Style><Style Selector="Button.transparent:pointerover /template/ ContentPresenter"><Setter Property="Background" Value="Transparent"></Setter></Style> </Styles>
4.9 当前目录结构
去除/bin、/obj,让显示简洁。
│ App.axaml │ App.axaml.cs │ app.manifest │ AvaloniaApplication2.csproj │ Program.cs │ ViewLocator.cs │ ├─Assets │ ├─Fonts │ │ AkkoPro-Bold.ttf │ │ AkkoPro-Regular.ttf │ │ Phosphor-Fill.ttf │ │ Phosphor.ttf │ │ │ └─Images │ icon.svg │ logo.svg │ ├─Styles │ AppDefaultStyles.axaml │ ├─ViewModels │ ActionsPageViewModel.cs │ HistoryPageViewModel.cs │ HomePageViewModel.cs │ MacrosPageViewModel.cs │ MainViewModel.cs │ ProcessPageViewModel.cs │ ReporterPageViewModel.cs │ ViewModelBase.cs │ └─ViewsActionsPageView.axamlActionsPageView.axaml.csHistoryPageView.axamlHistoryPageView.axaml.csHomePageView.axamlHomePageView.axaml.csMacrosPageView.axamlMacrosPageView.axaml.csMainView.axamlMainView.axaml.csProcessPageView.axamlProcessPageView.axaml.csReporterPageView.axamlReporterPageView.axaml.csSettingsPageView.axamlSettingsPageView.axaml.cs