XAML C# WPF

XAML定义

  XAML是一种相对简单、通用的声明式编程语言,它适合于构建和初始化.NET对象。

  XAML仅仅是一种使用.NET API的方式,把它与HTML、可伸缩向量图形(SVG)或其他特定领域的格式或语言作比较是完全错误的。XAML由一些规则(告诉解析器和编译器如何处理XML)和一些关键字组成,但它自己没有任何有意义的元素。因此,如果在没有WPF这样的框架的基础上讨论XAML,就如同在没有.NET Framework的基础上讨论C#一样。

  XAML在WPF中扮演的角色通常是令人困惑的,因此第一件要搞清楚的事情是WPF和XAML可以独立使用,它们并不是互相依赖的。虽然XAML最初是为WPF而设计,但它也可以应用于其他技术(如WF)。由于XAML的通用性,实际上可以把它应用于任何.NET技术。然而,是否在使用WPF时使用XAML是可选的,每一件XAML能做的事情完全可以由任何一种你喜欢的.NET语言来实现(但反过来则不行)。但是,由于XAML的诸多好处,很少会看到现实世界中使用WPF却不使用XAML的情况。

元素和特性

  XAML规范定义了一些规则,用于把.NET命名空间、类型、属性和事件映射为XML命名空间、元素和特性。以下面为例,它定义了一个WPF按钮,跟另一段与之功能一致的C#代码比较一下:

  XAML:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Content="OK" Click="button_Click"/>

  C#:

System.Windows.Controls.Button b = new System.Window.Controls.Button();
b.Content = "OK";
b.Click += new System.Windows.RoutedEventHandler(button_Click);

  虽然这两段代码功能是相同的,但如果去除掉XAML中的Click特性,你可以很快地在IE浏览器中查看XAML,还会看到一个活生生的按钮放在浏览器窗口中。而C#代码则必须要额外的代码编译方可使用。

  在XAML中定义一个XML元素(叫作对象元素)与在.NET中实例化一个对应的对象(总是使用默认的构造函数)是等价的。设置对象元素的一个特性(attribute),与设置一个同名属性(property attribute,称为属性特性)或者为一个同名事件设置一个事件处理程序(也称为事件特性),也是等价的。

生成和事件处理的顺序

  在运行时(run-time)模式下,为任何一个XAML声明的对象设置属性之前,总要添加一些事件处理程序,这样就可以让某个事件在属性被设置时被触发,而不用担心XAML使用特性的顺序。

  至于多个属性集或添加多个事件处理程序,它们总会遵照一定顺序,即属性特性和事件属性是在对象元素中指定的。这一排序方式不会在实际应用中产生影响,因为.NET设计指南指出:类应该允许以任何顺序设置属性,添加事件处理程序也是如此。

命名空间

  比较上述XAML代码示例和相应的C#代码示例,最神秘的地方在于XAML命名空间(http://schemas.microsoft.com/winfx/2006/xaml/presentation)是如何被映射到.NET命名空间(System.Windows.Controls)上的。该映射及其他WPF命名空间的映射是在WPF程序集中硬编码完成的,里面有好几个Xmlns-DefinitionAttribute自定义特性的实例。(在schemas.microsoft.com这个URL中不存在网页,这仅仅是一个人为设定的字符串,就像其他命名空间一样。)

  XAML文件的根对象元素属性指定至少一个XML命名空间,用于验证自己和子元素。你可以(在根元素或子元素上)声明额外的XML命名空间,但每一个命名空间下的标识符都必须有一个唯一的前缀。例如,WPF的XAML文件都会使用第二个命名空间加上前缀x(记作xmlns:x而不仅仅是xmlns):

  xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml

  这是XAML语言命名空间,用于映射System.Windows.Markup命名空间中的类型,而且它也定义了XAML编译器或解析器中的一些特殊的指令。这些指令通常是作为XAML元素的特性出现的,因此,它们看上去像宿主元素的属性,但实际上并不是如此。

  我们把http://schemas.microfost.com/winfx/2006/xaml/presentation作为默认(主要)命名空间,把http://schemas.microsoft.com/winfx/2006-/xaml作为次要命名空间。次要命名空间的前缀是x,这仅仅是一个规则,就像C#文件要以using System;指令开始一样。你可以改写原来那个XAML文件,含义是相同的:

