XAML语法基础
XAML(eXtensible Application Markup Language)是WPF用于描述界面的声明式标记语言,它基于XML语法,本质上是一种对象实例化和属性赋值的描述方式,我们在XAML中写的每一个元素、每一个特性,最终都会映射成.NET对象的创建和属性设置。
元素(Element)和特性(Attribute)
以下面按钮的XAML为例:
<Button Content="确定" Background="Red" Width="100" Height="30" />
在XML的官方定义和标准术语中,标签内的键值对如Content="确定"被称为特性(Attribute),标签本身如<Button>被称为元素(Element)。
XAML与对象实例化和属性赋值
XAML并没有什么魔法,一份XAML和一段C#代码是可以严格等价的,比如下面这段XAML描述了一个红色背景、带文字的按钮。
<Button Content="确定" Background="Red" Width="100" Height="30" />
它与下面的C#代码完全等价,元素名对应类型,特性(Attribute)对应属性赋值。
var button = new Button
{
Content = "确定",
Background = Brushes.Red,
Width = 100,
Height = 30
};
这里注意Background="Red",XAML写的是一个字符串,而C#中是Brush对象,中间是靠WPF内置的类型转换器(TypeConverter)自动完成转换的,这使得XAML可以用非常简洁的字符串表达颜色、长度、枚举等复杂类型。
嵌套的元素对应对象之间的父子关系,下面这段XAML在窗口中放了一个StackPanel,StackPanel里又放了两个控件。
<Window x:Class="Gacfox.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:local="clr-namespace:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<TextBlock Text="请输入姓名:" />
<TextBox />
</StackPanel>
</Window>
XAML命名空间
XAML本质还是XML,元素名前可以有命名空间前缀,前缀通过xmlns声明映射到一组.NET类型。上面例子中出现过以下命名空间声明:
默认命名空间:xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation",它映射了System.Windows、System.Windows.Controls等WPF核心命名空间,所以Button、TextBox这些元素名不需要加前缀就能直接使用。
XAML语言命名空间:xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml",前缀x是约定俗成的,它映射的是XAML语言本身的指令,例如x:Class、x:Name等,这些不是普通的.NET属性,而是交给XAML编译器特殊处理的。
如果想在XAML中使用自己写的类或第三方程序集中的类型,需要用clr-namespace语法引入CLR命名空间,例如xmlns:local="clr-namespace:Gacfox.Demo"把当前程序集的Gacfox.Demo命名空间映射为local前缀,之后就可以写<local:MyUserControl />这样的元素,如果类型在别的程序集中,还要追加程序集名,例如clr-namespace:Some.Namespace;assembly=SomeAssembly。
属性赋值的两种方式
特性语法
特性语法就是我们前面一直在用的形式,把属性写成XML特性,值是字符串,由类型转换器解析。
<Button Content="确定" Background="Red" FontSize="16" />
特性语法简洁,但值只能是字符串,遇到属性值本身是复杂对象时就力不从心了。
属性元素语法
属性元素语法用父元素.属性名形式的子元素来为属性赋值,值可以是任意复杂的对象树,下面的例子为按钮的背景设置了一个线性渐变画刷。
<Button Content="确定" Width="120" Height="36">
<Button.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="LightBlue" Offset="0" />
<GradientStop Color="SteelBlue" Offset="1" />
</LinearGradientBrush>
</Button.Background>
</Button>
这里的Button.Background并不是一个新的控件,而是用于给Button的Background属性赋值的语法标记。特性语法和属性元素语法可以混用,但注意同一个属性只能用其中一种赋值一次。
内容属性语法
我们已经看到<Button Content="确定" />这样的写法,其实对于按钮还可以省略Content,直接把内容写在元素内部。
<Button Width="120" Height="36">确定</Button>
这是因为Button把Content标记为了内容属性(ContentProperty),写在元素标签之间的内容会自动赋给内容属性。WPF中,每个类最多只能指定一个内容属性,StackPanel、Grid这类面板的内容属性是Children,所以我们直接在标签里写子控件即可。
内容属性同样支持放复杂对象,按钮的内容甚至可以是图形加文字的组合。
<Button Width="120" Height="36">
<StackPanel Orientation="Horizontal">
<Ellipse Width="12" Height="12" Fill="Green" />
<TextBlock Text="运行中" Margin="6,0,0,0" />
</StackPanel>
</Button>
标记扩展语法
标记扩展(Markup Extension)是XAML中一种特殊的赋值语法,用一对花括号{}表示,它把属性值的求值交给一个扩展对象在运行时完成,而非简单的字面量转换。常见的标记扩展有如下几种。
<Window.Resources>
<SolidColorBrush x:Key="MainBrush" Color="SteelBlue" />
</Window.Resources>
<StackPanel>
<!-- 数据绑定:把Text绑定到数据上下文的Name属性 -->
<TextBlock Text="{Binding Name}" />
<!-- 引用静态资源:使用上面定义的画刷 -->
<TextBlock Text="标题" Foreground="{StaticResource MainBrush}" />
<!-- 显式赋空值 -->
<Image Source="{x:Null}" />
</StackPanel>
{Binding ...}:数据绑定,WPF数据驱动的核心{StaticResource Key}:引用资源字典中定义的资源{x:Null}:显式把属性赋为null
有关标记扩展的更多细节我们到对应章节再深入讲解。
x:前缀常用指令
x:前缀的指令由XAML编译器处理,常用的有以下几个。
| 指令 | 说明 |
|---|---|
x:Class |
指定XAML对应的代码隐藏类,只在根元素上使用 |
x:Name |
给元素命名,编译后生成同名成员字段,可在代码中访问 |
x:Key |
给资源字典中的条目指定键,配合{StaticResource}/{DynamicResource}使用 |
x:Type |
传递一个类型本身(而非实例),常用于样式TargetType="{x:Type Button}" |
x:Null |
表示null值 |
其中x:Name用得最多,它相当于Winform属性面板中的(Name),命名的元素可以在C#代码中直接用同名成员变量访问。
<TextBox x:Name="inputTextBox" />
string text = inputTextBox.Text;
事件在XAML中的挂接
XAML中可以直接用特性语法把事件挂接到代码隐藏类的方法上。
<Button Content="确定" Click="okButton_Click" />
对应的事件处理函数写在代码隐藏类中,签名为void 方法名(object sender, RoutedEventArgs e),其中不同事件的参数类型可能不同,不过大多数都是RoutedEventArgs及其派生类。
namespace Gacfox.Demo;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void okButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("点击了确定");
}
}
事件挂接在编译期就会校验方法签名是否匹配,写错名字会直接编译失败,比运行时才报错友好得多。除了Click,控件的常用事件都可以这样挂接。WPF事件背后是一套比Winform更强大的路由事件机制,我们将在后续章节详细讨论。