XAML

XAML 概述

XAML (Extensible Application Markup Language) 是一种基于 XML 的标记语言。在 WinUI 中,它可以像写说明书一样定义界面。如果你写过 HTML,你会对这种由标签组成的语言很眼熟。让我们打开 MainWindow.xaml ,这里定义了程序运行时的主界面。

在此之前,请自行编译或在微软商店中下载 WinUI 3 Gallery,此应用展示了所有可用的 WinUI 3 控件和样式。

XAML 是什么

在传统的 C# 编程中,你创建一个界面需要写很多“命令式”的代码:

1
2
3
4
5
6
// 命令式:一步步告诉计算机怎么做
var stackPanel = new StackPanel();
var button = new Button();
button.Content = "点击我";
stackPanel.Children.Add(button);
this.Content = stackPanel;

而 XAML 将上述过程序列化为一种文本格式:

1
2
3
4
<!-- 声明式:告诉计算机我要什么结果 -->
<StackPanel>
    <Button Content="点击我" />
</StackPanel>

当程序运行时,XAML 解析器会读取这段 XML,根据标签名去内存里 new 出对应的对象,根据属性名去调用 set 操作,最后根据层级关系把它们组装成一棵对象树(Object Tree)。


在设计上,XAML 是“部分类(Partial Class)”的 UI 延伸。

在 WinUI 中,你经常会看到一个 XAML 文件对应一个 .xaml.cs 文件。

  • XAML 文件: 定义了类的外在结构(UI 元素、布局)。

  • Code-behind (.cs): 定义了类的内在逻辑(事件处理、业务计算)。

通过 XAML 顶部的 x:Class="MyApp.MainWindow",编译器会将这两部分合并。

  • 本质上 XAML 被编译成了 C# 或 C++ 代码。你在 XAML 里写的 <Button x:Name="myButton" />,编译器会自动在后台生成一个 private Button myButton; 的成员变量。

  • 这就是为什么你可以在 .cs 文件里直接操作 XAML 里定义的控件。

XAML 的强大不仅仅在于创建对象,而是在于它引入了依赖属性(Dependency Properties)和标记扩展(Markup Extensions)。

  • 依赖属性: 传统的 OOP 对象属性只是简单的变量。而 XAML 对象的属性是可以“监听”的。当一个数据源变了,XAML 里的 UI 会自动变化。

  • 标记扩展(花括号语法): 例如 {Binding ...}{ThemeResource ...}

    • 这证明了 XAML 不仅仅是静态文本。

    • 花括号里的内容是一段会在运行时执行的小脚本。它告诉 XAML 解析器:“不要直接给这个属性赋值字符串,而是去执行一段逻辑(比如去寻找绑定的数据源,或者去系统设置里查一下现在的颜色主题)”。

XAML 是“视觉树(Visual Tree)”的构建工具。在 WinUI 中,界面是由成百上千个微小的对象组成的,如果你用代码去维护这些对象的层级关系(父子关系、嵌套关系),代码会变得极其混乱且难以维护。

XAML 解决了“复杂层级的可视化表达”:

  • 它用 XML 的嵌套结构完美映射了 UI 的树状结构。

  • 它让开发者能够以上帝视角俯瞰整个界面的布局,而不需要关心对象是如何在一个个内存地址中被链接起来的。

简单来讲,XAML 是为了让开发者能够用“描述结果”的方式,去编写原本需要通过大量“命令代码”才能构建出来的复杂 OOP 对象树。

一些基础语法

一、基础属性声明方式

1. 属性语法(Attribute Syntax)

直接在元素标签中以 属性="值" 形式声明:

