逻辑树与可视树

  XAML天生就是用来呈现用户界面的,这是由于它具有层次化的特性。在WPF中,用户界面由一个对象树构建而成,这棵树叫作逻辑树。

  WPF用户界面的逻辑树也并不一定用XAML创建,它完全可能用过程式代码来实现。

  逻辑树的概念很直观,但为什么要关注它呢?因为几乎WPF的每一方面(属性、资源、事件等)都有与逻辑树相关联的行为。如,属性值有时会沿着树自动传递给子元素,而触发的事件可以自底向上或自顶向下遍历树。

  与逻辑树类似的一个概念是可视树。可视树基本上是逻辑树的扩展,在可视树中,节点都被打散,分放到核心可视组件中。可视树提供了一些详细的可视化实现,而不是把每个元素当作一个“黑盒”。如,虽然ListBox从逻辑上讲是一个单独的控件,但它的默认可视呈现是由更多的原始WPF元素组成的:一个Border对象、两个ScrollBar及其他一些元素。

  并非所有的逻辑树节点都会出现在可视树中,只有从System.Windows.Media.Visual或System.Windows.Media.Visual3D派生的元素才会被包含进去。其他元素不会包含在内,因为它们自己并没有与生俱来的呈现行为。

  使用System.Windows.LogicTreeHelper和System.Windows.Media.VisualTreeHelper这两个有些对象的类可以方便地遍历逻辑树和可视树。

  注意:不要根据具体的可视树写代码。逻辑树是静态的,不会受到程序员的干扰(例如动态添加/删除)元素,但只要用户切换不同的Windows主题,可视树就会改变。

  遍历和打印逻辑树和可视树的示例代码:

using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;

public parital class AboutDialog : Window
{
public AboutDialog()
{
IntializeComponent();
PrintLogicalTree(0, this);
}
    protected override void OnContentRendered(EventArgs e)
    {
        base.OnContentRendered(e);
        PrintVisualTree(0, this);
    }

void PrintLogicalTree(int depth, object obj)
{
//打印对象,使用前置空格表示深度
Debug.WriteLine(new string('', depth) + obj);

//有时,叶子节点不是DependencyObject,如string
if(!(obj is DependencyObject)) return;

//递归调用每个逻辑子节点
foreach(object child in LogicalTreeHelper.GetChildren(obj as DependencyObject))
PrintLogicalTree(depth + 1, child);
}

void PrintVisualTree(int depth, DependencyObject obj)
{
//打印对象,使用前置空格表示深度
Debug.WriteLine(new string('', depth) + obj);
//递归调用每个可视子节点
for(int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}

  虽然在Window的构造函数中就可以遍历逻辑树,但可视树真到Window完成至少一次布局后才会有节点,否则是空的。这也是为什么PrintVisualTree是在On-ContentRendered中调用的,因为OnContentRendered是在布局完成后才被调用的。

依赖属性

  WPF引入了一个新的属性类型叫作依赖属性,整个WPF平台中都会使用到它,用来实现样式化、自动数据绑定、动画等。

  依赖属性在任何时刻都是依靠多个提供程序来判断它的值的。这些提供程序可以是一段一直在改变值的动画,或者一个父元素的属性值从上慢慢传递给子元素等。依赖属性的最大特征是其内建的传递变更通知的能力。

  添加这样的智能给属性,其动力在于能够声明标记中直接启用富功能。WPF友好声明设计的关键在于它使用了很多属性。例如,Button控件有96个公共属性。属性可以方便地在XAML中设置而不用程序代码。但如果依赖属性没有额外的垂直传递,在不写额外代码的情况下,很难在设置属性这样简单的动作中获得想要的结果。

依赖属性的实现

  实际上,依赖属性仅仅是普通的.NET属性,只不过它已融入到了WPF架构中。它完全是由WPF API实现的,没有一种.NET语言天生就能理解依赖属性。

  下例展示了一个Button如何有效地实现一个叫IsDefault的依赖属性:

public class Button : ButtonBase
{
//依赖属性
public static readonly DependencyProperty IsDefaultProperty;

static Button()
{
//注册属性
Button.IsDefaultProperty = DependencyProperty.Register("IsDefault", typeof(bool),
typeof(Button),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsDefaultChanged)));
...
}

//.NET属性包装器(可选)
public bool IsDefault
{
get { return (bool)GetValue(Button.IsDefaultProperty); }
set { SetValue(Button.IsDefaultProperty, value); }
}

//属性改变的回调(可选)
private static void OnIsDefaultChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{ ... }
}

  IsDefaultProperty静态成员是真正的依赖属性,类型为System.Windows.DependencyProperty。按规则,所有的DependencyProperty成员都必须是public、static,并且有一个Property作为后缀。依赖属性通常调用DependencyProperty.Register静态方法创建,这样的方法需要一个名称(IsDefault)、一个属性类型(bool)及拥有这个属性的类(Button类)。通过不同的Register方法重载,你可以传入metadata(元数据)来告诉WPF如何处理该属性、如何处理属性值改变的回调、如何处理强制值转换、及如何验证值。Button会在它的静态构造函数中调用Register的重载,给依赖属性一个默认值false,并为变更通知添加一个委托。

  最后,那个叫作IsDefault的传统.NET属性会调用继承自System.Windows.DependencyObject的GetValue和SetValue方法来实现自己的访问器,System.-Windows.DependencyObject是底层基类,这是拥有依赖属性的类必须继承的。GetValue返回最后一次由SetValue设置的值,如果SetValue从未被调用过,那么就是该属性注册时的默认值。IsDefault .NET属性并不是必需的,Button的使用者可能会直接调用GetValue/SetValue方法,因为它们是公开的。

  注意:在运行时,绕过了.NET属性包装器在XAML中设置依赖属性。

  虽然XAML编译器在编译时是依靠该属性包装器的,但在运行时WPF是直接调用GetValue和SetValue的。因此,为让使用XAML设置属性与使用过程式代码设置属性保持一致,在属性包装器中除了GetValue/SetValue调用外,不应该包含任何其他逻辑,这是至关重要的。如果要添加自定义逻辑,应该在注册的回调函数中添加。

  表面上看,上例代码像是一种冗长的呈现简单布尔属性的方式。然而,因为GetValue和SetValue内部使用了高效的稀疏存储系统,而IsDefaultProperty是一个静态成员(而不是一个实例成员),与典型的.NET属性相比,依赖属性的实现节省了保存每个实现所需要的内存。

  依赖属性的好处远不止节约内存而已。它把相当一部分代码集中起来,并做标准化处理。

