Avalonia 学习笔记06. Page Layout(页面布局)

本节课程的目标是根据一个预先设计好的 UI 模型,使用 Avalonia XAML 来构建“设置”页面的结构。我们将重点放在如何使用 Grid 和 StackPanel 等布局控件来正确地放置元素,而将具体的样式(如颜色、字体、边框等)留到下一节课。这种将“结构”和“样式”分离的思路,是现代 UI 开发中的一个重要实践。

6.1 ViewModels\MainViewModel.cs

我们首先对 MainViewModel 做一个小修改,以解决 XAML 设计器中的一个预览问题。

using Avalonia.Svg.Skia;
using AvaloniaApplication2.Data;
using AvaloniaApplication2.Factories;
using AvaloniaApplication2.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;

namespace AvaloniaApplication2.ViewModels;

public partial class MainViewModel : ViewModelBase
{
    private PageFactory _pageFactory;
    
    [ObservableProperty]
    private bool _sideMenuExpanded = true;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HomePageIsActive))]
    [NotifyPropertyChangedFor(nameof(ProcessPageIsActive))]
    [NotifyPropertyChangedFor(nameof(ActionsPageIsActive))]
    [NotifyPropertyChangedFor(nameof(MacrosPageIsActive))]
    [NotifyPropertyChangedFor(nameof(ReporterPageIsActive))]
    [NotifyPropertyChangedFor(nameof(HistoryPageIsActive))]
    [NotifyPropertyChangedFor(nameof(SettingsPageIsActive))]
    private PageViewModel _currentPage;

    public bool HomePageIsActive => CurrentPage.PageName == ApplicationPageNames.Home;
    public bool ProcessPageIsActive => CurrentPage.PageName == ApplicationPageNames.Process;
    public bool ActionsPageIsActive => CurrentPage.PageName == ApplicationPageNames.Actions;
    public bool MacrosPageIsActive => CurrentPage.PageName == ApplicationPageNames.Macros;
    public bool ReporterPageIsActive => CurrentPage.PageName == ApplicationPageNames.Reporter;
    public bool HistoryPageIsActive => CurrentPage.PageName == ApplicationPageNames.History;
    public bool SettingsPageIsActive => CurrentPage.PageName == ApplicationPageNames.Settings;

    /// <summary>
    /// 仅用于设计时 (Design-Time only constructor)。
    /// </summary>
    /// <remarks>
    /// 这个无参数的构造函数是专门为 XAML 设计器准备的。
    /// 因为我们引入了依赖注入,原来的构造函数需要一个 `PageFactory` 参数,
    /// 这是在程序“运行时”才由 DI 容器提供的。
    /// XAML 设计器在“设计时”无法提供这个参数,会导致预览失败。
    /// 通过添加这个无参数构造函数,并为 `CurrentPage` 设置一个默认页面(这里是 SettingsPageViewModel),
    /// 设计器就能够成功实例化 MainViewModel,从而正常显示 MainView 的预览界面。
    /// </remarks>
    public MainViewModel()
    {
        CurrentPage = new SettingsPageViewModel();
    }
   
    public MainViewModel(PageFactory pageFactory)
    {
        _pageFactory = pageFactory;
        GoToHome();
    }
    
    [RelayCommand]
    private void SideMenuResize()
    {
        SideMenuExpanded = !SideMenuExpanded;
    }

    [RelayCommand]
    private void GoToHome()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Home);
    }
    
    [RelayCommand]
    private void GoToProcess()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Process);
    }
    
    [RelayCommand]
    private void GoToMacros()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Macros);
    }
    
    [RelayCommand]
    private void GoToActions()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Actions);
    }
    
    [RelayCommand]
    private void GoToReporter()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Reporter);
    }
    
    [RelayCommand]
    private void GoToHistory()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.History);
    }
    
    [RelayCommand]
    private void GoToSettings()
    {
        CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Settings);
    }
}

 

6.2 ViewModels\SettingsPageViewModel.cs

这是为“设置”页面创建的新的 ViewModel。我们在这里添加一些临时的示例数据,以便在 UI 上看到列表的显示效果。

using System.Collections.Generic;
using AvaloniaApplication2.Data;
using CommunityToolkit.Mvvm.ComponentModel;

namespace AvaloniaApplication2.ViewModels;

public partial class SettingsPageViewModel : PageViewModel
{
    // 使用 [ObservableProperty] 特性,CommunityToolkit.Mvvm 会自动为私有字段 `_locationPaths`
    // 生成一个名为 `LocationPaths` 的公共属性。
    // 这个属性会自动实现 INotifyPropertyChanged 接口,当它的值改变时,会通知 UI 更新。
    [ObservableProperty]
    private List<string> _locationPaths;
    
