Avalonia 学习笔记04. Page Navigation(页面导航)
视频链接:https://youtu.be/RW4fvs8qnjE?si=PXlszBf4pdB-x3mU
本节课的目标是实现应用内的页面切换功能。我们将创建一个核心的 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
│
└─Views
ActionsPageView.axaml
ActionsPageView.axaml.cs
HistoryPageView.axaml
HistoryPageView.axaml.cs
HomePageView.axaml
HomePageView.axaml.cs
MacrosPageView.axaml
MacrosPageView.axaml.cs
MainView.axaml
MainView.axaml.cs
ProcessPageView.axaml
ProcessPageView.axaml.cs
ReporterPageView.axaml
ReporterPageView.axaml.cs
SettingsPageView.axaml
SettingsPageView.axaml.cs
浙公网安备 33010602011771号