[-]javascript
闲话WPF之一(WPF的结构) html
WPF进入咱们的生活已经不少年。(写这句话让我想起来了“我不作大哥好多年”。) 我的认为在UI的实践中,用户须要的是易于操做的,更加绚丽的界面。这两个应该是最基本、也是最重要的宗旨。而对于开发人员就是要用最简单的方法开发出尽量漂亮的界面,而且效率也不能太差。(要求是否是有些过度啦!)除了在一些Web开发和特殊的应用中,不多有开发组配备单独的美工,至少目前是这样吧!根据本身目前对WPF的了解程度,感受WPF在其中某些方面确实有超强的震撼力。
客观上讲,Vista操做系统确实给咱们带来了无可比拟的视觉效果。我本身深有体会,在近2个月的时间里天天都是在Vista下的开发,回家后看到XP系统,始终有些不爽的感受。
WPF能够认为是MS利用原有.NET框架的一些特点,加上DirextX的产物。从下图的WPF组件中,咱们能够看出最底层仍然是一些内核API。(如下两张图片都来自互联网。)
其中红色显示的组件是WPF的核心。Milcore是一个和DirectX交互的非托管组件,非托管代码能带给咱们更高效的处理,能更好的和DirextX交互。WPF的全部显示都是由Dirext完成的。milcore中一个很是重要的功能就是Composition引擎,这个引擎对效率的要求很高,它的具体做用稍后介绍。因此milcore放弃了一些CLR的特征来换取效率。而另外两个红色的组件都是创建在CLR基础之上,利用了.NET的优点。 java
至于其中的User32组件有什么做用,偶目前的知道的就是在WPF的某些应用场景中为了某些兼容须要使用User32,其中就有DWM(桌面窗口管理)。DWM的内容又能够写上一大堆,感兴趣的朋友能够看SDK文档。git
咱们除了关心WPF的基本结构外,更重要的 是WPF提供了什么功能,请看下图:web
图中的每一个黄色块都是一种媒体类型。这就表示WPF能够处理几乎全部的媒体类型:位图、3D、音频、视频和文本等等。经过WPF,它集成了如今的GDI/GDI+、D3D/OPENGL以及多媒体的DSHOW等等。全部的东西都是等同对象,无论的3D仍是2D,或者文本。windows
结构图中的Animate块贯串了整个的结构,由于在WPF中咱们能够对全部的可视内容进行动画操做。这是很是让人期待的功能。Animate下面咱们再次看到了Composition引擎,前面提到过它是位于milcore组件中。开发过程当中,咱们的界面元素功能有多种,好比图片,视频等等,最后显示到窗口的内容能够认为只是一张图片(准确说是Surface)。这个引擎的做用就是合成这些图片和视频元素最后进行提交显示。缓存
怎么感受是废话一堆啊!我准备好了,你们的西红柿、鸡蛋不用吝啬的,尽管杂吧!安全
在我开始看WPF文档开始的几天里,脑子里造成了一种错误的想法:WPF不就是XAML码?当时的感受就是郁闷啦,我学习WPF还得弄这个东西。给人的第一感受就是WPF很复杂。虽然对WPF的熟悉和了解还不是特别多,但如今已经知道这确实是一种错误的想法。性能优化
Charles Petzold先生曾有一篇文章介绍了WPF、XAML的一些关系(The Two APIs)。文章中说明了WPF为何很复杂:由于WPF有两套API,一套用于普通的编码访问(好比C#、VB.NET等其中.NET支持的语言。而另一套就是基于XML的API,被称为XAML(Extensible Application Markup Language)。服务器
XAML实现UI代码和应用程序逻辑代码的分离。在.NET 3.0和Windows Vista中,XAML与WPF一块儿创建整个的UI。因为XAML是基于XML的,因此每一个XAML代码都确定是一个完整的XML文件。XAML继承了XML全部的定义和规则。XAML与其余XML扩展不一样之处就是他所表示的意义。每一个XAML元素是一个.NET CLR类。基于XML使得咱们很是容易扩展和操做XAML。利用XAML的WPF这种关系,开发人员能够单独的设计漂亮的UI,也许真正的美工会更多的出现。咱们能够把程序逻辑写在单独的文件或者是内联嵌入到XML文件。
在XAML中使用得最多的XML功能应该有三个:命名空间、属性和子元素。
先看一个简单的XAML的例子:
<Window x:Class="FirstXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="FirstXAML" Height="200" Width="300"
>
<Canvas>
</Canvas>
</Window>
其中的xmlns就是XML中的名字空间,在W3C中xmlns是以下定义的:
XML namespaces provide a simple method for qualifying element and attribute names used in Extensible Markup Language documents by associating them with namespaces identified by URI references.
简单地说就是xmlns提供了一种方法把URI引用的名字空间定义为当前XML文件的元素和属性的默认命名空间。这里表示当前这个XML文档,也就是咱们的XAML文件,它的默认的命名空间就是http://schemas.microsoft.com/winfx/2006/xaml/presentation。
而后是属性和子元素,XML对属性的表示除了能够用Property外,还能够用子元素,在XAML中也是如此,看一个简单的例子:
<Button Width="6">
<Button.Background>White</Button.Background>
</Button>
例子当中就使用了属性和子元素两种方式来指定属性。其中的Width是直接用属性表示,Background属性是用子元素表示。在多数时候,但不是全部,你能够自由选择这两种表示方式之一。
XAML被编译为BAML(Binary Application Markup Language)文件。一般,BAML文件比XAML更小,编译后的BAML都是Pre-tokenized的,这样在运行时能更快速的加载、分析XAML等等。这些BAML文件被以资源的形式嵌入到Assembly当中。同时生成相应的代码(文件名称是**.g.cs或者**.g.vb),这些代码根据XAML元素分别生成命名的 Attribute字段。以及加载BAML的构造函数。
最后,关于XAML的优势,我附上一点翻译过来的条款,可能更直观:
XAML除了有标记语言、XML的优势外,还有以下一些优势:
用XAML设计UI更简单
XAML比其余的UI设计技术所需编码更少。
XAML设计的UI方便转移、方便在其余环境提交。好比在Web或Windows Client。
用XAML设计动态UI很是容易
XAML给UI设计人员带来新的革命,如今全部的设计人员再也不须要.NET开发的知识一样能够设计UI。在不远的未来,终端用户能够看到更漂亮的UI。
在前一篇文章中,指出xmlns的做用是设置XML文件的命名空间。相似的,xmlns:x的做用也是指定命名空间。这里为何是x而不是其余的,咱们能够简单的理解为其只是MS的一个命名而已,没有任何特殊的意义,固然,为了不和它的冲突,咱们定义本身的命名空间的时候不能是x。
而另外一个x:Class的做用就是支持当前Window所对应的类,前面已经说过每一个XAML元素都是一个CLR类型,这里的x:Class是Window的一个属性,属性的内容指出当前的窗口类是FirstXAML名字空间下的Windows1。为何须要类,而不所有用XAML实现?XAML的主要做用仍是编写UI部分,咱们仍然须要用代码对程序逻辑进行更深层次的控制。
好了,这是两个最基本的名字空间。一样地,名字空间也能够自定义,而且这个自定义会给咱们带来很大的方便。咱们定义以下的一个类:
namespace DataBind4Image { public class GroupData { //具体的细节忽略 } }
若是想在XAML文件中使用这个GroupData类对象,咱们就能够经过自定义的名字空间引入这个类:
xmlns:local="clr-namespace:DataBind4Image"
这里的后缀local只是一个标识,你能够设置为任何你喜欢的惟一标识。经过这个引入定义咱们就能够在XAML文件中用local来标识DataBind4Image当中的任何类。访问GroupData类时只须要加上local就能够识别了:<local:DrawingGroupData/>
利用名字空间,除了能够引入咱们定义的当前工程的类,还能够引入任何的Assembly。直接看例子是最简单的:
<Window x:Class="WindowsApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=System" > <ListBox> <sys:String>One</sys:String> </ListBox> </Window>
例子当中引入.NET的System Assembly,经过它咱们就能够直接使用System的任何类。利用这种相似的方式,咱们能够在XAML中使用几乎全部的DOTNET框架类。
最后说明一下在XAML中inline嵌入程序逻辑处理代码的状况。利用<CDATA[…]]>关键字引入处理代码。这种状况在实际当中不太合适,咱们不该该采用UI和逻辑混合的方式。详细的解释能够参数Windows SDK文档。
<![CDATA[ void Clicked(object sender, RoutedEventArgs e) { button1.Content = "Hello World"; } ]]></x:Code>
前面提到过每一个XAML元素表示一个.NET CLR类。多数的XAML元素都是从System.Windows.UIElement, System.Windows.FrameworkElement, System.Windows.FrameworkContentElement和System.Windows.ContentElement继承。没有任何的XAML元素与.NET CLR的抽象类对应。可是不少元素都有一个抽象类的派生类对应。
一般有以下四种通用的XAML元素:
Root元素:Windows和Page是最经常使用的根元素。这些元素位于XAML文件的根元素,并包含其余元素。
Panel元素:帮助布置UI位置。经常使用的是StackPanel, DockPanel, Grid和Canvas。
Control元素:定义XAML文件的控件类型。容许添加控件并自定义。 Document元素:帮助实现文档提交。主要分为Inline和Block元素组,帮助设计的外观相似文档。一些有名的Inline元素有Bold,LineBreak, Italic。Block元素有Paragraph, List, Block, Figure和Table。
XAML元素的属性与.NET类对象的属性相似,XAML的面向对象特征使得它的行为与以前的HTML相似。每一个属性(其实是类属性)继承了父元素的属性或者重载(若是从新设置了属性)。
说明:这里的Win32特指Vista操做系统以前的全部图形系统:GDI、GDI+、Direct3D。
GDI是当今应用程序的主流图形库,GDI图形系统已经造成了不少年。它提供了2D图形和文本功能,以及受限的图像处理功能。虽然在一些图形卡上支持部分GDI的加速,可是与当今主流的Direct3D加速相比仍是很弱小。GDI+开始出现是在2001年,它引入了2D图形的反走样,浮点数坐标,渐变以及单个象素的Alpha支持,还支持多种图像格式。可是,GDI+没有任何的加速功能(所有是用软件实现)。
当前版本的WPF中,对一些Win32功能尚未很好的支持,好比WMF/EMF文件,单个象素宽度的线条等等。对于这些需求还须要使用GDI/GDI+来实现。
在Windows Vista中,GDI和GDI+仍然支持,它们与WPF并行存在,可是基本上没有任何功能性的改进。对GDI和GDI+的改进主要集中在安全性和客户相关问题上。WPF的全部提交都不依赖于GDI和GDI+,而是Direct3D。而且全部的Primitive都是经过Direct3D的本地接口实现的。还记得我前面随笔中提到过的Milcore吗?它就是和Direct3D交互的非托管代码组件。因为WPF的大部分代码都是以托管代码的形式存在的,因此WPF中有不少托管、非托管的交互。固然,在一些图形卡不支持WPF所须要的功能时,WPF也提供了稍微低效的软件实现,以此来支持在某些PC上运行WPF应用程序。
在Windows Vista中,Direct3D的关键改进就是引入了新的显示驱动模型。VDDM驱动模型虚拟化了显卡上的资源(主要是显示内存),提供了一个调度程序,所以多个基于Direct3D的应用程序能够共享显卡(好比WPF应用程序和基于WPF的Windows Vista桌面窗口管理)。VDDM的健壮性、稳定性也获得了提升,大量的驱动操做从内核(Kernel)模式移动到了用户(User)模式,这样提升了安全性,也简化了显示驱动的开发过程。
在Windows Vista中存在两个版本的Direct3D:Direct3D 9和Direct3D 10。WPF依赖于Direct3D 9,这样能更普遍的解决兼容性问题。另一个很是重要的缘由就是为Vista的服务器版本提升方便,由于服务器版本的Vista对显卡和Direct3D基本上没有任何的要求。同时WPF也支持Direct3D 10。Direct3D 10依赖与VDDM,只能在Windows Vista上使用。因为Windows XP没有VDDM,虽然Microsoft作了很大的努力来改善XP中Direct3D 9相关驱动,提升内容的显示质量,可是因为XP中没有对显卡资源的虚拟化,强制全部的应用程序都用软件提交。
WPF对某些多媒体的功能支持还须要依赖老的技术,好比DirectShow。当咱们进行音频视频的捕捉或者其它任务时,只能直接用DirectShow实现,而后再用HwndHost嵌入到WPF内容当中。
利用相似的技术,咱们能够在WPF应用程序中显示自定义格式的内容。经过提供自定义的DirectShow CODEC,而后用Media元素实现和WPF内容毫无限制的集成。
另外,WPF对XPS等文档的打印输出也获得了极大的改善。XPS文档自己的规范也极大的提升了其打印的质量,XPS文档的规范能够参考MSDN的资料。除了打印,Vista操做系统中对远程的改进也部分依赖于WPF,好比有远程协助、远程桌面和终端服务等等。它们的实现过程是经过发送一系列的“远程”命名到客户端,客户根据本身PC的性能和命名进行显示,这样显示的质量能获得极大的提升。
在WPF中,对Direct3D进行各类封装。固然,若是你自己对Direct3D/OpenGL很熟悉,也能够直接在WPF中使用。封装后的Direct3D更容易使用。而且在Web应用程序(XBAP)也可使用Direct3D。在WPF中使用的Direct3D,没有直接用非托管代码控制所拥有的灵活性,也不能直接对硬件进行底层控制。
WPF中全部的提交都是矢量形式的,咱们能够对图像或窗口进行任意级的放缩,而图像的质量不会有任何的损耗。
在前面关于XAML的Post当中,简单说明了XAML若是引入自定义名称空间。还提到过XAML基本上也是一种对象初始化语言。XAML编译器根据XAML建立对象而后设置对象的值。好比:
<Button Width=”100”/>
很明显,咱们设置Button的宽度属性值为100。可是,这个“100”的字符串为何能够表示宽度数值呢?在XAML中的全部属性值都是用文本字符串来描述,而它们的目标值能够是double等等。WPF如何将这些字符串转换为目标类型?答案是类型转换器(TypeConverter)。WPF之因此知道使用Double类型是由于在FrameworkElement类中的WidthProperty字段被标记了一个TypeConverterAttribute,这样就能够知道在类型转换时使用何种类型转换器。TypeConverter是一种转换类型的标准方法。.NET运行时已经为标准的内建类型提供了相应的TypeConverter。因此咱们能够用字符串值指定元素的属性。
然而并非全部的属性都标记了一个TypeConverterAttribute。这种状况下,WPF将根据属性值的目标类型,好比Brush,来判断使用的类型转换器。虽然属性自己没有指定TypeConverterAttribute,可是目标类型Brush本身标记了一个TypeConverterAttribute来指定它的类型转换器:BrushConverter。因此在转换这种属性时将自动使用目标值类型的BrushConverter将文本字符串类型的属性值转换为Brush类型。
类型转换器对开发人员有什么做用呢?经过它咱们能够实现自定义的类型转换。下面一个例子演示了如何从Color类型转换为SolidColorBrush。
[ValueConversion(typeof(Color), typeof(SolidColorBrush))] public class ColorBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Color color = (Color)value; return new SolidColorBrush(color); }
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return null; } }
而后咱们能够在资源中定义一个ColorBrushConverter 实例(src是一个自定义命名空间,引入了ColorBrushConverter 类所在的Assembly)。
<Application.Resources> <src:ColorBrushConverter x:Key="ColorToBrush"/> </Application.Resources>
最后使用这个自定义的类型转换器:
<DataTemplate DataType="{x:Type Color}"> <Rectangle Height="25" Width="25" Fill="{Binding Converter={StaticResource ColorToBrush}}"/> </DataTemplate>
其实WPF所使用的这种类型转换从.Net Framework1.0已经开始并普遍应用。有兴趣的朋友能够参考MSDN的介绍:通用类型转换(Generalized Type Conversion)
闲话WPF之六(XAML的标记兼容性(Markup Compaibility))
继续XAML的话题,在前一个Post当中简单介绍了XAML的类型转换器(TypeConverters)。此次介绍一些XAML标记兼容性(Markup Compatibility)的相关内容。
利用XAML标记兼容性实现更增强大的注释功能
写过XAML的朋友应该都知道:在XAML中能够经过<!--****-->标记来实现注释。可是,利用XAML标记兼容性,还提供了其它更增强大的注释功能。 <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:c="Comment" mc:Ignorable="c"> <Canvas> <Button c:Width="100" Height="50">Hello</Button> </Canvas> </Window>
看见了Width前面的c前缀吗?它的做用就是注释掉Width属性。是否是感受比标记注释的方法简单。并且这个c前面不但能够应用在属性上,也能够直接应用在实例上,以下:
<Window xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:c="Comment" mc:Ignorable="c"> <Canvas> <c:Button Width="100" Height="50">Hello</c:Button> </Canvas> </Window>
上面的代码就所有注释掉了Button实例。固然,这种方法不建议在最后的发布XAML文档中出现。只适合在XAML文档的开发过程当中使用。
XAML标记的向后兼容性
XAML支持XAML文档的向前和向后兼容性。为了帮助说明XAML标记的向后兼容性,咱们看一个自定义的View类,其中定义了一个Color类型的颜色属性Color_Prop。
public class CLYLView { Color _color; public Color Color_Prop { get { return _color; } set { _color = value; } } }
很简单,在XAML中,咱们能够以下使用这个CLYLView类:
<CLYLView Color=”Red” xmlns=”… assembly-V1-uri…”>
注意其中的xmlns=”… assembly-V1-uri…”,这就是一个所谓的XmlnsCompatibleWith属性。经过它咱们指定了包含CLYLView的特定Assembly。
如今,咱们向V2版本的CLYLView添加了一个Content属性。以下所示:
public class CLYLView { Color _color; Content _content; public Color Color_Prop { get { return _color; } set { _color = value; } } public Content Content_Prop { get { return _content; } set { _content = value; } }
}
如今咱们能够这样使用V2版本的CLYLView实例:
<CLYLView Color=”Red” Content=”Unknown” xmlns=”... assembly-v2-uri…”/>
可是,咱们仍然但愿在V2版本的CLYLView支持V1版本。知足这种需求,咱们能够用XmlnsCompatableWith声明一个新的Assembly与老的Assembly兼容。XAML加载器看到了XmlnsCompatableWith属性,就会把默认地把全部对V1的引用处理为V2的引用。
向后兼容最大的一个好处就是:当咱们只有新版的Assembly时,全部对老版Assembly的引用仍然是可读的,不会出现任何的错误。
前一个Post当中,咱们简单介绍了XAML的向后兼容性,以及利用标记兼容性实现注释的功能。如今,咱们接着讨论XAML的向前兼容性问题。
一样地,咱们用一个简单的例子来帮助说明XAML的向前兼容性。假设有一个自定义的CLYLButton,实现了一个Light属性。在V1版本它的默认属性值是Blue(蓝光)。在V2版本中支持属性值Green(绿光)。假设咱们在程序中利用Light属性实现了绿光效果。可是,若是刚好目标机器上的V2版本意外地被替换为了V1版本。此时,程序的行为应该怎么样呢?崩溃,不,咱们但愿它在没有V2的状况下能利用V1版本的默认值实现蓝光效果。如何实现且看XAML标记的向前兼容性。向前兼容性表示经过标记兼容性名字空间的Ignorable属性标识元素、属性和类,使它们能够动态的支持向前版本。
<CLYLButton V2:Light="Green" xmlns="...assembly-v1-uri..." xmlns:V2="...assembly-V2-uri..." xmlns:mc=http://schemas.micrsoft.com/winfx/2006/markup-compatibility mc:Ignorable="V2" />
这就利用了标记兼容性名字空间的Ignorable属性。mc:Ignorable=”V2”表示全部用V2前缀关联的名字空间中元素或者属性都是能够忽略的。若是如今只有V1版本的CLYLButton,上面的代码就被XAML加载器解释为:
<CLYLButton Light=”Blue” xmlns=”… assembly-V1-uri …”/>
若是如今有V2版本的CLYLButton,上面的代码将被XAML加载器解释为:
<CLYLButton Light=”Green” xmlns=”… assembly-V2-uri …”/>
XMAL标记兼容性除了可应用在属性上,还能够应用在元素之上。仍然经过例子进行说明,定义以下的一个类:
[ContentProperty("Buttons")] public class CElement { List<CLYLButton> _buttons = new List<CLYLButton>(); public List<CLYLButton> Buttons { get { return _buttons; } }
关于ContentProperty的用法能够参考MSDN文档ContentPropertyAttribute Class
一样,咱们能够以下编写XAML代码,使其能够同时兼容两个版本的CElement。
<CElement mc:Ignorable="V2" xmln="...assembly-v1-uri..." xmlns:V2="...assembly-V2-uri..." xmlns:mc="http://schemas.micrsoft.com/winfx/2006/markup-compatibility"> <CLYLButton Light="Blue" /> <V2:CLYLButton Light="Green"/> </CElement>
这样,若是加载器有V2版本,则Green属性值生效。若是没有则被忽略。相似地,咱们还能够彻底自动地处理名字空间的类:
<CElement mc:Ignorable="v2" xmln="...assembly-v1-uri..." xmlns:V2="...assembly-v2-uri..." xmlns:mc="http://schemas.micrsoft.com/winfx/2006/markup-compatibility"> <V2:Favor/> </CElement>
加载时,若是没有V2版本存在,Favor类实例一样将被忽略。
在Markup Compatibility中,除了有前面介绍的Comment、Ignorable属性修饰外,另外一个有趣的就是AlternateContent。利用AlternateContent,咱们能方便的实现可选内容。好比,咱们的程序使用了V2版本Assembly的CLYLButton类,可是,若是没有找到这个Assembly,那么它对应的内容自动用另外一个指定版本V1替换,而不是兼容性体现的忽略。看下面的例子:
<CElement mc:Ignorable="v2" xmln="...assembly-v1-uri..." xmlns:v2="...assembly-v2-uri..." xmlns:mc="http://schemas.micrsoft.com/winfx/2006/markup-compatibility"> <mc:AlternateContent> <mc:Choice Requires="V2"> <CLYLButton Light="Green" Shape="Dog" /> <V2:Favor/> </mc:Choice> <mc:Fallback> <CLYLButton Light="Blue"/> </mc:Fallback> </mc:AlternateContent> </CElement>
这一段XAML代码在有V1版本的Assembly时将被视为:
<CElement xmln="...assembly-v1-uri..."> <CLYLButton Light="Blue"/> </CElement>
若是有V2版本的Assembly,编译的结果以下:
<CElement xmln="...assembly-v1-uri..."> <CLYLButton Light="Green"/> <Favor/> </CElement>
这部分的内容来自于即将出版的新书《WPF Unleashed》的第三章样章。关于什么是逻辑树,咱们先看下面的一个伪XAML代码的例子:
<Window ......> <StackPanel> <Label>LabelText</Lable> </StackPanel> </Window>
在这样一个简单UI中,Window是一个根结点,它有一个子结点StackPanel。而StackPanel有一个子结点Label。注意Label下还有一个子结点string(LabelText),它同时也是一个叶子结点。这就构成了窗口的一个逻辑树。逻辑树始终存在于WPF的UI中,无论UI是用XAML编写仍是用代码编写。WPF的每一个方面(属性、事件、资源等等)都是依赖于逻辑树的。
视觉树基本上是逻辑树的一种扩展。逻辑树的每一个结点都被分解为它们的核心视觉组件。逻辑树的结点对咱们而言基本是一个黑盒。而视觉树不一样,它暴露了视觉的实现细节。下面是Visual Tree结构就表示了上面四行XAML代码的视觉树结构:
并非全部的逻辑树结点均可以扩展为视觉树结点。只有从System.Windows.Media.Visual和System.Windows.Media.Visual3D继承的元素才能被视觉树包含。其余的元素不能包含是由于它们自己没有本身的提交(Rendering)行为。
在Windows Vista SDK Tools当中的XamlPad提供查看Visual Tree的功能。须要注意的是XamlPad目前只能查看以Page为根元素,而且去掉了SizeToContent属性的XAML文档。以下图所示:
注意图中工具栏特别标记的地方。咱们能够看到Visual Tree确实比较复杂,其中还包含有不少的不可见元素,好比ContentPresenter。Visual Tree虽然复杂,可是在通常状况下,咱们不须要过多地关注它。咱们在从根本上改变控件的风格、外观时,须要注意Visual Tree的使用,由于在这种状况下咱们一般会改变控件的视觉逻辑。
WPF中还提供了遍历逻辑树和视觉树的辅助类:System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper。注意遍历的位置,逻辑树能够在类的构造函数中遍历。可是,视觉树必须在通过至少一次的布局后才能造成。因此它不能在构造函数遍历。一般是在OnContentRendered进行,这个函数为在布局发生后被调用。
其实每一个Tree结点元素自己也包含了遍历的方法。好比,Visual类包含了三个保护成员方法VisualParent、VisualChildrenCount、GetVisualChild。经过它们能够访问Visual的父元素和子元素。而对于FrameworkElement,它一般定义了一个公共的Parent属性表示其逻辑父元素。特定的FrameworkElement子类用不一样的方式暴露了它的逻辑子元素。好比部分子元素是Children Collection,有是有时Content属性,Content属性强制元素只能有一个逻辑子元素。
WPF引入了一种新的属性:Dependency属性。Dependency属性的应用贯串在整个WPF当中。Dependency属性根据多个提供对象来决定它的值。而且是及时更新的。提供对象能够是动画,不断地改变它的值。也能够是父元素,它的属性值被继承到子元素。毫无疑问,Dependency属性最大的特色就是内建的变化通知功能。提供Dependency属性功能主要是为了直接从声明标记提供丰富的功能。WPF声明的友好设计的关键是大量的使用属性。若是没有Dependency属性,咱们将不得不编写大量的代码。关于WPF的Dependency属性,咱们将重点研究以下三个方面:
1、变化通知功能:属性的值被改变后,通知界面进行更新。
2、属性值的继承功能:子元素将继承父元素中对应属性名的值。
3、支持多个提供对象:咱们能够经过多种方式来设置Dependency属性的值。
先看如何实现一个标准的Dependency属性。
public class Button : ButtonBase
{
// The dependency property
public static readonly DependencyProperty IsDefaultProperty;
static Button()
{
// Register the property
Button.IsDefaultProperty = DependencyProperty.Register(“IsDefault”, typeof(bool), typeof(Button),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(OnIsDefaultChanged)));
…
}
// A .NET property wrapper (optional)
public bool IsDefault
{
get { return (bool)GetValue(Button.IsDefaultProperty); }
set { SetValue(Button.IsDefaultProperty, value); }
}
// A property changed callback (optional)
private static void OnIsDefaultChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e) { … }
…
}
在上面的实现代码中,System.Windows.DependencyProperty类表示的静态字段IsDefaultProperty才是真正的Dependency属性。为了方便,全部的Dependency属性都是公有、静态的,而且还有属性后缀。一般建立Dependency属性可用静态方法DependencyProperty.Register。参数的属性名称、类型、使用这个属性的类。而且能够根据重载的方法提供其余的通知事件处理和默认值等等。这些相关的信息可参考FrameworkPropertyMetadata类的多个重载构造函数。
最后,实现了一个.NET属性,其中调用了从System.Windows.DependencyObject继承的GetValue、SetValue方法。全部具备Dependency属性的类都确定会继承这个类。GetValue方法返回最后一次设置的属性值,若是尚未调用一次SetValue,返回的将是Register方法所注册的默认值。并且,这种.NET样式的属性封装是可选的,由于GetValue/SetValue方法自己是公有的。咱们能够直接调用这两个函数,可是这样的封装使代码更可读。
虽然这种实现方式比较麻烦,可是,因为GetValue/SetValue方法使用了一种高效的小型存储系统,以及真正的Dependency属性是静态字段(不是实例字段),Dependency属性的这种实现能够大大的减小每一个属性实例的存储空间。想象一下,若是Button有50个属性,而且所有是非静态的实例字段,那么每一个Button实例都含有这样50个属性的空间,这就存在很大的空间浪费。除了节省空间,Dependency属性的实现还集中、而且标准化了属性的线程访问检查、提示元素从新提交等等。
在前一个Post中,曾提到将要重点研究Dependency属性的三个方面:变化通知;属性值的继承;支持多个提供对象。下面,我将分别就这三个内容进行简单地说明。
【变化通知】
在任什么时候候,只要Dependency属性的值发生了变化,WPF能够自动地根据属性的元数据触发不一样的行为。前面提到过:Dependency属性最大的特色就是内建的变化通知功能。这种内建变化通知所提供的最值得注意的就是属性触发器(Property Trigger),就是它使用咱们不须要编写任何的程序代码就能在属性变化使执行自定义行为。请看下面XAML编码的一个属性触发器例子:
<Trigger Property=”IsMouseOver” Value=”True”>
<Setter Property=”Foreground” Value=”Blue”/>
</Trigger>
它的功能就是在属性值IsMouseOver变为True的时,将属性Foreground的值设置为Blue。并且,它会在IsMouseOver变为False时自动将Foreground的值设置为原来的值。就是这样简单的三行代码完成了咱们曾经须要多个函数、变量才能实现的功能。
使用属性触发器时须要注意:触发器默认适用于每一个类对象。 并且,在WPF 3.0中因为人为的限制,Property Trigger将不能应用在单独的元素。只能应用在某个Style对象之中。所以,若是想在某个单独对象上实现Property Trigger。必须用以下的XAML进行封装:
<Button MinWidth=”75” Margin=”10”>
<Button.Style>
<Style TargetType=”{x:Type Button}”>
<Style.Triggers>
<Trigger Property=”IsMouseOver” Value=”True”>
<Setter Property=”Foreground” Value=”Blue”/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
OK
</Button>
【属性值继承】
属性值继承是指在设置逻辑树某个结点元素的属性后,它的全部之结点都继承这个属性值(固然,前提是子元素必须支持这个属性)。咱们仍然利用闲话WPF之八中的一个例子进行说明:
<Window FontSize=”30”>
<StackPanel>
<Label>LabelText</Lable>
</StackPanel>
</Window>
咱们修改了Window是FontSize属性为30。经过实际观察将发现它的子元素Label的FontSize也变为了30。注意这里的StackPanel是一个容器元素,它自己并不支持FontSize属性。
如今咱们给上面的Window添加一个状态栏。XAML代码以下:
<Window ......>
<StackPanel>
<Label>LabelText</Lable>
<StatusBar>This is a Statusbar</StatusBar>
</StackPanel>
</Window>
这时你会发现:虽然StatusBar支持这个FontSize这个属性,它也是Window的子元素,可是它的字体大小却没有变化。为何呢?由于并非全部的元素都支持属性值继承。还存在以下两种例外的状况:
1、部分Dependency属性在用Register注册时能够指定Inherits为不可继承。
2、若是有其余更高优先级方法设置了其余的值。(关于优先级的介绍且看下面分解。)
部分控件如StatusBar、Menu和Tooptip内部设置它们的字体属性值以匹配当前系统的设置。这样用户经过控制面板能够修改它们的外观。这种方法存在一个问题:StatusBar等截获了从父元素继承来的属性,而且不影响其子元素。好比,若是咱们在StatusBar中添加了一个Button。这个Button的字体属性会由于StatusBar的截断没没有改变,将保留其默认值。
附加说明:属性值继承的最初设计只适用于元素Tree,如今已经进行多个方面的扩展。好比,值能够传递下级看起来像Children,但在逻辑或者视觉Tree中并非Children的某些元素。这些假装的子元素能够是触发器、属性的值,只要它是从Freezable继承的对象。对于这些内容没有很好的文档说明。咱们只须要能使用就行没必要过多关心。
在前一个Post中,重点说明了Dependency属性的变化通知和属性值的继承两个方面,下面咱们再看看Dependency属性所支持的多个提供对象。
【支持多个提供对象】
WPT提供了独立的、强大的机制来设置Dependency属性的值。因为支持多个提供对象,若是没有很好的机制来处理这些提供对象,那么Dependency属性的值将是不可预测的、系统也将变得混乱。Dependency属性的值取决于这些提供对象,它们以必定的顺序和优先级排列。
下图说明了WPF在计算Dependency属性最终值的5个步骤:
基本的计算过程是:
肯定基础值====>计算值(若是是表达式)===>应用动画====>强制值===>值验证
1、肯定基础值
多数的提供对象都会影响基础值,下面以优先级顺序列出了能够设置多数Dependency属性值的八个提供对象:
1、Local Value 2、Style Triggers 3、Template Triggers 4、Style Setters 5、Theme Style Triggers 6、Theme Style Setters 7、Property Value Inheritance 8、Default Value
Local Value技术上表示任何对DependencyObject.SetValue的调用。它的最多见形式就是在XAML或者代码中的属性赋值。由于咱们一般用.NET的属性方式封装了对GetValue/SetValue的调用。Regitser注册时指定的默认值位于处理过程的最后一步。关于其它的提供对象,如Style、Template将在之后介绍,敬请关注后续内容。
2、计算值
若是第一步获得的是一个表达式,WPF将计算表达式以获得一个具体的值。在3.0版本的WPF中,只有动态资源或者是数据绑定才可能有表达式。也许未来版本的WPF会支持其它类型的表达式。
3、应用动画
若是当前有一个或者多个动画在运行,它们具备修改当前属性值、或者彻底替代它的能力。所以,动画的级别比其它属性提供对象都高,甚至是Local Value,咱们必须记住这一点。
4、强制值
在处理完全部的提供对象后,WPF将最终的属性值传递到CoerceValueCallback委派。若是Dependency属性在注册时提供了这样的委派,那么就应该根据自定义逻辑返回一个新的值。好比ProgressBar,当全部提供对象最后所提供的值超出了其定义的最大、最小值范围时,ProgressBar将利用这个CoerceValueCallback委派限制在这个范围以内。
5、值验证
最后,前缀的强制值将传递给ValidateValueCallback委派,若是Dependency属性注册了这个委派。当值有效时委派必须返回True,不然返回False。返回False将抛出异常,终止整个进程。
附加说明:若是咱们不知道给定的Dependency属性的值来源于何处,能够调用静态的DependencyPropertyHelper.GetValueSource方法。它做为调试时的辅助工具,有时能给咱们提供帮助。方法会返回一个ValueSource结构。ValueSource结构中的属性成员BaseValueSource、IsExpression、IsAnimated、IsCoerced分别表示了前面列出的八个提供对象的相应类型。注意:请不要在最后的发布产品中使用这个方法,由于在未来版本的WPF中可能有不一样的行为。只应该将其做为调试工具。
在前面,我用三篇短小的Post对Dependency属性进行了说明。如今,咱们再继续看一种特殊的Dependency属性:Attached属性。Attached属性能够很是高效地Attach到其余的对象中。
咱们仍然用前面的一个简单XAML代码为例:
<Window>
<StackPanel>
<Label>LabelText</Lable>
</StackPanel>
</Window>
如今,若是须要对StackPanel及其子元素设置字体大小,应该如何作呢?在Window元素中,它有一个属性FontSize,能够直接设置。可是,StackPanel本身自己并无FontSize这样的属性。这就该Attached属性出场了。这里咱们须要用定义在TextElement元素中的Attached属性FontSize来设置StackPanel的字体。
<Window>
<StackPanel TextElement.FontSize=”30”>
<Label>LabelText</Lable>
</StackPanel>
</Window>
这样,StackPanel的子元素就能经过属性值继承获得新的FontSize属性。对于这样的XAML代码,XAML编译器或者解析器看到这种语法时,就要求TextElement(有时也称为Attached属性提供者)有相应的静态方法SetFontSize来设置对应的属性值。所以,上面的Attached属性设置代码,能够以下用C#实现:
StackPanel panel = new StackPanel();
TextElement.SetFontSize(panel, 30);
从这里的代码能够看出,Attached属性并不神秘。只是调用方法把元素和不相关的属性关联起来。而SetFontSize实现也比较简单。它只是调用了Dependency属性访问函数所调用的DependencyObject.SetValue方法。注意调用的对象是传入的DependencyObject,而不是当前的实例:
public static void SetFontSize(DependencyObject element, double value)
{
element.SetValue(TextElement.FontSizeProperty, value);
}
一样地,Attached属性也定义了对应的GetXXX函数。它调用的DependencyObject.GetValue方法:
public static double GetFontSize(DependencyObject element)
{
return (double)element.GetValue(TextElement.FontSizeProperty);
}
与普通的Dependency属性同样,这些GetXXX和SetXXX方法除了实现对GetValue和SetValue的调用,不能作任何其余额外的工做。
其实,在WPF应用中,Attached属性更多的用来控制UI的布局。除了前面的StackPanel,还有Grid等等。
补充说明:上面的代码还有一个问题须要说明。咱们设置StackPanel的字体属性时用的是TextElement元素。为何不用其余的元素Control、Button呢?
这个问题的关键之处在于Dependency属性的注册方法。我曾在Dependency属性[1]作过简单的说明。咱们看看Element的FontSizeProperty属性的注册代码:
TextElement.FontSizeProperty = DependencyProperty.RegisterAttached(
“FontSize”, typeof(double), typeof(TextElement), new FrameworkPropertyMetadata(
SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsMeasure),
new ValidateValueCallback(TextElement.IsValidFontSize));
这里与咱们前面的IsDefault属性相似,只是RisterAttached方法优化了Attached属性须要的属性元数据的处理过程。
另外一方面,Control的FontSize属性是在TextElement元素已经注册的属性之上调用AddOwner方法,获取一个彻底相同的实例引用:
Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(
typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize,
FrameworkPropertyMetadataOptions.Inherits));
因此,在实现Attached属性时咱们使用的是TextElement,而不是Control等等。
资源是保存在可执行文件中的一种不可执行数据。经过资源咱们能够包含图像、字符串等等几乎是任意类型的数据。如此重要的功能,.NET Framework固然也是支持的,其中内建有资源建立、定位、打包和部署的工具。在.NET中能够建立.resx和.resources文件。其中.resx由XML项组成。.resx只是一种中间格式,不能被应用程序直接使用,它必须用工具转换为.resource格式。
在WPF中,资源的含义和处理方式与传统的Win32和Windows Forms资源有所区别。首先,不须要建立.resx文件,只须要在工程中指出资源便可,其它全部的工做都由WPF完成。其次,WPF中的资源再也不像.NET中有资源ID,在XAML中引用资源须要使用Uri。最后,在WPF的资源中,几乎能够包含全部的任意CLR对象,只要对象有一个默认的构造函数和独立的属性。在WPF自己的对象中,能够声明以下四种对象:Style、Brushes、Templates和DataSource。
在定义具体的资源以前,咱们先考虑以下几个相关的问题:
1、资源的有效范围:在WPF中,全部的框架级元素(FrameworkElement或者FrameworkContentElement)都有一个Resource属性。也就是说。咱们能够在全部这类元素的Resource子元素中定义属性。在实践中,最经常使用的三种就是三种根元素所对应的资源:Application、Page和Window。顾名思义,在Application根元素下定义的资源将在当前整个应用程序中可见,均可以访问。在Page和Window中定义的元素只能在对应的Page和Window中才能访问。
2、资源加载形式:WPF提供了两种资源类型:Static资源和Dynamic资源。
两种的区别主要有两点:A)、Static资源在编译时决议,而Dynamic资源则是在运行时决议。B)、Static资源在第一次编译后即肯定了相应的对象或者值。此后不能对其进行修改,即便修改为功也是没有任何意义的,由于其它使用资源的对象不会获得通知。Dynamic资源不一样,它只有在运行过程当中真正须要时,才会在资源目标中查找。因此咱们能够动态的修改Dynamic资源。显而易见,Dynamic资源的运行效率将比Static资源低。
3、无论是Static资源仍是Dynamic资源,全部的资源都须要设置Key属性:x:Key=”KeyName”。由于WPF中的资源没有资源ID,须要经过资源Key来标识以方便之后访问资源。范围资源时咱们根据资源的类型使用StaticResource或者DynamicResource标记扩展。
好了,对WPF中的资源全部了解后,咱们看一些简单的例子:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<StackPanel.Resources>
<SolidColorBrush x:Key="MyBrush" Color="gold"/>
</StackPanel.Resources>
<TextBlock Foreground="{StaticResource MyBrush}" Text="Text"/>
</StackPanel>
</Window>
在这个例子中,咱们在StackPanel元素的Resource子元素中定义了一个SolidColorBrush资源。而后在后面经过StaticResouce标记扩展,利用前面的x:Key属性访问定义好的资源。
资源除了能够在XAML声明外,还能够经过代码进行访问控制。支持Resource属性的对象均可以经过FindResource、以及Resource.Add和Resource.Remove进行控制:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resouce>
<SolidColorBrush x:Key="MyBrush" Color="gold"/>
</Window.Resouce>
</Window>
咱们先在代码XAML的Window.Resource中定义了一个MyBrush。在代码中能够以下对其进行访问:
SolidColorBrush brush = this.FindResource("MyBrush") as SolidColorBrush;
若是须要进一步修改或者删除资源时,可以下编码:
this.Resouce.Remove(“MyBrush”); //删除MyBrush资源
this.Resouce.Add(“MyBrush”); //从新动态添加资源
说明:以上三处的this引用都是特指咱们定义MyBrush的元素Window。读者朋友可根据实际状况修改。
数据绑定,这是WPF提供的一个真正的优势。除了能够用在传统的绑定环境中,数据绑定已经被扩展应用到控件属性上。学习应用数据绑定,也能真正的体现XAML的好处。到底什么是数据绑定呢?也许你从字面上已经理解的很不错了。经过数据绑定,咱们在应用程序UI和程序逻辑之间创建了一种联系。正常创建绑定后,在数据的值发生改变后,绑定到数据的元素将自动更新、体现出数据的变化。
一样,咱们先看几个相关的知识点:
1、DataContext属性。设置DataContext属性,其实就是指定数据上下文。那么数据上下文又是什么呢?又是一个新的概念:数据上下文容许元素从它的父元素继承数据绑定的数据源。很简单,在某个元素的DataContext中指定的值,那么在这个元素的子元素也可使用。注意,若是咱们修改了FrameworkElement或者FrameworkContentElement元素的DataContext属性,那么元素将再也不继承DataContext值。也就是说新设置的属性值将覆盖父元素的设置。如何设置DataContext属性,稍后说明。
2、数据源的种类。也许,WPF提供的数据绑定只是实现了一项普通的功能而已,可是,WPF中所支持的多种数据源使得它的数据绑定功能将更增强大。如今,WPF支持以下四种绑定源:
(1)、任意的CLR对象:数据源能够是CLR对象的属性、子属性以及Indexers。对于这种类型的绑定源,WPF采用两种方式来获取属性值:A)、反射(Reflection);B)、CustomTypeDescriptor,若是对象实现了ICustomTypeDescriptor,绑定将使用这个接口来获取属性值。
(2)、XML结点:数据源能够是XML文件片段。也能够是XMLDataProvider提供的整个XML文件。
(3)、ADO.NET数据表。我对ADO.NET的了解不够,在此不作过多评论。
(4)、Dependency对象。绑定源能够是其它DependencyObject的DependencyProperty属性。
3、数据绑定的方式:(1)、OneWay,单一方向的绑定,只有在数据源发生变化后才会更新绑定目标。(2)、TwoWay,双向绑定,绑定的两端任何一端发生变化,都将通知另外一端。(3)、OneTime,只绑定一次。绑定完成后任何一端的变化都不会通知对方。
在上面的第二点我介绍了数据源的种类,注意这里的概念和下面要说明的指定数据源的方式的区别。目前,指定数据源有三种方式,咱们能够经过任何一种方式来指定上述的任何一种数据源:
(1)、经过Source标记。咱们能够在使用Binding使用Source标记显式指定数据源。
(2)、经过ElementName标记。这个ElementName指定了一个已知的对象名称,将使用它做为绑定数据源。
(3)、经过RelativeRource标记。这个标记将在后面说明ControlTemplate和Style时再进行说明。
如今咱们说明了不少和数据源相关的内容。可是再绑定的时候,咱们还须要指定绑定对象的属性名称。因此WPT提供了一个Path标记。它被用来指定数据源的属性。也便是数据源将在数据源对象的Path所指定的属性上寻找属性值。
如今,理论的东西讲了一堆,我将在后面用一些简单的例子进行说明。
在上一个Post当中,我叙述了WPF中的数据绑定相关的一堆理论知识。如今,咱们将对其中的某些方面经过实例作进一步的分析。
在介绍WPF数据绑定源的种类时,第一种就是任意的CLR对象。这里须要注意的是WPF虽然支持任意的CLR对象,可是一个普通的CLR对象类却不行。咱们还须要在CLR对象类上实现一种变化通知机制。
WPF把这种通知机制封装在了INotifyPropertyChanged接口当中。咱们的CLR对象类只要实现了这个接口,它就具备了通知客户的能力,一般是在属性改变后通知绑定的目标。
下面是一个简单的例子,实现了一个支持通知功能的Camera类:
using System;
using System.ComponentModel;
using System.Windows.Media.Media3D;
namespace LYLTEST
{
public class Camera : INotifyPropertyChanged
{
private PerspectiveCamera m_Camera;
public event PropertyChangedEventHandler PropertyChanged;
public Camera()
{
m_Camera = new PerspectiveCamera();
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public PerspectiveCamera CameraProp
{
get { return m_Camera; }
set
{
if (value != m_Camera)
{
this.m_Camera = value;
NotifyPropertyChanged("CameraProp");
}
}
}
}
}
这一段代码很简单,首先引入类中使用的INotifyPropertyChanged和PerspectiveCamera须要的名字空间。这里与普通CLR类的区别在于首先有一个公有的PropertyChangedEventHandler事件类型。而后咱们在.NET属性包装CameraProp判断属性是否发生了变化,若是是,则用当前是属性名称字符串“CameraProp”调用另外一个私有函数NotifyPropertyChanged。由它根据属性的名称构造一个PropertyChangedEventArgs对象,并完成对PropertyChanged的调用。它才是属性变化时真正应该调用的一个通知事件。
最后一点,若是咱们须要通知因此的属性都发生了变化,则将上面的属性字符串“CameraProp”用参数NULL替代便可。
在本系列的之十三中简单介绍了WPF中资源的资源。可是,没有给出任何具体的实例,在这个Post中将给出一个动态资源的例子,也算是响应daxian110的请求。并适当的扩展在前一个Post当中没有涉及的知识。
咱们先看一个例子程序:
<Window x:Class="WindowsApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WindowsApplication1" Height="150" Width="100" Loaded="OnLoaded" > <Canvas> <Button Click="OnClick" Canvas.Left="10" Canvas.Top="20" Width="80" Height="30" Content="{DynamicResource TestRes1}"/> <Button Canvas.Left="10" Canvas.Top="60" Width="80" Height="30" Content="{DynamicResource TestRes2}"/> </Canvas> </Window>
程序很简单,在窗口中添加了两个按钮,咱们须要关注的是其中对Content属性。这个属性的做用就是设置按钮的内容。为何这里的名称不是Text,而是Content?如此命名的缘由和WPF中控件一个很是重要的概念有关:WPF中几乎任何的控件(也就是Element)均可以做为一个容器存在。也就是说咱们在Content属性中能够包含其它任何你想显示的内容。不止是字符串文本。这种抽象的处理使咱们能够把全部的内容等同对待,减小了不少处理上的麻烦。在本例子中,Content属性被和一个TestRes1和TestRes2关联起来。这个TestRes究竟是什么呢?这就是动态资源的名称。具体的内容在显示按钮的时候决定。
注意上面Window中的Loaded属性,经过它咱们能够设置一个函数名称,它将Window加载完成后被调用。下面就看看如何用代码控制TestRes:
private void OnLoaded(object sender, RoutedEventArgs e) { string szText1 = "Res Text1"; this.Resources.Add("TestRes1", szText1);
string szText2 = "Res Text2"; this.Resources.Add("TestRes2", szText2); }
OnLoaded是Window1类中的一个成员函数,在这个函数里,咱们须要添加资源,由于咱们的XAML中须要使用TestRes1和TestRes2,运行时若是找不到对应资源,程序将失败。
如今,咱们调用Add方法添加资源。第一个参数是资源的名称,第二个参数是添加的资源对象。
程序的运行效果如图1:
图1 图2
接下来咱们看看修改资源的方法。在上面XAML的第一个按钮的Click属性中咱们指定了一个OnClick事件方法。它将在点击按钮时调用,如今咱们经过这个事件来修改另外一个按钮的Content资源:
private void OnClick(object sender, RoutedEventArgs e) { string szText = "New Res Text"; this.Resources.Remove("TestRes2"); this.Resources.Add("TestRes2", szText); }
OnLoaded实现一样的简单,先调用Remove方法删除已有的TestRes2资源,而后从新添加一个新的TestRes2资源对象。点击第一个按钮后,下面按钮的文本将自动修改成新的资源对象。运行效果如图2。
XAML加载器在分析XAML文件时,发现StaticResource,将会在当前Element的资源中查找指定的Key,若是查找失败,将沿着逻辑树向上查找,直到Root元素。若是尚未找到资源,再查找Application下定义的资源。在Application中定义的资源适用于整个应用程序。相似于全局对象。注意:使用Static资源时,不能向前引用。即便偶尔程序运行成功,向前引用的效率将很是低,由于它须要查找全部的ResourceDictionay。对于这种状况,使用DynamicResource将更适合。
另外一方面,XAML加载器发现DynamicResource时,将根据当前的属性设置建立一个表达式,直到运行过程当中资源须要,才根据表达式从资源中查找相关内容进行计算,返回所需的对象。注意,DynamicResource的查找于StaticResource基本相似,除了在定义了Style和Template时,会多一个查找目标。具体的细节可参数MSDN。
继续相同的话题:WPF中的资源。此次我将尝试从另一个角度来分析WPF中的资源:资源编译行为,以及如何根据应用程序的须要选择适当的类型。
首先创建一个默认的WPF工程,而后向工程中添加一个ICON资源。在添加资源后,咱们能够选择资源的类型,以下图所示:
从图中的下拉列表咱们能够看到资源所支持的各类类型。主要支持的编译行为是Resource和Content。若是选择为Resource,再用文本方式打开C#工程文件(*.csproj文件),其中咱们为发现以下的内容:
<ItemGroup>
<Resource Include="WTL2007.ico" />
</ItemGroup>
若是选择为Content,看到的资源项内容应该是:
<ItemGroup>
<Content Include="WTL2007.ico" />
</ItemGroup>
那么,这二者之间有什么区别呢?咱们先看Resource类型。若是某个文件在工程文本中被标识为Resource,这个文件将被嵌入到应用程序所在的Assembly。若是咱们设置了Localizable元数据,对应的Resource文件将位于卫星Assembly。
工程编译后,工程中的全部指定资源文件一块儿建立一个.resources文件。对于本地化应用程序,将加载对应的卫星Assembly。
若是文件标识为Content,而且将CopyToOutputDirectory设置为Always或者PerserveNewest。这个文件被拷贝到编译输出目录与应用程序Assembly一块儿。
<Content Include="WTL2007.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
编译时,标识为Content的文件都会被建立一个文件映射关系。运行时,根据指定的Uri,WPF的加载机制将根据实际状况加载资源。
无论咱们所引用的类型是Resource仍是Content,在代码中,咱们能够经过简单的相关Uri来访问这些资源:
<Object Property=”WTL2007.ico”/>
下面有几个比较好的建议,能够帮助咱们选择资源的编译Action。对于下面的这些需求,应该选择Resource:
1、文件是本地化文件
2、应用程序部署后再也不但愿文件被修改
3、若是不但愿对文件进行单独的部署或者管理,移动应用程序时没必要担忧资源的位置
对于下面的一些资源文件需求,咱们应该选择Content:
1、文件不是本地化文件
2、但愿文件在部署后能够被替换
3、但愿文件能够被下载更新等等(注意不能是包含在应用程序Assembly)。
4、某些不能被设置为Resource类型的文件,不然WPF不能识别。
在前一个Post当中,我从资源编译行为的角度讨论了WPF中的资源。可是,无论是Resource仍是Content都是在编译时声明资源。若是咱们打破这个限制,不但愿指定彻底确认的资源地址。WPF提供了一种相似IE地址定位的抽象,它根据应用程序部署的位置决议。
WPF将应用程序的起源地点进行概念上的抽象。若是咱们的应用程序位于http://yilinglai.cnblogs.com/testdir/test.application。咱们应用程序的起源地点是http://yilinglai.cnblogs.com/testdir/,那么咱们就能够在应用程序中这样指定资源位置:
<Image Source=”pack://siteoforigin:,,,/Images/Test.JPG”/>
经过这种包装的Uri,使用资源的引用更加灵活。那么,这种相似Internet应用程序的资源包装Uri指定方式有什么优势呢?
1)、应用程序Assembly创建后,文件也可被替代。
2)、可使文件只在须要使才被下载。
3)、编译应用程序时,咱们不须要知道文件的内容(或者文件根本不存在)。
4)、某些文件若是被嵌入到应用程序的Assembly后,WPF将不能加载。好比Frame中的HTML内容,Media文件。
这里的pack://实际上是一种URI(Uniform Resource Identifiers)语法格式。pack://<authority><absolute_path>,其中的authority部分是一个内嵌的URI。注意这个URI也是遵照RFC 2396文档声明的。因为它是被嵌入到URI当中,所以一些保留字符必须被忽略。在咱们前面的例子中,斜线(”/”)被逗号(”,”)代替。其它的字符如”%”、”?”都必须忽略。
前面例子中的siteoforigin能够理解为一种authority的特例。WPF利用它抽象了部署应用程序的原始站点。好比咱们的应用程序在C:\App,而在相同目录下有一个Test.JPG文件,访问这个文件咱们能够用硬编码URI file:///c:/App/Test.JPG。另一种方法就是这种抽象性:pack://siteoforigin:,,/Test.JPG。这种访问方法的便利是不言而喻的!在XPS文档规范中,对URI有更好的说明。有兴趣朋友能够在此下载。
也许你看到如今对此的理解有些问题。不用太着急,随着你对WPF愈来愈熟悉,会有更多的体会。对于WPF的新手(我也是),对于此点没必要过分纠缠。由于WPF的Application类中提供了一些有用的方法:
Application.GetResourceStream (Uri relativeUri);
Application.GetContentStream(Uri relativeUri);
Application.GetRemoteStream (Uri relativeUri);
经过使用这些函数,隐藏了URI定位的细节。从这些函数的名称咱们能够看出,它们分别对应于我在前面介绍的三种类型:Content、Resource和SiteofOrigin。
最后,简单的说明一下另外一种使用资源的方式,直接定义资源,不使用任何的属性,具体的用法看例子就明白了:
<StackPanel Name="sp1"> <StackPanel.Resources> <Ellipse x:Key="It1" Fill="Red" Width="100" Height="50"/> <Ellipse x:Key="It2" Fill="Blue" Width="200" Height="100"/> </StackPanel.Resources> <StaticResource ResourceKey="It1" /> <StaticResource ResourceKey="It2" /> </StackPanel>
【传递事件】
WPF在.NET简单事件通知之上添加了不少基础结构。传递事件的设计使得事件能够与元素树一块儿很好的工做。事件发生后,能够在视觉树和逻辑树自动地进行上下传递,咱们不须要添加任何额外的代码。
传递事件使得咱们不须要过多关注于视觉树,这样封装对于咱们理解WPF的元素合成很是重要。好比,咱们点击一个按钮的事件,在点击的时候咱们实际上点击的是一个ButtonChrome或者TextBlock,也就是说咱们点击的是Button的内容元素。正是由于事件能够沿视觉树传递,Button才发现这个事件,而且能够处理。所以,咱们能够给Button的Content当中添加任意的元素,而不会对事件有任何的影响。若是没有这样的事件传递,咱们点击Button内的元素时,必须手动编写代码触发Button点击事件。
传递事件的的实现和行为与Dependency属性相似。一样,咱们看看如何实现简单的传递事件。多数时候,传递事件并不比普通的.NET事件难。与Dependency属性同样,.NET语言(除了XAML)自己并不明白传递目标。这些支持都是基于WPF API。
public class Button { // 传递的事件 public static readonly RoutedEvent ClickEvent;
static Button() { // 注册事件 Button.DoubleClickEvent = EventManager.RegisterRoutedEvent(“Click”, RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Button)); … } // .NET事件保证 (可选的) public event RoutedEventHandler Click { add { AddHandler(Button.ClickEvent, value); } remove { RemoveHandler(Button.ClickEvent, value); } }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { … // 激发事件 RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this)); … } … }
从上面的实现能够看出,事件与Dependency属性有不少类似之处。也是定义一个静态的RoutedEvent成员,一样在静态构造函数里注册事件。为了方便,也包装了一个普通的.NET事件。这里的AddHandler/RemoveHandler不是从DependencyObject派生,而是更高一级的基类System.Windows.UIElement。这两个方法为相应的事件添加/删除一个委派。在OnMouseLeftButtonDown中,咱们构造一个事件参数,传入事件源对象this,而后调用RaiseEvent函数。
【事件策略和处理函数】
注册WPF事件时,咱们须要为传递事件选择一种策略,这个策略指定了事件在元素树中传递的方式。WPF支持这样三种策略:
Tunneling:事件首先在根元素激发,而后到达树下的每一个元素直到源元素(或者有处理函数处理这个事件终止了传递)。
Bubbling:事件首先在源元素激发,而后向上直到根元素(或者有处理函数处理这个事件终止了传递。
Direct:事件只在源元素激发。这与普通的.NET事件同样,除了参与事件触发器。
在上面的例子中,咱们注册的事件策略就是Bubbling。
传递事件的处理函数的参数与普通.NET事件同样。第一个参数System.Object表示处理函数依附的元素。第二个的System.EventArgs派生类,提供了以下四个有用的属性:
Source:逻辑树中激发事件的原始元素。
OriginalSource:视觉树中激发事件的原始元素。
Handled:布尔值,表示事件是否被处理。
RoutedEvent:实际的传递事件对象(好比Button.ClickEvent)。这个对于相同的处理函数处理多个传递事件时很是有用,能够用来区别传递事件。
Source和OriginalSource表明了逻辑树和视觉树对象。这有利于咱们进行一些低级控制,可是对于有的事件,不须要区别它们,这两个的值是相同的。
前一个Post当中介绍了WPF如何处理事件的传递过程。如何定义传递事件,而且对事件进行了分类。如今,咱们看看WPF究竟是如何处理Bubbling和Tunneling事件的。最后介绍了Attached事件。
在UIElement类,预约义了不少的传递事件,好比键盘、鼠标等等。其中大多数是Bubbling事件,其中不少的事件都还有一个对应的Tunneling事件。全部的Tunneling事件都是Preview前缀命名,它们都在对应的Bubbling事件以前激发。好比PreviewMouseMove这个Tunneling事件是在MouseMove这个Bubbling事件以前激发的。
Tunneling事件的好处就是能够有机会改变或者取消后面的Bubbling事件。WPF内建的响应事件只会对Bubbling事件进行响应,固然,前提了Bubbling和Tunneling同时定义。这种行为有什么好处呢?看下面的一个例子:好比,咱们想实现一种特殊的编辑框,只容许输入一些特定的字符。之前的实现方法在处理编辑框的KeyDown或者编辑框的WM_CHAR事件,而后判断新输入的字符是否知足条件,若是不知足,咱们再把编辑框的值设置为原来的值。这种实现技术会有字符的一个回退过程。而在WPF中,实现方法不一样,直接在PrevewKeyDown等Tunneling事件中处理,若是是不须要的字符,把事件设置为已经处理过。这样这个事件就不会进入到后面的Bubbling事件KeyDown中,WPF也根本不会显式这个字符。这种方法的效果将比以前的回退处理好不少。
虽然咱们能够经过RoutedEventArgs参数的Handled属性为True来终止事件的传递。可是,有时候咱们须要某个事件始终被接受处理,这能够经过程序代码实现。使用重载的AddHanlder方法。好比,咱们给窗口添加一个鼠标右键的处理方法(其中MRBD_Handler是类的一个事件方法):
public AboutDialog() { InitializeComponent(); this.AddHandler(Window.MouseRightButtonDownEvent, new MouseButtonEventHandler(MRBD_Handler), true); }
这样,任何条件下,MRBD_Handler均可以接收到窗口的鼠标右键事件。即便鼠标右键是点击在窗口中的某个子控件之上。
【Attached事件】
与Attached属性相似,WPF的Element在事件没有定义的状况下也支持Tunneling或者Bubbling事件。好比,咱们能够在一个简单的窗口程序中这样指定事件函数:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” x:Class=”Window1” Button.Click=”Button_Click” <Button Text="TestButton" Width="50" Height="30"> </Window>
例子中,由于Window自己没有定义Click事件,因此咱们必须指定Click事件属性的名称前缀,也就是定义事件的类名。通过这样的定义后,点击在Window中的TestButton,也会激发属性声明的Click事件,调用对应的Button_Click方法。
为何这样的定义能够经过呢?首先编译时,XAML会看到Button类确实定义了一个Click的.NET事件。在运行时,会直接调用AddHandler把这两个事件依附到Window对应的类当中。因此上面用XAML属性声明的事件代码与下面的程序代码等效:
public Window1 { InitializeComponent(); this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Click)); }
最近比较忙些,好多天没有写WPF了。今天,咱们继续回到前面的话题:WPF中的数据处理。前面讲过,经过实现INotifyPropertyChanged,咱们能够改变使任意的CLR对象支持WPF的绑定源。可是,INotifyPropertyChanged一般只应用在单个的类属性上。在现实应用中,咱们还会遇到另一种状况:咱们须要监视某一堆的数据是否发生变化。也就是说咱们绑定的数据源再也不是一个单独数据对象。好比,绑定源是一个数据表时,咱们但愿在表中任何一条数据发生变化就能获得通知。(这里暂不考虑WPF绑定对ADO.NET的支持。)
WPF提供了一个ObservableCollection类,它实现了一个暴露了INotifyPropertyChanged的数据集合。也就是说咱们不须要本身对每一个单独的数据实现INotifyPropertyChanged结构。咱们先看看如何实现一个简单的绑定数据集合。
namespace NSLYL { public class LYLDataObj { public LYLDataObj(string name, string description) { this.name = name; this.description = description; }
public string Name { get { return name; } set { name = value; } }
public string Description { get { return description; } set { description = value; } } private string name; private string description; }
public class LYLDataObjCol : ObservableCollection<LYLDataObj> { public LYLDataObjCol() { this.Add(new LYLDataObj("Microsot", "Operating System")); this.Add(new LYLDataObj("Google", "Search")); } } }
代码很简单,基本上就是这样的一个模板。而后,咱们就能够把LYLDataObjCol绑定到一个须要多项数据的Element之上,好比ListBox、ComboBox等等。
<ListBox ItemsSource="{StaticResource dataObj}" .../>
绑定以后,只要个人LYLDataObjCol对象发送了变化,ListBox、ComboBox的数据也会有对应的变化。
到如今,咱们已经知道在绑定的时候有两种指定数据源的方式:1、DataContext,关于它咱们在这个Post有简单介绍。2、直接用Binding类的Source属性。那么,咱们在使用的时候如何区别呢?首先,Source的优先级比DataContext高,只有Source不存在,或者在当前Source到不到须要的属性时才会查找DataContext。除此以外,这二者没有真正的区别,只是建议使用Source,它能有助于咱们调试应用程序。由于经过它能够明确的获得Source的信息。而DataContext支持一种继承。能够在父Element指定Source源。这同时也成为了DataContext的一个优势:若是多个Element须要绑定同一个Source源,那么咱们只须要在一个地方指定DataContext,就能够在其子Element使用。
Style是一种修改属性值是方法。咱们能够将其理解为对属性值的批处理。对批处理你们应该不会感到默认。对,经过Style咱们能够批量修改属性的值。先从一个简单的Style例子开始:
<Window x:Class="Viewer3D.WindowSettins" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Viewer3D Settings" > <Window.Resources> <Style TargetType="CheckBox"> <Setter Property="Height" Value="20"/> <Setter Property="Width" Value="50"/> <EventSetter Event="Checked" Handler="Checked_Click"/> <Setter Property="VerticalAlignment" Value="Center"/> </Style> </Window.Resources> </Window>
第一感受你可能会奇怪,为何Style在资源里呢?我我的直接将理解为“批处理”的缘故。所以Style是修改多个对象的属性值,它不从属于单独的元素对象。另外一个疑惑的问题是Style没有设置x:Key属性。这是一个很是关键的设置。若是咱们设置了Style的x:Key属性,至关于在当前Window是资源中定义了一个名称为x:Key设定值的Style对象。记住定义的效果至关于对象。若是没有设置x;Key,那么这个Style将对属于这个Window中全部CheckBox生效。这就起到了批处理的效果。
首先设定的是Style的TargetType属性,它表示咱们但愿修改的目标类型。而后定义一个Setters的集合。每一个Setter都表示修改的一个属性或者事件。Property设置属性名称,Value设置属性值。Event设置事件名称,Handler设置事件的响应函数名称。只要你在Resource作了相似的定义,在此Window中所使用的任何ChekcBox都会默认这些属性值。是否是很方便呢?咱们在此定义一次,能够节省不少代码。
也许你还会问:这样的统一修改属性太武断、霸道了吧!也许是的。咱们只修改部分Element的属性值,而但愿对某些特殊的Element作特殊处理。这样的需求WPF固然也是支持的。看看下面的代码:
<Style BasedOn="{StaticResource {x:Type CheckBox}}" TargetType="CheckBox" x:Key="WiderCheckBox"> <Setter Property="Width" Value="70"/> </Style>
WPT经过BasedOn对这种特殊的Style提供了支持。很明显,BasedOn的意思是咱们当前的Style基于在资源的CheckBox。这里又看到了x;Key扩展标记。由于咱们须要的是一个特例,一个特殊的Style对象。为了之后引用这个Style,咱们须要x:Key的标识做用。其它的代码与前面相似。
定义后,引用这个特殊Style的CheckBox的代码是这样的:
<CheckBox Style="{StaticResource WiderCheckBox}">Win</CheckBox>
你已经看到,咱们在CheckBox中指定了Style属性,并引用前面的StaticResource标记。
经过前面的介绍,咱们已经知道WPF支持用Style Setters修改控件的属性值,以改变控件的外观。咱们知道,WPF的任何控件都有视觉树和逻辑树。可是Style有它本身的局限性:它只能修改控件已有树型结构的属性,不能修改控件的树型层次结构自己。而在实际运用中,咱们经常须要对控件进行更高级的自定义。此时,能够须要使用ControlTemplate才能实现。
在WPF中,ControlTemplate用来定义控件的外观。咱们能够为控件定义新的ControlTemplate来实现控件结构和外观的修改。一样,咱们先看一个例子:
<Style TargetType="Button"> <Setter Property="OverridesDefaultStyle" Value="True"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid> <Ellipse Fill="{TemplateBinding Background}"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
从例子代码咱们能够看出,ControlTemplate含有模板的语义。也就是说它影响的应该是多个控件。而这个功能刚好能够利用Style实现。因此,在理解了Style以后,这样的代码应该不会感到陌生。首先把OverridesDefaultStyle设置为True,表示这个控件不使用当前Themes的任何属性。而后用Setters修改控件的Template属性。咱们定义了一个新的ControlTemplate来设置新的值。
一样地,ControlTemplate也使用TargetType属性,其意义与Style的TargetType同样。它的x:Key属性也是如此。而后,由一个Grid来表示控件的视觉内容。其中的TemplateBinding与Binding相似,表示当前Ellipse的显示颜色与Button的Background属性保持同步。TemplateBinding能够理解为Binding在模板中的特例。而另外一个ContentPresenter与WPF的基本控件类型有关,一种是ContentControl,一个是ItemControl。在上面的例子中定义的是基于ContentControl的Button。因此使用ContentPresenter来表示内容的显示。
WPF中每一个预约义的控件都有一个默认的模板,所以,在咱们学习自定义模板(也就是自定义控件)以前,能够先熟悉了解WPF的默认模板。为了便于查看模板的树形结构层次,咱们能够将模板输出为XML文件格式,这样能有助于理解。
XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = new string(' ', 4); settings.NewLineOnAttributes = true; StringBuilder strbuild = new StringBuilder(); XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings); XamlWriter.Save(ctrl.Template, xmlwrite);
这里的ctrl是一个实例化的Control类。而且Control须要已经显示在屏幕上,不然Control.Template可能为NULL。
前面关于ControlTempalte的Post当中,只说明了如何定义的外观。若是对于很复杂的自定义控件,一般咱们还须要在ControlTemplate使用Resource。很显然,Resource的目的是便于实现元素的重用。另外,咱们的自定义模板一般是在XAML中完成的,由于用代码实现是很是烦琐的。对于小的应用程序,这个ControlTemplate通常直接定义在XAML的根元素。对于大的应用程序,一般应该定义在专门的资源XAML文件中,根元素是ResourceDictionary。
无论定义在什么地方,除了前面用Style定义外观,以及用Resource实现元素重用外,ControlTemplate包括一个Trigger元素,它描述在控件属性发生变化时控件的外观如何变化。好比自定义Button时须要考虑鼠标在Button上移动时控件的外观。Trigger元素也是可选的,好比文本标签元素,它通常不包括Trigger。
在ControlTemplate中使用资源很简单,与其余元素中的资源同样: <ControlTemplate x:Key="templateThermometer" TargetType="{x:Type ProgressBar}"> <ControlTemplate.Resources> <RadialGradientBrush x:Key="brushBowl" GradientOrigin="0.3 0.3"> <GradientStop Offset="0" Color="Pink" /> <GradientStop Offset="1" Color="Red" /> </RadialGradientBrush> </ControlTemplate.Resources> <!-- 忽略其余相关内容--> </ControlTemplate>
接下来是Trigger的使用。利用Trigger对象,咱们能够接收到属性变化或者事件发生,并据此作出适当的响应。Trigger自己也是支持多种类型的,下面是一个属性Trigger的例子:
<Style TargetType="ListBoxItem"> <Setter Property="Opacity" Value="0.5" /> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Opacity" Value="1.0" /> <!--其余的Setters-> </Trigger> </Style.Triggers> </Style>
这段代码设置ListBoxItem的Opacity属性的默认值为0.5。可是,在IsSelected属性为True时,ListBoxItem的Opacity属性值为1。从上面的代码还能够看出,在知足一个条件后,能够触发多个行为(定义多个Setters)。一样地,上面的Triggers也是一个集合,也能够添加多个Trigger。
注意上面的多个Trigger是相互独立的,不会互相影响。另外一种状况是须要知足多个条件时才触发某种行为。为此,WPF提供了MultiTrigger以知足这种需求。好比:
<Style TargetType="{x:Type Button}"> <Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsMouseOver" Value="True" /> <Condition Property="Content" Value="{x:Null}" /> </MultiTrigger.Conditions> <Setter Property="Background" Value="Yellow" /> </MultiTrigger> </Style.Triggers> </Style>
这就表示只有IsMouseOver为True、Content为NULL的时候才将Background设置为Yellow。
以上的Trigger都是基于元素属性的。对于鼠标移动等事件的处理,WPF有专门的EventTrigger。但因EventTrigger多数时候是和Storyboard配合使用的。所以,我将在后面介绍动画的时候详细说明EventTrigger。
另外一方面,如今所讨论的Trigger都是基于属性的值或者事件的。WPF还支持另外一种Trigger:DataTrigger。显然,这种数据Trigger用于数据发生变化时,也就是说触发条件的属性是绑定数据的。相似地,数据Trigger也支持多个条件:MultiDataTrigger。他们的基于用法和前面的Trigger相似。
在实际应用中,ControlTemplate是一个很是重要的功能。它帮助咱们快速实现很Cool的自定义控件。下面我以Windows Vista SDK中的例子ControlTemplateExamples为基础,简单地分析ControlTemplate的使用。这个例子工程很是丰富,几乎包含了全部的标准控件。因此,在实现自定义控件时,能够先参考这样进行适当的学习研究。
首先是App.xaml文件,这里它把Application.StartupUri属性设置为Window1.xaml。而后把工程目录Resource下全部的控件xaml文件都合成为了应用程序范围内的资源。
<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Resources\Shared.xaml" /> <!-- 这里省略 --> <ResourceDictionary Source="Resources\NavigationWindow.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>
这样的用法颇有借鉴意义。在WPF中实现Skin框架也随之变得很是简单。值须要动态使用不一样的XAML文件便可。而后是Window1.xaml文件。它里面几乎把全部的控件都显示了一遍。没有什么多说的。重点看Resource目录下的自定义控件文件。这里的控件太多,不可能每一个都说说。我只挑选其中的Button.xaml为例:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Shared.xaml"/> </ResourceDictionary.MergedDictionaries>
<!-- Focus Visual -->
<Style x:Key="ButtonFocusVisual"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate> <Border> <Rectangle Margin="5" StrokeThickness="3" Stroke="#60000000" StrokeDashArray="4 2"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!--...............--> </ResourceDictionary>
由于这个XAML文件做为资源使用,因此其根元素是ResourceDictionary,而再也不是Window/Application等等。同时,资源文件也能够相互的嵌套,好比上面的包含的Shared.xaml文件。而后定义了一个Style,注意这里的目标类型为Control.Template,也就是针对全部的控件模板有效,因此Style添加了一个x:Key属性。这样就阻止Style适用于当前的全部控件。咱们必须显式的引用这个Style。相关内容,能够参考我前面的Style文章。
另外一个须要说明的是<ControlTemplate>的子元素,能够是任何的VisualTree。好比这里的Border,也能够是Grid等等。好了,如今定义了一个名为ButtonFocusVisual的模板,下面只须要引用它便可。
<Style TargetType="Button"> <!--.............--> <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/> <!--.............--> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button">
<Border x:Name="Border" ......./>
<ControlTemplate.Triggers> <Trigger Property="IsKeyboardFocused" Value="true"> <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DefaultedBorderBrush}" /> </Trigger> </ControlTemplate.Triggers>
</ControlTemplate> </Setter.Value> </Setter> </Style>
这是真正影响控件外观的代码。由于在定义Style的时候没有指定具体的x:Key,因此将影响全部的Button。如你所见,在FocusVisualStyle这个属性(类型是Style)上咱们用资源方式引用了前面定义的命名Style:ButtonFocusVisual。接下来是定义Template,并为其子元素Border定义了一个名称。而后就是ControlTemplate的触发器。在IsKeyboardFocused属性知足条件的状况下,咱们把Border(注意这个Border不是类型,而是具体的某个对象)的BorderBrush修改成另外一个静态资源。结合前面的Post,理解也就不难了。
最后,咱们还会发现一个有趣的问题:这个例子虽然是ControlTempalte,但工程名称倒是SimpleStyle,从这一点咱们也能够看出:Style和Template一般是配合使用才能真正的实现丰富的自定义功能。
在创建漂亮UI的同时,咱们还须要关注应用程序的性能,WPF尤为如此。下面从MS的文档中总结出了一些有用的性能优化点。在实际编写的过程当中,能够参考。这个Post非彻底原创,是根据一些文档总结出来的。
1、创建逻辑树的时候,尽可能考虑从父结点到子结点的顺序构建。由于当逻辑树的一个结点发生变化时(好比添加或删除),它的父结点和全部的子结点都会激发Invalidation。咱们应该避免没必要要的Invalidation。
2、当咱们在列表(好比ListBox)显示了一个CLR对象列表(好比List)时,若是想在修改List对象后,ListBox也动态的反映这种变化。此时,咱们应该使用动态的ObservableCollection对象绑定。而不是直接的更新ItemSource。二者的区别在于直接更新ItemSource会使WPF抛弃ListBox已有的全部数据,而后所有从新从List加载。而使用ObservableCollection能够避免这种先所有删除再重载的过程,效率更高。
3、在使用数据绑定的过程当中,若是绑定的数据源是一个CLR对象,属性也是一个CLR属性,那么在绑定的时候对象CLR对象所实现的机制不一样,绑定的效率也不一样。
A、数据源是一个CLR对象,属性也是一个CLR属性。对象经过TypeDescriptor/PropertyChanged模式实现通知功能。此时绑定引擎用TypeDescriptor来反射源对象。效率最低。 B、数据源是一个CLR对象,属性也是一个CLR属性。对象经过INotifyPropertyChanged实现通知功能。此时绑定引擎直接反射源对象。效率稍微提升。 C、数据源是一个DependencyObject,并且属性是一个DependencyProperty。此时不须要反射,直接绑定。效率最高。
4、访问CLR对象和CLR属性的效率会比访问DependencyObject/DependencyProperty高。注意这里指的是访问,不要和前面的绑定混淆了。可是,把属性注册为DependencyProperty会有不少的优势:好比继承、数据绑定和Style。因此有时候咱们能够在实现DependencyProperty的时候,利用缓存机制来加速访问速度:看下面的缓存例子:
public static readonly DependencyProperty MagicStringProperty = DependencyProperty.Register("MagicString", typeof(string), typeof(MyButton), new PropertyMetadata(new PropertyInvalidatedCallback(OnMagicStringPropertyInvalidated),new GetValueOverride(MagicStringGetValueCallback)));
private static void OnMagicStringPropertyInvalidated(DependencyObject d) { // 将缓存的数据标识为无效 ((MyButton)d)._magicStringValid = false; }
private static object MagicStringGetValueCallback(DependencyObject d) { // 调用缓存的访问器来获取值 return ((MyButton)d).MagicString; }
// 私有的CLR访问器和本地缓存 public string MagicString { get { // 在当前值无效时,获取最新的值保存起来 if (!_magicStringValid) { _magicString = (string)GetValueBase(MagicStringProperty); _magicStringValid = true; }
return _magicString; } set { SetValue(MagicStringProperty, value); } }
private string _magicString; private bool _magicStringValid;
另外,由于注册的DependencyProperty在默认是不可继承的,若是须要继承特性,也会下降DependencyProperty值刷新的效率。注册DependencyProperty属性时,应该把DefaultValue传递给Register方法的参数来实现默认值的设置,而不是在构造函数中设置。
5、使用元素TextFlow和TextBlock时,若是不须要TextFlow的某些特性,就应该考虑使用TextBlock,由于它的效率更高。
6、在TextBlock中显式的使用Run命令比不使用Run命名的代码要高。
7、在TextFlow中使用UIElement(好比TextBlock)所需的代价要比使用TextElement(好比Run)的代价高。
8、把Label(标签)元素的ContentProperty和一个字符串(String)绑定的效率要比把字符串和TextBlock的Text属性绑定的效率低。由于Label在更新字符串是会丢弃原来的字符串,所有从新显示内容。
9、在TextBlock块使用HyperLinks时,把多个HyperLinks组合在一块儿效率会更高。看下面的两种写法,后一种效率高。
A、 <TextBlock Width="600" > <Hyperlink TextDecorations="None">MSN Home</Hyperlink> </TextBlock> <TextBlock Width="600" > <Hyperlink TextDecorations="None">My MSN</Hyperlink> </TextBlock>
B、 <TextBlock Width="600" > <Hyperlink TextDecorations="None">MSN Home</Hyperlink> <Hyperlink TextDecorations="None">My MSN</Hyperlink> </TextBlock>
10、任与上面TextDecorations有关,显示超连接的时候,尽可能只在IsMouseOver为True的时候显示下划线,一直显示下划线的代码高不少。
11、在自定义控件,尽可能不要在控件的ResourceDictionary定义资源,而应该放在Window或者Application级。由于放在控件中会使每一个实例都保留一份资源的拷贝。
12、若是多个元素使用相同的Brush时,应该考虑在资源定义Brush,让他们共享一个Brush实例。
13、若是须要修改元素的Opacity属性,最后修改一个Brush的属性,而后用这个Brush来填充元素。由于直接修改元素的Opacity会迫使系统建立一个临时的Surface。
14、在系统中使用大型的3D Surface时,若是不须要Surface的HitTest功能,请关闭它。由于默认的HitTest会占用大量的CPU时间进行计算。UIElement有应该IsHitTestVisible属性能够用来关闭HitTest功能。