WPF学习(2) – 逻辑树和可视树、依赖属性、附加属性、路由事件、命令
2012-08-13 16:24 JustRun 阅读(1323) 评论(0) 编辑 收藏 举报1. 逻辑树和可视树
XAML天生就是用来呈现用户界面的,这是由于它具有层次化的特性。在WPF中,用户界面由一个对象树构建而成,这棵树叫作逻辑树
逻辑树的概念很直观,但是为什么要关注它呢?因为几乎WPF的每一方面(属性、事件、资源等)都有与逻辑树相关联的行为。例如,属性值有时会沿着树自动传递给子元素,而触发的事件可以自底向上或自顶向下遍历树.
可视树基本上是逻辑树的扩展,在可视树中,节点都被打散,分放到核心可视组件中。可视树提供了一些详细的可视化实现,而不是把每个元素当作一个“黑盒”。
逻辑树是静态的,不会受到程序员的干扰(例如动态添加/删除元素),但只要用户切换不同的Windows主题,可视树就会改变。
虽然在Window的构造函数中就可以遍历逻辑树,但可视树直到Window完成至少一次布局之后才会有节点,否则是空的. 要访问可视树需要在OnContentRendered,因为OnContentRendered是在布局完成之后才被调用的
2. 依赖属性
WPF引入了一个新的属性类型叫作依赖属性,整个WPF平台中都会使用到它,用来实现样式化、自动数据绑定、动画等。
依赖属性在任何时刻都是依靠多个提供程序来判断它的值的。这些提供程序可以是一段一直在改变值的动画,或者一个父元素的属性值从上慢慢传递给子元素等。依赖属性的最大特征是其内建的传递变更通知(change notification)的能力。
下面是WPF中依赖属性的实现
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) { ? } ? }
依赖属性实现:
- 必须是DependencyProperty类型
- 必须是Public、Static, 并且有一个Property作为后缀
- 调用DependencyProperty.Register静态方法创建的,这样的方法需要一个名称(依赖属性名称)、一个属性类型以及拥有这个属性的类
- 通过不同的Register方法重载,你可以传入metadata(元数据)来告诉WPF如何处理该属性、如何处理属性值改变的回调、如何处理强制值转换,以及如何验证值
- System.Windows.DependencyObject是底层基类,这是拥有依赖属性的类必须继承的。
注意 在运行时,绕过了.NET属性包装器在XAML中设置依赖属性。虽然XAML编译器在编译时是依靠该属性包装器的,但在运行时WPF是直接调用GetValue和SetValue的!因此,为了让使用XAML设置属性与使用过程式代码设置属性保持一致,在属性包装器中除了GetValue/SetValue调用以外,不应该包含任何其他逻辑,这是至关重要的。如果需要添加自定义逻辑,应该在注册的回调函数中添加。所有WPF的内建属性包装器都应遵守这个规则,因此这个警告是针对那些打算写带有依赖属性的自定义类的人的。
依赖属性的功能特征:
A) 变更通知(callback)
无论何时,只要依赖属性的值改变了,WPF就会自动根据属性的元数据(metadata)触发一系列动作。这些动作可以重新呈现适当的元素、更新当前布局、刷新数据绑定等。内建的变更通知最有趣的特性之一是属性触发器,它可以在属性值改变时执行自定义动作,而不用更改任何过程式代码
B) 属性值继承
术语“属性值继承”(简称属性继承)并不是指传统的面向对象的类继承,而是指属性值自顶向下沿着元素树传递(类似于CSS样式的影响)
属性值的继承行为是由以下两种因素决定的:
- 并不是每个依赖属性都参与属性值继承的。(从其内部来讲,依赖属性会通过传递FrameworkPropertyMetadataOptions.Inherits给DependencyProperty.Register方法来完成继承。)
- 有其他一些优先级更高的源来设置这些属性值
C) 对多个提供程序的支持
WPF有许多强大的机制可以独立地去尝试设置依赖属性的值。如果没有设计良好的机制来处理这些完全不同的属性值提供程序,这个系统会变得混乱,属性值会变得不稳定。当然,正如它们的名字所表达的,依赖属性就是设计为以一致的、有序的方式依靠这些提供程序
(意思是对属性的影响存在于多个方面)
- 判断基础值
下面的代码清单显示了8个提供程序,它们可以设置大多数依赖属性的值,优先级顺序从高到低为:
本地值、样式触发器、模板触发器、样式设置程序、主题样式触发器、主题样式设置程序、属性值继承、默认值
- 计算
如果第一步中的值是表达式(派生自System.Windows.Expression的一个对象),那么WPF会执行一种特殊的演算步骤——把表达式转换为具体的结果
- 应用动画
如果一个或者多个动画在运行,它们有能力改变当前的属性值(使用第二步计算出来的值作为输入)或者完全替代当前的属性值。因此,动画(第13章的话题)胜过其他任何属性值提供程序——就连本地值也不是它的“对手”!
- 限制
在所有属性值提供程序处理过之后,WPF将拿到一个几乎是终值的属性值,如果依赖属性已经注册了CoerceValueCallback,还会把这个属性值传递给CoerceValueCallback委托。在委托中,可能会对值进行限制和处理。
- 验证
最后,如果依赖属性已经注册了ValidateValueCallback,之前的限制中的值将被传入ValidateValueCallback委托。如果输入值有效,该回调函数必须返回true;否则就返回false。返回false将会导致抛出一个异常,并使整个流程被取消。
3. 附加属性
附加属性是依赖属性的一种特殊形式,可以被有效地添加到任何对象中。(有些类似于C#中的扩展方法)
<StackPanel TextElement.FontSize="30" TextElement.FontStyle="Italic" Orientation="Horizontal" HorizontalAlignment="Center"> <Button MinWidth="75" Margin="10">Help</Button> <Button MinWidth="75" Margin="10">OK</Button> </StackPanel>
等价的C#代码
StackPanel panel = new StackPanel(); TextElement.SetFontSize(panel, 30); TextElement.SetFontStyle(panel, FontStyle.Italic);
StackPanel中没有FontSize属性和FontStyle属性, 但是可以设置TextElement的附加属性。
附加属性设置了当前空间的附加属性的值, 如TextElement.FontSize, 它本身不会对当前元素产生影响。
但是会对当前元素的子元素产生影响。因为子元素的对应属性,如FontSize, 会从父元素设置的TextElemetn.FontSize中读取。
4. 路由事件
路由事件是专门设计用于在元素树中使用的事件。当路由事件触发后,它可以向上或向下遍历可视树和逻辑树,用一种简单而且持久的方式在每个元素上触发,而不需要使用任何定制代码。
例如,Button有一个Click事件,这是基于底层的MouseLeftButtonDown事件或者KeyDown事件实现的。当用户的鼠标指针位于标准按钮之上,且按下鼠标左键的时候,它们实际上是与ButtonChrome或者TextBlock可视子元素在交互。由于事件遍历了可视树,所以Button元素最终会发现这个事件,并处理该事件。
A) 路由事件的实现
路由事件也是由公共的静态RoutedEvent成员加上一个约定的Event后缀名构成的
路由事件的注册很像在静态构建器中注册依赖属性,它会定义一个普通的.NET事件或者一个事件包装器(event wrapper),这样可以保证在过程式代码中使用起来更加熟悉,并且可以在XAML中用事件特性语法(event attribute syntax)添加一个事件处理程序。与属性包装器一样,事件包装器在访问器中只能调用AddHandler和RemoveHandler,而不应该做其他事情。
public class Button : ButtonBase { // The routed event public static readonly RoutedEvent ClickEvent; static Button() { // Register the event Button.ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Button)); ? } // A .NET event wrapper (optional) public event RoutedEventHandler Click { add { AddHandler(Button.ClickEvent, value); } remove { RemoveHandler(Button.ClickEvent, value); } } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { ? // Raise the event RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this)); ? } ? }
B) 路由策略和事件处理程序
当注册完毕后,每个路由事件将选择3个路由策略中的一个。所谓路由策略就是事件触发遍历整棵元素树的方式,这些策略由RoutingStrategy枚举值提供。
- Tunneling(管道传递)——事件首先在根元素上被触发,然后从每一个元素向下沿着树传递,直到到达源元素为止(或者直到处理程序把事件标记为已处理为止)。
- Bubbling(冒泡)——事件首先在源元素上被触发,然后从每一个元素向上沿着树传递,直到到达根元素为止(或者直到处理程序把事件标记为已处理为止)。
- Direct(直接)——事件仅在源元素上触发。这与普通.NET事件的行为相同,不同的是这样的事件仍然会参与一些路由事件的特定机制,如事件触发器。
路由事件处理程序
路由事件的事件处理程序有一个签名,它与通用.NET事件处理程序的模式匹配:第一个参数是一个System.Object对象,名为sender,第二个参数(一般命名为e)是一个派生自System.EventArgs的类。传递给事件处理程序的sender参数就是该处理程序被添加到的元素。参数e是RoutedEventArgs的一个实例(或者派生自RoutedEventArgs),RoutedEventArgs是EventArgs的一个子类,它提供了4个有用的属性:
- Source——逻辑树中一开始触发该事件的元素。
- OriginalSource——可视树中一开始触发该事件的元素(例如,TextBlock或者标准Button元素的ButtonChrome子元素)。
- Handled——布尔值,设置为true表示标记事件为已处理,这就是用于停止Tunneling或Bubbling的标记。
- RoutedEvent——真正的路由事件对象(如Button.ClickEvent),当一个事件处理程序同时被用于多个路由事件时,它可以有效地识别被触发的事件。
C) 附加事件
类似于附加属性:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="AboutDialog" ListBox.SelectionChanged="ListBox_SelectionChanged" Button.Click="Button_Click" Title="About WPF Unleashed" SizeToContent="WidthAndHeight" Background="OrangeRed"> </Window>
5. 命令
WPF提供了内建的命令支持,这是一个更为抽象且松耦合的事件版本。尽管事件是与某个用户动作(如点击一个Button或者选中一个ListBoxItem)相关联的,但命令表示的是那些与用户界面分离的动作。
大多数命令的能力来自于下面3种特性:
- WPF定义了许多内建命令。
- 命令自动支持输入手势(input gesture),如键盘快捷方式。
- 有些WPF控件有一些与不同命令关联的内建行为。
A) 内建命令
命令是任何一个实现了ICommand接口(位于System.Windows.Input命名空间)的对象,每个对象定义了3个简单的成员:
- Execute——执行特定命令的逻辑的方法。
- CanExecute——如果命令允许被执行,则该方法返回true;如果不允许执行,则返回false。
- CanExecuteChanged——无论何时,只要CanExecute的值改变,该事件就会触发
像Button、CheckBox和MenuItem这样的控件有相关的逻辑会与任何命令做交互。它们会有一个简单的Command属性(类型为ICommand),当设置了Command属性后,无论何时Click事件触发,这些控件会自动调用命令的Execute方法(只要CanExecute返回true时)。另外,它们会自动保持IsEnabled的值与CanExecute的值同步,这是通过CanExecuteChanged事件实现的
更加幸运的是,WPF甚至已经定义了一系列命令:
- ApplicationCommands——Close、Copy、Cut、Delete、Find、Help、New、Open、Paste、Print、PrintPreview、Properties、Redo、Replace、Save、SaveAs、SelectAll、Stop、Undo等。
- ComponentCommands——MoveDown 、MoveLeft 、MoveRight 、MoveUp 、ScrollByLine 、ScrollPageDown、ScrollPageLeft、ScrollPageRight、ScrollPageUp、SelectToEnd、SelectToHome、SelectToPageDown、SelectToPageUp等。
- MediaCommands——ChannelDown 、ChannelUp 、DecreaseVolume 、FastForward 、IncreaseVolume、MuteVolume、NextTrack、Pause、Play、PreviousTrack、Record、Rewind、Select、Stop等。
- NavigationCommands——BrowseBack 、BrowseForward 、BrowseHome 、BrowseStop 、Favorites、FirstPage、GoToPage、LastPage、NextPage、PreviousPage、Refresh、Search、Zoom等。
- EditingCommands——AlignCenter、AlignJustify、AlignLeft、AlignRight、Correct-SpellingError、DecreaseFontSize、DecreaseIndentation、EnterLineBreak、EnterParagraphBreak、IgnoreSpellingError、IncreaseFontSize、IncreaseIndentation、MoveDown-
ByLine、MoveDownByPage、MoveDownByParagraph、MoveLeftByCharacter、MoveLeftByWord、MoveRightByCharacter、MoveRightByWord等。
上面的每个属性并不会返回实现ICommand的独特类型,相反,它们都是RoutedUICommand的实例。RoutedUICommand类不仅实现了ICommand接口,还可以像路由事件一样支持冒泡。
这些命令还有一个默认的Text属性,可以用来显示到控件上。
这些命令还对于这默认的键盘快捷操作,不如F1对应于Help命令。
这些命令,只有执行内容,CanExecute和CanExecuteChanged还需要另外指定。
使用的一个例子, 把help button的
helpButtion.Command = ApplicationCommands.Help

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名justrun(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)