当前位置: 首页 > news >正文

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)。但它不知道 HomePageIsActiveProcessPageIsActive 这些依赖它的属性也需要更新。所以,按钮的高亮状态不会自动变化。

[NotifyPropertyChangedFor] 的作用:
它就像一个“信使”或者“传话筒”。你把它贴在“源头”属性上,告诉它:“当你自己变化的时候,请顺便帮我通知一下其他几个相关的属性也变化了。”

[ObservableProperty]
// 当 CurrentPage 变化时,请顺便通知 HomePageIsActive 也变了
[NotifyPropertyChangedFor(nameof(HomePageIsActive))] 
// 当 CurrentPage 变化时,也请通知 ProcessPageIsActive 也变了
[NotifyPropertyChangedFor(nameof(ProcessPageIsActive))] 
// ... 其他按钮的IsActive属性也一样
private ViewModelBase _currentPage;

它的工作流程:

  1. 你点击了“Process”按钮,触发 GoToProcessCommand 命令。
  2. 命令执行 GoToProcess()方法,这句代码CurrentPage = _processPage;被调用。
  3. [ObservableProperty]的底层机制检测到_currentPage 的值变了,于是它准备发出CurrentPage属性已更改的通知。
  4. 在发出通知前,它看到了你贴的[NotifyPropertyChangedFor] 标签。
  5. 于是,它不仅发出了CurrentPage的通知,还一并发出HomePageIsActiveProcessPageIsActive等所有你在标签里指定的属性的“已更改”通知。
  6. 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="&#xE2C2;"></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="&#xE346;"></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="&#xE7F2;"></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="&#xE3EE;"></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="&#xEB7A;"></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="&#xE03A;"></Label><Label Classes="akko" Content="History" IsVisible="{Binding SideMenuExpanded}"></Label></StackPanel></Button></StackPanel><Button Classes="transparent" Grid.Row="1"><Label Classes="icon-only" Content="&#xE272;"></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="&#xE2C2;"></Label><Label Classes="akko" Content="Home"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content="&#xE346;"></Label><Label Classes="akko" Content="Process"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content="&#xE7F2;"></Label><Label Classes="akko" Content="Actions"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content="&#xE3EE;"></Label><Label Classes="akko" Content="Macros"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content="&#xEB7A;"></Label><Label Classes="akko" Content="Reporter"></Label></StackPanel></Button><Button HorizontalAlignment="Stretch"><StackPanel Orientation="Horizontal"><Label Classes="icon" Content="&#xE03A;"></Label><Label Classes="akko" Content="History"></Label></StackPanel></Button><Button><Label Classes="icon-only" Content="&#xE272;"></Label></Button><Button Classes="transparent" Grid.Row="1"><Label Classes="icon-only" Content="&#xE272;"></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

 

http://www.wxhsa.cn/company.asp?id=603

相关文章:

  • 判断左手坐标系和右手坐标系的方法
  • 题解:P11894 「LAOI-9」Update
  • 题解:P2012 拯救世界2
  • 今日随笔
  • 一键安装小雅Alist
  • 题解:AT_abc394_c [ABC394C] Debug
  • Lumion Pro 12.0 下载安装教程包含安装包下载、安装、激活超详细图文步骤
  • 题解:CF348C Subset Sums
  • 题解:CF351B Jeff and Furik
  • 题解:CF2118D1 Red Light, Green Light (Easy version)
  • Project Euler题解思路导航(私人)
  • 27届春招备战一轮复习--第五期
  • 阅读方式
  • Audition 2025(AU2025)超详细直装版下载安装教程保姆级
  • pg 解析select语句的返回值
  • 长乐一中 CSP-S 2025 提高级模拟赛 Day2
  • 费用流
  • [豪の学习笔记] 软考中级备考 基础复习#6
  • RAG
  • 手撕深度学习:矩阵求导链式法则与矩阵乘法反向传播公式,深度学习进阶必备!
  • CF *3200
  • 分享我在阿贝云使用免费虚拟主机的真实体验!
  • 软件测试工程师的职业天花板在哪里?如何突破?
  • 02020213 .NET Core重难点知识13-配置日志邮件服务案例、DI读取、DI与扩展方法、VS配置项目环境变量
  • GJOI 模拟赛题记录声明
  • Ubuntu 卸载 Firefox 浏览器
  • 录无法修改OneDrive文件的解决方法
  • 量子机器学习入门:三种数据编码方法对比与应用
  • 向量数据库
  • UGNX2506下载和安装教程包含激活教程步骤(超详细保姆级图文UGNX安装步骤)