变更通知

  无论何时,只要依赖属性的值变了,WPF就会自动根据属性的元数据(metadata)触发一系列动作。内建的变更通知最有趣的特性之一是属性触发器,它可以在属性值改变时执行自定义动作,而不用更改任何过程式代码。

  例如,你想让Button在鼠标移上去时变为蓝色。如果没有属性触发器的话,你要为每个Button添加两个事件处理程序,一个为MouseEvent事件准备,一个为MouseLeave事件准备。

<Button MouseEnter="Button_MouseEnter" MouseLeave="Button_MouseLeave" 
MinWidth
="75" Margin="10">
Help</Button>
<Button MouseEnter="Button_MouseEnter" MouseLeave="Button_MouseLeave"
MinWidth
="75" Margin="10">
OK</Button>

  下面的代码实现了这两个事件处理程序:

void Button_MouseEnter(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if(b != null) b.Foreground = Brushed.Blue;
}

void Mouse_MouseLeave(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if(b != null) b.Foreground = Brushed.Black;
}

  然而有了属性触发器,完全可以在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支持的3种触发器之一。数据触发器是属性触发器的另一种形式,它可以在任何.NET属性中工作(而不仅仅是依赖属性)。事件触发器会通过声明方式指定动作,该动作在路由事件触发时生效。

