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
│
└─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

 

posted @ 2025-09-10 21:43  Gordon管  阅读(51)  评论(0)    收藏  举报