关于WPF的xaml

1. wpf项目

  1. App.xaml分支:程序的主体。大家知道,在 Windows系统里,一个程序就是一个进程(Process)。Windows还规定,一个GUI进程需要有一个窗体(Window)作为“主窗体”。App.xaml文件的作用就是声明了程序的进程会是谁,同时指定了程序的主窗体是谁。在这个分支里还有一个文件——App.xaml.cs,它是App.xaml的后台代码。
  2. MainWindow.xaml分支:程序的主窗体。

2. xaml(extensible application markup language)的最基本构成

<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>

XAML是一种由XML派生而来的语言,所以很多XML中的概念在XAML是通用的。比如,使用标签声明一个元素(每个元素对应内存中的一个对象)时,需要使用起始标签<Tag>和终止标签</Tag>,夹在起始标签和终止标签中的XAML 代码表示是隶属于这个标签的内容。如果没有什么内容隶属于某个标签,则这个标签称为空标签,可以写为<Tag/>。

3. 关于xmlns

xmlns(xml-namespace)是在定义名称空间(namespace)
xmIns[:可选的映射前缀]="名称空间"

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

这一句没有加冒号,说明没有映射前缀,那么来自于这个命名空间的标签都不用加前缀,比如grid,如果改成

xmlns:n="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

那么整个变成

<n:Window x:Class="WpfApp2.MainWindow"
        xmlns:n="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">
    <n:Grid>

    </n:Grid>
</n:Window>

为什么名称空间看上去像是一个主页地址呢?其实把它copy到E的地址栏里尝试跳转也不会打开网页。这里只是XAML解析器的一个硬性编码( hard-coding),只要见到这些固定的字符串,就会把一系列必要的程序集(Assembly)和程序集中包含的.NET名称空间引用进来。

4. xaml中引用类库

把类库引用到项目中是引用其中名称空间的物理基础,尤论是C#还是XAML都是这样。一旦将一个类库引用进程序,就可以引用其中的名称空间。假攻找的类库程序集名为MyLibrary.dl,其中包含Common和 Controls 两个名称空间,而且已经把这个程序集引用进WPF项目,那么在XAML中引用这两个名称空间的语法是:

xmIns:映射名="clr-namespace:类库中名称空间的名字;assembly=类库文件名
xmins:common="clr-namespace:Common;assembly=MyLibrary"
xmins:controls-"clr-namespace:Controls;assembly=MyLibrary"

比如:

xmlns:yui="clr-namespace:YUI.WPF.YProperties;assembly=YUI.WPF"

一旦我们将类库中的名称空间引用XAML文档,我们就可以使用这些名称空间里的类。语法格式是:
<映射名:类名>.….</映射名:类名>
例如使用Common和 Controls 中的类,代码是这样:

<common:MessagePanel x:Name="window1"/>
<controls:LedButton x:Name="button1"/>

5. x命名空间

x名称空间里的成员(如x:Class、x:Name)是专门写给XAML编译器看、用来引导XAML编译器把XAML 代码编译成CLR代码的。

名称 种类
x:Array 标签扩展
x:Class Attribute
x:Classmodifier Attribute
x:Code XAML指令元素
x:FieldModifier Attribute
x:Key Attribute
x:Name Attribute
x:Null 标签扩展
x:Shared Attribute
x:Static 标签扩展
x:Subclass Attribute
x:Type 标签扩展
x:TypeArguments Attribute
x:Uid Attribute
x:XData XAML指令元素

5.1 x:Name