    public SettingsPageViewModel()
    {
        PageName = ApplicationPageNames.Settings;
        
        // TEMP: Remove
        // 在构造函数中,我们为 LocationPaths 初始化了一些临时的假数据。
        // 这使得我们在开发 UI 界面时,即使没有后端逻辑,也能看到列表的实际显示效果。
        // 这里使用了 C# 12 的集合表达式 `[...]` 和逐字字符串 `@""`,使代码更简洁。
        LocationPaths =
        [
            @"C:\Users\Luke\Downloads\TestActions",
            @"C:\Users\Luke\Documents\BatchProcess",
            @"X:\Shared\BatchProcess\Templates"
        ];
    }
}

 

6.3 Views\SettingsPageView.axaml

这是本节课的核心,即“设置”页面的 XAML 结构代码。我们一步步将设计图分解为 XAML 控件。

<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"
             xmlns:vm="clr-namespace:AvaloniaApplication2.ViewModels"
             mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="900"
             Background="{DynamicResource PrimaryBackground}"
             Foreground="{DynamicResource PrimaryForeground}"
             x:DataType="vm:SettingsPageViewModel"
             x:Class="AvaloniaApplication2.Views.SettingsPageView">
    
    <!-- 
    为设计器提供数据上下文(DataContext)。
    这行代码告诉 XAML 设计器在预览时使用 `SettingsPageViewModel` 的一个实例作为数据源。
    这使得在设计器中可以看到数据绑定(如 ItemsSource="{Binding LocationPaths}")的效果。
    它只在设计时生效,不影响程序实际运行。
    -->
    <Design.DataContext><vm:SettingsPageViewModel></vm:SettingsPageViewModel></Design.DataContext>
    
    <!-- 
    页面根布局:一个2行2列的 Grid。
    - ColumnDefinitions="*, *": 定义两个宽度相等的列,它们会平分所有可用宽度。
    - RowDefinitions="Auto, *": 定义两行。第一行(头部)的高度由其内容决定(Auto),第二行(内容区)占据所有剩余的高度(*)。
    这是构建经典“头+体”布局的常用方式。
    -->
    <Grid ColumnDefinitions="*, *" RowDefinitions="Auto, *">
        <!-- Header: 头部区域 -->
        <!-- Grid.ColumnSpan="2" 让这个头部的 Grid 横跨两列 -->
        <Grid Name="HeaderGrid" Grid.ColumnSpan="2">
            <!-- 
            Grid 是一个很好的堆叠控件。这里我们将 Image 和 StackPanel 放在同一个单元格中,
            它们会重叠在一起,Image 在下,StackPanel 在上。
            - Image 的 Stretch="UniformToFill" 属性确保图片在保持自身比例的同时,填满整个容器区域,超出部分会被裁剪。
            -->
            <Image Source="{SvgImage /Assets/Images/background-settings.svg}" Height="160" Stretch="UniformToFill"></Image>
            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
                <Label HorizontalAlignment="Center" Content="Settings"></Label>
                <Label HorizontalAlignment="Center" Content="Version 3.0.0.2 Beta"></Label>
                <Label HorizontalAlignment="Center" Content="Compiled Jul 07 2025"></Label>
            </StackPanel>
        </Grid>
        
        <!-- Left side content: 左侧内容区 -->
        <!-- 
        - Grid.Column="0" Grid.Row="1" 将这个 StackPanel 放置在主 Grid 的第一列、第二行。
        - Spacing="10" 为 StackPanel 内的直接子元素(如此处的 General 和 Location 两个区域)之间添加 10 个单位的垂直间距。
        - Margin="15" 在 StackPanel 的四周添加 15 个单位的外边距,起到内边距的效果,让内容不至于贴边。
        -->
        <StackPanel Grid.Column="0" Grid.Row="1" Spacing="10" Margin="15">
            <!-- General: “常规”设置区域 -->
            <StackPanel>
                <Label Content="General"></Label>
                <!-- 
                使用一个嵌套的 Grid 来对齐左侧的描述文本和右侧的控件。
                - ColumnDefinitions="*, Auto": 左列占据所有剩余空间,右列宽度由其内容(按钮、复选框)决定。
                -->
                <Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
                    <!-- Release license -->
                    <!-- TextBlock 用于显示长文本,因为它支持 TextWrapping="Wrap" 自动换行。Label 不支持。-->
                    <TextBlock TextWrapping="Wrap" Text="Remove license of BatchProcess from this machine and release the license back to the server ready to be transferred to another machine."></TextBlock>
                    <Button Grid.Row="0" Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <!-- &#xE2FE; 是一个图标字体的 Unicode 编码,用于显示图标 -->
                            <Label Classes="icon" Content="&#xE2FE;"></Label>
                            <Label Classes="akko" Content="Release License"></Label>
                        </StackPanel>
                    </Button>
                    
                    <!-- Skip Files -->
                    <TextBlock Grid.Row="1" Grid.Column="0" TextWrapping="Wrap" Text="Skip files if only Open, Save (Optional) and Close are Valid actions."></TextBlock>
                    <CheckBox Grid.Row="1" Grid.Column="1"></CheckBox>
                    
                    <!-- Duplicate Entries -->
                    <TextBlock Grid.Row="2" Grid.Column="0" TextWrapping="Wrap" Text="Allow duplicate entries of the same file in project list"></TextBlock>
                    <CheckBox Grid.Row="2" Grid.Column="1"></CheckBox>
                </Grid>
            </StackPanel>
            
            <!-- Location: “位置”设置区域 -->
            <StackPanel>
                <Label Content="Location"></Label>
                <Grid ColumnDefinitions="*, Auto">
                    <StackPanel>
                        <TextBlock TextWrapping="Wrap" Text="Add or remove the locations to search for Reporter Templates, Macros, Actions and other custom files or templates."></TextBlock>
                        <TextBlock TextWrapping="Wrap" Text="All sub-directories will be searched automatically"></TextBlock>
                    </StackPanel>
                    
                    <Button Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="akko" Content="+ Folder"></Label>
                        </StackPanel>
                    </Button>
                </Grid>
                <!--
                使用 ListBox 来显示文件夹路径列表。
                - ItemsSource="{Binding LocationPaths}" 是核心的数据绑定语法,它将此列表的数据源绑定到 ViewModel 中的 `LocationPaths` 属性。
                - 选择 ListBox 而不是更基础的 ItemsControl,是因为 ListBox 自带了选中、悬停等交互效果和基本样式,方便后续功能开发。
                -->
                <ListBox ItemsSource="{Binding LocationPaths}"></ListBox>
            </StackPanel>
        </StackPanel>
        
        <!-- Right Side Content: 右侧内容区 -->
        <StackPanel Grid.Column="1" Grid.Row="1" Spacing="10" Margin="15">
            <!-- SolidWorks Host -->
            <StackPanel>
                <Label Content="SolidWorks Host"></Label>
                <TextBlock TextWrapping="Wrap">
                    BatchProcess can work locally on the current machine, or on any machine accessible
                    over the network or even internet.<LineBreak /><LineBreak />
                    
                    Enter the machines IP address, network name or localhost for this machine.
                </TextBlock>
                <ComboBox></ComboBox>
                <Label Content="Connection established"></Label>
            </StackPanel>
            <!-- PDM Enterprise -->
            <StackPanel Spacing="15">
                <Label Content="PDM Enterprise"></Label>
                <TextBlock TextWrapping="Wrap" Text="If you are using PDM Enterprise enter the credentials below and test login. BatchProcess can then automatically handle checking in and out files from PDM Enterprise."></TextBlock>
                <!-- 
                这里使用三列等宽的 Grid,是为了让三个输入框(ComboBox, TextBox, TextBox)平分横向空间。
                如果使用水平 StackPanel,它们只会各自占据所需宽度,无法实现均分拉伸的效果。
                -->
                <Grid ColumnDefinitions="*, *, *">
                    <ComboBox HorizontalAlignment="Stretch"></ComboBox>
                    <TextBox Grid.Column="1"></TextBox>
                    <TextBox Grid.Column="2"></TextBox>
                </Grid>
                <StackPanel Orientation="Horizontal">
                    <Button Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE23E;"></Label>
                            <Label Classes="akko" Content="Login"></Label>
                        </StackPanel>
                    </Button>
                    <Button Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE094;"></Label>
                            <Label Classes="akko" Content="Refresh Vault"></Label>
                        </StackPanel>
                    </Button>
                </StackPanel>
                <Label Content="Connection Established"></Label>
            </StackPanel>
            <!-- Setting Cache -->
            <StackPanel Spacing="15">
                <Label Content="Setting Cache"></Label>
                <TextBlock TextWrapping="Wrap">
                    Various settings are stored locally including Processes, Actions, Macros, Reports and History. <LineBreak /><LineBreak />
                    If you are experiencing issues you can try clearing the cache (this won't remove the license).
                </TextBlock>
                
                <StackPanel Orientation="Horizontal">
                    <Button Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xEC54;"></Label>
                            <Label Classes="akko" Content="Clear Cache"></Label>
                        </StackPanel>
                    </Button>
                    <Button Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE5DE;"></Label>
                            <Label Classes="akko" Content="Export Cache"></Label>
                        </StackPanel>
                    </Button>
                    <Button Grid.Column="1" HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE20C;"></Label>
                            <Label Classes="akko" Content="Import Cache"></Label>
                        </StackPanel>
                    </Button>
                </StackPanel>
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

 

posted @ 2025-09-23 19:40  Gordon管  阅读(64)  评论(0)    收藏  举报