WPF的知识

来源:互联网 发布:c语言双引号里面的数字 编辑:程序博客网 时间:2024/06/06 05:31

闲话WPF之一(WPF的结构) 

 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的优势。 

至于其中的User32组件有什么作用,偶目前的知道的就是在WPF的某些应用场景中为了某些兼容需要使用User32,其中就有DWM(桌面窗口管理)。DWM的内容又可以写上一大堆,感兴趣的朋友可以看SDK文档。

我们除了关心WPF的基本结构外,更重要的 是WPF提供了什么功能,请看下图:



图中的每个黄色块都是一种媒体类型。这就表示WPF可以处理几乎所有的媒体类型:位图、3D、音频、视频和文本等等。通过WPF,它集成了现在的GDI/GDI+D3D/OPENGL以及多媒体的DSHOW等等。所有的东西都是等同对象,不管的3D还是2D,或者文本。

结构图中的Animate块贯串了整个的结构,因为在WPF中我们可以对所有的可视内容进行动画操作。这是非常让人期待的功能。Animate下面我们再次看到了Composition引擎,前面提到过它是位于milcore组件中。开发过程中,我们的界面元素功能有多种,比如图片,视频等等,最后显示到窗口的内容可以认为只是一张图片(准确说是Surface)。这个引擎的作用就是合成这些图片和视频元素最后进行提交显示。

怎么感觉是废话一堆啊!我准备好了,大家的西红柿、鸡蛋不用吝啬的,尽管杂吧!

闲话WPF之二(XAML概述) 

在我开始看WPF文档开始的几天里,脑子里形成了一种错误的想法:WPF不就是XAML码?当时的感觉就是郁闷啦,我学习WPF还得弄这个东西。给人的第一感觉就是WPF很复杂。虽然对WPF的熟悉和了解还不是特别多,但现在已经知道这确实是一种错误的想法。