Button xmlns:WpfNamespace="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Content="OK"/>

  当然,从可读性来讲,在使用这些常见的命名空间的时候不需要前缀(即原始的XML命名空间),其他一些命名空间则使用一个短前缀。

属性元素

  富创建是WPF的亮点之一,我们可以用Button来演示。你可以把任意内容放在Button里面,不仅限于文本,如下所示(在Button中嵌入了一个简单的方形来做一个VCR的停止按钮):

System.Windows.Controls.Button b = new System.Windows.Controls.Button();
System.Windows.Shapes.Rectangle r = new System.Windows.Shapes.Rectangle();
r.Width = 40;
r.Height = 40;
r.Fill = System.Windows.Media.Brushes.Black;
b.Content = r;  //将按钮中的内容设置为方格

  Button的Content属性是System.Object类型的,因此它很容易被设置到40*40的Rectangle对象。但如何才能在XAML中用属性特性语法做相同的事呢?你该为Content属性设置哪种字串才能完成C#中声明的Rectangle功能呢?没有这样的字串,但XAML提供了一种替代的语法来设置复杂的属性值,即属性元素。如下所示:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button.Content>
    <Rectangle Height="40" Width="40" Fill="Black"/>
Button.Content>
Button>

  Content属性被设置为一个XML元素而不是XML特性,Button.Content中的句点用于区分对象元素(object element)与属性元素(property element)。它们总会以“类型名,属性名TypeName.PropertyName”的形式出现,总会包含在“类型名”对象元素中,但它们没有属于自己的特性。

  属性元素语法也可以用于简单的属性值。下面的Button使用特性设置了两个属性,它们是Content和Background:

<Button xmlns="http://schema.microsoft.com/winfx/2006/xaml/presentation" Content="OK" Background="White"/>

  这等同于使用元素设置该Button的两个相同的属性:

复制代码
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button.Content>
     OK 
Button.Content>
<Button.Background>
     White
Button.Background>
Button>
复制代码

  当然,在任何可以使用特性的地方使用特性更为便捷。

类型转换器

  上例XAML文件中的“white”是如何与C#中的System.Windows.Media.Brushes.White等价的呢?这个示例提供了一些如何使用字符串设置XAML属性的细节,这些属性的类型即不是System.String,也不是System.Object。在这种情况下,XAML解析器或编译器必须寻找一个类型转换器,该转换器知道如何将一个字符串表达式转换为一种想要的数据类型。WPF提供了许多常用数据类型的类型转换器,如Brush、Color、FontWeight、Point等,它们都派生自Type-Converter的类(如BrushConverter、ColorConverter等),你也可以为自定义的数据类型写类型转换器。与XAML语言不同,类型转换器通常支持不区分大小写的字符串。

  如果没有Brush类型转换器,你就必须使用属性元素语法来设置XAML中的Background属性,如下所示:

复制代码
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Content="OK">
<Button.Background>
    <SolidColorBrush>
    <SolidColorBrush.Color>
        <Color A="255" R="255" G="255" B="255"/>
    SolidColorBrush.Color>
    SolidColorBrush>
Button.Background>
Button>
复制代码

  由上可见,类型转换器不仅增强了XAML的可读性,也使一些本来不能被表达的概念得以表达。

  下面的代码更精确地表达了运行时获取和执行适合Brush的类型转换器的过程:

System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Content = "OK";
b.Background = (Brush)System.ComponentModel.TypeDescriptor.GetConverter(typeof(Brush)).ConvertFromInvariantString("White");