属性值继承

  术语“属性值继承”并不是指传统的面向对象的类继承,而是指属性值自顶向下沿着元素树传递。

  属性值的继承行为由以下两个因素决定:

  1. 并不是每个依赖属性都参与属性值继承。(从内部来讲,依赖属性会通过传递FrameworkPropertyMetadataOptions.Inherits给DependencyProperty-.Register方法来完成继承。
  2. 有其他一些优先级更高的源来设置这些属性值。

对多个提供程序的支持

  WPF有许多强大的机制可以独立地去尝试设置依赖属性的值。如果没有设计良好的机制来处理这些完全不同的属性值提供程序,这个系统会变得混乱,属性值会变得不稳定。当然,正如它们的名字所表达的,依赖属性就是设计为以一致的、有序的方式依靠这些提供程序。

  下图展示了这5步流程,通过该流程,WPF运行每个依赖属性并最终计算出它的值。依靠依赖属性中内嵌的变更通知,这个流程才可自动发生。

判断基础值

  大多数属性值提供程序会把基础值的计算纳入考虑范畴。下面的清单显示了8个提供程序,它们可以设置大多数依赖属性的值,优先级顺序从高到低为:

  本地值->样式触发器->模板触发器->样式设置程序->主题样式触发器->主题样式设置程序->属性值继承->默认值

  本地值技术上的含义是任何对DependencyObject.SetValue的调用,但它通常会有一个简单的属性赋值,这是用XAML或过程式代码完成的。

  默认值指的是依赖属性注册时使用的初始值。

计算

  如果第一步中的值是表达式(派生自System.Windows.Expression的一个对象),那么WPF会执行一种特殊的演算步骤--把表达式转换为具体的结果。在WPF 3.0中,表达式仅在使用动态资源或数据绑定时起作用。

应用动画

  如果一个或多个动画在运行,它们有能力改变当前的属性值或完全替代当前的属性值。

限制

  在所有属性值提供程序处理过后,WPF将拿到一个几乎是终值的属性值,如果依赖属性已经注册了CoerceValueCallback,还会把这个属性值传递给Coerce-ValueCallback委托。该回调函数负责返回一个新的值,它是基于自定义逻辑实现的。

验证

  最后,如果依赖属性已经注册了ValidateValueCallback,之前的限制中的值将被传入ValidateValueCallback委托。如果输入值有效,该回调函数返回true,否则返回false。返回false将会导致抛出一个异常,并使整个流程被取消。

  如果没办法判断依赖属性从哪里获得当前值,那么可以得到静态方法DependencyPropertyHelper.GetValueSource作为调试助手。该方法将返回一个ValueSource结构,其中包含以下一些数据:一个BaseValueSource枚举值,它反映的是基础值从哪里来的(流程中的第一步);IsExpression、IsAnimated和IsCoerced几个布尔类型属性,它反映了第二步到第四步的信息。

  请不要在程序代码中使用这个方法,WPF以后的版本中将打破值计算的假设,会根据它的源类型采用不同的方式处理属性值,而不是根据假设WPF应用程序中的方式来处理。

  你很可能需要清除本地值,并让WPF从下一个最高优先级的提供程序中获得值,然后使用这个值来设置最终的属性值。DependencyObject提供了这样的机制,可通过调用ClearValue方法来实现。

//b为Button实例
b.ClearValue(Button.ForegroundProperty);

  Button.ForegroundProperty是一个DependencyProperty静态成员,在调用ClearValue后,会重新计算基础值,并把本地值从方程式中删除。

附加属性

  附加属性是依赖属性的一种特殊形式,可以被有效地添加到任何对象中。这可能听上去很奇怪,但这个机制在WPF中有多种应用。

  类似于WinForm那样的技术,许多WPF类定义了一个Tag属性(类型是System.Object),目的是为了存储每一个实例的自定义数据。但要添加自定义数据给任何一个派生自DependencyObject的对象,附加属性是一种更加强大、更加灵活的机制。通常我们会忽略一点,即可以用附加属性高效的向密封类(sealed class)的实例添加自定义数据。

  另外,大家对附加属性有一个曲解,虽然在XAML中设置它们依赖于SetXXX静态方法,但可在过程式代码中绕过这个方法,直接去调用DependencyObject-.SetValue方法。这意味着在过程式代码中,可以把任何一个依赖属性作为一个附加属性。如,下面的代码把ListBox的IsTextSearchEnabled属性添加到了Button控件上,并赋予该属性一个值:

//向Button添加一个不相关的属性,并把它的值设置为true
okButton.SetValue(ListBox.IsTextSearchEnabledProperty, true);

  虽然这似乎没有任何意义,但你可以用一种对应用程序或组件有意义的方式来随意使用这个属性值。

路由事件

  正如WPF在简单的.NET属性概念上添加了许多基础的东西一样,它也为.NET事件添加了许多基础的东西。路由事件是专门设计用于在元素树中使用的事件。当路由事件触发后,它可以向上或向下遍历可视树和逻辑树,用一种简单而且持久的方式在每个元素上触发,而不需要使用任何定制代码。

  事件路由让许多程序不去留意可视树的细节(对于样式重置来说这是很不错的),并且对于成功的WPF元素创作至关重要。

  以前一章中,对于VCR样式的Stop按钮来说,一个用户可能在Rectangle逻辑子元素上直接按下鼠标左键。由于事件遍历了逻辑树,Button元素还是会发现这个事件,并处理该事件。因此,你可以在一个元素(如Button)中嵌入任何复杂内容或设置一棵复杂的可视树,鼠标左键单击其中任何一个内部元素,仍然会触发父元素Button的Click事件。如果没有路由事件,内部内容的创造者或按钮的使用者不得不编写代码来把事件串起来。

  路由事件的实现和行为与依赖属性有许多相同的地方。

路由事件的实现

  与依赖属性一样,没有一种.NET语言(除XAML外)天生具有理解路由指派的能力。

  就像依赖属性是由公共的静态DependencyProperty成员加上一个约定的Property后缀名构成的一样,路由事件也是由公共的静态RoutedEvent成员加上一个约定的Event后缀名构成的。路由事件的注册很像静态构建器中注册依赖属性,它会定义一个普通的.NET事件或一个事件包装器,这样可以保证在过程式代码中使用起来更加熟悉,并且可以在XAML中用事件特性语法添加一个事件处理程序。与属性包装器一样,事件包装器在访问器中只能调用AddHandler和RemoveHandler,而不应该做其他事件。

public class Button : ButtonBase
{
//路由事件
public static readonly RoutedEvent ClickEvent;

static Button()
{
//注册事件
Button.ClickEvent = 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));
...
}
...
}

  这些AddHandler和RemoveHandler方法没有从DependencyObject继承,而是从System.Windows.UIElement继承的,UIElement是一个更高层的供元素(如Button元素)继承的基类。这些方法可以向一个适当的路由事件添加一个委托或从路由事件移除一个委托。在OnMouseLeftButtonDown中,它使用适当的RoutedEvent成员调用RaiseEvent来触发Click事件。当前的Button实例(this)被传递给事件的源元素(source element)。在代码清单中没有列出,但是作为对KeyDown事件的响应,Button的Click事件将被触发,这样就可以处理由空格健或回车键完成点击动作的情况。