Charles Petzold先生曾有一篇文章介绍了WPFXAML的一些关系(The Two APIs)。文章中说明了WPF为什么很复杂:因为WPF有两套API,一套用于普通的编码访问(比如C#VB.NET等其中.NET支持的语言。而另外一套就是基于XMLAPI,被称为XAMLExtensible Application Markup Language)。

XAML实现UI代码和应用程序逻辑代码的分离。在.NET 3.0Windows Vista中,XAMLWPF一起建立整个的UI。由于XAML是基于XML的,所以每个XAML代码都肯定是一个完整的XML文件。XAML继承了XML所有的定义和规则。XAML与其他XML扩展不同之处就是他所表示的意义。每个XAML元素是一个.NET CLR类。基于XML使得我们非常容易扩展和操作XAML。利用XAMLWPF这种关系,开发人员可以单独的设计漂亮的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中的名字空间,在W3Cxmlns是如下定义的:

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被编译为BAMLBinary 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方便转移、方便在其他环境提交。比如在WebWindows Client

   XAML设计动态UI非常容易

   XAMLUI设计人员带来新的革命,现在所有的设计人员不再需要.NET开发的知识同样可以设计UI。在不远的将来,终端用户可以看到更漂亮的UI

闲话WPF之三(XAML的名字空间) 

前一篇文章中,指出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元素:帮助实现文档提交。主要分为InlineBlock元素组,帮助设计的外观类似文档。一些有名的Inline元素有BoldLineBreak, ItalicBlock元素有Paragraph, List, Block, FigureTable

XAML元素的属性与.NET类对象的属性类似,XAML的面向对象特征使得它的行为与之前的HTML类似。每个属性(实际上是类属性)继承了父元素的属性或者重载(如果重新设置了属性)。

闲话WPF之四(WPFWin32 

说明:这里的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中所有的提交都是矢量形式的,我们可以对图像或窗口进行任意级的放缩,而图像的质量不会有任何的损耗。

闲话WPF之五(XAML中的类型转换) 

 在前面关于XAMLPost当中,简单说明了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的引用仍然是可读的,不会出现任何的错误。 

闲话WPF之七(XAML的向前兼容性) 

前一个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中,除了有前面介绍的CommentIgnorable属性修饰外,另一个有趣的就是AlternateContent。利用AlternateContent,我们能方便的实现可选内容。比如,我们的程序使用了V2版本AssemblyCLYLButton类,但是,如果没有找到这个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之八(WPF的逻辑树和视觉树) 

这部分的内容来自于即将出版的新书《WPF Unleashed》的第三章样章。关于什么是逻辑树,我们先看下面的一个伪XAML代码的例子: 

<Window ......>
     <StackPanel>
      <Label>LabelText</Lable>
     </StackPanel>
</Window>

在这样一个简单UI中,Window是一个根结点,它有一个子结点StackPanel。而StackPanel有一个子结点Label。注意Label下还有一个子结点stringLabelText),它同时也是一个叶子结点。这就构成了窗口的一个逻辑树。逻辑树始终存在于WPFUI中,不管UI是用XAML编写还是用代码编写。WPF的每个方面(属性、事件、资源等等)都是依赖于逻辑树的。 

视觉树基本上是逻辑树的一种扩展。逻辑树的每个结点都被分解为它们的核心视觉组件。逻辑树的结点对我们而言基本是一个黑盒。而视觉树不同,它暴露了视觉的实现细节。下面是Visual Tree结构就表示了上面四行XAML代码的视觉树结构:



并不是所有的逻辑树结点都可以扩展为视觉树结点。只有从System.Windows.Media.VisualSystem.Windows.Media.Visual3D继承的元素才能被视觉树包含。其他的元素不能包含是因为它们本身没有自己的提交(Rendering)行为。

Windows Vista SDK Tools当中的XamlPad提供查看Visual Tree的功能。需要注意的是XamlPad目前只能查看以Page为根元素,并且去掉了SizeToContent属性的XAML文档。如下图所示:



注意图中工具栏特别标记的地方。我们可以看到Visual Tree确实比较复杂,其中还包含有很多的不可见元素,比如ContentPresenterVisual Tree虽然复杂,但是在一般情况下,我们不需要过多地关注它。我们在从根本上改变控件的风格、外观时,需要注意Visual Tree的使用,因为在这种情况下我们通常会改变控件的视觉逻辑。

WPF中还提供了遍历逻辑树和视觉树的辅助类:System.Windows.LogicalTreeHelperSystem.Windows.Media.VisualTreeHelper。注意遍历的位置,逻辑树可以在类的构造函数中遍历。但是,视觉树必须在经过至少一次的布局后才能形成。所以它不能在构造函数遍历。通常是在OnContentRendered进行,这个函数为在布局发生后被调用。

其实每个Tree结点元素本身也包含了遍历的方法。比如,Visual类包含了三个保护成员方法VisualParentVisualChildrenCountGetVisualChild。通过它们可以访问Visual的父元素和子元素。而对于FrameworkElement,它通常定义了一个公共的Parent属性表示其逻辑父元素。特定的FrameworkElement子类用不同的方式暴露了它的逻辑子元素。比如部分子元素是Children Collection,有是有时Content属性,Content属性强制元素只能有一个逻辑子元素。 

闲话WPF之九(Dependency属性 [1]  

WPF引入了一种新的属性:Dependency属性。Dependency属性的应用贯串在整个WPF当中。Dependency属性根据多个提供对象来决定它的值。并且是及时更新的。提供对象可以是动画,不断地改变它的值。也可以是父元素,它的属性值被继承到子元素。毫无疑问,Dependency属性最大的特点就是内建的变化通知功能。提供Dependency属性功能主要是为了直接从声明标记提供丰富的功能。WPF声明的友好设计的关键是大量的使用属性。如果没有Dependency属性,我们将不得不编写大量的代码。关于WPFDependency属性,我们将重点研究如下三个方面:

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继承的GetValueSetValue方法。所有具有Dependency属性的类都肯定会继承这个类。GetValue方法返回最后一次设置的属性值,如果还没有调用一次SetValue,返回的将是Register方法所注册的默认值。而且,这种.NET样式的属性封装是可选的,因为GetValue/SetValue方法本身是公有的。我们可以直接调用这两个函数,但是这样的封装使代码更可读。

虽然这种实现方式比较麻烦,但是,由于GetValue/SetValue方法使用了一种高效的小型存储系统,以及真正的Dependency属性是静态字段(不是实例字段),Dependency属性的这种实现可以大大的减少每个属性实例的存储空间。想象一下,如果Button有50个属性,并且全部是非静态的实例字段,那么每个Button实例都含有这样50个属性的空间,这就存在很大的空间浪费。除了节省空间,Dependency属性的实现还集中、并且标准化了属性的线程访问检查、提示元素重新提交等等。 

闲话WPF之十(Dependency属性 [2]  

前一个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>

我们修改了WindowFontSize属性为30。通过实际观察将发现它的子元素LabelFontSize也变为了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、如果有其他更高优先级方法设置了其他的值。(关于优先级的介绍且看下面分解。)

部分控件如StatusBarMenuTooptip内部设置它们的字体属性值以匹配当前系统的设置。这样用户通过控制面板可以修改它们的外观。这种方法存在一个问题:StatusBar等截获了从父元素继承来的属性,并且不影响其子元素。比如,如果我们在StatusBar中添加了一个Button。这个Button的字体属性会因为StatusBar的截断没没有改变,将保留其默认值。

附加说明:属性值继承的最初设计只适用于元素Tree,现在已经进行多个方面的扩展。比如,值可以传递下级看起来像Children,但在逻辑或者视觉Tree中并不是Children的某些元素。这些伪装的子元素可以是触发器、属性的值,只要它是从Freezable继承的对象。对于这些内容没有很好的文档说明。我们只需要能使用就行不必过多关心。 

闲话WPF之十一(Dependency属性 [3]  

前一个Post中,重点说明了Dependency属性的变化通知和属性值的继承两个方面,下面我们再看看Dependency属性所支持的多个提供对象。 

【支持多个提供对象】

WPT提供了独立的、强大的机制来设置Dependency属性的值。由于支持多个提供对象,如果没有很好的机制来处理这些提供对象,那么Dependency属性的值将是不可预测的、系统也将变得混乱。Dependency属性的值取决于这些提供对象,它们以一定的顺序和优先级排列。

下图说明了WPF在计算Dependency属性最终值的5个步骤:

 

基本的计算过程是:

确定基础值====>计算值(如果是表达式)===>应用动画====>强制值===>值验证

1、确定基础值

多数的提供对象都会影响基础值,下面以优先级顺序列出了可以设置多数Dependency属性值的八个提供对象:

1Local Value                  2Style Triggers              3Template Triggers                   4Style Setters 
5Theme Style Triggers    6Theme Style Setters     7Property Value Inheritance      8Default Value

Local Value技术上表示任何对DependencyObject.SetValue的调用。它的最常见形式就是在XAML或者代码中的属性赋值。因为我们通常用.NET的属性方式封装了对GetValue/SetValue的调用。Regitser注册时指定的默认值位于处理过程的最后一步。关于其它的提供对象,如StyleTemplate将在以后介绍,敬请关注后续内容。

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结构中的属性成员BaseValueSourceIsExpressionIsAnimatedIsCoerced分别表示了前面列出的八个提供对象的相应类型。注意:请不要在最后的发布产品中使用这个方法,因为在将来版本的WPF中可能有不同的行为。只应该将其作为调试工具。 

闲话WPF之十二(Attached属性 ) 

在前面,我用三篇短小的PostDependency属性进行了说明。现在,我们再继续看一种特殊的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属性一样,这些GetXXXSetXXX方法除了实现对GetValueSetValue的调用,不能做任何其他额外的工作。

其实,在WPF应用中,Attached属性更多的用来控制UI的布局。除了前面的StackPanel,还有Grid等等。

补充说明:上面的代码还有一个问题需要说明。我们设置StackPanel的字体属性时用的是TextElement元素。为什么不用其他的元素ControlButton呢?

这个问题的关键之处在于Dependency属性的注册方法。我曾在Dependency属性[1]做过简单的说明。我们看看ElementFontSizeProperty属性的注册代码:

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属性需要的属性元数据的处理过程。

另一方面,ControlFontSize属性是在TextElement元素已经注册的属性之上调用AddOwner方法,获取一个完全相同的实例引用:

Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(

     typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize,

     FrameworkPropertyMetadataOptions.Inherits));

所以,在实现Attached属性时我们使用的是TextElement,而不是Control等等。 

闲话WPF之十三(WPF中的资源) 

资源是保存在可执行文件中的一种不可执行数据。通过资源我们可以包含图像、字符串等等几乎是任意类型的数据。如此重要的功能,.NET Framework当然也是支持的,其中内建有资源创建、定位、打包和部署的工具。在.NET中可以创建.resx.resources文件。其中.resxXML项组成。.resx只是一种中间格式,不能被应用程序直接使用,它必须用工具转换为.resource格式。 

WPF中,资源的含义和处理方式与传统的Win32Windows Forms资源有所区别。首先,不需要创建.resx文件,只需要在工程中指出资源即可,其它所有的工作都由WPF完成。其次,WPF中的资源不再像.NET中有资源ID,在XAML中引用资源需要使用Uri。最后,在WPF的资源中,几乎可以包含所有的任意CLR对象,只要对象有一个默认的构造函数和独立的属性。在WPF本身的对象中,可以声明如下四种对象:StyleBrushesTemplatesDataSource

在定义具体的资源之前,我们先考虑如下几个相关的问题:

1、资源的有效范围:在WPF中,所有的框架级元素(FrameworkElement或者FrameworkContentElement)都有一个Resource属性。也就是说。我们可以在所有这类元素的Resource子元素中定义属性。在实践中,最常用的三种就是三种根元素所对应的资源:ApplicationPageWindow。顾名思义,在Application根元素下定义的资源将在当前整个应用程序中可见,都可以访问。在PageWindow中定义的元素只能在对应的PageWindow中才能访问。

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

我们先在代码XAMLWindow.Resource中定义了一个MyBrush。在代码中可以如下对其进行访问:

SolidColorBrush brush = this.FindResource("MyBrush") as SolidColorBrush;

如果需要进一步修改或者删除资源时,可如下编码:

this.Resouce.Remove(“MyBrush”);      //删除MyBrush资源

this.Resouce.Add(“MyBrush”);         //重新动态添加资源

说明:以上三处的this引用都是特指我们定义MyBrush的元素Window。读者朋友可根据实际情况修改。

闲话WPF之十四(WPF的数据处理 [1]  

数据绑定,这是WPF提供的一个真正的优点。除了可以用在传统的绑定环境中,数据绑定已经被扩展应用到控件属性上。学习应用数据绑定,也能真正的体现XAML的好处。到底什么是数据绑定呢?也许你从字面上已经理解的很不错了。通过数据绑定,我们在应用程序UI和程序逻辑之间建立了一种联系。正常建立绑定后,在数据的值发生改变后,绑定到数据的元素将自动更新、体现出数据的变化。 

同样,我们先看几个相关的知识点:

1DataContext属性。设置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对象。绑定源可以是其它DependencyObjectDependencyProperty属性。

3、数据绑定的方式:(1)、OneWay,单一方向的绑定,只有在数据源发生变化后才会更新绑定目标。(2)、TwoWay,双向绑定,绑定的两端任何一端发生变化,都将通知另一端。(3)、OneTime,只绑定一次。绑定完成后任何一端的变化都不会通知对方。

在上面的第二点我介绍了数据源的种类,注意这里的概念和下面要说明的指定数据源的方式的区别。目前,指定数据源有三种方式,我们可以通过任何一种方式来指定上述的任何一种数据源:

1)、通过Source标记。我们可以在使用Binding使用Source标记显式指定数据源。

2)、通过ElementName标记。这个ElementName指定了一个已知的对象名称,将使用它作为绑定数据源。

3)、通过RelativeRource标记。这个标记将在后面说明ControlTemplateStyle时再进行说明。

现在我们说明了很多和数据源相关的内容。但是再绑定的时候,我们还需要指定绑定对象的属性名称。所以WPT提供了一个Path标记。它被用来指定数据源的属性。也即是数据源将在数据源对象的Path所指定的属性上寻找属性值。

现在,理论的东西讲了一堆,我将在后面用一些简单的例子进行说明。 

闲话WPF之十五(WPF的数据处理 [2] 

上一个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");

                }

            }

        }

    }

}