标记扩展

  标记扩展就像类型转换器一样,可用于扩展XAML的表达能力。它们都可以在运行时计算字符串特性的值,并生成一个合适的基于字符串的对象。WPF有好几个内建的标记扩展,你会发现它们都派生自本书最前面的内封中的MarkupExtension。

  与类型转换器不同的是,标记扩展是通过XAML的显式的、一致的语法调用的,因此,标记扩展是最好的扩展XAML的方法。

  只要特性值由花括号括起来,XAML编译器或解析器就会把它认作一个标记扩展值而不是一个普通的字符串。下面的按钮使用了3个不同的标记扩展类型,其中分别用到了3个不同的特性:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
Background="{x:Null}"
Height="{x:Static SystemParameters.IconHeight}"
Content="{Binding Path=Height, RelativeSource={RelativeSource Self}}"/>

  每个花括号中的第一个识别符是标记扩展类的名称。按照惯例,这样的类都以Extension后缀结尾,但在XAML中使用它时,可以不用该后缀。在上面例子中,NullExtension(即x:Null)和StaticExtension(即x:Static)是System.Windows.Markup命名空间的类,因此必须使用前缀x来定位它们。Binding(没有Extension后缀)是在System.Windows.Data命名空间下的,因此在默认的XML命名空间下就可以找到它。

  如果标记扩展支持,可使用逗号分隔的参数来指定它的值。

  定位参数(如本例中的SystemParameters.IconHeight)被作为字符串参数传入扩展类的相应构造函数中。命名参数(如本例中的Path和RelativeSource)可用来在已构造好的扩展对象上设置相应名字的属性。这些属性的值可以是标记扩展值自己,也可以是文本值,它们可通过普通的类型转换过程。你可能注意到设计和使用标记扩展与设计和使用自定义特性很相似,这是被有意设计的。

  在本例中,NullExtension允许设置Background笔刷为null。StaticExtension允许使用静态属性、字段、常量和枚举值,而不使用XAML写的硬编码字面值。在这个例子中,Button的高度是遵循操作系统当前的图标高度设置的,这一设置可通过System.Windows.SystemParameters类的IconHeight静态字段获得。Binding可以把Content设置为与它的Height属性相同的值。

  如果你需要设置一个属性特性值为字面字符串(以左花括号开始),就必须将其转义,我们可通过在其前面增加一对空花括号来实现,如:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
Content="{}{This is not a markup extension!}"/>

  此外,也可以使用属性元素语法,因为花括号在上下文中不会有特殊的意义:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    {This is not a markup extension}
Button>

  因为标记扩展是有默认构造函数的类,它们可以与属性元素语法一起使用,如下例:

复制代码
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button.Background>
    <x:Null/>
Button.Background>
<Button.Height>
    <x:Static Member="SystemParameters.IconHeight"/>
Button.Height>
<Button.Content>
    <Binding Path="Height">
    <Binding.RelativeSource>
        <RelativeSource Mode="Self"/>
    Binding.RelativeSource>
    Binding>
Button.Content>
Button>
复制代码

  该转换之所以可以执行是因为这些标记扩展都有与形参化的构造函数的实参(使用属性特性语法的定位实参)对应的属性。例如,StaticExtension有一个Member属性与之前传入到开参化构造函数中的实参意思是一样的,RelativeSource有一个对应于构造函数实参的Mode属性。

  由于标记扩展完成的实际工作对于每个扩展都是不同的。如下面的C#代码与使用NullExtension、StaticExtension和Binding的基于XAML表示的按钮是一个意思:

复制代码
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
//设置Background
b.Background = null;
//设置Height
b.Height = System.Windows.SystemParameters.IconHeight;
//设置Content
System.Windows.Data.Binding binding = new System.Windows.Data.Binding();
binding.Path = new System.Windows.PropertyPath("Height");
binding.RelativeSource = System.Windows.Data.RelativeSource.Self;
b.SetBinding(System.Windows.Controls.Button.ContentProperty, binding);
复制代码

  尽管如此,这里的代码与XAML解析器或编译器使用的机制是不同的,解析器和编译器是依靠每个标记扩展在运行时设置合适的值(本质上是通过调用每个类的ProvideValue方法来实现的)。与这一机制完全对应的过程式代码通常很复杂。