路由策略和事件处理程序

  当注册完成后,每个路由事件将选择3个路由策略中的一个。所谓路由策略就是事件触发遍历整棵元素树的方式,这些策略由RoutingStategy枚举值提供。

  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),当一个事件处理程序同时被用于多个路由事件时,它可以有效的识别被触发的事件。

  Source和OriginalSource的存在允许使用更高级别的逻辑树或更低级别的可视树。然而,这种区别仅对于像鼠标事件这样的物理事件有效。对于更抽象的事件来说,不需要与可视树中的某个元素建立直接关系(就像由于键盘支持的Click),WPF会传递相同的对象给Source和OriginalSource。

路由事件的实现

  UIElement类为键盘、鼠标、指示笔输入定义了许多路由事件。大多数路由事件是冒泡事件,但许多事件与管道事件是配对的。管道事件很容易被识别,因为按照惯例,它们的名字中都有一个Preview前缀,在它们的配对冒泡事件发生前,这些事件会立即被触发。例如,PreviewMouseMove就是一个管道事件,在MouseMove冒泡事件前被触发。

  为许多不同的行为提供一对事件是为了给元素一个有效地取消事件或在事件即将发生前修改事件的机会。根据惯例,(当定义了冒泡和管道的事件对后)WPF的内嵌元素只会在响应一个冒泡事件时采取行动,这样可以保证管道事件能够名副其实的做到“预览”。例如,在TextBox控件的Preview事件中对录入的文本进行校验,过滤不符合规范的文本。