x:Name 的作用有两个:
(1)告诉XAML编译器,当一个标签带有x:Name时除了为这个标签生成对应实例外还要为这个实例声明一个引用变量,变量名就是x:Name的值。
(2)将XAML标签所对应对象的Name 属性(如果有)也设为x:Name的值,并把这个值注册到UI树上,以方便查找。
经常会有初学者问:在XAML代码中是应该使用Name呢,还是 x:Name? Name属性定义在FrameworkElement类中,这个类是WPF 控件的基类,所以所有WPF 控件都具有Name这个属性。当一个元素具有Name属性时,你使用Name或x:Name效果是一样的。比如<Button x:Name="btn">和<Button Name="btn">,XAML编译器的动作都是声明名为btn的 Button类型变量并引用一个Button类型实例,而且此实例的Name属性值亦为 btn。此时,Name和 x:Name是可以互换的,只是不能同时出现在一个元素中。对于那些没有Name属性的元素,为了在XAML声明时也创建引用变量以便在C#代码中访问,我们就只能使用x:Name。因为x:Name的功能涵盖了Name属性的功能,所以全部使用x:Name以增强代码的统一性和可读性

5.2 x:FieldModifier

使用x:Name后,XAML标签对应的实例就具有了自己的引用变量,而且这些引用变量都是类的字段。既然是类的字段就免不了要关注一下它们的访问级别。默认情况下,这些字段的访问级别按照面向对象的封装原则被设置成了internal。在编程的时候,有时候我们需要从一个程序集访问另一个程序集中窗体的元素,这时候就需要把被访问控件的引用变量改为public级别,x:FieldModifier就是用来在XAML里改变引用变量访问级别的。
如果这样声明一个窗体中的控件:

<StackPanel>
<TextBox x:Name="textBox1" x:FieldModifier="public" Margin="5"/>
<TextBox x:Name="textBox2" x:FieldModifier="public" Margin="5"/>
<TextBox x:Name="textBox3" Margin="5">
</StackPanel>

textBox1和 textBox2的访问级别被设置为public,而textBox3的访问级别仍为默认的 internal(即程序集级别)。
因为x:FieldModifer是用来改变引用变量访问级别的,所以使用x:FieldModifier的前提是这个标签同时也使用x:Name,不然何来的引用变量呢?

6. 布局元素

6.1 Grid:网格

可以自定义行和列并通过行列的数量、行高和列宽来调整控件的布局。近似于HTML中的Table。
Grid的特点如下:
可以定义任意数量的行和列,非常灵活。
行的高度和列的宽度可以使用绝对数值、相对比例或自动调整的方式进行精确设定,并可设置最大和最小值。
内部元素可以设置自己的所在的行和列,还可以设置自己纵向跨几行、横向跨几列。可以设置Children元素的对齐方向。
基于这些特点,Grid适用的场合有:
UI布局的大框架设计。
大量U元素需要成行或者成列对齐的情况。
UI整体尺寸改变时,元素需要保持固有的高度和宽度比例。UI后期可能有较大变更或扩展。

对于Grid的行高和列宽,我们可以设置三类值:
绝对值: double数值加单位后缀(如上例)。
比例值: double数值后加一个星号(“*”)。
自动值:字符串 Auto。
为控件指定行和列遵循以下规则:
行和列都是从0开始计数。
指定一个控件在某行,就为这个控件的标签添加Grid.Row="行编号"这样一个Attribute,若行编号为0(即控件处于首行)则可省略这个Attribute.
指定一个控件在某列,就为此控件添加Grid.Column="列编号"这样的Attribute,若列编号为0则Attribute可以省略不写.
若控件需要跨多个行或列,请使用Grid.RowSpan="行数"和Grid.ColumnSpan="列数"两个Attribute.
可以用GridSplitter做可拖拽的分隔栏

6.2 StackPanel:栈式面板

可将包含的元素在竖直或水平方向上排成一条直线,当移除一个元素后,后面的元素会自动向前移动以填充空缺
StackPanel可以把内部元素在纵向或横向上紧凑排列、形成栈式布局,通俗地讲就是把内部元素像垒积木一样“撂起来”。垒积木大家都玩过,当把排在前面的积木块抽掉之后排在它后面的元素会整体向前移动、补占原有元素的空间。
基于这个特点,StackPanel适合的场合有:
同类元素需要紧凑排列(如制作菜单或者列表)。
移除其中的元素后能够自动补缺的布局或者动画。

