Avalonia 学习笔记04. Page Navigation(页面导航) (转载)
原文链接: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 │ └─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号