对象元素的子元素

  XAML文件就像所有的XML文件一样,必须有一个单独的根对象元素。对象元素是可以支持子对象元素的。一个对象元素可以有3种类型的子元素:一个内容属性值,集合项,或一个能够通过类型转换到它的父元素的值。

内容属性

  大多数WPF类(通过定制特性)指定了一个属性,该属性可以被设置为XML元素中的任何内容。这个属性叫作内容属性,它确实是一个让XAML呈现变得更轻便简单的捷径。从某种意义上讲,这些内容属性有点像VB中的默认属性。Button中的Content属性就是这样指定的,如:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Content="OK"/>

  可以被重写为:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
OK
Button>

  还有更复杂的方式:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button.Content>
    <Rectangle Height="40" Width="40" Fill="Black"/>
Button.Content>
Button>

  可以被重写为:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <Rectangle Height="40" Width="40" Fill="Black"/>
Button>

集合项

  XAML允许将项添加到支持索引的两种类型的集合中:List和Dictionary。

List

  List是实现了System.Collection.IList接口的集合,如System.Collections.ArrayList和许多WPF定义的集合类都是List。如,下面的XAML向ListBox添加了两个项,它的Items属性是实现了IList的ItemCollection类型:

<ListBox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> 
<ListBox.Items>
     <ListBoxItem Content="Item 1"/>
     <ListBoxItem Content="Item 2"/> 
ListBox.Items> 
ListBox>

  因为Items是ListBox的内容属性,可以进一步简化:

<ListBox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <ListBoxItem Content="Item 1"/>
    <ListBoxItem Content="Item 2"/>
ListBox>

Dictionary

  System.Windows.ResourceDictionary是WPF中的一个常用的集合类型,它实现了System.Collections.IDictionary接口,能够支持在过程式代码中添加、移除枚举键/值对。下面的XAML添加了两个Color对象到一个ResourceDictionary中。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Color x:Key="1" A="255" R="255" G="255" B="255"/>
    <Color x:Key="2" A="0" R="0" G="0" B="0"/>
ResourceDictionary>

  注意,在带有x:Key的XAML中指定的值总是被作为字符串处理的,除非使用标记扩展,但不会尝试使用类型转换。

  XAML是设计用来与.NET类型系统一起工作的,你可以在其中使用任何类型的.NET对象,也可以使用自己定义的对象。但对象必须以“友好声明”的方式进行设计。如果一个类没有默认构造函数,也没有提供有用的实例属性,那么它在XAML中是无法直接使用的。

  WPF程序集都被加上了XmlnsDefinitionAttribute属性,这样可以将.NET命名空间映射为XAML文件中的XML命名空间,但对于那个不是专门为XAML设计的程序集又该如何处理呢?它们的类型仍然可以使用,只需要一个特殊的指令作为XML命名空间就可以了。如下例:

System.Collections.Hashtable h = new System.Collections.Hashtable();
h.Add("key1", 1);
h.Add("key2", 2);

  以上代码在XAML中表示为:

<collections:Hashtable xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <sys:Int32 x:Key="key1">1sys:Int32>
    <sys:Int32 x:Key="key2">2sys:Int32>
collections:Hashtable>

  clr-namespace标记允许直接在XAML中放入一个.NET命名空间。

  为了避免混淆,在转换子元素时,任何一个有效的XAML解析器或编译器必须遵循下面的规则:

  1. 如果该类型实现了IList接口,就为每个子元素调用IList.Add。
  2. 否则,如果该类型实现了IDictionary,就为每个子元素调用IDictionary.Add,在该值的键和元素中使用x:Key特性值。
  3. 否则,如果父元素支持内容属性(由System.Windows.Markup.ContentPropertyAttribute表示),而且子元素的类型与该内容属性是兼容的,就把子元素作为它的值。
  4. 否则,如果子对象是普通文本,且有类型转换器将子对象转换为父类型(没有在父元素上设置属性),则把子元素作为类型转换器的输入,将输出作为父对象的实例。
  5. 其他情况下则抛出错误。