6.3 Canvas:画布

内部元素可以使用以像素为单位的绝对坐标进行定位,类似于WindowsForm编程的布局方式。
Canvas译成中文就是“画布”,显然,在Canvas里布局就像在画布上画控件一样。使用Canvas布局与在Windows Form窗体上布局基本上是一样的,只是在 Windows Form开发时我们通过设置控件的Left和Top等属性来确定控件在窗体上的位置,而WPF 的控件没有Left和Top等属性,就像把控件放在Grid里时会被附加上 Grid.Column和 Grid.Row属性一样,当控件被放置在 Canvas里时就会被附加上 Canvas.X和Canvas.Y属性。
Canvas很容易被从Windows Form迁移过来的程序员所滥用,实际上大多数时候我们都可以使用Grid或StackPanel等布局元素产生更简洁的布局。
Canvas适用的场合包括:
一经设计基本上不会再有改动的小型布局(如图标)。
艺术性比较强的布局。
需要大量使用横纵坐标进行绝对点定位的布局。
依赖于横纵坐标的动画。

6.4 DockPanel:泊靠式面板

内部元素可以选择泊靠方向,类似于在 Windows Form编程中设置控件的Dock属性。

6.5 WrapPanel:自动折行面板

内部元素在排满一行后能够自动折行,类似于HTML 中的流式布局。

7. Binding

数据源是一个对象,一个对象身上可能有很多数据,这些数据又通过属性暴露给外界。那么,其中哪个数据是你想通过Binding送达UI元素的呢?换句话说,UI上的元素关心的是哪个属性值的变化呢?这个属性就称为Binding 的路径(Path)。但光有属性还不行——Binding是一种自动机制,当值变化后属性要有能力通知Binding,让 Binding 把变化传递给UI元素。怎样才能让一个属性具备这种通知Binding值已经变化的能力呢?方法是在属性的set 语句中激发一个PropertyChanged事件。这个事件不需要我们自己声明,我们要做的是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。当为 Binding 设置了数据源后,Binding就会自动侦听来自这个接口的PropertyChanged事件。
实现了INotifyPropertyChanged接口的 Student类看起来是这样:

class Student : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;
	private string name;
	public string Name
    {
		get { return name; }
        set{name=value;}
		//激发事件
		if (this.PropertyChanged != null)
		{
    		this.PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Name"));
		}
    }
}

经过这样一升级,当Name属性的值发生变化时PropertyChanged事件就会被激发,Binding接收到这个事件后发现事件的消息告诉它是名为Name的属性发生了值的改变,于是就会通知Binding目标端的UI元素显示新的值。

在c#代码中绑定:

//准备数据源
stu =new Student();
//准备Binding
Binding binding = new Binding();
binding.Source = stu;
binding.Path= new PropertyPath("Name");
//使用 Binding连接数据源与 Binding目标
BindingOperations.SetBinding(this.textBoxName, TextBox.TextProperty, binding);

在准备Binding 的部分,先是用“Binding binding = new Binding();”声明Binding类型变量并创建实例,然后使用“binding.Source = stu;”为Binding实例指定数据源,最后使用“binding.Path=new PropertyPath("Name");”语句为Binding 指定访问路径。
把数据源和目标连接在一起的任务是使用“BindingOperations.SetBinding(...)”方法完成的。

这个方法的3个参数是我们记忆的重点:
第一个参数用于指定 Binding的目标,本例中是this.textBoxName。
与数据源的Path原理类似,第二个参数用于为 Binding 指明把数据送达目标的哪个属性。只是你会发现在这里用的不是对象的属性而是类的一个静态只读( static readonly)的DependencyProperty类型成员变量!这就是我们后面要详细讲述的与Binding 息息相关的依赖属性。其实很好理解,这类属性的值可以通过Binding 依赖在其他对象的属性值上,被其他对象的属性值所驱动。
第三个参数很明了,就是指定使用哪个Binding实例将数据源与目标关联起来。

