XAML 基础
一旦你理解一些基本法则,XAML标准是十分直白的:
- 在XAML文档中,每个元素都映射到.NET类的一个实例。元素的名字恰好匹配类的名字。例如,元素<Button就是一个Button对象。
- 就像任何XML文档,你能在一元素内部嵌套另一个元素。如你所见,XAML使每个类可以灵活地处理这种情况。嵌套通常代表着包含关系—换句话说,如果你在一个Grid元素内部发现一个Button元素,在你的用户界面上,有一个网格,它的内部包含一个按钮。
- 你能通过特性设置每个类的属性。但是,有些情况下,一个特性不足以处理这个工作。在这种情况下,你将使用带有一个特殊语法的嵌套标签。
XAML名字空间
除了提供一个类的名字,XAML解析器也需要知道.NET类位于哪个名字空间。为得出你真正希望的类,XAML解析器检查应用于元素的XML名字空间。
xmlns特性是XML专用于声明名字空间的一个特性。
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
这个名字空间是WPF核心名字空间。包括所有的WPF类,包括用于建立界面的控件。它没有名字空间前缀,是整个文档的默认名字空间。换句话说,没有前缀的元素自动放在这个名字空间。
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
这个名字空间是XAML名字空间。它包括影响文档解释的各种实用特征。这个名字空间对应于前缀x,这意味着,你能依靠在元素名称前放上名字空间前缀来应用它。
后台代码类
通过在顶级元素设置Class特性,将XAML文档与后台代码类相关联:
<Window x:Class="WindowsApplication1.Window1"
x名字空间前缀放置Class特性到XAML名字空间。事实上,Class特性告诉XAML解析器用给定的名字生成一个新的类。那类派生自由XML元素命名的类。换句话说,这个例子创造了一个名为Window1的新类,它派生自Window类。
Window1类在编译时自动地生成。一个Window1.xaml文件链接着一个设计器自动生成的后台代码部分类,和一个程序员手写的代码部分类。
InitializeComponent()方法
当你创造Window1类的一个实例时,默认构造函数调用InitializeComponent()方法。此方法由编译器自动生成。
命名元素
为了后台代码能处理元素,需要为元素起一个名字:
<Grid x:Name="grid1"
解析器为Window1
类添加一个名为grid1的字段:
private System.Windows.Controls.Grid grid1;
现在你可以使用grid1与Grid交互:
MessageBox.Show(String.Format("The grid is {0}x{1} units in size.",
grid1.ActualWidth, grid1.ActualHeight));
对于继承自FrameworkElement
的类来说,Name
特性前的x
前缀是可选的,去掉前面的x
不会改变代码的含义。
XAML的属性和事件
简单属性和类型转换
XAML元素的特性值总是一个普通文本字符串,而对象的属性可以是任何.net类型。
WPF定义了类型转换器,在普通文本字符串与.net类型之间建立了一座桥梁。
对于类型转换器,XAML解析器依次执行如下:
- 解析器查找属性声明的TypeConverter特性。
- 解析器查找相应类型声明的TypeConverter特性。
解析器没有找到TypeConverter,则会生成一个错误。
复杂属性
一般一个类的属性对应元素的一个特性,使用的是属性-特性语法(property-attribute syntax)。有时类的属性非常复杂,则使用属性-元素语法(property-element syntax)。
用属性元素语法,你添加一个带有Parent.PropertyName形式名字的子元素。例如,Grid有一个Background属性接受一个Brush对象,如果刷子比较复杂,你需要添加一个命名为Grid.Background的子标签,如下所示:
<Grid Name="grid1">
<Grid.Background>
...
</Grid.Background>
...
</Grid>
使这工作的关键细节是元素名字中的点号(.)。这是属性与其它类型的嵌套内容的根本区别。
然后,如何设置属性元素呢?答案是在嵌套元素内部,你能添加另一个标签实例化一个指定的类。例如,用一个坡度刷子设置背景。为了定义你希望的坡度,需要创造一个LinearGradientBrush对象。
使用XAML的规则,依靠使用带有LinearGradientBrush名字的一个元素,你能创造LinearGradientBrush对象:
<Grid Name="grid1">
<Grid.Background>
<LinearGradientBrush>
</LinearGradientBrush>
</Grid.Background>
...
</Grid>
下一步,你也需要指定坡度颜色。依靠用GradientStop对象的一个集合填充LinearGradientBrush.GradientStops属性。再一次,GradientStops属性太复杂的不能单独用一个特性值设置。代替,你需要依赖属性元素语法:
<Grid Name="grid1">
<Grid.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
...
</Grid>
最后,你能用一系列GradientStop对象填充GradientStops集合。每个GradientStop对象有一个Offset和Color属性。你能依靠使用普通的属性特性语法提供这二个值:
<Grid Name="grid1">
<Grid.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.00" Color="Red" />
<GradientStop Offset="0.50" Color="Indigo" />
<GradientStop Offset="1.00" Color="Violet" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
...
</Grid>
任何XAML标签集合都能被一套执行同样任务的代码语句替换。在此之前显示标签,用坡度填充背景,等价于下列代码:
var brush = new LinearGradientBrush();
var gradientStop1 = new GradientStop();
gradientStop1.Offset = 0;
gradientStop1.Color = Colors.Red;
brush.GradientStops.Add(gradientStop1);
var gradientStop2 = new GradientStop();
gradientStop2.Offset = 0.5;
gradientStop2.Color = Colors.Indigo;
brush.GradientStops.Add(gradientStop2);
var gradientStop3 = new GradientStop();
gradientStop3.Offset = 1;
gradientStop3.Color = Colors.Violet;
brush.GradientStops.Add(gradientStop3);
grid1.Background = brush;
标记扩展
标记扩展能用在嵌套标签中或在XML特性中。当他们用在特性中时,他们总是被花括号{}括起来。例如,这里是标记扩展的用法,这允许你引用另一个类的一个静态属性:
<Button x:Name="cmdAnswer" Foreground="{x:Static SystemColors.ActiveCaptionBrush}" />
标记扩展的语法是 {MarkupExtensionClass Argument}
,在这个例子中,标记扩展是StaticExtension类。(按照惯例,当引用一个扩展类时,你能丢弃最后的单词Extension)。
标记扩展的基类是System.Windows.Markup.MarkupExtension。它只有一个ProvideValue方法,用于获得所期望的类实例。前面的例子中,XAML解析器首先构造了一个StaticExtension类(传入字符串"SystemColors.ActiveCaptionBrush"作为构造函数的参数),然后调用类的ProvideValue()方法,获得由SystemColors.ActiveCaption.Brush静态属性所返回的对象。
前面例子的XAML块,等价于下面的代码:
cmdAnswer.Foreground = SystemColors.ActiveCaptionBrush;
因为标记扩展映射到类,它们也能作为嵌套属性被使用。例如,前面例子的等价表示法:
<Button x:Name="cmdAnswer">
<Button.Foreground>
<x:Static Member="SystemColors.ActiveCaptionBrush"></x:Static>
</Button.Foreground>
</Button>
依赖于标记扩展的复杂性和你希望设置的属性数目,这种语法有时更简单。
就像大多数标记扩展一样,StaticExtension
需要在运行时被估值,因为只有那时你才能决定当前的系统颜色。一些标记扩展能在编译时被估值。这包括NullExtension。
附加属性
除了普通的属性,XAML也包含附加属性的概念—此属性可以应用于几个控件,但是被定义在另一个类中。在WPF,附加属性被频繁用于控制布局。
当你放置一个控件到一个容器内部时,它获得附加的特征,依赖于容器的类型。(例如,如果你放置一个文本框到一个网格内部,你需要能选择它所处的网格单元格)这些附加的细节使用附加属性被设置。
附加属性总是使用两部分名字:DefiningType.PropertyName。这个两部分命名的语法将普通属性和附加属性区分开来。
例如,附加属性允许每个控件将自己放置在网格的行中:
<TextBox ... Grid.Row="0">
[Place question here.]
</TextBox>
<Button ... Grid.Row="1">
Ask the Eight Ball
</Button>
<TextBox ... Grid.Row="2">
[Answer will appear here.]
</TextBox>
附加属性不是真正的属性,它们被翻译为方法调用。XAML解析器调用形如DefiningType.SetPropertyName()的静态方法,例如,在先前XAML片段,定义类型是Grid类,属性是Row,因而解析器调用Grid.SetRow()。
当调用SetPropertyName()时,解析器传递二参数:被修改的对象和指定的属性值。例如,当你设置文本框控件的Grid.Row属性,XAML解析器执行这代码:
Grid.SetRow(txtQuestion, 0);
这个代码暗示行数保存在Grid对象中,但实际上,行数保存在txtQuestion文本框对象中。
因为所有WPF控件都是DependencyObject,并且DependencyObject被设计为依赖属性的无限集合。
事实上,以上代码只是下面代码的快捷方式:
txtQuestion.SetValue(Grid.RowProperty, 0);
附加属性是WPF的一个核心成分。他们充当一个多功能的可扩展性系统。例如,依靠定义Row属性作为一个附加属性,它保证能用于任何控件。
嵌套元素
XAML允许每个元素决定它如何处理嵌套元素。这种相互作用被中介通过三机制之一,按这个顺序被估值:
- 如果父元素实现IList,解析器调用IList.Add(子元素)
- 如果父元素实现IDictionary,解析器调用IDictionary.Add(子元素)。当使用一个词典集合,你必须也设置每个项目的x:Key特性一个关键字。
- 如果父元素被ContentProperty特性装饰,解析器使用子元素设置那属性。
例如,LinearGradientBrush能持有GradientStop对象的一个集合:
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.00" Color="Red" />
<GradientStop Offset="0.50" Color="Indigo" />
<GradientStop Offset="1.00" Color="Violet" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
XAML解析器知道LinearGradientBrush.GradientStops元素是一个复杂的属性因为它包含一个点号。但是,解析器处理标签内部(三GradientStop元素)有点不同。在这种情况下,它知道GradientStops属性返回一个GradientStopCollection对象,并且知道GradientStopCollection实现IList接口。如此,它假定(十分正确)每个GradientStop应该被添加到集合,依靠使用IList.Add()方法:
GradientStop gradientStop1 = new GradientStop();
gradientStop1.Offset = 0;
gradientStop1.Color = Colors.Red;
IList list = brush.GradientStops;
list.Add(gradientStop1);
一些属性可能支持一个以上类型的集合。在这种情况下,你需要添加一个说明集合类的标签,像这样:
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.00" Color="Red" />
<GradientStop Offset="0.50" Color="Indigo" />
<GradientStop Offset="1.00" Color="Violet" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
注意:如果集合默认为空,你需要包括说明集合类的标签,这样创造了集合对象。如果存在一个集合的默认实例,你只需要填充它,你能忽略那部分。
嵌套内容不总是指一个集合。例如,考虑Grid元素,它包含几个其它的控件:
<Grid Name="grid1">
...
<TextBox Name="txtQuestion" ... >
...
</TextBox>
<Button Name="cmdAnswer" ... >
...
</Button>
<TextBox Name="txtAnswer" ... >
...
</TextBox>
</Grid>
这些嵌套标签不是复杂属性因为他们没有包括点号。而且,Grid控件不是一个集合因为它没有实现IList或IDictionary。Grid真正支持的是ContentProperty特性,它是指可以接受任何嵌套内容的属性。从技术上,ContentProperty特性被应用于Panel类(Grid的基类),如同这样:
[ContentPropertyAttribute("Children")]
public abstract class Panel
这是指任何嵌套元素应该被用于设置Children属性。依赖于内容属性是否是一个集合属性(在这种情况下,它实现IList或IDictionary接口),XAML解析器处理它的方式有所不同。因为Panel.Children属性返回一个UIElementCollection,并且UIElementCollection实现IList,解析器使用IList.Add()方法添加嵌套内容到网格。
换句话说,当XAML解析器遇见之前的标记,它创造每个嵌套元素的一个实例并且使用Grid.Children.Add()方法传递它到网格:
txtQuestion = new TextBox();
...
grid1.Children.Add(txtQuestion);
cmdAnswer = new Button();
...
grid1.Children.Add(cmdAnswer);
txtAnswer = new TextBox();
...
grid1.Children.Add(txtAnswer);
接下来发生什么完全取决于控制如何实现内容属性。Grid显示它在一个看不见的行和列组成的布局中持有的所有控件。
ContentProperty特性频繁地被使用在WPF。不仅它被用于容器控件(诸如Grid)和包含视觉项目集合的控件(诸如ListBox和TreeView),它也被用于包含单个内容的控件。例如,文本框和按钮控件能持有单个元素或文本,但是他们都使用一个内容属性处理嵌套内容,像这样:
<TextBox Name="txtQuestion" ... >
[Place question here.]
</TextBox>
<Button Name="cmdAnswer" ... >
Ask the Eight Ball
</Button>
<TextBox Name="txtAnswer" ... >
[Answer will appear here.]
</TextBox>
TextBox
类使用ContentProperty
特性标记TextBox.Text
属性。Button
类使用ContentProperty
特性标记Button.Content
属性。XAML解析器使用提供的文本设置这些属性。
TextBox.Text属性只有允许字符串。但是,Button.Content属性接受任何元素。
因为Text和Content属性没有使用集合,你不能包括一个以上的内容。例如,如果你企图在一个按钮内部嵌套多个元素,XAML解析器将抛一个异常。如果你提供非文本内容(诸如一个长方形),解析器也抛出一个异常。
注意:ContentControl允许单个嵌套的元素,ItemsControl允许一个项集,Panel是布置一组控件的容器。ContentControl、ItemsControl和Panel基类都使用ContentProperty特性。他们的ContentProperty属性分别为:Panel.Children,TextBox.Text,Button.Content。
特殊字符和空白
见38页。
事件
特性除了映射到属性之外,特性也能被用于附加事件处理器。语法是EventName="EventHandlerMethodName"。
例如,为按钮附加点击事件处理器:
<Button ... Click="cmdAnswer_Click">
在许多情况下,你将在相同的元素上使用特性设置属性和附加事件处理器。WPF总是遵循相同的次序:首先它设置Name属性;然后它附加所有的事件处理器;最后它设置属性。这意味着当第一次设置属性时,就会触发相应的事件处理器。
使用其他名字空间
为使用一个没有定义在WPF名字空间中的类,你需要映射.NET名字空间到一个XML名字空间。
xmlns:Prefix="clr-namespace:Namespace;assembly=AssemblyName"
三个斜体的信息解释如下:
Prefix:这是你希望使用的XML前缀,是指在你XAML标记中的名字空间。例如,XAML语言使用x前缀。
Namespace:这是全限定的.NET名字空间名字。
AssemblyName:类型在这个装配体中被声明,没有.dll扩展名。你的工程必须引用这个装配体。如果你希望使用你的工程装配体,留空。
例如,这里是你将如何访问在System名字空间的基本类型,并映射他们到前缀sys:
xmlns:sys="clr-namespace:System;assembly=mscorlib"
这里是你将如何访问当前工程、在MyProject名字空间的你已经声明的类型,并映射他们到前缀local:
xmlns:local="clr-namespace:MyNamespace"
现在,为了创造一个位于某名字空间的类实例,你使用名字空间前缀:
<local:MyObject ...></local:MyObject>
在XAML使用的类必须有无参数的构造函数。另外,你只能使用类中公开的属性。XAML不允许你设置公开的字段或调用方法。
如果你试图创造一个原始类型(诸如一个字符串,日期,或数字的类型),你可以提供你数据的字符串表示法作为你标签的内容。XAML解析器将随后使用类型转换器转换字符串到合适的对象。
<sys:DateTime>10/30/2013 4:30 PM</sys:DateTime>
DateTime类使用TypeConverter特性链接它自己到DateTimeConverter。DateTimeConverter认识这字符串是一个有效的DateTime对象并且转换它。当你使用这技术,你不能使用特性设置你对象的属性。
装载和编译XAML
仅代码
纯代码WPF,常用于根据数据库记录填充窗口。动态添加或替换窗口控件。
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
public class Window1 : Window
{
private Button button1;
public Window1()
{
InitializeComponent();
}
private void InitializeComponent()
{
// Configure the form.
this.Width = this.Height = 285;
this.Left = this.Top = 100;
this.Title = "Code-Only Window";
// Create a container to hold a button.
var panel = new DockPanel();
// Create the button.
button1 = new Button();
button1.Content = "Please click me.";
button1.Margin = new Thickness(30);
// Attach the event handler.
button1.Click += button1_Click;
// Place the button in the panel.
IAddChild container = panel;
container.AddChild(button1);
//alternative
panel.Children.Add(button1);
// Place the panel in the form.
container = this;
container.AddChild(panel);
//alternative
this.AddChild(panel);
//alternative
this.Content = panel;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
button1.Content = "Thank you.";
}
}
启动窗口的代码:
public class Program : Application
{
[STAThread()]
static void Main()
{
Program app = new Program();
app.MainWindow = new Window1();
app.MainWindow.ShowDialog();
}
}
代码和未编译的XAML
一个任意的文本文件如"TextFile1.txt"
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button Name="button1" Margin="30">Please click me.</Button>
</DockPanel>
创建窗口的代码:
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
public class Window2 : Window
{
private Button button1;
public Window2(string xamlFile)
{
// Configure the form.
this.Width = this.Height = 285;
this.Left = this.Top = 100;
this.Title = "Dynamically Loaded XAML";
// Get the XAML content from an external file.
DependencyObject rootElement;
using (FileStream fs = new FileStream(xamlFile, FileMode.Open))
{
rootElement = (DependencyObject)XamlReader.Load(fs);
}
// Insert the markup into this window.
this.Content = rootElement;
// Find the control with the appropriate name.
button1 = (Button)LogicalTreeHelper.FindLogicalNode(rootElement, "button1");
//alternative
var frameworkElement = (FrameworkElement)rootElement;
button1 = (Button)frameworkElement.FindName("button1");
// Wire up the event handler.
button1.Click += button1_Click;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
button1.Content = "Thank you.";
}
}
启动窗口:
public class Program : Application
{
[STAThread()]
static void Main()
{
Program app = new Program();
System.Environment.CurrentDirectory = @"D:\Application Data\Visual Studio\Projects\WpfApplication2";
app.MainWindow = new Window2("TextFile1.txt");
app.MainWindow.ShowDialog();
}
}
代码和编译的XAML
当你编译一个WPF应用时,Visual Studio使用两阶段编译处理。第一阶段是编译XAML文件到BAML。例如,如果你的工程包含一个文件Window1.xaml,编译器将创造一个临时文件Window1.baml并且把它放在obj\Debug子文件夹中。与此同时,一个部分类被创造,为了你的窗口,使用你的选择的语言。例如,如果你使用C#,编译器将在obj\Debug文件夹中创造一个文件Window1.g.cs。g代表生成(generated)。
public partial class Window1 : System.Windows.Window,
System.Windows.Markup.IComponentConnector
{
// The control fields.
internal System.Windows.Controls.TextBox txtQuestion;
internal System.Windows.Controls.Button cmdAnswer;
internal System.Windows.Controls.TextBox txtAnswer;
private bool _contentLoaded;
// Load the BAML.
public void InitializeComponent()
{
if (_contentLoaded)
{
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("window1.baml",
System.UriKind.RelativeOrAbsolute);
System.Windows.Application.LoadComponent(this, resourceLocater);
}
// Hook up each control.
void System.Windows.Markup.IComponentConnector.Connect(int connectionId,
object target)
{
switch (connectionId)
{
case 1:
txtQuestion = ((System.Windows.Controls.TextBox)(target));
return;
case 2:
cmdAnswer = ((System.Windows.Controls.Button)(target));
cmdAnswer.Click += new System.Windows.RoutedEventHandler(
cmdAnswer_Click);
return;
case 3:
txtAnswer = ((System.Windows.Controls.TextBox)(target));
return;
}
this._contentLoaded = true;
}
}
仅XAML
没有创造任何代码,使用一个XAML文件,这被称为松XAML文件。松XAML文件能直接用ie浏览器打开。
为尝试一个松XAML页面,将正常的XAML文件做如下改动:
- 移除根元素的
Class
属性。 - 移除所有绑定事件处理器的属性。
Window
元素改为Page
元素。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章