处理单击鼠标中键的事件在哪里?

  如果浏览一遍UIElement或ConentElement提供的所有鼠标事件,可以找到MouseLeftButtonDown、MouseLeftButtonUp、MouseRightButtonDown、MouseRightButtonUp事件,但有些鼠标上出现的附加按键该怎么办呢?

  这一信息可以通过更加通用的MouseDown和MouseUp事件获得。传入这样的事件处理程序的参数包括一个MouseButton枚举值,它表示鼠标状态Left、Right、Midle、XButton1、XButton2,还有一个MouseButtonState枚举值,表示这个按钮是Pressed还是Released。

中止路由事件是一种假象

  虽然在事件处理程序中设置RoutedEventArgs参数的Handled属性为true,可以终止管道传递或冒泡,但是进一步沿着树向上或向下的每个处理程序还是可以收到这些事件。这只能在代码中完成。在任何时候,都应该尽可能地避免处理已处理过的事件,因为事件应该是在第一时间被处理的。

  总之,终止管理传递或冒泡仅仅是一种假像而已。更加准确的说法应该是,当一个路由事件标记为已处理时,管道传递和冒泡仍然会继续,但默认情况下,事件处理程序只会处理没有处理过的事件。

附加事件

  通过附加事件,WPF可以通过一个没有定义过该事件的元素来完成路由事件的管道传递和冒泡。

  附加事件与附加属性操作起来很像。每个路由事件都可以被当作附加事件使用。

  由于需要传递许多信息给路由事件,可以用上层的“megahandler”来处理每一个管道或冒泡事件。这个处理程序通过分析RoutedEvent对象判断哪个事件被触发了,并把RoutedEventArgs参数转换为一个合适的子类,然后继续。

命令

  WPF提供了内建的命令支持,这是一个更为抽象且松耦合的事件版本。尽管事件是与某个用户动作相关联的,但命令表示的是那些与用户界面分离的动作,最标准的命令示例是剪切(Cut)、复制(Copy)、粘贴(Paste)。应用程序总能通过许多同步的机制提供这些动作:Menu控件中的MenuItem、ContextMenu控件中的MenuItem、ToolBar控件中的Button、键盘快捷方式等。

内建命令

  命令是任何一个实现了ICommand接口(位于System.Windows.Input命名空间)的对象,每个对象定义了3个简单的成员:

  Execute:执行特定命令的逻辑的方法。

  CanExecute:如果命令允许被执行,则返回true,否则返回false。

  CanExecuteChanged:无论何时,只要CanExecute的值改变,该事件就会触发。

  如果需要创建剪切、复制和粘贴命令,可以定义3个实现ICommand接口的类,找一个地方存储这3个类(如放在主窗口的静态成员中),从相关的事件处理程序中调用Execute(当CanExecute返回true时),处理CanExecuteChanged事件,改变相关用户界面中的IsEnabled属性。

  像Button、CheckBox、MenuItem这样的控件有相关的逻辑会与任何命令做交互。它们会有一个简单的Command属性(类型为ICommand),当设置了Command属性后,无论何时Click事件触发,这些控件会自动调用命令的Execute方法(只要CanExecute返回true时)。另外,它们会自动保持IsEnabled的值与CanExecute的值同步,这是通过CanExecuteChanged事件实现的。通过这种给属性赋值的方式,任何逻辑在XAML下都是可以实现的。

  同时,WPF已经定义了一系列命令,因此不需要为Cut、Copy和Paste命令实现ICommand对象,也不用担心在哪里保存这些命令。WPF有5个类的静态属性实现了WPF的内建命令:

  ApplicationCommands----Close、Copy、Cut、Delete、Find、Help、New、Open、Paste、Print、PrintPreview、Properties、Redo、Replace、Save、SaveAs、SelectAll、Stop、Undo等。

  其他4个类为ComponentCommands、MediaCommands、NavigationCommands、EditCommands。

  每个属性返回RoutedUICommand的实例,RoutedUIElement类不仅实现了ICommand接口,还可以像路由事件一样支持冒泡。

posted on 2011-11-12 16:31  辛勤的代码工  阅读(1914)  评论(0编辑  收藏  举报