在xaml中相互绑定(把textbox的text属性与slider的value关联):

<Window x:Class="WpfApplication1.Window1"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="Control as Source"
		Height="110"Width="300">
	<StackPanel
		<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider1}"BorderBrush="Black"
Margin="5"/>
		<Slider x:Name="slider1" Maximum="100" Minimum="0" Margin="5"/>
    </StackPanel>
</Window>
Text="{Binding Path=Value,ElementName=slider1}"

因为Binding 类的构造器本身可以接收Path作为参数,也可以写作:

Text="{Binding Value,ElementName=slider1}"

因为在C#代码中我们可以直接访问控件对象,所以一般也不会使用Binding 的 ElementName 属性,而是直接把对象赋值给Binding 的Source属性。

控制Binding数据流向的属性是Mode,它的类型是BindingMode枚举。BindingMode可取值为TwoWay、OneWay、OnTime、OneWayToSource和Default。这里的Default值是指Binding 的模式会根据目标的实际情况来确定,比如若是可编辑的(如TextBox.Text属性),Default就采用双向模式;若是只读的(如TextBlock.Text)则采用单向模式。

空间属性Binding 的另一个属性——UpdateSourceTrigger,它的类型是UpdateSourceTrigger枚举,可取值为PropertyChanged、LostFocus、Explicit和Default。显然,对于TextBox默认值Default 的行为与LostFocus一致,我们只需要把这个属性改为PropertyChanged,则 Slider 的手柄就会随看我们在1extBOX里的制入m以变位置。

binding到不同对象

Binding的源是数据的来源,所以,只要一个对象包含数据并能通过属性把数据暴露出来,它就能当作 Binding 的源来使用。包含数据的对象比比皆是,但必须为Binding 的Source 指定合适的对象Binding才能正确工作,常见的办法有:

  1. 普通CLR类型单个对象指定为Source:包括.NET Framework自带类型的对象和用户自定义类型的对象。如果类型实现了INotifyPropertyChanged 接口,则可通过在属性的 set语句里激发PropertyChanged事件来通知 Binding数据已被更新。

  2. 普通CLR集合类型对象指定为Source:包括数组、List<T>、ObservableCollection<T>等集合类型。实际工作中,我们经常需要把一个集合作为ItemsControl派生类的数据源来使用,一般是把控件的ItemsSource属性使用Binding 关联到一个集合对象上。

    在使用集合类型作为列表控件的ltemsSource时一般会考虑使用ObservableCollection<T>代替List<T>,因为ObservableCollection<T>类实现了INotifyCollectionChanged和INotifyPropertyChanged接口,能把集合的变化立刻通知显示它的列表控件,改变会立刻显现出来。

    将listbox的数据绑定到数组:

    public Window1()
    {
        InitializeComponent();
    	//准备数据源
        List<Student> stuList = new List<Student>()
        {
        	new Student(){id=0, Name="Tim", Age=29},
            new Student(){id=1, Name="Tom", Age=28},
            new Student(){id=2, Name="Kyle", Age=27},
            new Student(){id=3, Name="Tony", Age=26},
            new Student(){id=4, Name="Vina", Age=25},
            new Student(){id=5, Name="Mike", Age=24},
        };
        //为ListBox 设置 Binding
        this.listBoxStudents.ItemsSource = stuList;
        this.listBoxStudents. DisplayMemberPath = "Name";
        //为TextBox设置 Binding
        Binding binding = new Binding("SelectedItem.id"){Source = this.listBoxStudents};
        this.textBoxld.SetBinding( TextBox.TextProerty, binding);
    }
    
  3. ADO.NET 数据对象指定为Source:包括DataTable和 DataView等对象。

  4. 使用XmlDataProvider把 XML数据指定为Source:XML作为标准的数据存储和传输格式几乎无处不在,我们可以用它表示单个数据对象或者集合;些WPF控件是级联式的(如TreeView和 Menu),我们可以把树状结构的XML 数据作为源指定给与之关联的Binding。

  5. 把依赖对象(Dependency Object)指定为Source:依赖对象不仅可以作为 Binding 的目标,同时也可以作为Binding 的源。这样就有可能形成Binding链。依赖对象中的依赖属性可以作为 Binding的 Path。

  6. 把容器的DataContext指定为Source (WPF Data Binding的默认行为):有时候我们会遇到这样的情况—我们明确知道将从哪个属性获取数据,但具体把哪个对象作为 Binding源还不能确定。这时候,我们只能先建立一个 Binding、只给它设置Path而不设置Source,让这个Binding自己去寻找Source。这时候,Binding 会自动把控件的DataContext当作自己的Source(它会沿着控件树一层一层向外找,直到找到带有Path指定属性的对象为止)。通过ElementName指定Source:在C#代码里可以直接把对象作为Source赋值给Binding,但XAML无法访问对象,所以只能使用对象的Name属性来找到对象。

  7. 通过Binding的 RelativeSource属性相对地指定Source:当控件需要关注自己的、自己容器的或者自己内部元素的某个值就需要使用这种办法。

  8. 把ObjectDataProvider对象指定为Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,我们可以使用这两种对象来包装数据源再把它们指定为Source。

  9. 把使用LINQ检索得到的数据对象作为Binding的源。