这一段代码很简单,首先引入类中使用的INotifyPropertyChangedPerspectiveCamera需要的名字空间。这里与普通CLR类的区别在于首先有一个公有的PropertyChangedEventHandler事件类型。然后我们在.NET属性包装CameraProp判断属性是否发生了变化,如果是,则用当前是属性名称字符串“CameraProp”调用另一个私有函数NotifyPropertyChanged。由它根据属性的名称构造一个PropertyChangedEventArgs对象,并完成对PropertyChanged的调用。它才是属性变化时真正应该调用的一个通知事件。

最后一点,如果我们需要通知所以的属性都发生了变化,则将上面的属性字符串“CameraProp”用参数NULL替代即可。 

闲话WPF之十六(WPF中的资源 [2] 

在本系列的之十三中简单介绍了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属性被和一个TestRes1TestRes2关联起来。这个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);
}

OnLoadedWindow1类中的一个成员函数,在这个函数里,我们需要添加资源,因为我们的XAML中需要使用TestRes1TestRes2,运行时如果找不到对应资源,程序将失败。

现在,我们调用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基本类似,除了在定义了StyleTemplate时,会多一个查找目标。具体的细节可参数MSDN

闲话WPF之十七(WPF中的资源 [3] 

继续相同的话题:WPF中的资源。这次我将尝试从另外一个角度来分析WPF中的资源:资源编译行为,以及如何根据应用程序的需要选择适当的类型。 

首先建立一个默认的WPF工程,然后向工程中添加一个ICON资源。在添加资源后,我们可以选择资源的类型,如下图所示:



从图中的下拉列表我们可以看到资源所支持的各种类型。主要支持的编译行为是ResourceContent。如果选择为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的文件都会被创建一个文件映射关系。运行时,根据指定的UriWPF的加载机制将根据实际情况加载资源。

不管我们所引用的类型是Resource还是Content,在代码中,我们可以通过简单的相关Uri来访问这些资源:

<Object Property=”WTL2007.ico”/>

下面有几个比较好的建议,可以帮助我们选择资源的编译Action。对于下面的这些需求,应该选择Resource

1、文件是本地化文件

2、应用程序部署后不再希望文件被修改

3、如果不希望对文件进行单独的部署或者管理,移动应用程序时不必担心资源的位置

对于下面的一些资源文件需求,我们应该选择Content

1、文件不是本地化文件

2、希望文件在部署后可以被替换

3、希望文件可以被下载更新等等(注意不能是包含在应用程序Assembly)。

4、某些不能被设置为Resource类型的文件,否则WPF不能识别。 

闲话WPF之十八(WPF中的资源 [4]  

前一个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://其实是一种URIUniform 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的新手(我也是),对于此点不必过度纠缠。因为WPFApplication类中提供了一些有用的方法:

Application.GetResourceStream (Uri relativeUri);

Application.GetContentStream(Uri relativeUri);

Application.GetRemoteStream (Uri relativeUri);

通过使用这些函数,隐藏了URI定位的细节。从这些函数的名称我们可以看出,它们分别对应于我在前面介绍的三种类型:ContentResourceSiteofOrigin

最后,简单的说明一下另一种使用资源的方式,直接定义资源,不使用任何的属性,具体的用法看例子就明白了:

<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之十九(WPF中的传递事件 [1]  

【传递事件】

WPF.NET简单事件通知之上添加了很多基础结构。传递事件的设计使得事件可以与元素树一起很好的工作。事件发生后,可以在视觉树和逻辑树自动地进行上下传递,我们不需要添加任何额外的代码。

传递事件使得我们不需要过多关注于视觉树,这样封装对于我们理解WPF的元素合成非常重要。比如,我们点击一个按钮的事件,在点击的时候我们实际上点击的是一个ButtonChrome或者TextBlock,也就是说我们点击的是Button的内容元素。正是因为事件可以沿视觉树传递,Button才发现这个事件,并且可以处理。因此,我们可以给ButtonContent当中添加任意的元素,而不会对事件有任何的影响。如果没有这样的事件传递,我们点击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)。这个对于相同的处理函数处理多个传递事件时非常有用,可以用来区别传递事件。

SourceOriginalSource代表了逻辑树和视觉树对象。这有利于我们进行一些低级控制,但是对于有的事件,不需要区别它们,这两个的值是相同的。

闲话WPF之二十(WPF中的传递事件 [2]  

前一个Post当中介绍了WPF如何处理事件的传递过程。如何定义传递事件,并且对事件进行了分类。现在,我们看看WPF到底是如何处理BubblingTunneling事件的。最后介绍了Attached事件。

UIElement类,预定义了很多的传递事件,比如键盘、鼠标等等。其中大多数是Bubbling事件,其中很多的事件都还有一个对应的Tunneling事件。所有的Tunneling事件都是Preview前缀命名,它们都在对应的Bubbling事件之前激发。比如PreviewMouseMove这个Tunneling事件是在MouseMove这个Bubbling事件之前激发的。

Tunneling事件的好处就是可以有机会改变或者取消后面的Bubbling事件。WPF内建的响应事件只会对Bubbling事件进行响应,当然,前提了BubblingTunneling同时定义。这种行为有什么好处呢?看下面的一个例子:比如,我们想实现一种特殊的编辑框,只允许输入一些特定的字符。以前的实现方法在处理编辑框的KeyDown或者编辑框的WM_CHAR事件,然后判断新输入的字符是否满足条件,如果不满足,我们再把编辑框的值设置为原来的值。这种实现技术会有字符的一个回退过程。而在WPF中,实现方法不同,直接在PrevewKeyDownTunneling事件中处理,如果是不需要的字符,把事件设置为已经处理过。这样这个事件就不会进入到后面的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属性类似,WPFElement在事件没有定义的情况下也支持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中的数据处理 [3] 

最近比较忙些,好多天没有写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之上,比如ListBoxComboBox等等。

<ListBox ItemsSource="{StaticResource dataObj}" .../>

绑定之后,只要我的LYLDataObjCol对象发送了变化,ListBoxComboBox的数据也会有对应的变化。

到现在,我们已经知道在绑定的时候有两种指定数据源的方式:1DataContext,关于它我们在这个Post有简单介绍。2、直接用Binding类的Source属性。那么,我们在使用的时候如何区别呢?首先,Source的优先级比DataContext高,只有Source不存在,或者在当前Source到不到需要的属性时才会查找DataContext。除此之外,这两者没有真正的区别,只是建议使用Source,它能有助于我们调试应用程序。因为通过它可以明确的得到Source的信息。而DataContext支持一种继承。可以在父Element指定Source源。这同时也成为了DataContext的一个优点:如果多个Element需要绑定同一个Source源,那么我们只需要在一个地方指定DataContext,就可以在其子Element使用。

闲话WPF之二二(WPF中的Style 

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属性。这是一个非常关键的设置。如果我们设置了Stylex:Key属性,相当于在当前Window是资源中定义了一个名称为x:Key设定值的Style对象。记住定义的效果相当于对象。如果没有设置x;Key,那么这个Style将对属于这个Window中所有CheckBox生效。这就起到了批处理的效果。

首先设定的是StyleTargetType属性,它表示我们希望修改的目标类型。然后定义一个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的标识作用。其它的代码与前面类似。

定义后,引用这个特殊StyleCheckBox的代码是这样的:

<CheckBox Style="{StaticResource WiderCheckBox}">Win</CheckBox>

你已经看到,我们在CheckBox中指定了Style属性,并引用前面的StaticResource标记。

闲话WPF之二三(WPF中的ControlTemplate [1] 

通过前面的介绍,我们已经知道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属性,其意义与StyleTargetType一样。它的x:Key属性也是如此。然后,由一个Grid来表示控件的视觉内容。其中的TemplateBindingBinding类似,表示当前Ellipse的显示颜色与ButtonBackground属性保持同步。TemplateBinding可以理解为Binding在模板中的特例。而另一个ContentPresenterWPF的基本控件类型有关,一种是ContentControl,一个是ItemControl。在上面的例子中定义的是基于ContentControlButton。所以使用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

闲话WPF之二四(WPF中的ControlTemplate [2] 

前面关于ControlTempaltePost当中,只说明了如何定义的外观。如果对于很复杂的自定义控件,通常我们还需要在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>

这段代码设置ListBoxItemOpacity属性的默认值为0.5。但是,在IsSelected属性为True时,ListBoxItemOpacity属性值为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>

这就表示只有IsMouseOverTrueContentNULL的时候才将Background设置为Yellow

以上的Trigger都是基于元素属性的。对于鼠标移动等事件的处理,WPF有专门的EventTrigger。但因EventTrigger多数时候是和Storyboard配合使用的。因此,我将在后面介绍动画的时候详细说明EventTrigger

另一方面,现在所讨论的Trigger都是基于属性的值或者事件的。WPF还支持另一种TriggerDataTrigger。显然,这种数据Trigger用于数据发生变化时,也就是说触发条件的属性是绑定数据的。类似地,数据Trigger也支持多个条件:MultiDataTrigger。他们的基于用法和前面的Trigger类似。

闲话WPF之二五(WPF中的ControlTemplate [3] 

在实际应用中,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)上我们用资源方式引用了前面定义的命名StyleButtonFocusVisual。接下来是定义Template,并为其子元素Border定义了一个名称。然后就是ControlTemplate的触发器。在IsKeyboardFocused属性满足条件的情况下,我们把Border(注意这个Border不是类型,而是具体的某个对象)的BorderBrush修改为另一个静态资源。结合前面的Post,理解也就不难了。

最后,我们还会发现一个有趣的问题:这个例子虽然是ControlTempalte,但工程名称却是SimpleStyle,从这一点我们也可以看出:StyleTemplate通常是配合使用才能真正的实现丰富的自定义功能。

闲话WPF之二六(WPF性能优化点) 

在建立漂亮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、使用元素TextFlowTextBlock时,如果不需要TextFlow的某些特性,就应该考虑使用TextBlock,因为它的效率更高。

6、在TextBlock中显式的使用Run命令比不使用Run命名的代码要高。

7、在TextFlow中使用UIElement(比如TextBlock)所需的代价要比使用TextElement(比如Run)的代价高。

8、把Label(标签)元素的ContentProperty和一个字符串(String)绑定的效率要比把字符串和TextBlockText属性绑定的效率低。因为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有关,显示超链接的时候,尽量只在IsMouseOverTrue的时候显示下划线,一直显示下划线的代码高很多。

11、在自定义控件,尽量不要在控件的ResourceDictionary定义资源,而应该放在Window或者Application级。因为放在控件中会使每个实例都保留一份资源的拷贝。

12、如果多个元素使用相同的Brush时,应该考虑在资源定义Brush,让他们共享一个Brush实例。

13、如果需要修改元素的Opacity属性,最后修改一个Brush的属性,然后用这个Brush来填充元素。因为直接修改元素的Opacity会迫使系统创建一个临时的Surface

14、在系统中使用大型的3D Surface时,如果不需要SurfaceHitTest功能,请关闭它。因为默认的HitTest会占用大量的CPU时间进行计算。UIElement有应该IsHitTestVisible属性可以用来关闭HitTest功能。

原创粉丝点击