1
2
<Button Content="点击" Width="200" Height="50" 
        Background="LightBlue" IsEnabled="True"/>
  • 适用于字符串、数字、布尔值、枚举、简单结构体(如 Thickness)等

  • 无法设置复杂对象(如 StyleDataTemplate

2. 属性元素语法(Property Element Syntax)

当属性值是复杂对象时,使用子元素形式:

1
2
3
4
5
6
7
8
<Button>
    <Button.Background>
        <LinearGradientBrush>
            <GradientStop Color="Red" Offset="0"/>
            <GradientStop Color="Blue" Offset="1"/>
        </LinearGradientBrush>
    </Button.Background>
</Button>
  • 适用于:BrushStyleDataTemplateControlTemplate

  • 两种语法等价转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <!-- 属性语法 -->
    <Button Background="Red"/>
    
    <!-- 属性元素语法 -->
    <Button>
        <Button.Background>
            <SolidColorBrush Color="Red"/>
        </Button.Background>
    </Button>
    

二、特殊属性类型

1. 附加属性(Attached Properties)

由其他类型定义、附加到当前元素的属性,语法:定义类型.属性名

1
2
3
4
5
6
7
8
9
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    
    <Button Content="按钮" 
            Grid.Row="0"/> <!-- 附加属性:指定所在行 -->
</Grid>
  • 常见附加属性:

    • Grid.Row / Grid.Column

    • Canvas.Left / Canvas.Top

    • ScrollViewer.VerticalScrollBarVisibility

2. 依赖属性(Dependency Properties)

WinUI 控件属性底层基于依赖属性系统,支持:

  • 数据绑定

  • 动画

  • 样式设置

  • 属性值继承

开发者通常无需直接操作依赖属性,但理解其存在有助于调试绑定/样式问题。


三、属性值类型与转换器

XAML 通过 类型转换器(TypeConverter) 将字符串自动转换为目标类型:

属性类型示例说明
字符串Content="Hello"直接赋值
数字Width="200"转为 double
布尔值IsEnabled="False"True/False(不区分大小写)
枚举HorizontalAlignment="Center"枚举成员名
颜色Background="Red"预定义颜色名 / #FF0000 / #FFFF0000
ThicknessMargin="10,5,10,5"顺序:左,上,右,下(支持 1/2/4 个值)
DateTimeDatePicker.Date="2024-01-15"ISO 8601 格式

💡 自定义类型需实现 TypeConverter 或使用属性元素语法。


四、标记扩展(Markup Extensions)

标记扩展以 {} 包裹,用于动态解析属性值:

1. 资源引用
1
2
3
4
5
6
7
8
<!-- 静态资源(编译时解析) -->
<Page.Resources>
    <SolidColorBrush x:Key="PrimaryBrush" Color="#0078D4"/>
</Page.Resources>
<Button Background="{StaticResource PrimaryBrush}"/>

<!-- 系统主题资源(随系统主题变化) -->
<Button Background="{ThemeResource SystemAccentColor}"/>
2. 数据绑定
1
2
3
4
5
<!-- x:Bind(编译时绑定) -->
<TextBlock Text="{x:Bind ViewModel.UserName, Mode=OneWay}"/>

<!-- Binding(运行时反射绑定) -->
<TextBlock Text="{Binding UserName, Mode=TwoWay}"/>
特性x:BindBinding
性能⚡ 编译时生成代码🔁 运行时反射
类型安全✅ 有❌ 无
默认 ModeOneTimeOneWay
路径起点页面/用户控件DataContext
3. 其他常用标记扩展
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- 空值 -->
<Image Source="{x:Null}"/>

<!-- 类型引用 -->
<Style TargetType="{x:Type Button}"/>

<!-- 静态成员 -->
<TextBlock Text="{x:Static sys:DateTime.Now}"/> <!-- 需 xmlns:sys="using:System" -->

<!-- 相对源(Binding 专用) -->
<TextBox Text="{Binding RelativeSource={RelativeSource Self}, Path=Tag}"/>

五、事件声明语法

事件通过属性语法直接关联后台方法:

1
<Button Click="Button_Click"/>
1
2
3
4
5
// MainWindow.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    // 处理逻辑
}
  • 事件处理器必须是 partial class 中的 private 方法

  • 参数类型需匹配路由事件签名(通常为 (object, RoutedEventArgs)


六、命名空间与 x: 前缀

1. 核心命名空间
1
2
3
4
<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MyApp">
  • xmlns:默认 WinUI 控件命名空间

  • xmlns:x:XAML 语言基础特性(如 x:Namex:Key

  • xmlns:local:自定义类型命名空间

2. 关键 x: 指令
指令用途示例
x:Name代码中引用元素<Button x:Name="myButton"/>myButton.Content = "New";
x:Key资源字典中标识资源<Style x:Key="MyStyle" TargetType="Button"/>
x:Class指定后台代码类<Page x:Class="MyApp.MainPage">
x:Load条件性加载元素(WinUI 3.0+)<Button x:Load="{x:Bind IsAdmin}"/>
x:Phase优化列表虚拟化加载<TextBlock x:Phase="1"/>

七、属性路径(Property Path)

用于绑定/动画中访问嵌套属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- 简单属性 -->
<TextBlock Text="{x:Bind User.Name}"/>

<!-- 索引器 -->
<TextBlock Text="{x:Bind Users[0].Name}"/>

<!-- 附加属性(需括号) -->
<TextBlock Text="{Binding Path=(Grid.Row), RelativeSource={RelativeSource Self}}"/>

<!-- 多级嵌套 -->
<TextBlock Text="{x:Bind ViewModel.CurrentOrder.Items[0].ProductName}"/>

布局

不同于 HTML 从上到下的流式布局,XAML 使用“容器”的概念来管理布局。因为 Windows 窗口可以拉大拉小,屏幕分辨率也各不相同,XAML 采用的是一种比例化的布局方案,而不是将坐标固定死。你可以把布局想象成是在不同的盒子里装东西。

Grid(网格)

Grid 是 WinUI 中最灵活、最强大的布局控件之一。它允许你通过行(Rows)列(Columns)来划分空间,类似 Excel 表格。

在使用 Grid 时,需要先定义行与列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<Grid>
    <!-- 定义 2 行 -->
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <!-- 第一行:高度随内容而定(Auto) -->
        <RowDefinition Height="*"/>
        <!-- 第二行:高度占据剩余所有空间(*) -->
    </Grid.RowDefinitions>

    <!-- 定义 2 列 -->
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200"/>
        <!-- 第一列:固定 200 像素 -->
        <ColumnDefinition Width="*"/>
        <!-- 第二列:宽度占据剩余空间 -->
    </Grid.ColumnDefinitions>

    <!-- 使用附加属性把控件放到指定格子里并居中排列 -->
    <TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center" Text="这是左上角" />
    <Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Content="占据剩余大空间的右下角" />
</Grid>

Grid 的三种尺寸单位:

  • 绝对尺寸 (如 100):固定的像素值,不随窗口大小而变化。

  • 自动尺寸 (Auto):控件有多大,格子就缩放成多大。

  • 比例尺寸 (*):自适应布局的关键。如果第一列是 1*,第二列是 2*,则第二列永远是第一列宽度的两倍。

*1*的简写,在定义了绝对尺寸和自动尺寸之后,*会自动分配剩余的空间 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="200"/>
        <!-- 阶段1:固定占据200px -->
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="2*"/>
        <!-- 阶段2:在剩余空间中创建1:1:2的布局结构 -->
    </Grid.RowDefinitions>

    <Border Grid.Row="0" BorderBrush="Green" BorderThickness="1"/>
    <Border Grid.Row="1" BorderBrush="Red" BorderThickness="1"/>
    <Border Grid.Row="2" BorderBrush="Red" BorderThickness="1"/>
    <Border Grid.Row="3" BorderBrush="Red" BorderThickness="1"/>
</Grid>

试着调整窗口高度,看看布局是否正确发生了变化。

StackPanel(堆栈面板)

StackPanel 是一种简单直观的布局容器,用于将子元素沿单一方向(水平或垂直)线性排列。适合构建列表式、流式布局。

1
2
3
4
5
6
7
<!-- 垂直堆叠(默认) -->
<!-- 使用Spacing来统一设置间距 -->
<StackPanel Spacing="10">
    <Button Content="按钮1"/>
    <Button Content="按钮2"/>
    <Button Content="按钮3"/>
</StackPanel>
1
2
3
4
5
6
7
<StackPanel Orientation="Horizontal" 
            Spacing="8" 
            HorizontalAlignment="Right">
    <Button Content="保存" Icon="Save"/>
    <Button Content="撤销" Icon="Undo"/>
    <Button Content="重做" Icon="Redo"/>
</StackPanel>

运行Sample 3 ,你会看到三个按钮都靠在左上角。我们可以通过HorizontalAlignmentVerticalAlignment 属性来更改对齐行为。

控件

控件是界面上真正能被用户感知与交互的元素。

文本控件

TextBlock —— 静态文本

1
2
3
4
<TextBlock Text="I am super excited to be here!" 
           FontFamily="Arial"FontSize="24" 
           FontStyle="Italic" TextWrapping="WrapWholeWords"
           CharacterSpacing="200" Foreground="CornflowerBlue" />

如果你希望文本可以被选中,使用IsTextSelectionEnabled="True"

TextBox —— 输入框

1
<TextBox Header="Enter your name:" PlaceholderText="Name" />

如果需要输入密码,则应该使用PasswordBox控件

按钮

Button —— 标准按钮

1
<Button Content="Standard XAML button" Click="Button_Click"/>

.xaml.cs代码中处理事件绑定,并通过IsEnabled属性控制其可用性

DropDownButton —— 带下拉菜单的按钮

1
2
3
4
5
6
7
8
9
<DropDownButton Content="Email">
    <DropDownButton.Flyout>
        <MenuFlyout Placement="Bottom">
            <MenuFlyoutItem Text="Send"/>
            <MenuFlyoutItem Text="Reply"/>
            <MenuFlyoutItem Text="Reply All"/>
        </MenuFlyout>
    </DropDownButton.Flyout>
</DropDownButton>

ToggleSwitch —— 开关控件

1
<ToggleSwitch Header="Toggle Switch" OffContent="Off" OnContent="On" IsOn="False" />

集合控件(Collection Controls)

具体的控件演示可以在 WinUI 3 Gallery 的Collections中找到。在这里先介绍DataTemplateItemTemplate的概念。

DataTemplate

DataTemplate定义了展示数据项的模板。假设我们有一个Person类:

1
2
3
4
5
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

如果我们想在界面上展示一个或多个Person对象,就需要先定义它长什么样。

1
2
3
4
5
6
<DataTemplate x:DataType="local:Person">
    <StackPanel Orientation="Horizontal" Spacing="12">
        <TextBlock Text="{x:Bind Name}" FontWeight="Bold"/>
        <TextBlock Text="{x:Bind Age}" Foreground="Gray"/>
    </StackPanel>
</DataTemplate>

上述代码定义了一个用于Person类型的DataTemplate,其中用StackPanel垂直排布了两个TextBlock,分别用于展示NameAgeDataTemplate不是控件,而是模板定义,通过 {x:Bind} 将数据属性映射到 UI 元素。

ItemTemplate

ItemTemplateItemsControl及其派生类(ListView/GridView/ComboBox)的属性,指定集合中的每一项使用哪个 DataTemplate 渲染。是控件的属性(不是独立元素),其值类型为 DataTemplate

1
2
3
<!-- 将 DataTemplate 挂载到 ListView 的 ItemTemplate 属性 -->
<ListView ItemsSource="{x:Bind People}" 
          ItemTemplate="{StaticResource PersonTemplate}"/>

完整的ListView示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<ListView ItemsSource="{x:Bind People}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="local:Person">
            <StackPanel Orientation="Horizontal" Spacing="12">
                <TextBlock Text="{x:Bind Name}" FontWeight="Bold"/>
                <TextBlock Text="{x:Bind Age}" Foreground="Gray"/>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

后台代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public sealed partial class MainWindow : Window
{
    public ObservableCollection<Person> People { get; } = new();

    public MainWindow()
    {
        InitializeComponent();
        People.Add(new Person { Name = "Alice", Age = 30 });
        People.Add(new Person { Name = "Bob", Age = 25 });
    }
}