WPF内部结构树以及一个探查控件

WPF对初学者来说一个比较复杂的概念是它用两个树来组织其元素的。了解一些WPF的同学一般都知道它们分别是逻辑树(Logical Tree)和视觉树(Visual Tree)。而这两者的关系,以及一个界面中元素究竟如何与另一个元素在这两棵树上联系起来却相当复杂,很难一言两语涵盖其规则。而树和WPF中的元素类的特性有关系,也对应了XAML构成,所以非常重要,是比较深入理解WPF的关键。网上有不少文章就是专门讨论这个问题的。

比如以下是一些关于这个问题的比较经典的文章:

http://www.codeproject.com/Articles/21495/Understanding-the-Visual-Tree-and-Logical-Tree-in

http://blogs.msdn.com/b/mikehillberg/archive/2008/05/23/of-logical-and-visual-trees-in-wpf.aspx

其实,我学和用WPF至今,也没有把这些内容完全弄明白,甚至很大部分都不是很明白。但是WPF的一个好处是其基本使用可以不用涉及这些。但当要开发比较高级的界面程序,用到比较多的WPF控件及其嵌套和事件响应,尤其是要启用大量关系复杂的WPF Templates和Styles以发挥出WPF的真正能量的时候,对树的深入理解,以及对各控件属性的了解就显得必不可少。

为此在对基本原理有些了解以后,我制作了一个小小的WPF用户控件(User Control),当这个控件和一个WPF容器绑定的时候,其内含的树形视图(TreeView)将会显示出被绑定容器中的元素在这两个关系树上的位置和相互关系。

这个显示树的根节点就是这个容器。这里的问题是如何显示两棵树?答案很简单,由于根节点只有一个,只要将每个节点的两组孩子(也包括同时是视觉和逻辑的孩子)都放在其下,用颜色来标识即可。

