使用.Net Core开发WPF App系列教程( 四、WPF中的XAML)
XAML介绍
XAML(Extensible Application Markup Language)(发音:zammel)可扩展应用程序标记语言。XAML是为构建应用程序用户界面而创建的一种新的描述性语言。XAML提供了一种便于扩展和定位的语法来定义和程序逻辑分离的用户界面,而这种实现方式和ASP.NET中的"代码后置"模型非常类似。XAML是一种解析性的语言,尽管它也可以被编译。它的优点是简化编程式上的用户创建过程,应用时要添加代码和配置等。
说明:
1、UWP中的XAML和WPF中的XAML会略有不同,本文介绍的所有XAML功能只适用于WPF
2、如果了解XML,学习XAML会感觉更轻松
XAML编译
XAML经过编译后,会生成BAML(Binary Application Markup Language 二进制应用程序标记语言)。BAML是标记化的,这样可以用较短的标记来代替XAML。
在项目的obj文件夹下,可以找到编译后的BAML文件。这是临时文件,当可以运行的dll生成后,BAML已经被嵌入到dll中去了。
说明:
BAML可以被ILSpy、.Net Reflactor这样的反编译工具反编译成XAML
XAML基础
当我们创建一个WPF工程后,打开MainWindow.xaml可以看到如下代码
<Window x:Class="WpfApp2.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:WpfApp2" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> </Grid> </Window>
该文档包含两个元素:Window元素及Grid元素
说明:
这里所指元素其实就是类,如Window类代表的是System.Windows.Window类
顶级Window元素代表整个窗口,Grid元素中可以放置任何控件。
通常来说,顶级元素一般为窗口(Window),页(Page),用户控件(UserControl)等。
与所有XML文档一致,在XAML文档中只能有一个顶级元素。
再查看Window元素,会发现有三个属性Title、Height、Width。这样是Window类所具备的属性,设置不同的属性,窗口也会发生相应的改变。
说明:
Window类的常用属性
Title | 窗口的标题 |
Width | 窗口的宽度 |
Height | 窗口的高度 |
Visibility | 指示窗口可见性 |
TopMost | 指定窗口是否是顶层窗口 |
WindowState | 指示窗口的状态(最大化,最小化等) |
WindowStyle | 指示窗口的样式(有无边框等) |
WindowStartupLocation | 指示窗口的启动位置 |
ShowInTaskbar | 指示窗口是否在状态栏显示 |
这里只介绍了一些常用的属性,完整的属性可以访问以下链接进行查看
https://docs.microsoft.com/en-us/dotnet/api/system.windows.window?view=netcore-3.1#properties
XAML命名空间
举个简单的栗子:
WPF内置控件有一个Button类,当我们引用了某个三方组件中,也有一个Button类,那这个时候,XAML是如何区分的呢?
我们可以看到Windows元素中有这样一段代码,这里其实就是对XAML命名空间进行声明。
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:WpfApp2"
xmlns特性是XML中的一个特性,用来声明命名空间。在WPF中,是依靠System.Windows.Markup.XmlnsDefinitionAttribute特性来实现这个功能。
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
上面这一行是WPF核心命名空间,它包含了WPF的类。这个命名空间没有使用前缀,所以整个文档都会使用这个命名空间
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"实际上对应的代码是:
1 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Controls"); 2 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Documents"); 3 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Shapes"); 4 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Shell"); 5 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Navigation"); 6 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Data"); 7 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows"); 8 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Controls.Primitives"); 9 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Media.Animation"); 10 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Input"); 11 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Media");
1 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
上面这一行是XAML命名空间,它包含了XAML的各种特性。这个命名空间使用了前缀x:,所以在使用时,也需要带上x:来使用该命名空间。
如
<x:MyElementName></x:MyElementName>
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"实际上对应的代码是:
1 System.Windows.Markup.XmlnsDefinitionAttribute("http://schemas.microsoft.com/winfx/2006/xaml", "System.Windows.Markup");
如何通过XAML命名空间引用三方组件或系统组件
引用三方或系统组件的过程是先引入命名空间前缀,再通过命名空间前缀使用该命名空间下的相应类。
这里以Nuget包BlurWindow为例,这个包里有一个窗体透明的BlurWindows类。
1、安装nuget包
2、引入命名空间前缀
1 xmlns:blur="clr-namespace:TianXiaTech;assembly=BlurWindow"
3、使用外部组件
<blur:BlurWindow x:Class="ImportXmlns.MainWindow"> ...... </blur:BlurWindow>
可以在本文末查看示例代码
后台代码类
XAML只用于构造用户界面,那它的事件处理程序代码在哪呢?
一般我们在WPF中添加窗口时,可以到一个xxx.xaml会带一个xxx.xaml.cs,这个xxx.xaml.cs就是后台代码类,用于处理事件。
而这个这台代码类是通过Class特性来进行连接的。Class带了前缀x,说明这里是XAML语言中通用的部分
1 <Window x:Class="xClass.MainWindow"
这个后台代码类在创建窗口时,会自动生成,我们可以看一下它的结构
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 } 7 }
可以看到这是一个分部类(partial class)
说明:
虽然这是一个分部类,但建议不要创建多个后台代码类文件,事件的逻辑代码全部写在默认的.cs文件里即可。
这个类比较简单,只有一个构造函数,构造函数里执行了
InitializeComponent();
这个 InitializeComponent() 的作用就是调用System.Windows.Application类的LoadComponent()方法从程序集中提取BAML,并用它来构造用户界面。当解析BAML时,会创建控件对象,设置其属性并关联所有事件处理程序
如果不执行InitializeComponent() ,UI就不会被正常加载 ,而只显示默认空白窗口。所以当我们自己添加构造函数时,也要确保执行了InitializeComponent()
命名元素
可以通过x:Name或Name来对元素进行命名,以便在后台代码类中进行操作。
1 window.Title = "Naming Element"; 2 3 grid.Background = Brushes.LightSkyBlue;
使用Name命名后,系统实际上为我们生成了以下代码
说明:
Name和x:Name的区别
Name:这个元素包含Name属性,就可以直接使用Name属性对这个元素进行命名。
x:Name: x是实际上引入了XAML的相关功能,所以当一个元素没有Name属性时,就可以使用x:Name来对元素进行命名。概括来说就是x:Name是XAML的功能,是通用的。不管元素有没有Name属性
所以不管元素有没有Name属性,都使用x:Name来进行命名肯定是不会错的
XAML中的属性
属性
属性我们在前面已经有一些简单的介绍,像下面这样
1 <Label Content="WPF Property" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="20" FontWeight="Bold" Foreground="LightSkyBlue" FontFamily="Arial"/>
说明:
目前暂时只介绍属性如何使用,而不会针对某个控件。控件的使用会在后面的文章中说明
类型转换器
XAML属性的值总是纯文本字符串,但对象的属性可以是任何.Net类型。在上面的示例中,
HorizontalAlignment和VerticalAlignment使用的是枚举类型(System.Windows.Horizontalalignment 和 System.Windows.VerticalAlignment)
FontFamily使用的是字符串类型
FontSize使用的是整型
ForeGround使用的是Brush类型
那系统是如何从纯文本字符串转换到对应类型的呢? 这里就用到了类型转换器(TypeConverter)
XAML通过以下两个步骤来查找类型转换器:
1、检查属性声明。查找TypeConverter特性。这个特性可以指示,使用哪个类来进行转换
2、如果在属性声明中没有TypeConverter特性,XAML解析器将检查对应数据类型的类声明。例如,Foreground属性使用的是Brush对象,那XAML解析就会去检查Brush类是否使用TypeConverter特性修饰。
查找System.Windows.Media.Brush源码,可以看到,Brush类使用了TypeConverter(typeof(BrushConverter))进行修饰,所以Brush及其子类都会使用BrushConverter类进行转换
1 [Localizability(LocalizationCategory.None, Readability=Readability.Unreadable), ValueSerializer(typeof(BrushValueSerializer)), TypeConverter(typeof(BrushConverter))] 2 public abstract class Brush : Animatable, IFormattable, DUCE.IResource 3 { 4 ...... 5 }
如果属性声明或类声明都没有与其关联的类型转换器,XAML解析器就会报错。
说明:
XAML元素是case sensitive(大小写敏感)
而元素的属性不是case sensitive
嵌套元素
XAML文档就是一颗嵌套的元素树。
可以看到下面的示例代码:
Window元素包含了Grid元素,Grid元素包含了Button、Label、TextBox元素。
1 <Window> 2 <Grid> 3 <Button Content="OK"/> 4 <Label Content="label"/> 5 <TextBox/> 6 </Grid> 7 </Window>
XAML让每个元素决定如何处理嵌套元素,处理顺序如下:
1、如果父元素实现了IList接口,解析器将调用IList.Add()方法,并且为该方法传入子元素作为参数
2、如果父元素实现了IDictionary接口,解析器将调用IDictionary.Add()方法,并且为该方法传递子元素作为参数。 当使用字典集合时,需要设置x:Key特性,以便指定键名。
3、如果父元素使用ContentProperty特性进行修饰,解析器将使用子元素设置相应的属性
Grid控件支持ContentProperty特性,这个特性是继承自System.Windows.Controls.Panel类。可以看到Panel类的部分声明如下:
[Localizability(LocalizationCategory.Ignore), ContentProperty("Children")] public abstract class Panel : FrameworkElement, IAddChild
这表示可以使用任何子元素或内部文本来设置Children的属性。XAML解析器根据是否是集合属性(集合实现了IList或IDictionary接口),采用不同方式处理内容属性。因为Panel.Children属性返回一个UIElementCollection对象,而UIElementCollection类又实现了IList接口,所以解析器使用IList.Add()方法将嵌套的内容添加到Grid控件中
上面的XAML代码实际上对应以下的C#代码
1 Grid grid = new Grid(); 2 3 Button button = new Button(); 4 button.Content = "OK"; 5 6 Label label = new Label(); 7 label.Content = "label"; 8 9 TextBox textBox = new TextBox(); 10 11 grid.Children.Add(button); 12 grid.Children.Add(label); 13 grid.Children.Add(textBox); 14 15 this.Content = grid;
说明:
在WPF中,会经常使用ContentProperty特性。
就Grid而言,Grid类使用ContentProperty特性来标识Panel.Children属性。
而其它控件会用 ContentProperty来标识 其它属性。
如Button类会使用ContentProperty特性标识 Button.Content属性(System.Windows.Controls.Button类继承自System.Windows.Controls.ContentControl)
1 [DefaultProperty("Content"), ContentProperty("Content")] 2 public class ContentControl : Control, IAddChild
复杂属性
有了类型转器的理论基础后,又遇到了一个新的问题。那就是如果属性需要的对象自己还拥有一组属性,这种情况应该怎么办?
XAML提供了property-element syntax(属性元素语法)。这里也比较简单
例如:Grid控件有一个Background属性,如果我们想使用更复杂的Brush,就需要用到这种语法,
如下:
<Grid> <Grid.Background> <ImageBrush ImageSource="xx.jpg"/> </Grid.Background> </Grid>
目前我们先不用管这些代码实现了什么功能,只需要了解属性元素语法这种语法格式就可以了。
property-element syntax最核心的就是这个 (.)符号,它把属性和嵌套内容区分开来。
扩展标记
当我们需要将属性值设置为一个已经存在的对象,或者希望将属性值绑定到另一个控件来动态地设置属性值。这个时候就需要使用标记扩展。
语法如下:
{标记扩展类 参数}
如:
1 <Label Foreground="{x:Static SystemColors.SystemColors.ControlLightBrush}"></Label>
XAML中定义了以下扩展标记
扩展标记 | 功能说明 |
x:Type | 为命名类型提供 Type 对象。 此扩展最常用于样式和模板。在后面的文章中会经常用到此扩展标记 |
x:Static | 生成静态值。 这些值来自于值类型代码实体,它们不直接是目标属性值的类型,但可以计算为该类型。上面的示例代码演示了如何使用x:Static |
x:Null |
null 指定为属性的值,可用于特性或属性元素值。有时候我们不想使用默认值或继承的值,就需要使用x:Null来进行置空 |
x:Array | 这个用得比较少,不详细介绍 了。可以访问以下链接了解详细 https://docs.microsoft.com/en-us/dotnet/desktop-wpf/xaml-services/xarray-markup-extension |
WPF中定义了以下扩展标记(仅限WPF)
StaticResource |
通过替换已定义资源的值来为属性提供值。 StaticResource 计算最终在 XAML 加载时进行,并且在运行时没有访问对象图的权限 |
DynamicResource |
通过将值推迟为对资源的运行时引用来为属性提供值。 动态资源引用强制在每次访问此类资源时都进行新查找,且在运行时有权访问对象图。 为了获取此访问权限,WPF 属性系统中的依赖项属性和计算出的表达式支持 DynamicResource 概念。 |
Binding |
使用在运行时应用于父对象的数据上下文来为属性提供数据绑定值。 此标记扩展相对复杂,因为它会启用大量内联语法来指定数据绑定 |
RelativeSource | 提供用于Binding的源信息,该信息可以在运行时对象树中导航多个可能的关系。 对于在多用途模板中创建的绑定,或在未充分了解周围的对象树的情况下以代码创建的绑定,此标记扩展为其提供专用源 |
TemplateBinding | 使控件模板能够使用模板化属性的值,这些属性来自于将使用该模板的类的对象模型定义属性 |
ColorConvertedBitmap |
这个用得比较少,可以访问以下链接了解详细https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/colorconvertedbitmap-markup-extension |
ComponentResourceKey |
这个用得比较少,可以访问以下链接了解详细https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/componentresourcekey-markup-extension |
ThemeDictionary |
这个用得比较少,可以访问以下链接了解详细https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/themedictionary-markup-extension |
附加属性
附加属性是 XAML 中引入的一种编程概念,
附加属性可以理解为:这种属性可用于多个控件,但这一类属性不是在控件内部定义,而是在其它类中定义。
附加属性常用于控件布局。自己封装控件时,也会用到。
附加属性语法格式如下:
typeName.propertyName
附加属性的工作原理如下:
每个控件都拥有自己内部定义的属性。但当在容器中放置控件时,控件会根据容器的类型而获得额外特性。这时就需要使用附加属性来设置这些附加的细节
例如我们在布局时,常用一种结构如下
1 <Grid> 2 <Grid.RowDefinitions> 3 <RowDefinition/> 4 <RowDefinition/> 5 </Grid.RowDefinitions> 6 7 <Grid.ColumnDefinitions> 8 <ColumnDefinition/> 9 <ColumnDefinition/> 10 </Grid.ColumnDefinitions> 11 12 <!--通过附加属性来设置在Grid的哪一行,哪一列--> 13 <Label Content="HelloWorld" Grid.Row="1" Grid.Column="1"/> 14 </Grid>
附加属性是一个编程概念,并不是真正的属性。我们在设置附加属性的时候,这们实际上被转换为方法调用。
格式如下
DefiningType.SetPropertyName()
1 Grid.SetRow(label, 1); 2 Grid.SetColumn(label, 1);
当调用SetPropertyName()方法时,解析器传递两个参数:被修改的对象,指定的属性值。
说明:
如果是刚开始接触WPF,可能不好理解附加属性这一概念。目前来说,只需要学会在XAML中使用即可,随着学习的深入,我们会慢慢理解 这一概念。
XAML中的事件
在XAML中,为控件关联事件处理程序比较简单。语法格式为:事件名=“事件处理程序方法名”
如Button的Click事件,在XAML中:
1 <Button Content="OK" Click="Button_Click"/>
在后台代码中可以看到系统生成的事件处理程序
说明:
1、在Visual Studio 2019中,键入 事件名=后,Visual Studio 2019提示生成默认的事件处理程序,这个时候我们点击回车,就可以生成相关代码
2、如果需要修改事件处理程序方法名,可以在手动输入方法名后,按F12,系统会帮我们生成相关代码。
XAML中的特殊符号
XAML中的特殊符号和XML里是一样的,可参考以下链接
https://www.cnblogs.com/zhaotianff/p/12469944.html
推荐阅读:
https://www.w3school.com.cn/xml/index.asp
https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/xaml-syntax-in-detail
本文示例代码:
https://github.com/zhaotianff/DotNetCoreWPF/tree/master/四、WPF中的XAML