binding的数据转换

Binding还有另外一种机制称为数据转换(Data Convert),当Source端Path所关联的数据与Target端目标属性数据类型不一致时,我们可以添加数据转换器(Data Converter)。上面提到的问题实际上是double类型与string 类型互相转换的问题,因内处理咫木比权间中,2次些1人库就自动替我们做了。但有些类型之间的转换就不是WPF能替我们做的了,例如下面这些情况:

  1. Source里的数据是Y、N和X三个值(可能是char类型、 string类型或自定义枚举类型),UI 上对应的是CheckBox控件,需要把这三个值映射为它的 IsChecked属性值(boo1?类型)。
  2. 当TextBox里已经输入了文字时用于登录的Button才会出现,这是string类型与Visibility枚举类型或bool类型之间的转换(Binding 的 Mode将是OneWay)。
  3. Source里的数据可能是Male 或Female ( string或枚举),UI上对应的是用于显示头像的Image控件,这时候需要把Source里的值转换成对应的头像图片URI(亦是OneWay)。

当遇到这些情况时,我们只能目己动手写Converter,方法是创建一个类并让这个类实现IValueConverter接口。IValueConverter接口定义如下:

public interface IValueConverter
{
	object Convert(object value, Type targetType, object parameter, CultureInfo culture);
	object ConvertBack(object value,Type targetType, object parameter, Culturelnfo culture);
}

8.模板

在WPF中,通过引入模板(Template)微软将数据和算法的“内容”与“形式”解耦了。WPF中的Template分为两大类:

ControlTemplate是算法内容的表现形式,一个控件怎样组织其内部结构才能让它更符合业务逻辑、让用户操作起来更舒服就是由它来控制的。它决定了控件“长成什么样子”,并让程序员有机会在控件原有的内部逻辑基础上扩展自己的逻辑。

DataTemplate是数据内容的表现形式,一条数据显示成什么样子,是简单的文本还是直观的图形动画就由它来决定。

一言蔽之,Template就是“外衣”——ControlTemplate是控件的外衣DataTemplate是数据的外衣。

实际项目中,ControlTemplate 主要有两大用武之地:

通过更换ControlTemplate改变控件外观,使之具有更优的用户使用体验及外观。

借助ControlTemplate,程序员与设计师可以并行工作,程序员可以先用WPF标准控件进行编程,等设计师的工作完成后,只需把新的ControlTemplate应用到程序中就可以了。