编译:将XAML与过程式代码混合使用

  大多数WPF应用程序是XAML与过程式代码的混合体。

在运行时加载和解析XAML

  WPF的运行时XAML解析器公开为两个类,它们都位于System.Windows.Markup命名空间中:XamlReader和XamlWriter。XamlReader包含了一些对静态Load方法的重载,而XamlWriter包含了一些对静态Save方法的重载。因此,用任何一种.NET语言写的程序都可以在运行时依赖XAML,而不用程序员付出太多努力。

XamlReader

  XamlReader.Load方法的设置将解析XAML,创建合适的.NET对象,然后返回一个根元素的实例。因此,如果在当前目录下有一个XAML文件叫作MyWindow-.xaml,它包含了一个Window对象作为根结点,那么可以使用下面的代码来加载和获得Window对象。

Window window = null;
using(FileStream fs = new FileStream("MyWindow.xaml", FileMode.Open, FileAccess.Read))
{
    //获得根元素,该元素是一个Window对象
    Window = (Window)XamlReader.Load(fs);
}

  在Load返回之后,整个XAML文件的对象层级将在内存中被实例化,因此就不再需要XAML文件了。退出using代码块后,FileSteam将被立即关闭。由于可向XamlReader传入一个任意的Stream(或使用另一个重载来传入System.Xml.XmlReader对象),所以有许多可选择的方式来获得XAML的内容。

  XamlReader也定义了LoadAsync实例方法用于异步加载和解析XAML内容。在加载大文件或网络文件时,可使用LoadAsync保持用户界面处于响应状态。CancelAsync方法用于停止处理,LoadCompleted事件可让我们知道处理何时完成。

  既然已有一个根元素的实例存在,就可利用适当的内容属性或集合属性来获得子元素。下面的代码假设Window有一个类行为StackPanel的子元素,Stack-Panel的第5个子对象是一个OK Button。

复制代码
Window window = null;
using(FileStream fs = new FileStream("MyWindow.xaml"), FileMode.Open, FileAccess.Read))
{
    //获得根元素,该元素是一个Window对象
    window = (Window)XamlReader.Load(fs);
}
//通过(硬编码知识)遍历子元素获取OK按钮
StackPanel panel = (StackPanel)window.Content;
Button okButton = (Button)panel.Children[4];
复制代码

  有了这个Button的引用,就可以做任何想做的事:设置额外的属性,添加事件处理程序,或执行一些无法用XAML完成的动作。

  但使用硬编码索引和其他关于用户界面结构假设的代码并不能让人满意。XAML支持元素命名,这样就可以从过程式代码中找到这些元素并放心地使用它们。

命名XAML元素

  XAML语言命名空间有一个Name关键字,它是用来给元素命名的。示例如下:

<Button x:Name="okButton">OKButton>

  上例代码可改如下:

复制代码
Window window = null;
using(FileStream fs = new FileStream("MyWindow.xaml"), FileMode.Open, FileAccess.Read)) 
{
     //获得根元素,该元素是一个Window对象
    window = (Window)XamlReader.Load(fs); 
} 
//通过按钮的名称获得OK按钮Button 
okButton = (Button)window.FindName("okButton");
复制代码

  FindName并不仅仅在Window类中存在,在FrameworkElement、FrameworkContentElement及许多重要的WPF类的基类中也有FindName的定义。

