先搜集几个好的wpf博客,有空常看:
http://www.cnblogs.com/lzhp/tag/WPF%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA/ (WPF深入浅出)
XAML综述:
关于XAML需要记住一些重要事项如下:
一个XAML树由带有一个根元素的正确嵌套的各种元素组成。
在这种树中每个元素都代表一个.NET对象。其元素总是某一个.NET类名。
每个元素可以包含对各种特性的赋值。
一个特性对应这个元素代表的那个.NET对象的一个属性。
无论这个属性的类型如何,特性的值总是一个字符串string。XAML解析器自动对这个string进行转换成适当的类型的值。
XAML语法包含4中类型的结构:
对象元素语法:这种结构由指定树的各种元素的标签组成。它还包含在元素开始标签中的各种特性。
属性元素语法:一些类型的某些属性太复杂而不能通过一个纯string设定。这些情况下,你可以使用在一个元素的内容部分嵌套各种属性元素标签的集合的属性元素语法。要注意属性元素中不能含有特性。
附加属性语法:这种结构能让你附加一个不同.NET对象的属性到这个元素。暂无过多描述。
标记扩展:这类结构能让.NET对象执行运行时的行为以指定一个值到一个的行为。标记扩展是一个勾向XAML外部的类的钩子,那个外部的类被称为扩展类。【一般只能用在特性赋值上面,以键值对形式出现。通常以大括号的形式出现,常用在如Binding绑定和Resource资源使用以及Style样式设定时】
主要难点总结如下:
首先说明一下,WPF中控件主要包括两类:内容控件ContentControl和项目控件ItemsControl,内容属性默认分别对应Content属性和Items属性。还有两种的混合控件就不多叙述了。
下面简单说一下布局:
在布局中常常使用控件的HorizontalAlignment属性和VerticalAlignment属性。它们决定了控件摆放在容器中的“卡槽”的那篇区域。如下图:
【另外,对于控件的margin属性,是在这个控件先在一个卡槽内,然后距这个卡槽四周多远距离,如果不设margin属性,也不设置控件的大小,默认情况下控件是填满整个卡槽的。而且margin如果设置为5,且控件的大小设置好,控件又在卡槽的中心,此时空间方圆5单位外可能是空白,并不一定要有东西,如上图的Certer按钮,可以这样理解margin为5表示此控件方圆大于或等于5单位范围可能有其他控件,“小于5单位范围一定是空隙”,就想象成一个带一圈空隙的控件该摆哪还摆哪。】
一、依赖项属性
【先说明一下依赖项属性并没有在XAML中有对应的写法或语法来体现,即并不能直接用在XAML代码中,要定义的话必须在后台代码中定义一个常规的依赖项属性。后面讲到的如附加属性就是一种特殊的依赖项属性(准确的说法是附近属性使用了依赖项属性机制)还有样式Style也隐式的使用依赖项属性,他们在XAML代码中有简单的书写体现,但实际上是通过自动调用依赖项属性的东西运行的。所以说道依赖项属性并不是和附近属性一样在XAML中体现的语法样式,而是一种机制,XAML语法中很多语法形式之所以书写简单但功能强大就是背后隐式的自动调用了依赖项属性的机制。所以讨论XAML语法时并没有包含依赖项属性。】
依赖属性就是可以自己没有值,并能够通过Binding从数据源获取值(依赖在别人身上)的属性。拥有依赖属性的对象被称为“依赖对象”。与传统的CLR属性和面向对象相比依赖属性有很多新颖之处,其中包括:
- 节省实例对内存的开销。
- 属性值可以通过Binding依赖在其它对象上。
依赖属性较之CLR属性在内存使用方面迥然不同。前面已经说过,实例的CLR属性都包装着一个非静态的字段(或者说由一个非静态的字段在后台支持)。思考这样一个问题:TextBox有138个属性,假设每个CLR属性都包装着一个4字节的字段,如果程序运行的时候创建了一个10列1000行的的一个TextBox列表,那么这些字段将暂用4*138*10*100=5.26M内存!在这100多个属性里面,最常用的也就Text属性,这就是说大多数内存会被浪费掉。
怎么避免这种浪费呢?让我们去思考一个现实世界中存在的问题:一个登山队员,他的全套装备有很多,包括登山服、登山靴、登山仗、护目镜、绳索、无线电、水、食品甚至还有氧气瓶等。倘若是去等珠穆朗玛峰,这些装备都要带上,要是去登香山呢?如果也背着氧气瓶岂不怪哉!所以,实际的一点办法就是---用得着的就带上,用不着的就不带,有必要的时候可以借别人的用一下。
其实,这就是WPF中依赖属性的原理。传统的.NET开发中,一个对象所暂用的内存空间在调用New操作符进行实例化的时候就已经决定了,而WPF允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据的时候能够获得默认值。借用其它对象的数据或者实时分配空间的能力----这种对象称为依赖对象而他这种实时获取数据的能力则依靠依赖属性来实现。在WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的Binding目标被数据所驱动。
如下图:
从上图可以看出以来项属性实际上【内存空间】被WPF属性系统内部所持有。需要一个DependencyProperty(依赖项属性标识符,元数据)实例和一个DependedcyObject实例共同指向WPF属性系统内部那个唯一的依赖项属性。【从外部整体上看也可以认为依赖项属性由依赖项属性标识符实例和依赖项对象实例构成】
在WPF系统中,依赖对象的概念被DependencyObject类所实现。依赖属性的概念则由DependencyProperty来实现。DependencyObject具有public object GetValue(DependencyProperty dp)和public void SetValue(DependencyProperty dp,object value)两个方法。
DependencyObject是WPF系统中相当底层的一个基类,如下图所示:
从这棵继承树上可以看出,WPF的所有控件都是依赖对象。WPF的类库在设计的时候充分利用了依赖属性的优势,UI控件的绝大多数属性已经依赖化了。
下面举一个例子,通过window1对象的Sides属性改变来调用OnSidesChanged函数在window1上画不同的多边形:其中DependencyProperty
上面通过设置元数据的PropertyChangedCallback属性绑定住了OnsedesChanged方法。对于DependencyProperty.Register方法参数描述如下:
二、附加属性
附加属性是一个属性被声明在一个类中,但是这个声明的属性却被使用在另一种类的对象中的特殊的依赖项属性。如:
下图展示了附加属性和常规依赖项属性的差别:
从图中可以看出,附加属性常使用GetXXX和SetXXX方法而不使用GetValue和SetValue方法,图中通过GetDock方法调用Button实例的GetValue方法。实际上是通过使用DockPanel的依赖项属性标识符和按钮继承与依赖项对象类的GetValue方法从WPF属性系统中获取最终需要的依赖项属性值的。
附加属性的GetXXX方法中存在如下语句调用:return Button对象.GetValue(DockPanel依赖项属性标识符对象); 从而使用DockPanel的依赖项属性标识符和button对象的附加依赖性类的GetValue方法从WPF属性系统内部调出所用数据。
总之:【依赖项属性可以看成是为了节约内存,手段是通过一个描述符号和一块纯二进制内存空间描述(WPF属性系统内部就是简单的连续内存块),这样就不会出现内存块之间碎片等情况】
三、数据绑定
(1)可以通过使用Binding标记扩展在XAML中创建一个绑定,也可以在后置代码中创建一个绑定。
通过绑定保持同步的数据元素必须是属性。一个属性被叫做来源属性,另一个叫做目标属性。【可以将Binding表达式说成绑定到数据源就容易区分了】
【记住绑定Binding总是绑定一个对象的,而Binding扩展标记中出现的其他字符串肯定是这个对象中的属性的名称】
目标属性必须总是一个依赖项属性,因为绑定内部是通过WPF的属性系统监视到数据源的变化并更新到目标的。
示例如下图:
对上例绑定过程描述如下图:
上面的语句是使用XAML语句定义的绑定。也可以在后置代码中定义一个绑定,示例如下图:
要求什么时候文本框中内容变化,label标签显示的内容也同时更新,后置代码的方法如下:
下图展示了上面代码的说明,其中BindingExpression被显示在目标对象的DependencyObject部分之内,因为是WPF属性系统在管理各个值和其更新。
绑定方向使用Binding对象的Mode属性,绑定元素间数据更新的时机使用Binding对象的UpdateSourceTrigger属性,即触发器属性。
删除绑定使用:BiningOperations.ClearBinding(目标对象,目标对象的属性); 或BiningOperations.ClearAllBindings(目标对象);
上面的例子是两个UI元素的属性之间的绑定,当然也可以将一个UI元素的属性绑定到一个自定义的类的对象的属性上,方法同上面一样即可。【由此可见绑定的目标元素一般是UI元素,因为他的绑定属性必须是依赖项属性,而绑定的数据源没什么特殊要求】
(2)数据语境:DataContext【数据上内容】,每个派生自FrameworkElement的类都有DataContext属性。DataContext属性要求是树中某个高层的元素的属性,因为当系统找到一个没有Source的设定的Binding的对象时,他将开始向上搜索元素树中带有DataContext属性设定的一个元素,如果他找到某个,他就使用那个值作为绑定的来源。
比如下面的例子:
如果不使用DataContext后置(后台)代码如下:
使用DataContext后置(后台)代码可以修改如下(sp是指StackPanel对象):
省略了Binding对象的数据源属性的设置,因为运行时,系统会在StackPanel的DataContext中找到数据源。
(3)ItemsControl(如Listbox控件)的集合的绑定
ItemsControl类的空间有两个地方用于存放他们的数据列表,一个用来存储和使用内部项目的集合(子项目都存储在父控件对象的内部),另一个用于外部项目的集合(子项目都存放在外部,父控件里只包含了这个外部集合的引用或指针而已),如下图:
各种ItemsContrl控件可以存储他们的项目集合在一个Items属性或ItemSource属性【Items和Content并列地位,只不过用在不同的控件中罢了】,但只能使用这两者中的一个,不能同时使用。
下面举一个例子,通过选择ComboxBox的不同项,显示相关内容在三个Label中:
下面是其后置代码。它创建了一个Person对象的数组,然后把这个数组分配到ItemsSource属性,这样ComBox的数据源就来自这个数组。
现在Person数组就附加到了ComboBox的ItemsSource属性,如下图,但是ComboxBox复合框还不能正常显示,因未指定Person的某个对象的哪个属性显示在列表中。
为了让复合框知道怎么显示内容,需要设置复合框的DisplayMemberPath属性,可以在XAML中完成,也可以在后台代码中书写,在XAML中书写如下:
下图展示了那个ComboBox现在的状态和程序运行显示出的结果:
注意,从上图可以看出,虽然显示的是Shirley,但实际上复合框此时选定的那个项是一个Person对象,只不过显示出的是这个对象的某个属性。
下面要做的是当选择复合框的某一项后让三个Label显示相关内容。
对上面代码分析如下:首先绑定StackPanel的数据内容属性为复合框的当前选择的对象(Person对象),此时当选择复合框时,DataContent的内容就是一个Person对象了,然后在后台代码中设置三个Label个绑定到DataContent代表的数据源的那个属性就行了。Label绑定的数据源不用设置,他会自动向树的高层找到DataContent的。
总之,数据绑定是一个极其强大的功能,能让你可以用很少或几乎不用额外的代码来关联各种属性。
四、资源
WPF中资源分两类,一类是通常意义的资源(其它程序语言都在使用),是在从外部引入的如图片图标等资源,他们不由程序创建,而是外部已经存在且可以直接使用。这类资源可以以如Jpg等形式存在,当然也可以被编译成二进制,此时一般称为这种一般意义的资源为汇编资源或二进制资源。
另一类是把对象看出资源,这种资源用来描述存储在一个对象字典中的各种对象。一般称为“对象资源”或“逻辑资源”或“XAML资源”。
下面主要讲对象资源:
在FrameworkElement类中有一个属性叫做Resources,是一个资源字典(ResourceDictionary)类型(本质是键值对的集合或容器),因为至今所学到的所有的元素都派生自FrameworkElement类,所以他们都包含有这种内置容器。举例:
上面代码中,FindResource方法在当前元素上开始向上搜索元素树,使用给定的键查询每个元素的Resources属性,如果找到资源则返回其引用,否则抛出一个异常。如果使用TryFindResource方法则在未找到资源时,不抛出异常而是返回Null值。另外当给btn1的Background属性赋值时还可以使用下面的直接读取“索引器”语法:
btn1.Background=(Brush)sp.Resources["background"];
补充,如果FindResource方法到达元素树的顶层还未能找到资源,他会在放弃前再搜寻两个位置,即Application对象和各种系统资源。Application对象也有Resources属性,所以也可以把共享的资源放在Application对象中。
上面举的例子在创建资源时使用的是c#后台代码的形式,也可以使用纯的XAML代码而不需要再写任何后台代码,如下:
此时,需要在每个资源的XAML声明中,使用x:Key特性来设定其键。注意StaticResource静态资源和DynamicResource动态资源区别仅仅在于是否更新对应在资源字典中的引用,具体区别见下面例子的讲解。下面代码是一个在Window中定义一个画刷资源,然后在StackPanel、TextBlok和连个按钮中共享使用这个的资源的例子:
这里简单说一下静动态资源的区别,如使用上面的代码,此时如果在后台代码中增加如下语句:Window1对象.Resource["gradBrush"]=Brushes.Black;
即新建一个Black画刷对象然后赋给资源字典中的[gradBrush]索引对应的引用,也即修改了资源字典中存放的那个引用(c#中引用本质是个指针),此时,三个使用动态资源的元素就会变成黑色背景,但btn1由于使用静态资源所以不会变背景色。
下面补充一下对于普通资源(一般意义上的资源)的使用,在XAML代码中可以如下使用:
《Image Source="\Picture\Balloons.jpg" /》
但在后台代码写的话要稍微复杂点,要用到Uri这个包装资源路径的对象,如下代码:
Uri对象构造函数的第二个参数用来指定是使用绝对路径还是相对路径,默认是使用绝对路径。
五、样式
样式和Html中的css一样,是用来设定一些属性的一个群集。有两种样式:命名样式和目标样式。
样式也是一种资源,且是“键值对”形式的“对象资源”或“逻辑资源”。和上面讲的“对象资源”一样,只不过上面的资源只对应于一个属性的值的共享,而样式是对应多个属性的值的共享。
使用命名样式,需要显示给这个样式元素(对象)一个名称,以后使用这个名称。使用目标样式(也叫类型样式),在声明这个样式时,需要确定目标作用在什么类型控件上,然后这个样式会自动应用到这种类型的所有元素。
下面代码是命名样式:
上面代码中,setter称为“设定器”,设定器中的特性Property必须是一个依赖项属性(背地里是通过这种机制实现样式的使用的)。另外,如果一个父类型设定了样式,则子类会也可以使用那个样式。例如,使用Control.FontWeight作为样式,则Button、Label、Window和所有其它派生自Control的元素都可以使用这个样式。
下面代码是类型样式,类型样式会自动应用到父元素下的所有指定类型(如Button)的子元素上:
上面的样式会应用到Window下的所有Button类型的对象,但不会应用到Button类型的子类,这点和命名样式刚好相反。且不能设置x:Key特性,否则会阻止这个类型样式的自动应用。
【前面将资源的时候说了Resources属性是一个ResourceDictionary资源字典类型的对象,所以Style也应该是一个键值对的对象,这里没有写x:Key特性应理解为默认在系统内部为其增加了一个键。】
总之:两种样式使用如下
事件设定器:除了上面所说的常规设定器Setter用来设定静态的样式外,还可以使用事件设定器EventSetter(和常规Setter并列地位)来设定“事件样式”,即这个样式中包含有事件,使用这个样式的控件也自动套用了这个事件。如下代码:
触发器(属性触发器):Style中除了包含Setters设定器集合外还可以包含Triggers集合等,设定器Setter属性其实是Style对象的默认属性,Setters对象是各种Setter对象的一个集合,所以才可以使用省略的写法直接使用Setter而不使用Setters。完整写法如下:
下面具体讲触发器(也叫属性触发器),其实就是一个“条件样式”,当属性的值等于给定的值后就触发给定的事件处理程序。
对上面的代码,触发器中IsMouseOver是被监控的属性,True是被监控的值,当鼠标在一个Button的上方时,这个Button的IsMouseOver就是True和触发器触发条件一致,就将Button样式设为触发器中定义的样式,鼠标移走时,Button又恢复原来样式。
多项触发器:和普通触发器处并列地位,就行普通设定器和事件设定器之间关系一样。多项触发器监视多个条件,当多个条件同时满足时才改变样式为触发器中定义的样式,此时由于多个条件没法写在一个Trigger元素中,就引入了Condition元素作为MutiTrigger的子元素,如下:
除了常规的触发器Trigger和多项触发器MutiTrigger外,还有EnentTrigger事件触发器、DataTrigger触发器和MutiDataTrigger触发器,此不赘述。
六、控件模版
创建一个控件(Button)实例时,这个控件(Button)实例也被称为模版父。
模板父(控件实例)的ControlTemplate(控件模版)用来设定一个视觉树,决定了控件的外观。每个模板父都含有一个Template类型的引用(指针),可以用ControlTemplate的实例来实例化这个引用。如下图:
上图中,箭头表示指针的指向,没有箭头的连接符只表示包含或组成关系。
在XAML中声明一个控件时,会自动创建一个控件的实例(模板父),然后实例化这个模板父的默认类型的ControlTemplate实例来设定这个控件的外观树,从而决定外观。
外观树是逻辑树的一个扩展视图,它包含了所有用于生成控件外观的各种视觉子部件,如下图表示一个Window和一个Button的外观树的图:
下面是建立一个控件模版的例子,一个带圆角巨型边框的按钮:
注意,控件模版被存储在Resources集合中【也是一个资源】,所以要有一个x:Key属性形成键值对的形式以便之后能通过名字获取这个资源(对象资源),使用时需要指定按钮的Template属性到一个StaticResource标记扩展。另外TextBlock文本块默认是属于内容呈现器ContentPresenter的组成部分,内容呈现器可以参加上面的外观树的图,下面也有讲解内容呈现器的说明。
上面代码结果显示按钮的内容Content为“Click Here”而不是“Click Me”,是因为内容呈现器决定了显示的内容。好的办法是在模版中使用内容呈现器,但同时必须要设置模版的TargetType属性指定是什么类型的模版。【这部分内容Illustrated WPF翻译的不很明白,先死记硬背吧,只需记住使用控件模版时应该使用内容呈现器ContentPresenter来显示控件(模板父)的Content属性】
上面的代码结果是各个按钮的内容显示各个内容。
模版绑定TemplateBinding:
上面讲到的内容呈现器不包括Padding属性而只包括Margin属性,这也是应该的,因为内容呈现器就代表内容Content部分,应该紧紧包围着文字才对。如果一个控件就包含一个边框,这个边框里面就包含一个内容呈现器,怎样才能是内容部分和边框分开一段空隙呢,这就要设置内容呈现器的Margin属性了,但是如果想把这个Margin属性不在模版中设置,而留给以后灵活设置,怎么办呢?这就需要把内容呈现器的Margin属性绑定到一个控件属性,以后设置这个控件属性后,就后把对应的值绑定到Margin了【可以理解所有控件的绑定Binding的值都是在后天自动运行且是最后执行的行为,也就是等其他所有属性都设置好了,才实现绑定】
如下面例子,画两个椭圆按钮,第二个文字和边框有一段空隙:
控件模版触发器:
这个触发器和样式中的触发器很相似,只不过这个Trigger中的Setter设定器需要指定3个属性而不是2个,多了一个TargetName目标名称属性。这是因为样式设定的是控件整体的某个属性的值,而模版需要设定指定设置控件的哪个子部件的某个属性的值。
如下示例展示了一个按钮分别在鼠标默认,鼠标悬停和单击状态下的边框等的状态改变:
数据模版:
使用和控件模版很相似,数据模版主要用在改变显示数据的外观,常常可以在模版中包含一个表格,定义显示多行多列的数据,数据的绑定使用普通的Binding扩展标记,而控件模版一般使用模版绑定标记Templateinding。
举例如下:
上述代码中模版中绑定的数据源写在后台listPeople.ItemsSource=people;(people是个数组对象,ItemsSource也可以像DataContent一样当Binding对象的数据源吗?估计是可以)。运行结果如下:
七、路由事件和命令
三种路由策略:直接路由,只触发当前元素的事件;冒泡路由策略,从当前元素向树的上层传递;隧道路由策略,从树的顶层元素向下传递直到当前元素,使用Preview前缀。
冒泡示例如下,依次执行Image、Label和Border的MouseUp事件处理程序
注,隧道事件需要使用PreviewMouseUp属性,如果要在当前事件处理程序中终止事件的传递,需要让事件参数RoutedEventArgs的Handled属性为真。
先补充一个使用剪切板的代码示例,这个示例和事件以及命令无关,只是感觉比较常用:
命令:
WPF提供了一个框架用于创建和处理命令,给出了一个更高一层的更为抽象的模型来处理来自不同命令起源(如按钮或菜单项)的事件。
使用WPF内置命令的例子如下,两个按钮剪切和粘贴,结果是从一个TextBox中剪切并粘贴到另一个TextBox中: