WPF内容全览
1 1 结构概览
1.1 1.1 框架结构
图中红色部分(PresentationFramework, PresentationCore, 和milcore)是WPF的主要代码部分,其只有一部分是非托管代码-milcore,milcore之所以用非托管代码编写的目的是为了紧密地和DirectX集成。WPF中的所有显示都是通过DirectX引擎,这样就允许高效利用软硬件来渲染。WPF需要很好的控制程序的内存和执行。milcore对WPF性能极其敏感,所以它放弃了CLR提供的许多优势而采用非托管模式来换取更好的性能
1.2 1.2 类层次
DispatcherObject类
Dispatcher是一个抽象基类,用于绑定到一个线程上的类。与Windows窗体类似,WPF也要求仅从创建线程中调用方法和属性。WPF应用程序使用为人熟知的单线程亲和(Single-Thread Affinity,STA)模型,这意味着整个用户界面由单个线程拥有。从另一个线程与用户界面进行交互是不安全的。通过继承自DispathcerObject类,用户界面中的每个元素都可以检查代码是否在正确的线程上运行,并能通过访问调度程序为用户界面线程封送代码。
Application类
在WPF应用程序中,会创建Application类的一个实例.这个类实现了Singleton模式,用于访问应用程序的窗口,资源和属性
DependencyObject类
DenpendencyObject是所有支持依赖属性的类的基类。依赖属性可以依赖其他输入,例如主题和用户喜好。依赖属性与数据绑定,动画,资源和样式一起使用。
Visual类
所有可见元素的基类都是Visual。这个类包含点击测试和转换等特性
UIElement类
所有需要基本显示功能的WPF元素的抽象基类是UIElement。这个类提供了鼠标移动,拖放,按键的通道和起泡事件;提供了可以由派生类重写的虚显示方法;以及布局方法。WPF不再使用Window句柄,这个类就可以用Window句柄
FrameworkElement类
FrameworkElement派生自基类UIElement,实现了由基类定义的方法的默认代码
Shape类
Shape是所有图形元素的基类,例如Line,Ellipse,Polygon,Rectange
Control类
Control派生自FrameworkElement,是所有用户交互元素的基类
Panel类
Panel派生自FrameworkElement,是所有面板的抽象基类,这个类的Children属性用于面板中的所有UI元素,定义了安排子控件的方法。派生自Panel的类为子控件的布置方式定了不同的类,例如WrapPanel,StackPanel,Canvas,Grid
ContentControl类
是所有有单个内容的控件的基类,如Label,Button。 内容控件的默认样式是受限制的,但可以使用模版改变其外观,ContentControl模型的类型具有一个Content属性.Content属性的类型为Object因此ContentControl中可以放置的内容没有任何限制
以下控件在使用ContentControl内容模型:
Button,ButtonBase,CheckBox,ComboBoxItem,ContentControl,Frame,GridViewColumHeader,GroupItem,Label,ListBoxItem,ListViewItem,NavigationWindow,RadioButton,RepeatButton,ScrollViewer,StatusBarItem, ToggleButton, ToolTip, UserControl, Window
Content中只能放置一个控件
HeaderedContentControl类
HeaderedContentControl类继承ContentControl类, 表示带有Header的ContentControl,其除了具有ContentControl的Content属性外,还具有一个Header属性
以下控件在使用HeaderedContentControl模型
Expander,GoupBox,TabItem
ItemsControl类
从ItemsControl继承的控件包含一个对象集合,可以使用ItemsSource属性或Items属性来填充一个ItemsControl.
以下控件在使用ItemsControl模型
ComboBox , ContextMenu, ListBox, ListView, Menu,StatusBar,TabControl,TreeView
HeaderedItemsControl类
HeaderedItemsControl从ItemsControl类继承. HeaderedItemsControl定义Header属性,该属性遵从相同的规则,因为HeaderedContentControl. WPF的Header属性附带三个从HeaderedItemControl继承的控件:
MenuItem,ToolBar,TreeViewItem
2 2 内容呈现
2.1 2.1 布局
分两个过程:测量和排列。
Measure:父元素根据可用大小询问子元素所需大小,
Arrange排列:然后根据布局逻辑确定子元素的实际大小和位置。
2.1.1 2.1.1 两步布局过程#
WPF的布局大致上分为Measure和Arrange两步,布局元素首先递归地用Measure计算所有子元素所需的大小,然后使用Arrange实现布局。
以StackPanel为例,当StackPanel需要布局的时候,它首先会得知有多少空间可用,然后用这个可用空间询问Children的所有子元素它们需要多大空间,这是Measure;得知所有子元素需要的空间后,结合自身的布局逻辑将子元素确定实际尺寸及安放的位置,这是Arrange。
当StackPanel需要重新布局(如StackPanel的大小改变),这时候StackPanel就重复两步布局过程。如果StackPanel的某个子元素需要重新布局,它也会通知StackPanel需要重新布局。
2.1.2 2.1.2 MeasureOverride#
MeasureOverride在派生类中重写,用于测量子元素在布局中所需的大小。简单来说就是父元素告诉自己有多少空间可用,自己再和自己的子元素商量后,把自己需要的尺寸告诉父元素。
2.1.3 2.1.3 DesiredSize#
DesiredSize指经过Measure后确定的期待尺寸。下面这段代码演示了如何使用MeasureOverride和DesiredSize:
Copy
protected override Size MeasureOverride(Size availableSize)
{
Size panelDesiredSize =newSize();
// In our example, we just have one child.
// Report that our panel requires just the size of its only child.
foreach(UIElement childinInternalChildren)
{
child.Measure(availableSize);
panelDesiredSize = child.DesiredSize;
}
returnpanelDesiredSize ;
}
2.1.4 2.1.4 InvalidateMeasure#
InvalidateMeasure使元素当前的布局测量无效,并且异步地触发重新测量。
2.1.5 2.1.5 IsMeasureValid#
IsMeasureValid指示布局测量返回的当前大小是否有效,可以使用InvalidateMeasure使这个值变为False。
2.2 2.2 布局面板容器
StackPanel DockPanel WarpPanel
Canvas
Grid UniformGrid
ScorllView ViewBox
Border
2.3 2.3 控件
文本:TextBlock TextBox Label Button
选择:RadioButton checkbox ComboBox
列表:ListBox ListView DataGrid
树:TreeView Menu
GroupBox分组
Expander折叠器
TabControl页签
ToolTip提示信息
图片:Image
视频:MediaElement媒体
Border
标签文本框密码框按钮控件
1、Label 文本标签 父类 ContentControl
2、TextBox 文本框 编辑与显示 父类 TextBoxBase --Control 特殊内容控件
3、VerticalContentAlignment:垂直居中对齐方式
4、HorizontalContentAlignment:水平居中对齐方式
5、VerticalAlignment:垂直对齐方式
6、HorizontalAlignment:水平对齐方式
7、PasswordBox:密码框 PasswordChar:* 父类 Control
8、Button 按钮 ContentControl
9、WPF 允许控件没有Name属性值 后台代码需要引用对象,需要设置Name
Button按钮控件
1、Button —父类:ContentControl
2、Content:文本
3、Background:背景色/图片
4、ImageSource:指定图片位置
5、Foreground:前景色、文本字体颜色、大小
6、BorderBrush:边框颜色
7、BorderThickness:边框粗细
RadioButton 单选按钮控件
1、RadioButton 单选按钮控件 内容控件
2、同一组单选按钮,它们是互斥的关系
3、设置一个组名,不同组名的单选按钮,它们不具有互斥的关系
4、GroupName
5、应用:只能从中选择一个
6、RadioButton:单选按钮
7、IsChecked:设置单选按钮的选择状态(true或false)
CheckBox复选框控件
1、CheckBox:复选框控件 ToggleButton:父类
2、CheckBox:复选框 允许可以选择多个 ContentControl
3、常用属性:Content、Name、IsChecked(true false null)、IsThreeState:设置三种状态(true、空白、false)、Tag(属性)
4、获取已勾选选项 父容器—Grid、Children —子元素的集合
5、事件:Checked:选中 UnChecked:取消选中 Click
Image图片控件
1、属性介绍:
①Stretch:获取或设置描述的值应如何拉伸 System.Windows.Controls.Image加载目标矩形。
None = 0,内容保持其原始大小。
Fill = 1,调整内容的大小以填充目标尺寸,不保留纵横比。
Uniform = 2,在保留内容原有纵横比的同时调整内容的大小,以适合目标尺寸。
UniformToFill = 3,在保留内容原有纵横比的同时调整内容的大小,以填充目标尺寸。如果目标矩形的纵横比不同于源矩形的纵横比,则对源内容进行剪裁以适合目标尺寸。
②StretchDirection:获取或设置一个值图像如何缩放。
UpOnly = 0,向上内容缩放,仅当小于父级。如果内容较大,向下调用不执行。
DownOnly = 1,向下内容缩放,仅当大于父级。如果内容较小,向上调用不执行。
Both = 2,与父的内容拉伸基于 System.Windows.Media.Stretch 模式。
③Source:获取或设置图像的System.Windows.Media.ImageSource。
2、代码指定图像源:
第一种相对路径(Relative)。
两种绝对路径(Absolute):application:/// 和 siteoforigin:///。
siteoforigin图片文件生成:内容。 application—资源、内容
pack URI 方案:pack://授权/路径
授权指定包含部件的程序包的类型,而路径则指定部件在程序包中的位置。
URiKind:判断路径是相对还是绝对
Border边框控件
Border边框:围绕在其他元素周围或背景色
BorderBrush:边框颜色
BorderThickness:边框粗细
CornerRadius:边框圆角的弧度
Background:边框内部背景色
应用:布局面板一起使用、作为任意控件的边框显示。
Border边框:只能有一个元素作为它的子元素
ComboBox下拉框控件
1、ComboBox下拉框 ---- 条目控件(ItemsControl)
2、IsDropDowmOpem:设置下拉框的收缩状态
3、手动添加项:①ComboBoxItem列表框(内容控件)
②IsSelected:选中状态(false或true)
绑定数据:①代码里ItemSource IEnumerable List
②Items.Add
③DataContext =List ItemSource="{Binding}"
ListBox列表框控件
1、ListBox控件介绍 列表框 ---- Selector(父类) 条目控件(ItemsControl)
2、ListBox控件:其中包含可选择的项的列表
3、ListBox控件:①手动添加项、②绑定数据源
4、两个ListBox中的项相互移动,不太适合指定ItemsSource,Items.Add方式添加项,可以灵活的添加或移除
DatePicker日期控件
1、DatePicker日期控件:下拉部分—可视化的日历控件
2、DisplayDateStart:设置日期显示的开始时间
3、DisplayDateEnd:设置日期显示的结束时间
4、DisplayDate:设置日期的当天时间
Calendar可视化日历控件
1、DisplayDateStart:获取或设置可在日历中的第一个日期。
2、DisplayDateEnd:获取或设置可在日历中的日期范围内的最后日期。
3、DisplayMode:获取或设置一个值,该值指示是否日历显示月、年、或十年。
4、DisplayDate:获取或设置要显示的日期
5、SelectedDate:获取或设置当前选定的日期,默认值为null
Slider滑块控件
Slider:移动来选择一个范围的值:改其他控件的属性值 (数值型)
Orientation:方向
TickPlacement:轨道相关的刻度的位置
TickFrequency:刻度之间的间隔
ProgressBar进度条控件
ProgressBar进度条:显示某个操作的进度过程 。
Orientation:进度条的方向 默认水平
IsIndeterminate:显示进度条实际值的呈现方式
2.3.1 集合控件属性SelectedValue SelectedValuePath
SelectedValuePath:
DisplayMebmberPath :只将复合数据中的某一维度显示出来。
ComboBox的ItemsSource需要绑定一个集合类属性,比如界面元素类的DataContenx为BackViewModel对象,在BackViewModel类中定义了:ObservableCollection<Student> Students=....,可将Students绑定到ComboBox上。在ComboBox中,必须要显示在下拉列表框中的显示字段用DisplayMemberPath属性指出来,比如显示的为Name属性,则写法为:<ComboBox ItemsSource={Binding Sutdents},DisplayMemberPath="Name">。此时,该下拉列表框还不能接收选中的对象,为了接收选中的对象,需要将选中对象与程序的某个属性相互绑定。这时,可能有不同的绑定方式,可归纳为两种:
(1)第一种,将集合类中被Selected的成员直接绑定到BackViewModel对象的一个Student属性,比如名字为CurrentStudent,那么可以直接用SelectedItem进行绑定。
<ComboBox ItemsSource={Binding Sutdents},SelectedItem={Binding CurrentStudent},DisplayMemberPath="Name">
(2)第二中,如果程序不是直接将选中的对象绑定到Student类型的属性,而是绑定到Student成员类型的属性。例如Student含有Name、Gender、Age等属性,而只是需要绑定Name属性。这是,在BackViewModel类中定义一个string StudentName属性,然后利用SelectedValue和SelectedValuePath进行绑定,写法如下:
<ComboBox ItemsSource={Binding Sutdents},SelectedValue={Binding StudentName},SelectedValuePath="Name",DisplayMemberPath="Name">
也就是说,SelectedValue和SelectedValuePath应该是配对使用的,其作用是将集合对象中的被选中子对象的某个属性绑定到BackViewModel对象的某个属性,实现被选择对象的属性的实时记录。
那么,默认情况下,SelectedValue/SelectedValuePath实现的是双向绑定吗,经过测试,答案确实是双向绑定的。不过,通过后台代码修改StudentName属性时,修改后的结果需要是Students所有成员的名字之一,否则ComboBox显示未选择状态(即显示为空的状态)。
2.4 2.4 图形
2.5 2.5 动画
计算机中的动画一般是定格动画,也称之为逐帧动画,它通过每帧不同的图像连续播放,从而欺骗眼和脑产生动画效果。
也就是说,我们要产生动画,只需要连续刷新界面即可。
Storyboard
2.5.1 2.5.1 线性插值动画
类型+Animation
线性过程:对元素的某个依赖属性在开始和结束值之间逐步增加的线性过程。
From,to,Duration,RepeatBehavior。
2.5.2 2.5.2 关键帧动画
以时间为节点,让属性在指定的时间节点上达到指定的值。
类型+Animation+UsingKeyFrames
2.5.3 2.5.3 路径动画
让元素沿指定的路径运动。
类型+Animation+UsingPath
类型:double,matrix,point
2.5.4 2.5.4 应用动画---事件触发器的动作开始故事板
<EventTrigger RoutedEvent=”Label.MouseEnter”>
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation To=”SkyBlue” Storyboard.TargetName=”” Storyboadrd.TargetProperty=”” />
2.6 2.6 媒体
3 3 动态交互
3.1 3.1 数据绑定(单向:后台数据驱动到前台,双向:互相更新)
WPF数据绑定为应用程序提供了一种表示数据和与数据交互的简单而又一致的方法。元素能够以公共语言运行库 (CLR) 对象和 XML 的形式绑定到各种数据源中的数据。
绑定时可以进行数据转换,数据验证。
1)一、数据绑定的基本概念:
数据绑定涉及到两个方面:一个是绑定源,再一个是绑定目标。绑定源即控件绑定所使用的源数据,绑定目标即数据显示的控件。
1、对于绑定源,在WPF可以是以下四种:
- CLR对象:可以绑定到CLR类的公开的属性、子属性、索引器上
- ADO.Net对象:例如DataTable、DataView等
- XML文件:使用XPath进行解析
- DependencyObject:绑定到其依赖项属性上,即控件绑定控件
2、对于绑定目标,必须是WPF中的DependencyObject,将数据绑定到其依赖项属性上。
2)二、绑定的基本方式
根据数据流的方向,WPF中的数据绑定分为以下四种:
- OneWay 绑定导致对源属性的更改会自动更新目标属性,但是对目标属性的更改不会传播回源属性。此绑定类型适用于绑定的控件为隐式只读控件的情况。例如,您可能绑定到如股票行情自动收录器这样的源,或许目标属性没有用于进行更改的控件接口(如表的数据绑定背景色)。如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。
- TwoWay 绑定导致对源属性的更改会自动更新目标属性,而对目标属性的更改也会自动更新源属性。此绑定类型适用于可编辑窗体或其他完全交互式 UI 方案。大多数属性都默认为 OneWay 绑定,但是一些依赖项属性(通常为用户可编辑的控件的属性,如 TextBox 的 Text 属性和 CheckBox 的 IsChecked 属性)默认为 TwoWay 绑定。确定依赖项属性绑定在默认情况下是单向还是双向的编程方法是:使用 GetMetadata 获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。
- OneWayToSource 与 OneWay 绑定相反;它在目标属性更改时更新源属性。一个示例方案是您只需要从 UI 重新计算源值的情况。
- OneTime绑定 ,该绑定会导致源属性初始化目标属性,但不传播后续更改。这意味着,如果数据上下文发生了更改,或者数据上下文中的对象发生了更改,则更改会反映在目标属性中。如果您使用的数据的当前状态的快照适于使用,或者这些数据是真正静态的,则适合使用此绑定类型。如果要使用源属性中的某个值初始化目标属性,并且事先不知道数据上下文,则也可以使用此绑定类型。此绑定类型实质上是 OneWay 绑定的简化形式,在源值不更改的情况下可以提供更好的性能。
每个依赖项属性的默认值都不同。一般情况下,用户可编辑控件属性(例如文本框和复选框的属性)默认为双向绑定,而多数其他属性默认为单向绑定。确定依赖项属性绑定在默认情况下是单向还是双向的编程方法是:使用 GetMetadata 来获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。
3)三、实现数据源更改影响目标更改
如果要实现数据源更改时,改变目标的值(即上图中的OneWay方式及TwoWay方式的由绑定源到绑定目标方向的数据绑定),需使数据源对象实现System.ComponentModel命名空间的INotifyPropertyChanged接口。INotifyPropertyChanged接口中定义了一个PropertyChanged事件,在某属性值发生变化时引发此事件,即可通知绑定目标更改其显示的值。例如:
1: using System.ComponentModel;
2:
3: namespace BasicWPFDataBinding
4: {
5: public class MyData : INotifyPropertyChanged
6: {
7: #region INotifyPropertyChanged Members
8: public event PropertyChangedEventHandler PropertyChanged;
9: #endregion
10:
11: public MyData()
12: {
13: Name = "Tom";
14: }
15:
16: private string _Name;
17: public string Name
18: {
19: set
20: {
21: _Name = value;
22:
23: if (PropertyChanged != null)
24: {
25: // 引发PropertyChanged事件,
26: // PropertyChangedEventArgs构造方法中的参数字符串表示属性名
27: PropertyChanged(this,new PropertyChangedEventArgs("Name"));
28: }
29: }
30: get
31: {
32: return _Name;
33: }
34: }
35: }
36: }
4)四、实现绑定目标的值更改影响绑定源的值
若实现实现绑定目标的值更改影响绑定源的值(即上图中TwoWay的由绑定目标到绑定源方向,及OneWayToSource),可以设置相应控件绑定时的UpdateSourceTrigger的值,其值有三种:
- PropertyChanged:当绑定目标属性更改时,立即更新绑定源。
- LostFocus:当绑定目标元素失去焦点时,更新绑定源。
- Explicit:仅在调用 UpdateSource 方法时更新绑定源。
多数依赖项属性的UpdateSourceTrigger 值的默认值为 PropertyChanged,而 Text 属性的默认值为 LostFocus。
5)绑定源的指定方式
ElementName—通过源的x:name指定源。
Source——指定绑定源。Source指向数据源对象的使用,经常配合Window.Resources使用。
ItemSource——集合数据源。
RelativeSource——绑定父(自己)控件的属性。Mode:self和FindAncestor。
<TextBox x:Name="textBox1" FontSize="24" Margin="10" Text="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1},Path=Name}"/>
DataContext——数据上下文。数据环境。从自己开始,向父控件寻找包含对应path的DataContext[c1] 。如果第一次找到的DataContext不包含此path指定的属性。第二次才有呢
5)自定义的绑定类型转换
如定义自定义的绑定类型转换,需要定义一个类,对于这个类要求:
- 实现System.Windows.Data命名空间的IValueConverter接口,这个接口有两个抽象方法,对应两个方向的转换
- 为此类添加System.Windows.Data命名空间的ValueConversion这个Attribute,指定转换的源类型和目标类型
6)通过代码实现数据绑定
通过代码实现数据绑定,使用的是System.Windows.Data命名空间的Binding类,主要使用Binding类的如下的属性:
- Source属性:绑定到的数据源
- Mode属性:绑定的模式(OneTime、OneWay、TwoWay、OneWayToSource或Default)
- Path属性:绑定到的数据源的属性
- Converter属性:绑定时所使用的类型转换器
在绑定目标控件上使用SetBinding方法添加数据绑定。例如将MyData的Name属性绑定到txtName控件的Text属性上,使用MyColorConverter转换器将MyBindingColor的ColorObject属性绑定到rec控件的Fill属性上:
1: MyData data = new MyData();
2:
3: Binding binding1 = new Binding();
4: binding1.Source = data;
5: binding1.Mode = BindingMode.OneWay;
6: binding1.Path = new PropertyPath("Name");
7:
8: txtName.SetBinding(TextBox.TextProperty, binding1);
9:
10:
11: MyBindingColor color = new MyBindingColor();
12:
13: Binding binding2 = new Binding();
14: binding2.Source = color;
15: binding2.Mode = BindingMode.OneWay;
16: binding2.Path = new PropertyPath("ColorObject");
17: binding2.Converter = new MyColorConverter();
18:
19: rec.SetBinding(Rectangle.FillProperty, binding2);
7)二、实现绑定数据的验证:
对于绑定数据的验证,系统采用如下的机制:
使用 WPF 数据绑定模型可以将 ValidationRules 与 Binding 对象相关联。当绑定目标的属性向绑定源属性传递属性值时(仅限TwoWay模式或OneWayToSource模式),执行ValidationRule中的Validate方法,实现对界面输入数据的验证,界面数据绑定时指定验证规则。
定义验证可以采用以下三种:
- DataErrorValidationRule:检查由源对象的 IDataErrorInfo 实现所引发的错误,要求数据源对象实现System.ComponentModel命名空间的IDataErrorInfo接口。
- 自定义验证规则:定义一个类,继承ValidationRule抽象类,实现其Validate方法,验证某一输入。
- 定义控件模板
如果要为相应的控件添加一些辅助的控件,可以使用控件模板,如出现验证错误时,不使用系统默认的红色边框,而是在文本框后添加一个红色的星号:
1: <ControlTemplate x:Key="validErrorTextBoxTemplate">
2: <DockPanel>
3: <AdornedElementPlaceholder/>
4: <TextBlock Foreground="Red" FontSize="20">*</TextBlock>
5: </DockPanel>
6: </ControlTemplate>
并在每一个输入的TextBox中添加:
1: Validation.ErrorTemplate="{StaticResource validErrorTextBoxTemplate}"
3.2 3.2 路由事件(前台到后台)
路由事件的处理模型常用的有两种:
- 冒泡事件:由子控件位次向父容器传递,大部分的路由事件都是冒泡事件
- 隧道事件:由父容器位次向其子容器、控件传递,一般PreXXX事件属性隧道事件
使用路由事件响应方法中的e.Handled = true;意味着此事件已经被处理,将不再传递,默认e.Handled的值为false,意味着此路由事件还未处理完整,事件将依据其模型继续向下处理(即执行其他的事件处理方法)
3.3 3.3 命令(前台到后台)
事件没有约束力。命令有逻辑约束。
命令绑定:将命令与事件处理绑定。
命令:
命令(源)发出者:指定命令和命令目标。
命令绑定:绑定命令和命令的事件处理(canExecute,Executed)
3.4 3.4 触发器
3.4.1 3.4.1 数据触发器
DataTrigger,用在数据模板中。当绑定的数据匹配指定值时,可调用Setter和EventSetter。
3.4.2 3.4.2 属性触发器
用于控件模板或样式中。当属性匹配指定值时,调用Setter和EventSetter。
3.4.3 3.4.3 事件触发器
EventTrigger,用来指定动画。
3.4.4 3.4.4 多条件触发器
包括多数据触发器和多属性触发器。
3.5 3.5 样式
可包括设置器, 触发器, 资源。
使用方式:内联方式和资源方式。
3.6 3.6 资源
保存样式、对象、传统的资源(图片,二进制数据。)
静态资源:只从资源集合中获取对象一次。
动态资源:需要时都重新获取。
4 4 依赖属性
https://www.cnblogs.com/Zhouyongh/archive/2009/09/10/1564099.html
4.1 4.1 属性带来的问题
- 因继承而带来的对象膨胀。每次继承,父类的字段都被继承,这样,继承树的低端对象不可避免的膨胀。
- 大多数字段并没有被修改,一直保持着构造时的默认值,可否把这些字段从对象中剥离开来,减少对象的体积。
- 依赖属性DependencyProperty,它里面存储前面我们提到希望抽出来的字段。DP内部维护了一个全局的Map用来储存所有的DP,对外暴露了一个Register方法用来注册新的DP。当然,为了保证在Map中键值唯一,注册时需要根据传入的名字和注册类的的HashCode取异或来生成Key。这里最关键的就是最后一个参数,设置了这个DP的默认值。
- 然后定义了DependencyObject来使用DP。首先使用DependencyProperty.Register方法注册了一个新的DP(NameProperty),然后提供了GetValue和SetValue两个方法来操作DP。最后,类似前面例子中的NormalObject,同样定义了一个属性Name,和NormalObject的区别是,实际的值不是用字段来保存在DependencyObject中的,而是保存在NameProperty这个DP中,通过GetValue和SetValue来完成属性的赋值取值操作。
4.2 4.2 简介
WPF在DP的PropertyMedata中加入了PropertyChangedCallback以及CoerceValueCallback等。这些Delegate可以在构造PropertyMetadata时传入,在SetValue过程中,会取得对应的PropertyMetadata,然后回调PropertyChangedCallback。这个PropertyMetadata可以在构建DP时传入,也可以在子类调用OverrideMetadata时传入,这就保证了同一个DP不同的DependencyObject可以有不同的应用。WPF对此进行了很多扩展,定义了一套属性赋值的规则,包括计算(calculate)、限制(Coerce)、验证(Validate)等等。
4.3 4.3 使用DependencyProperty
一个简单的使用如下:
1: public class SimpleDO : DependencyObject
2: {
3: public static readonly DependencyProperty IsActiveProperty =
4: DependencyProperty.Register("IsActive", typeof(bool), typeof(SimpleDO),
5: new PropertyMetadata((bool)false));
6:
7: public bool IsActive
8: {
9: get { return (bool)GetValue(IsActiveProperty); }
10 set { SetValue(IsActiveProperty, value); }
11: }
12: }
SimpleDO sDo = new SimpleDO();
sDo.IsActive = true;
这里是使用DependencyProperty.Register来注册DP的,Register函数有很多重载,一个最全的形式如下:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType,
PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
前4个参数在前篇文章已有介绍,主要是用来确定DP在全局Map中的键值,属性的类型以及内部属性元数据。最后一个参数是一个delegate,用来验证数据的有效性。
抛开验证的过程不说,先来看看PropertyMetadata。前篇提到,这个PropertyMetadata是可以子类化的,子类可以调用OverrideMetadata来重写PropertyMetadata。WPF属性系统对于依赖属性支持的策略就封装在Metadata中,那么这个PropertyMetada都有哪些呢?
常见的主要有FrameworkPropertyMetadata,UIPropertyMetadata以及PropertyMetadata。他们的继承关系是F->U->P。以最复杂的来说,FrameworkPropertyMetadata都提供了哪些功能呢?
4.3.1 4.3.1 FrameworkPropertyMetadata
FrameworkPropertyMetadata的构造函数提供了很多重载,一个最复杂的构造函数如下:
public FrameworkPropertyMetadata( object defaultValue,
FrameworkPropertyMetadataOptions flags,
PropertyChangedCallback propertyChangedCallback,
CoerceValueCallback coerceValueCallback,
bool isAnimationProhibited,
UpdateSourceTrigger defaultUpdateSourceTrigger);
其中第一个参数是默认值,最后两个参数分别是是否允许动画,以及绑定时更新的策略,这个不详细解释了。重点看一下里第三、四两个参数,两个CallBack。结合前面提到的ValidateValueCallback,这三个Callback分别代表Validate(验证),PropertyChanged(变化通知)以及Coerce(强制)。当然,作为Metadata,FrameworkPropertyMetadata只是储存了策略信息,WPF属性系统会根据这些信息来提供功能并在适当的时机回调传入的delegate。
那么WPF属性系统确定属性值的规则又是怎样呢?
4.4 4.4 处理DependencyProperty的规则
借用一个常见的图例,介绍一下WPF属性系统对依赖属性操作的基本步骤:
- 第一步,确定Base Value,对同一个属性的赋值可能发生在很多地方。比如控件的背景(Background),可能在Style或者控件的构造函数中都对它进行了赋值,这个Base Value就要确定这些值中优先级最高的值,把它作为Base Value。
- 第二步,估值。如果从第一步得到的值是一个表达式值(Expression),比如说一个绑定,WPF属性系统需要把它转化成一个实际值。
- 第三步,动画。如果当前属性正在作动画,那么因动画而产生的值会优于前面获得的值,这个也就是WPF中常说的动画优先。
- 第四步,强制。如果我们在FrameworkPropertyMetadata中传入了CoerceValueCallback,WPF属性系统会回调我们传入的的delagate,进行数据的强制赋值。在属性赋值过程中,Coerce拥有最高的优先级,这个优先级要大于动画的优先级别。
- 第五步,验证。如果在Register的时候传入了ValidateValueCallback,那么最后WPF会调用我们传入的delegate,来验证数据的有效性。当数据无效时会抛出异常来通知。
5 关键概念
5.1 逻辑树和视觉树
逻辑树相当于控件结构树。视觉树包含了单独控件的外观图层。从根本上改变控件的风格、外观时,需要注意Visual Tree的使用,因为在这种情况下我们通常会改变控件的视觉逻辑。
WPF中还提供了遍历逻辑树和视觉树的辅助类:System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper。注意遍历的位置,逻辑树可以在类的构造函数中遍历。但是,视觉树必须在经过至少一次的布局后才能形成。所以它不能在构造函数遍历。通常是在OnContentRendered进行,这个函数为在布局发生后被调用。
5.2 静态资源和动态资源
静态资源(StaticResource)指的是在程序载入内存时对资源的一次性使用,之后就不再访问这个资源了。--适用于不变的资源。
动态资源(DynamicResource)指的是在程序运行过程中然会去访问资源。--适用于变化的资源。
5.3 Label与TextBlock
5.3.1 继承关系
TextBlock直接继承于FrameworkElement,而Label继承于ContentControl。这样看来,Label可以做这样的事情:
1.可以定义一个控件模板(通过Template属性)
2.可以显示出string以外的其他信息(通过Content属性)
3.为Label内容添加一个DataItemplate(通过ContentTemplate属性)
4.做一些FrameworkElement元素不能做的事情
5.3.2 视觉树
TextBlock的visual tree不包含任何子元素,而Label却复杂的多。它有一个border属性,最后通过一个TextBlock来显示内容。这样看来label其实就是一个个性化的TextBlock。
6 5 线程模型
WPF应用程序都至少有两个线程,一个用于UI绘制,其隐藏于后台,另一个用于管理UI,包括用响应用户输入执行后台代码。WPF 要求将其大多数对象与 UI 线程进行关联。这称之为线程关联,意味着要使用一个 WPF 对象,只能在创建它的线程上使用。在其他线程上使用它会导致引发运行时异常。 UI 线程的作用是用于接收输入、处理事件、绘制屏幕以及运行应用程序代码。
在 WPF 中绝大部分控件都继承自 DispatcherObject,甚至包括 Application。这些继承自 DispatcherObject 的对象具有线程关联特征,也就意味着只有创建这些对象实例,且包含了 Dispatcher 的线程(通常指默认 UI 线程)才能直接对其进行更新操作。
WPF引入了Dispatcher的概念,这个Dispatcher的主要功能类似于Win32中的消息队列,在它的内部函数,仍然调用了传统的创建窗口类,创建窗口,建立消息泵等操作。Dispatcher本身是一个单例模式,构造函数私有,暴露了一个静态的CurrentDispatcher方法用于获得当前线程的Dispatcher。对于线程来说,它对Dispatcher是一无所知的,Dispatcher内部维护了一个静态的List<Dispatcher> _dispatchers, 每当使用CurrentDispatcher方法时,它会在这个_dispatchers中遍历,如果没有找到,则创建一个新的Dispatcher对象,加入到_dispatchers中去。Dispatcher内部维护了一个Thread的属性,创建Dispatcher时会把当前线程赋值给这个Thread的属性,下次遍历查找的时候就使用这个字段来匹配是否在_dispatchers中已经保存了当前线程的Dispatcher。
DispatcherObject 类有两个主要职责:提供对对象所关联的当前 Dispatcher 的访问权限,以及提供方法以检查 (CheckAccess) 和验证 (VerifyAccess) 某个线程是否有权访问对象(派生于 DispatcherObject)。CheckAccess 与 VerifyAccess 的区别在于 CheckAccess 返回一个布尔值,表示当前线程是否可以使用对象,而 VerifyAccess 则在线程无权访问对象的情况下引发异常。通过提供这些基本的功能,所有 WPF 对象都支持对是否可在特定线程(特别是 UI 线程)上使用它们加以确定。
WPF规定了(事实上在.net2.0中便已规定了)UI元素只能由创建该元素的线程来访问。
WPF UI都继承了DispatchObject。
Dispatcher来维持着这一规定,并组织着消息循环。Dispatcher负责检测访问对象的线程与对象创建线程是否一致,不一致则抛出异常。值得一提的是,上面提到了Dispatcher维持着一个规矩“只有创建该对象的线程可以访问该对象”。这里的对象不仅仅是指一些UI控件(比如Button),而是所有的派生于DispatcherObject类的对象。
其他线程的函数,改变UI线程的元素时:使用(UI线程的元素拥有的)Dispatcher的Invoke或BeginInvoke方法。这里的其他线程是在UI线程中创建的,属于UI的子线程。
this.myData. Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
this.myData.TheData = 5;
});
Dispatcher暴露了两个方法,Invoke和BeginInvoke,这两个方法还有多个不同参数的重载。其中Invoke内部还是调用了BeginInvoke
在这个BeginInvoke内部,会把执行函数method与参数args封装成DispatcherOperation,并按priority加入到PriorityQueue中,这个返回值就是内部创建的DispatcherOperation。也就是说每调用一次Invoke和BeginInvoke,就向Dispatcher中加入了一个任务。
Dispatcher的PriorityQueue中的Work Item是有优先级的,这可以让高优先级的项能有更多的工作时间。比如界面绘制比处理用户输入的优先级要高,这使得界面动画更加流畅。这也就是为什么,我们在调用Dispatcher.Invoke ( DispatcherPriority,…) 与Dispatcher. BeginInvoke (DispatcherPriority,…)要传入一个优先级参数的原因。下面是对各个优先级的说明:
|
优先级 |
说明 |
|
Inactive |
工作项目已排队但未处理。 |
|
SystemIdle |
仅当系统空闲时才将工作项目调度到 UI 线程。这是实际得到处理的项目的最低优先级。 |
|
ApplicationIdle |
仅当应用程序本身空闲时才将工作项目调度到 UI 线程。 |
|
ContextIdle |
仅在优先级更高的工作项目得到处理后才将工作项目调度到 UI 线程。 |
|
Background |
在所有布局、呈现和输入项目都得到处理后才将工作项目调度到 UI 线程。 |
|
Input |
以与用户输入相同的优先级将工作项目调度到 UI 线程。 |
|
Loaded |
在所有布局和呈现都完成后才将工作项目调度到 UI 线程。 |
|
Render |
以与呈现引擎相同的优先级将工作项目调度到 UI 线程。 |
|
DataBind |
以与数据绑定相同的优先级将工作项目调度到 UI 线程。 |
|
Normal |
以正常优先级将工作项目调度到 UI 线程。这是调度大多数应用程序工作项目时的优先级。 |
|
Send |
以最高优先级将工作项目调度到 UI 线程。 |
总结,UI线程中创建的子线程,访问UI线程创建的对象时,必须通过UI线程的对象的dispatcher,invoke或begininvoke来访问。这样就是同线程访问。Invoke后
创建此子线程的对象---在UI线程中,其dispatcher的thread是UI线程。
public MainWindow()
{
InitializeComponent();
Thread t = new Thread(ModifyContent);
t.Start();
}
private void ModifyContent()
{
string label = this.lbl.Name; //无法访问Name等属性
this.lbl.Dispatcher.Invoke(() => //能访问Dispatcher
{
this.btn.Content = "123"; //Dispatcher只要与创建this.btn的线程是同一个的对象都可以,通过其Dispatcher
} );
}
7 6 MVVM
MVVM设计模式基于MVC这种将UI和逻辑分离的结构思想
为了解决现实世界中的问题,我们需要将现实世界中的事物加以抽象, 然后得到了Domain Object, 无论贫血的还是富血的, 我们都可以简单地把他们归结为"由现实世界抽象出来的模型",
也就是我们的model, 也就M-V-VM中的"M".
但其无法与我们的用户进行交互, 所以, 我们需要为其创建一个界面(视图, View), 该视图可以与用户输入设备进行交互。通过Binding,
我们可以实现数据的传递; 通过Command, 我们可以实现操作的调用.(AttachBehavior的作用稍后再谈).
Binding和Command是可以写在XAML中的, 这样看来XAML后面对于的CS文件可以被完全抛弃或不予理会了. 这样的XAML文件正是美工所需要的. 而这些对于Binding以及Command的定义描述以及其他相关信息的代码应该放在那里呢, 当然不是View, 更不是Model, 是"ViewModel". ViewModel是为这个View所量身定制的, 它包含了Binding是所需的相关信息,比如Converter以及为View的Binding提供DataContext,
它包含了Command的定义以便View层可以直接使用, 另外,它还是一个变种的Controler, 它得负责业务流程的调度.
采用MVVM的架构可以获得以下好处:
1. 项目可测试更高,从而可以执行单元测试
2. 将UI和业务的设计完全分开,View和UnitTest只是ViewModel的两个不同形式的消费者
3. 有助于我们区别并哪些是UI操作,哪些是业务操作,而不是将他们全混在CodeBehind中
.Model的职责
Model主要提供基础实体的属性以及每个属性的验证逻辑。
Model不包含数据的调用,但是可以包含简单的非数据调用的操作,如产生序列号或者合并字段。
对于WCF产生的客户端代理类,Models中应有与之相对应的类结构定义。
ViewModel的职责
ViewModel是MVVM架构中最重要的部分,ViewModel中包含属性,命令,方法,事件,属性验证等逻辑。为了与View以及Model更好的交互来满足MVVM架构,ViewModel的设计需要注意一些事项或者约束:
ViewModel的属性:ViewModel的属性是View数据的来源。这些属性可由三部分组成:
一部分是Model的复制属性。
另一部分用于控制UI状态。例如一个弹出窗口的控件可能有一个IsClose的属性,当操作完成时可以通过这个属性更改通知View做相应的UI变换或者后面提到的事件通知。
第三部分是一些方法的参数,可以将这些方法的参数设置成相应的属性绑定到View中的某个控件,然后在执行方法的时候获取这些属性,所以一般方法不含参数。
ViewModel的命令:ViewModel中的命令用于接受View的用户输入,并做相应的处理。我们也可以通过方法实现相同的功能。
ViewModel的事件: ViewModel中的事件主要用来通知View做相应的UI变换。它一般在一个处理完成之后触发,随后需要View做出相应的非业务的操作。所以一般ViewModel中的事件的订阅者只是View,除非其他自定义的非View类之间的交互。
ViewModel的方法:有些事件是没有直接提供命令调用的,如自定义的事件。这时候我们可以通过CallMethodAction来调用ViewModel中的方法来完成相应的操作。
8.View及Codebehind
View中使用Command:View中的Button等控件可以直接绑定Command属性调用ViewModel中的Command
View中使用CallMethodAction :一些不支持Command的控件,可以用一个CallMethodAction触发器来执行ViewModel中的方法。注意的是方法当中往往包含一些参数,这些参数一般可以通过给ViewModel设置相应的属性来绑定到相关的输入控件,如TextBox。
View中使用DataTrigger:除了模型属性,还有一部分是状态属性,这往往是ViewModel通过属性更改的方式通知View做出相关的UI操作,例如触发一段动画,或者切换控件状态等等。这个时候可以使用一些触发器,当状态值不同时做出相应的UI变换。
View的CodeBehind中初始化子View的ViewModel上下文:View一般由父View调用,所以View的ViewModel一般由父View来初始化。比如当点击人脉按钮,需要显示人脉的View的时候,就由主框架初始化人脉的ViewModel,并显示人脉的View。
View的CodeBehind中订阅子View的UI事件:除了通过状态属性的变更触发View中的触发器,看一种选择是在View的CodeBehind中订阅ViewModel的UI事件。
9.View及ViewModel交互模式总结
由以上解析我们可以总结出View和ViewModel的交互模式:
1. 父View在CodeBehind中初始化子ViewModel
2. 父View在CodeBehind中订阅子ViewModel的UI事件
3. 父View将子ViewModel赋值给子View的DataContext,并显示子View
4. 父View调用子ViewModel获取数据的方法,子ViewModel调用数据服务获取数据
5. ViewModel的数据通过Binding传递给View
6. View接受用户输入,并通过Command或者CallMethodAction交给ViewModel做业务处理
8 WPF性能
硬件:带图形加速的设备。
8.1 位图
RenderOptions.SetBitmapScalingMode(imageObject,BitmapScalingMode.LowQuality);
8.2 选择合适的元素
生成树时应避免使用UIElements作为子或嵌套控件。用结构简单的元素或面板代替复杂的,比如TextBlock代替Label,canvas代替panel。
8.3 建立逻辑树或者视觉树的时候,遵循Top-Down的原则
TextBlock textBlock = new TextBlock();
textBlock.Text = "Default";
DockPanel parentPanel = new DockPanel();
DockPanel childPanel;
myCanvas.Children.Add(parentPanel);
myCanvas.Children.Add(textBlock);
for (int i = 0; i < 150; i++)
{
textBlock = new TextBlock();
textBlock.Text = "Default";
parentPanel.Children.Add(textBlock);
childPanel = new DockPanel();
parentPanel.Children.Add(childPanel);
parentPanel = childPanel;
}
8.4 静态资源代替动态资源
静态资源是预定义的资源,可以连接到XAML属性,它类似于编译时绑定,不会影响性能,另一方面,动态资源涉及到运行时查找和对象的构建,从而会影响到性能。但也需要注意,静态资源需要在编译时展示。
8.5 当你想显示大型数据时,使用UI虚拟化的控件
想象一下一个组合框绑定大量行时的样子,它会让组合框中项目的展现变得非常慢,这是因为在这种情况下,程序需要计算每个项目的具体显示位置,使用WPF时,你可以延迟这个行为,这就叫做UI虚拟化,它只会在其可见范围内生产项目显示需要的容器。
要实现这种效果,你需要将相应控件的IsVirtualizing属性设为True,例如,Listbox经常用来绑定大型数据集,它是UI虚拟化的重要候选者,其它适宜UI虚拟化的控件包括Combobox,ListView和TreeView。
8.6 使用延迟滚动增强用户体验
如果你还记得可滚动的DataGrid或ListBox,它们往往会降低整个应用程序的性能,因为在滚动时会强制连续更新,这是默认的行为,在这种情况下,我们可以使用控件的延迟滚动(Deferred Scrolling)属性增强用户体验。你需要做的仅仅是将IsDeferredScrollingEnabled附加属性设为True。
8.7 使用卸载事件卸载不必要的动画
动画肯定会占用一定的资源,如果处置方式不当,将会消耗更多的资源,如果你认为它们无用时,你应该考虑如何处理他们,如果不这样做,就要等到可爱的垃圾回收器先生来回收资源。
例如,假设要删除一个StoryBorad,在Unload事件中使用StoryBorad的Remove方法,下面的例子来自MSDN。
<EventTrigger
RoutedEvent="Page.Unloaded" >
<EventTrigger.Actions>
<RemoveStoryboard
BeginStoryboardName="myBeginStoryboard" />
</EventTrigger.Actions>
</EventTrigger>
8.8 使用容器回收提高性能
你可以通过回收执行虚拟化的容器来提高性能,下面的代码片段将ViruatlizationMode设为Recycling,它让你可以获得更好的性能。当用户滚动或抵达另一个项目时,它强制重复使用容器对象。
settingVirtualizingStackPanel.VirtualizationMode="Recycling"
8.9 预测图像绘制能力
使用RenderCapability.Tier属性确定机器是支持硬件加速,还是部分硬件加速,疑惑没有硬件加速,下面的代码显示了你要如何检查Tier。
int displayTier =
(System.Windows.Media.RenderCapability.Tier > 16)
if (displayTier == 0)
{
//no hardware acceleration
}
else if (displayTier
== 1)
{
//partial hardware acceleration
}
else
{
//supports hardware acceleration
}
确定了之后,你就可以有选择性地选择那些在用户硬件上工作得很好的功能
8.10 使用WPF分析工具分析WPF程序
分析WPF程序是理解其行为很重要的一步,市场上有大量现成的WPF程序分析工具,如Snoop,WPFPerf,Perforator和Visual Profiler,其中Perforator和Visual Profiler是WPF Performance Suite的一部分,要了解这些工具的用法,请去它们的项目主页。
浙公网安备 33010602011771号