当用户点击一个目标控件时,树形视图中的中对应的节点会被高亮选中。有趣的是,由于Visual Studio的IDE对WPF开发支持,如果这个控件和目标容器同时放到设计窗体下,只要在XAML中将该控件和目标容器绑定,无需代码(C# Code Behind),无需执行,只要通过Build,控件树形视图就能显示目标容器的内容,如图1所示,其中蓝色表示作为逻辑树孩子,黄色表示作为视觉树孩子,浅绿色表示同时作为两者。可见树和XAML有很高的相关性(XAML实际上是省略了一些树的节点的版本)。

图1 设计期控件对容器内元素的展示

这个控件采用MVVM设计模式实现,View端就是其内含的树形控件,Model端就是被绑定的目标元素(节点),ViewModel是连接两者的控制翻译体。几乎所有逻辑全在ViewModel中。

有趣的是,我在开始做之前就估计这个东西做起来比较简单,而事实上它比我想象的还容易。只要对两个树、WPF以及MVVM有所了解就不难实现。而不难实现的主要原因也还是这个实例恰好利用了MVVM模式的特点和强大之处。

以下简要说明一下代码(看完代码这个设计思路也就清楚了),

先看这个控件的XAML。很简单就是包含了一个树形控件,连属性什么的也不需要设置,就命了个名称,在Code Behind中有一处要引用。

 1 <UserControl x:Class="WpfTreeViewer.WpfTreeViewerControl"
 2              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
 5              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 6              mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
 7     <UserControl.Resources>
 8         <ResourceDictionary Source="ControlResources.xaml"/>
 9     </UserControl.Resources>
10     <Grid>
11         <TreeView x:Name="WpfTreeView">
12         </TreeView>
13     </Grid>
14 </UserControl>

这里有一处资源的引用,其实可以直接内嵌在这里,但为了清晰分置在资源XAML中,如下。这个XAML是关键所在,它规定了这个树形控件和ViewModel如何绑定,从而决定了其行为和样式。这个需要和ViewModel一起来看。

 1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 2                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 3                     xmlns:ViewModel="clr-namespace:WpfTreeViewer.ViewModel">
 4     <Style TargetType="TreeViewItem">
 5         <Setter Property="IsExpanded" Value="True"/>
 6         <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
 7     </Style>
 8 
 9     <HierarchicalDataTemplate DataType="{x:Type ViewModel:WpfTreeNodeViewModel}"
10                               ItemsSource="{Binding Path=Children}">
11         <StackPanel Orientation="Horizontal">
12             <TextBlock Foreground ="{Binding Path=RelationBrush}">
13                 <TextBlock.Text>
14                     <Binding Path="DisplayName"/>
15                 </TextBlock.Text>
16             </TextBlock>
17         </StackPanel>
18     </HierarchicalDataTemplate>
19 </ResourceDictionary>

所以接下来来看这个ViewModel,如下。ViewModel在一侧和对应的数据模型Model(这里就是目标容器中的一个具体的元素)一一对应,另一侧和表现视图View中的表现元素(这里就是树的一个节点)一一对应。对于树的节点,它需要暴露几个在上述XAML中引用到的属性,其中一个是Children,它指示了节点的儿子,于是树可以沿着各个节点伸展下去;另一个是IsSelected属性,它用于树节点的选中。这里的设计是高亮选中被点击的元素所对应的树节点。WPF默认的树形视图控件是单选的,但对这里的使用已经足够,因为只会有一个元素被单击。显然这里的传递应该是单向的。但上述指定为TwoWay,原因比较特殊,因为我们这里用代码来屏蔽用户选择对另一侧的影响的(我觉得应该有更好的解决方案,例如通过属性指定在视图侧树形控件不可被用户点击选择,但目前还没找到这个方案);还有一个是规定绘制颜色的Brush,用来设置节点文本的背景色以指示节点的属性。IsExpanded默认设置为True,这样树形视图控件默认展开,于是在设计期就能查看效果(如前面图1所示)。ViewModel中主要完成将一个元素绑定之后递归绑定所有子元素的逻辑,由其静态函数Create()肇始。

  1 namespace WpfTreeViewer.ViewModel
  2 {
  3     public class WpfTreeNodeViewModel : ViewModelBase<object>
  4     {
  5         #region Enumerations
  6 
  7         public enum RelationsWithParent
  8         {
  9             Logical = 0,
 10             Visual,
 11             LogicalAndVisual
 12         };
 13 
 14         #endregion
 15 
 16         #region Constants
 17 
 18         private readonly Color[] _reltionColorMap = new[] { Colors.Blue, Colors.Orange, Colors.Chartreuse };
 19 
 20         #endregion
 21 
 22         #region Properties
 23 
 24         #region Exposed as ViewModel
 25 
 26         public bool IsSelected
 27         {
 28             get { return _isSelected; }
 29             set { }
 30         }
 31 
 32         private bool IsSelectedInternal
 33         {
 34             set
 35             {
 36                 _isSelected = value;
 37                 OnPropertyChanged("IsSelected");
 38             }
 39         }
 40 
 41         public ObservableCollection<WpfTreeNodeViewModel> Children
 42         {
 43             get { return _children ?? (_children = new ObservableCollection<WpfTreeNodeViewModel>()); }
 44         }
 45 
 46         public Brush RelationBrush
 47         {
 48             get { return new SolidColorBrush(RelationColor); }
 49         }
 50 
 51         public Color RelationColor
 52         {
 53             get { return _relationColor; }
 54             set
 55             {
 56                 if (value == _relationColor) return;
 57                 _relationColor = value;
 58                 OnPropertyChanged("RelationColor");
 59                 OnPropertyChanged("RelationBrush");
 60             }
 61         }
 62 
 63         #endregion
 64 
 65         #region Internal use
 66 
 67         public RelationsWithParent RelationWithParent
 68         {
 69             get { return _relationWithParent; }
 70             set
 71             {
 72                 if (value == _relationWithParent) return;
 73                 _relationWithParent = value;
 74                 RelationColor = _reltionColorMap[(int)value];
 75             }
 76         }
 77 
 78         #endregion
 79 
 80         #endregion
 81 
 82         #region Construcotrs
 83 
 84         private WpfTreeNodeViewModel(object model)
 85             : base(model)
 86         {
 87             _relationWithParent = RelationsWithParent.Logical;
 88             _relationColor = _reltionColorMap[(int)RelationWithParent];
 89             
 90             IsSelected = false;
 91             if (Model is FrameworkElement)
 92             {
 93                 ((FrameworkElement)Model).PreviewMouseDown += ModelOnPreviewMouseDown;
 94             }
 95         }
 96 
 97         #endregion
 98 
 99         #region Methods
100 
101         public static WpfTreeNodeViewModel Create(DependencyObject model)
102         {
103             var viewModel = new WpfTreeNodeViewModel(model);
104 
105             MapNode(viewModel, model);
106 
107             return viewModel;
108         }
109 
110         private static void MapNode(WpfTreeNodeViewModel viewModel, object model)
111         {
112             var dobj = model as DependencyObject;
113 
114             if (dobj == null)
115             {
116                 // TODO generate a suitable name
117                 viewModel.DisplayName = model.ToString();
118                 return;
119             }
120 
121             var mergedChildren = new HashSet<object>();
122             var mergedChildrenDesc = new Dictionary<object, RelationsWithParent>();
123 
124             var logicChildren = LogicalTreeHelper.GetChildren(dobj);
125             foreach (var logicChild in logicChildren)
126             {
127                 mergedChildren.Add(logicChild);
128                 mergedChildrenDesc[logicChild] = RelationsWithParent.Logical;
129             }
130 
131             if (dobj is Visual || dobj is Visual3D)
132             {
133                 var visualChildrenCount = VisualTreeHelper.GetChildrenCount(dobj);
134                 for (var i = 0; i < visualChildrenCount; i++)
135                 {
136                     var visualChild = VisualTreeHelper.GetChild(dobj, i);
137                     if (!mergedChildren.Contains(visualChild))
138                     {
139                         mergedChildren.Add(visualChild);
140                         mergedChildrenDesc[visualChild] = RelationsWithParent.Visual;
141                     }
142                     else if (mergedChildrenDesc[visualChild] == RelationsWithParent.Logical)
143                     {
144                         mergedChildrenDesc[visualChild] = RelationsWithParent.LogicalAndVisual;
145                     }
146                 }
147             }
148             // TODO generate a suitable name
149             viewModel.DisplayName = dobj.GetType().ToString();
150 
151             foreach (var child in mergedChildren)
152             {
153                 var childViewModel = new WpfTreeNodeViewModel(child)
154                 {
155                     RelationWithParent = mergedChildrenDesc[child]
156                 };
157                 viewModel.Children.Add(childViewModel);
158                 MapNode(childViewModel, child);
159             }            
160         }
161 
162         private void ModelOnPreviewMouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
163         {
164             if (_lastSelected != null)
165             {
166                 _lastSelected.IsSelectedInternal = false;
167             }
168 
169             IsSelectedInternal = true;
170             _lastSelected = this;
171         }
172 
173         #endregion
174 
175         #region Fields
176 
177         private ObservableCollection<WpfTreeNodeViewModel> _children;
178         private RelationsWithParent _relationWithParent;
179         private Color _relationColor;
180 
181         private static WpfTreeNodeViewModel _lastSelected;
182         private bool _isSelected;
183 
184         #endregion
185     }
186 }

如此在这个用户控件的主体Code Behind中主要只需暴露一个Root属性,用于外部调用者绑定容器控件即可:

 1 namespace WpfTreeViewer
 2 {
 3     /// <summary>
 4     /// Interaction logic for WpfTreeViewerControl.xaml
 5     /// </summary>
 6     public partial class WpfTreeViewerControl
 7     {
 8         #region Fields
 9 
10         public static DependencyProperty RootProperty = DependencyProperty.Register("Root", typeof (DependencyObject),
11                                                                                     typeof (WpfTreeViewerControl),
12                                                                                     new PropertyMetadata(null,
13                                                                                                          PropertyChangedCallback));
14         #endregion
15 
16         #region Properties 
17 
18         public DependencyObject Root
19         {
20             get { return (DependencyObject)GetValue(RootProperty); }
21             set { SetValue(RootProperty, value); }
22         }
23 
24         #endregion
25 
26         #region Constructors
27 
28         public WpfTreeViewerControl()
29         {
30             InitializeComponent();
31         }
32 
33         #endregion
34 
35         #region Methods
36 
37         private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
38         {
39             var control = dependencyObject as WpfTreeViewerControl;
40             var rootViewModel = WpfTreeNodeViewModel.Create((DependencyObject)e.NewValue);
41             System.Diagnostics.Trace.Assert(control != null);
42             control.WpfTreeView.ItemsSource = new List<object> { rootViewModel };
43         }
44 
45         #endregion
46     }
47 }

主窗体的XAML(包括容器定义)大致如图1中XAML文本编辑器中所示。

执行期就如下图所示(树节点选中高亮还存在一些微小的问题,但貌似不算Bug):

图2 运行截图

托WPF-MVVM的福,程序显得过于简单,就不代码维护了,但肯定可以增加不少特性用于进一步WPF学习和分析。源码下载(用VS2010和VS2012打开应该没有任何问题),欢迎拍砖:

http://sdrv.ms/PtBq54

代码现托管在:

https://github.com/lincolnyu/WpfTreeViewer

posted @ 2012-09-21 00:41  quanben  阅读(407)  评论(0编辑  收藏  举报