基于WPF的开发
一、XAML 语言概述
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。
二、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>
2.1 类继承关系
前面提到过每个XAML元素表示一个.NET CLR类。
多数的XAML元素都是从`System.Windows.UIElement, System.Windows.FrameworkElement,
System.Windows.FrameworkContentElement和System.Windows.ContentElement`继承。
没有任何的XAML元素与.NET CLR的抽象类对应。但是很多元素都有一个抽象类的派生类对应。
-
System.Object 类:大家都知道在.Net中所有类型的根类型,在图中没有画出来,DispatcherObject 就继承于它,所以它是整个应用系统的基类。
-
System.Windows.Threading.DispatcherObject 类:WPF 中的绝大多数对象是从 DispatcherObject 派生的,它提供了用于处理并发和线程的基本构造。WPF 是基于调度程序实现的消息系统。
-
System.Windows.DependencyObject类:WPF基本所有的控件都实现了依赖属性,它表示一个参与依赖项属性系统的对象。
-
System.Windows.Media.Visual类:为 WPF 中的呈现提供支持,其中包括命中测试、坐标转换和边界框计算等。
-
System.Windows.UIElement 类:UIElement 是 WPF 核心级实现的基类,该类建立在 Windows Presentation Foundation (WPF) 元素和基本表示特征基础上。
-
System.Windows.FrameworkElement类:为 Windows Presentation Foundation
(WPF) 元素提供 WPF 框架级属性集、事件集和方法集。此类表示附带的 WPF 框架级实现,它是基于由UIElement定义的
WPF 核心级 API 构建的。 -
System.Windows.Controls.Control 类:表示 用户界面 (UI) 元素的基类,这些元素使用 ControlTemplate 来定义其外观。
-
System.Windows.Controls.ContentControl类:表示包含单项内容的控件。
-
System.Windows.Controls.ItemsControl 类:表示一个可用于呈现项的集合的控件。
-
System.Windows.Controls.Panel类:为所有 Panel 元素(布局)提供基类。使用 Panel 元素在 Windows Presentation Foundation (WPF) 应用程序中放置和排列子对象。
-
System.Windows.Sharps.Sharp类:为 Ellipse、Polygon 和 Rectangle
之类的形状元素提供基类。
除了上面的图以外,还有几个命名空间也很重要,如下:
- System.Windows.Controls.Decorator 类:提供在单个子元素(如 Border 或
Viewbox)上或周围应用效果的元素的基类。 - System.Windows.Controls.Image 类:表示显示图像的控件。
- System.Windows.Controls.MediaElement类:表示包含音频和 /或视频的控件。
通常有如下四种通用的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类似。每个属性(实际上是类属性)继承了父元素的属性或者重载(如果重新设置了属性)。
三、逻辑树和视觉树
- WPF类控件的派生关系图,紫色的部分开始才算是进入WPF的框架里。
那么,对于开发人员,基于WPF的应用程序开发又会是怎样呢?为了实现应用程序UI和逻辑的分离、
设计人员可以更快速、高效地完成UI,微软在WPF中引入了一种全新的基于XML的、
声明式编码语言——XAML。让我们先睹为快,用XAML实现一个“Hello, World”:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml "
Title=”Hello, World”>
<Label>Hello, World</Label>
</Window>
```使用过XML或者某些脚本的读者应该对这种编程方式非常亲切。是的,XAML改变了我们过去的编程方式。
关于XAML和WPF实在有太多的内容,限于篇幅,
这里我将向大家介绍WPF中两个非常重要的概念:视觉树和逻辑树,它们对我们理解WPF编程模式有非常大的帮助。
为了更加形象地理解视觉树和逻辑树,我们看一个稍微复杂些的“Hello, World”程序:
```yaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title=”Hello, World”>
<StackPanel>
<Label>Hello, World</Label>
</StackPanel>
</Window>
3.1逻辑树
在这个简单的应用程序中,Window是一个根结点,它有一个子结点StackPanel,而StackPanel有一个子结点Label,注意Label下还有一个子结点String( 也就是标签控件显示的字符串内容),它同时也是一个叶子结点。这四个对象构成了窗口的一个逻辑树结构。逻辑树结构始终存在于WPF的UI中,不管UI是用XAML编写还是用代码编写。WPF的每个方面(属性、事件、资源等等)都是依赖于逻辑树的。
图5:WPF逻辑树结构
3.2 视觉树
如果将逻辑树和视觉树对比理解,逻辑树通常是指我们在UI逻辑中涉及到的界面元素。
而视觉树是指系统在显示UI过程中显示的所有元素,它暴露了视觉的实现细节。
图6:WPF的视觉树
并不是所有的逻辑树结点都可以扩展为视觉树结点。只有从System.Windows.Media.Visual和System.Windows.Media.Visual3D继承的元素才能被视觉树包含。其他的元素不能包含是因为它们本身没有属于自己的提交(Rendering)行为。
在Windows Vista SDK Tools当中的XamlPad提供查看视觉树的功能。需要注意的是XamlPad目前只能查看以Page为根元素,并且去掉了SizeToContent属性的XAML文档。
我们可以看到视觉树确实比较复杂,其中还包含有很多的不可见元素,比如ContentPresenter。视觉树虽然复杂,但是在一般情况下,我们不需要过多地关注它。我们从根本上改变控件的风格、外观时,需要注意视觉树的使用,因为在这种情况下我们通常会改变控件的视觉逻辑。
3.3 遍历逻辑树和视觉树的辅助类
- WPF中还提供了遍历逻辑树和视觉树的辅助类:
System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper。注意遍历的位置,逻辑树可以在类的构造函数中遍历。但是,视觉树必须在经过至少一次的布局发生之后才能形成,所以它不能再构造函数遍历。通常是在OnContentRendered进行,这个函数在布局发生后被调用。
其实,每个树结点元素本身也包含了遍历的方法。比如,Visual类包含了三个保护成员方法VisualParent、VisualChildrenCount、GetVisualChild。通过它们可以访问结点的父元素和子元素。而对于FrameworkElement,它通常定义了一个公共的Parent属性表示其逻辑父元素。特定的FrameworkElement子类用不同的方式暴露了它的逻辑子元素。比如部分子元素是Children Collection,有时Content属性,Content属性强制元素只能有一个逻辑子元素。
3.4 WPF在Windows Vista中的应用——桌面窗口管理器DWM
桌面窗口管理器(Desktop Window Manager,DWM)是Windows Vista中的一个新组件。她建立在WPF核心图形层组件(milcore)基础之上。她利用WPF的Composition引擎进行多个窗口内容的合成。她的出现几乎改变了Windows Vista中应用程序的屏幕像素显示方式。
DWM是在Windows Vista后出现的操作系统窗口渲染程序,在中文化的系统中被描述为“桌面窗口管理器”,该进程随着系统一起启动,同时提供一些Api给其他程序进行调用。这里展示其中的一个功能——由窗口向DWM订阅其他窗口的缩略图,并由DWM绘制到请求的窗口上。
桌面窗口管理器
通过桌面窗口管理器的桌面合成,应用程序的显示不再是直接画到屏幕上,而是一个显示内存中的一个离屏Surface。然后由桌面窗口管理器将这些Surface合成显示到屏幕之上。
从用户体验的角度看,启用DWM后,提供的视觉效果有毛玻璃框架、3D窗口变换动画、窗口翻转和高分辨率支持。其中最明显的特征有:任务栏窗口的实时缩略图;Alt-Tab和Win-Tab组合键所看到的效果。
在桌面窗口管理器(Desktop Window Manager,DWM)中,我们的每个窗口都用一个Surface(“表面;表层;外观)表示,都可以视为是3D的网格。虽然每个窗口仍是一个矩形,但它们都位于一个3D空间之中。窗口操作(比如最大化,还原等等)的实现已经发生了变化,它们都是对网格进行3D变换实现的,这与以往有了很大的区别。
在Windows Vista中窗口的毛玻璃效果非常绚丽。在窗口的边界,我们可以看到窗口下面的内容。这其中同时具有透明和模糊的效果。但是,在实现毛玻璃时,为了不让下面的窗口内容过于清晰影响上面的窗口,DWM组还对下面的窗口实现了模糊效果。其中的实现要点有:
- 1、 模糊下面的内容,这是由自定义的像素Shader实现。这个Shader是一个完全运行于GPU的小程序,它可以并行处理多个像素。
- 2、 模糊只是针对窗口边界下的部分内容。这些内容需要从不同的缓冲中提取出来。
- 3、 摸索的方法类似于平均值处理:一个像素的值等于其邻居像素的平均值。
众所周知,Direct3D支持多个Surface,最后显示不同Surface时是通过Flip(翻转)实现的,DWM也是如此。这样实现的结果就是,不会再出现以前WM_PAINT消息响应带来的的Tearing(撕裂; 撕开)效果,从而桌面变得更平滑。
现在,我们的桌面可以称得上是一个全屏幕的Direct3D应用程序。不管是老式只支持帖图加速的图形处理器,还是新型的高速图形处理器,我们都需要操作图形处理器的存储系统(显存)。这引出了两个重要的问题:
1、在窗口很多时,运行DWM需要的内存将是一个问题,它随着用户的窗口数增加而增加。
2、DWM会与其它的应用程序共享内存资源。比如DirectX应用程序、视频回放和WPF应用程序等等。
微软利用全新的Windows显示驱动模型WDDM解决这些问题。WDDM是Windows Vista及后续操作系统中的DirectX驱动模型。WDDM主要提供三项功能:
-
1、虚拟化显示内存。
这是最关键的功能。虚拟化显示内存后,这就意味着显示内存与系统内存一样。我们知道,在系统内存中如果内存分配完毕,此时却还有新的分配要求,就会产生第二存储页面,然后由系统管理存储页面和主存储页面的算法和机制来控制内存分配。现在,主存储是显示内存,而第二存储页面是系统内存。在显示存储和系统内存都分配完后,将使用磁盘作为视频内存表面。当然,这种情况比较少见,但是这样的设计使得WDDM足够的健壮,应用程序的可靠性也得到增强。对WDDM而言,它将实现非常关键的功能:执行内存的分配、实现分配内存和真正的显示内存的控制。WDDM本身也在不断的改进中。 -
2、允许与GPU的交互。
既然WDDM已经实现了显示内存的虚拟化,那么这就意味着WDDM具有调整应用程序的GPU命令优先级的功能。这种功能通常是由WDDM调度程序实现。因此WDDM必须能中断GPU的某些操作,并保存操作的上下文,以备在必要时恢复操作继续运行。基于这项功能,WDDM提供了两种级别的调度支持:(1)、基本调度。它是基于DirectX9的WDDM驱动和硬件所支持的调度粒度。也就是说单独的Primitive和Shader程序不能被中断,上下文交换必须在它们完成后进行。
(2)、高级调度。它是基于DirectX10的WDDM驱动和硬件所支持的调度粒度。这种调用支持比Primitive和Shader更细粒度的中断。注意,虽然DirectX10支持高级调度,但是它并不是DirecX10所必须的。也就是说,只有部分硬件支持高级调度。桌面窗口管理器使用DirectX9,因此它是支持基本调度。
-
3、允许Direct3D表面可以跨进程共享
共享Direct3D表面对于重定向Direct3D应用程序非常重要。因为Vista必须要和以前的应用程序兼容,就必须支持以前用GDI、DirectX编写的应用程序。WDM必须把这些应用程序的窗口重定向到Surface,然后由WDM统一合成,最后显示单一的桌面Surface。需要注意的一点是:WDM只重定向Top-level的窗口。而对于MDI应用程序,它所有的Top-level窗口、子窗口会被合成为一个单独的Surface,然后交给DWM合成。
3.5 窗口类
在 WPF 中,窗口由用于执行以下操作的 Window 类封装:
- 显示窗口。
- 配置窗口的大小、位置和外观。
- 托管特定于应用程序的内容。
- 管理窗口的生存期。
下图展示了窗口的构成部分:
Screenshot that shows parts of a WPF window.
窗口分为两个区域:非工作区和工作区。
窗口的非工作区由 WPF实现,它包括大多数窗口所共有的窗口部分,包括:
标题栏 (1-5)。
图标 (1)。
标题 (2)。
最小化 (3)、最大化 (4) 和关闭 (5) 按钮。
包含菜单项的系统菜单 (6)。 单击图标 (1) 时出现。
边框 (7)。
窗口的工作区是窗口的非工作区内部的区域,由开发人员用于添加特定于应用程序的内容,例如菜单栏、工具栏和控件。
工作区 (8)。
大小调整手柄 (9)。 这是添加到工作区 (8) 的控件。
四、XAML中的类型转换
转换(Converters)
有时候数据的格式并不只是简答的显示,比如bool类型需要显示成三态,可以通过一个转换器来实现。
在前面关于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>
五、Dependency属性
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属性的实现还集中、并且标准化了属性的线程访问检查、提示元素重新提交等等。
曾提到将要重点研究Dependency属性的三个方面:变化通知;属性值的继承;支持多个提供对象。下面,我将分别就这三个内容进行简单地说明。
5.1 【变化通知】
在任何时候,只要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>
5.2【属性值继承】
属性值继承是指在设置逻辑树某个结点元素的属性后,它的所有之结点都继承这个属性值(当然,前提是子元素必须支持这个属性)。
我们仍然利用闲话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继承的对象。对于这些内容没有很好的文档说明。我们只需要能使用就行不必过多关心。
5.3【支持多个提供对象】
重点说明了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中可能有不同的行为。只应该将其作为调试工具。
六、数据绑定
数据绑定,这是WPF提供的一个真正的优点。
除了可以用在传统的绑定环境中,数据绑定已经被扩展应用到控件属性上。
学习应用数据绑定,也能真正的体现XAML的好处。
到底什么是数据绑定呢?也许你从字面上已经理解的很不错了。通过数据绑定,我们在应用程序UI和程序逻辑之间建立了一种联系。正常建立绑定后,在数据的值发生改变后,绑定到数据的元素将自动更新、体现出数据的变化。
同样,我们先看几个相关的知识点:
数据模板描述了数据显示的外观,我们如何使得这些外观和数据关联起来呢?这就是数据绑定(data binding)来做的事情。
WPF的数据绑定是一个强大的功能,它允许单项和双向的绑定,当对象更改时UI会自动刷新,当UI操作后数据也可以自动更改。
6.1 DataContext属性。
设置DataContext属性,其实就是指定数据上下文。
那么数据上下文又是什么呢?又是一个新的概念:
数据上下文允许元素从它的父元素继承数据绑定的数据源。
很简单,在某个元素的DataContext中指定的值,那么在这个元素的子元素也可以使用。
注意,如果我们修改了FrameworkElement或者FrameworkContentElement元素的DataContext属性,那么元素将不再继承DataContext值。也就是说新设置的属性值将覆盖父元素的设置。如何设置DataContext属性,稍后说明。
6.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属性。
6.3 数据绑定的方式–INotifyPropertyChanged
- (1)、OneWay,单一方向的绑定,只有在数据源发生变化后才会更新绑定目标。
- (2)、TwoWay,双向绑定,绑定的两端任何一端发生变化,都将通知另一端。
- (3)、OneTime,只绑定一次。绑定完成后任何一端的变化都不会通知对方。
在上面的第二点我介绍了数据源的种类,注意这里的概念和下面要说明的指定数据源的方式的区别。目前,指定数据源有三种方式,我们可以通过任何一种方式来指定上述的任何一种数据源:
- (1)、通过Source标记。我们可以在使用Binding使用Source标记显式指定数据源。
- (2)、通过ElementName标记。这个ElementName指定了一个已知的对象名称,将使用它作为绑定数据源。
- (3)、通过RelativeRource标记。这个标记将在后面说明ControlTemplate和Style时再进行说明。
现在我们说明了很多和数据源相关的内容。但是再绑定的时候,我们还需要指定绑定对象的属性名称。所以WPF提供了一个Path标记。它被用来指定数据源的属性。也即是数据源将在数据源对象的Path所指定的属性上寻找属性值。
现在,理论的东西讲了一堆,我将在后面用一些简单的例子进行说明。
我叙述了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替代即可。
6.4 多数据绑定的方式–ObservableCollection
今天,我们继续回到前面的话题: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使用。
七、资源
- 资源是保存在可执行文件中的一种不可执行数据。
- 通过资源我们可以包含图像、字符串等等几乎是任意类型的数据。
如此重要的功能,.NET Framework当然也是支持的,其中内建有资源创建、定位、打包和部署的工具。
在.NET中可以创建.resx和.resources文件。其中.resx由XML项组成。.resx只是一种中间格式,不能被应用程序直接使用,它必须用工具转换为.resource格式。
7.21 在WPF中,资源的含义和处理方式与传统的Win32和Windows Forms资源有所区别
- 在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属性❌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。读者朋友可根据实际情况修改。
我们先看一个例子程序:
<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方法添加资源。第一个参数是资源的名称,第二个参数是添加的资源对象。
接下来我们看看修改资源的方法。在上面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。
7.2 资源编译行为的角度-资源的类型Resource和Content
另外一个角度来分析WPF中的资源- 资源的类型Resource和Content
继续相同的话题: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不能识别。
7.3 类IE地址定位–包装的Uri
我从资源编译行为的角度讨论了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://<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如何处理事件的传递过程
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代表了逻辑树和视觉树对象。这有利于我们进行一些低级控制,但是对于有的事件,不需要区别它们,这两个的值是相同的。
十 、RoutedEventArgs
介绍了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中的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属性。这是一个非常关键的设置。如果我们设置了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中的ControlTemplate
- **控件模板(control template):**控件模板定义一个控件的外观,这个控件主要和UI相关,一般也不和Data有关。每个控件都有个一个缺省的控件模板。
- **数据模板(data template):**数据模板负责定制任何一个.Net对象的外观,这对于非UIElement控件非常重要,非UIElement控件的默认模板仅仅是一个TextBlock,其中包含了一个由ToString方法反会的字符串。
- content presenter
通过Templates讲解,我们现在知道控件模板定义控件外观,数据模板定义特定的数据如何显示,那么现在如何将定义的数据显示在控件外观上显示呢?那就需要content presenter了。每个控件都有一个默认的ContentPresenter用于显示Content内容,我们称这种控件为内容控件,ContentPresenter负责将ContentControl的Content属性显示出来
12.1 ControlTemplate 产生的背景
- 通过前面的介绍,我们已经知道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。
12.2 在Resource中如何使用ControlTemplate
前面关于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类似。
12.3 如何使用ControlTemplate 的案例
在实际应用中,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文章。
另一个需要说明的是的子元素,可以是任何的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通常是配合使用才能真正的实现丰富的自定义功能。
十三、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、使用元素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功能。
十四、Application-WPF应用程序生存周期
WPF和 传统的WinForm 类似, WPF 同样需要一个 Application 来统领一些全局的行为和操作,并且每个 Domain (应用程序域)中只能有一个 Application 实例存在。和 WinForm 不同的是 WPF Application 默认由两部分组成 : App.xaml 和 App.xaml.cs,这有点类似于 Delphi Form(我对此只是了解,并没有接触过Delphi ),将定义和行为代码相分离。当然,这个和WebForm 也比较类似。XAML 从严格意义上说并不是一个纯粹的 XML 格式文件,它更像是一种 DSL(Domain Specific Language,领域特定语言),它的所有定义都直接映射成某些代码,只是具体的翻译工作交给了编译器完成而已。WPF应用程序由System.Windows.Application类来进行管理。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
static class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}
**WinForm**
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
namespace WPFApplications
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
[STAThread]
static void main()
{
// 定义application对象作为整个应用程序入口
//application app = new application();
方法一:调用run方法,参数为启动的窗体对象 ,也是最常用的方法
//window2 win = new window2();
//app.run(win);
// 方法二:指定application对象的mainwindow属性为启动窗体,然后调用无参数的run方法
//window2 win = new window2();
//app.mainwindow = win;
//win.show();
// win.show()是必须的,否则无法显示窗体
//app.run();
// 方法三:通过url的方式启动
//app.startupuri = new uri("window2.xaml", urikind.relative);
//app.run();
app.run()方法之前
//app.shutdownmode = shutdownmode.onexplicitshutdown;
//app.run(win);
}
}
}
**WPF**
<Application x:Class="MVVMCommandLesson.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MVVMCommandLesson"
StartupUri="Window1.xaml">
<Application.Resources>
</Application.Resources>
</Application>
14.1 Application对象的事件
名称 | 描述 |
---|---|
Activated | 当应用程序成为前台应用程序时发生,即获取焦点。 |
Deactivated | 当应用程序停止作为前台应用程序时发生,即失去焦点。 |
DispatcherUnhandledException | 在异常由应用程序引发但未进行处理时发生。 |
Exit | 正好在应用程序关闭之前发生,且无法取消。 |
FragmentNavigation | 当应用程序中的导航器开始导航至某个内容片断时发生,如果所需片段位于当前内容中,则导航会立即发生;或者,如果所需片段位于不同 内容中,则导航会在加载了源 XAML 内容之后发生。 |
LoadCompleted | 在已经加载、分析并开始呈现应用程序中的导航器导航到的内容时发生。 |
Navigated | 正好在应用程序关闭之前发生,且无法取消。 |
Navigating | 正好在应用程序关闭之前发生,且无法取消。 |
NavigationFailed | 正好在应用程序关闭之前发生,且无法取消。 |
NavigationProgress | 正好在应用程序关闭之前发生,且无法取消。 |
NavigationStopped | 在调用应用程序中的导航器的 StopLoading 方法时发生,或者当导航器在当前导航正在进行期间请求了一个新导航时发生(没大用到)。 |
SessionEnding | 在用户通过注销或关闭操作系统而结束 Windows 会话时发生。 |
Startup | 在调用 Application 对象的 Run 方法时发生。 |
应用程序的事件处理可以:
1、在App.xaml中做事件的绑定,在App.xaml.cs文件中添加事件的处理方法
在App.xaml文件中:
<Application x:Class="WPFApplications.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
Startup="Application_Startup"
Exit="Application_Exit"
DispatcherUnhandledException="Application_DispatcherUnhandledException">
<Application.Resources>
Application.Resources>
Application>
在App.xaml.cs文件中:
public partial class App : Application
{
[STAThread]
static void Main()
{
// 定义Application对象作为整个应用程序入口
Application app = new Application();
// 方法一:调用Run方法,参数为启动的窗体对象 ,也是最常用的方法
Window2 win = new Window2();
app.Run(win);
}
private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{ }
private void Application_Exit(object sender, ExitEventArgs e)
{ }
}
2、在自定义的类中可以做正常的C#的事件绑定:
public partial class App : Application
{
[STAThread]
static void Main()
{
// 定义Application对象作为整个应用程序入口
Application app = new Application();
// 调用Run方法,参数为启动的窗体对象 ,也是最常用的方法
Window2 win = new Window2();
app.Startup += new StartupEventHandler(app_Startup);
app.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(app_DispatcherUnhandledException);
app.Run(win);
}
static void app_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
throw new NotImplementedException();
}
static void app_Startup(object sender, StartupEventArgs e)
{
throw new NotImplementedException();
}
}
如果通过XAML启动窗体的话,也会编译成为为如下的程序,默认路径为Debug文件夹得App.g.cs文件:
public partial class App : System.Windows.Application {
///
/// InitializeComponent
///
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent() {
#line 4 "..\..\App.xaml"
this.StartupUri = new System.Uri("Window5.xaml", System.UriKind.Relative);
#line default
#line hidden
}
///
3 /// Application Entry Point.
///
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static void Main() {
WPFApplications.App app = new WPFApplications.App();
app.InitializeComponent();
app.Run();
}
}
14.2 WPF应用程序生存周期
14.3.Window
对于WPF应用程序,在Visual Studio和Expression Blend中,自定义的窗体均继承System.Windows.Window类.大家都可能听说过或者看过Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation这本书,它里面就是用XAML和后台代码两种形式来实现同一个功能,那么我们这里定义的窗体也由两部分组成:
14.4 窗体的生命周期
-
1、显示窗体
构造函数
Show()、ShowDialog()方法:Show()方法显示非模态窗口,ShowDialog()方法显示模态窗口,这个基本和WinForm类似
Loaded事件:窗体第一次Show()或ShowDialog()时引发的事件,通常在此事件中加载窗体的初始化数据,但如果用了MVVM模式,基本就不在这里面写。 -
2、关闭窗体
Close()方法:关闭窗体,并释放窗体的资源
Closing事件、Closed事件:关闭时、关闭后引发的事件,通常在Closing事件中提示用户是否退出等信息。 -
3、窗体的激活
Activate()方法:激活窗体
Activated、Deactivated事件:当窗体激动、失去焦点时引发的事件
4、窗体的生命周期