编译XAML

  XAML编译包括三项事件:

  1. 将一个XAML文件转换为一种特殊的二进制格式。
  2. 将转换好的内容作为二进制资源嵌入到正在被创建的程序集中。
  3. 执行链接操作,将XAML和过程式代码自动连接起来。

  如果你不在乎将XAML文件和过程式代码融合,那么只需把它添加到VS的WPF项目中来,并用界面中的Build动作来完成编译即可。但如果要编译一个XAML文件并将它与过程式代码混合,第一步要做的就是为XAML文件的根元素指定一个子类,可以用XAML语言命名空间中的Class关键字来完成。例如:

"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyNamespace.MyWindow">
...

  在一个独立的源文件中(但是在同一个项目中),可以一个子类,并添加任何想添加的成员:

复制代码
namespace MyNamespace
{
    partial class MyWindow : Window
    {
        public MyWindow
        {
            //一定要调用,这样才能加载XAML定义的内容
            InitializeComponent();
            ...
        }
        Any other members can go here...
    }
}
复制代码

  通常我们把这样的文件叫作代码隐藏文件。如果你引用XAML中的任何一个事件处理程序(通过事件特性,如Button的Click特性),这里就是我们定义这些事件处理程序的地方。

  当在VS中创建一个基于WPF的C#或VB项目,或当使用“Add New Item...”来添加某WPF项目时,VS会自动创建一个XAML文件,并把x:Class作为根元素,同时创建一个具有部分类定义的代码隐藏源文件,最后把两者连接起来,这样代码构建(build)才能顺利进行。

BAML

  BAML是Binary Application Markup Language的缩写,意思是二进制应用程序标记语言,它其实是被解析、标记化,最后转换为二进制形式的XAML。虽然大块的XAML代码可以被表示为过程式代码,但XAML到BAML的编译过程不会生成过程源代码。因此,BAML不像MSIL,它是一个压缩的声明格式,比加载和解析普通的XAML文件快,且比普通XAML文件要小。BAML仅仅是XAML编译过程的详细实现,没有任何直接公开的方法,在未来可能会被一些其他东西所取代。

生成的源代码

  x:Class只能在要编译的XAML文件中使用。但在没有x:Class的情况下,编译XAML文件也是没有问题的。这意味着没有对应的代码隐藏文件,因此不能使用任何需要过程式代码才能实现的特性。

  每个生成的源文件中包含了一个由根对象元素中的x:Class指定的类的部分类定义。XAML文件中的每个已命名的元素在该部分类中都有一个成员(默认是私有的),这些成员的名称就是元素名称。其中还有一个InitializeComponent方法用于完成一大堆烦人的工作。

BAML可以反编译为XAML吗?

  可以,无论怎么声明,任何一个公司的.NET类实例都可以被序列化为XAML。第一个步骤是获取一个实例,这个实例是用来作为根对象的。如果你还没有这个对象,可以调用静态的System.Windows.Application.LoadComponent方法,如下所示:

System.Uri uri = new System.Uri("MyWindow.xaml", System.UriKind.Relative);
Window window = (Window)Application.LoadComponent(uri);

  由URI指定的名称并不要求物理上存在一个独立的xaml文件。当指定了一个合适的URI后,LoadComponent可以自动获得作为资源嵌入的BAML。实际上,VS自动生成的InitializeComponent方法就是调用Application.LoadComponent来加载嵌入的BAML。

  获得根元素的实例后,可以使用System.Windows.Markup.XamlWrite类来获得根元素的XAML表示。XamlWriter包含了5个静态Save方法的重载,是最简单的返回一个适当XAML字符串的方法,通过传入一个对象实例来实现。如:

string xaml = XamlWriter.Save(window);

XAML关键字

  XAML语言的命名空间(http://schemas.microsoft.com/winfx/2006/xaml)定义了一批XAML编译器或解析器必须特殊处理的关键字。它们主要控制元素如何被提供给过程式代码,但即使没有过程式代码,有一些关键字还是有用的。如Key、Name、Class、Subclass和Code。

你可能感兴趣的:(WPF初学,c#,C#,wpf,WPF)