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中有一处要引用。
<UserControl x:Class="WpfTreeViewer.WpfTreeViewerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary Source="ControlResources.xaml"/>
</UserControl.Resources>
<Grid>
<TreeView x:Name="WpfTreeView">
</TreeView>
</Grid>
</UserControl>
这里有一处资源的引用,其实可以直接内嵌在这里,但为了清晰分置在资源XAML中,如下。这个XAML是关键所在,它规定了这个树形控件和ViewModel如何绑定,从而决定了其行为和样式。这个需要和ViewModel一起来看。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:WpfTreeViewer.ViewModel">
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True"/>
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
<HierarchicalDataTemplate DataType="{x:Type ViewModel:WpfTreeNodeViewModel}"
ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Foreground ="{Binding Path=RelationBrush}">
<TextBlock.Text>
<Binding Path="DisplayName"/>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
所以接下来来看这个ViewModel,如下。ViewModel在一侧和对应的数据模型Model(这里就是目标容器中的一个具体的元素)一一对应,另一侧和表现视图View中的表现元素(这里就是树的一个节点)一一对应。对于树的节点,它需要暴露几个在上述XAML中引用到的属性,其中一个是Children,它指示了节点的儿子,于是树可以沿着各个节点伸展下去;另一个是IsSelected属性,它用于树节点的选中。这里的设计是高亮选中被点击的元素所对应的树节点。WPF默认的树形视图控件是单选的,但对这里的使用已经足够,因为只会有一个元素被单击。显然这里的传递应该是单向的。但上述指定为TwoWay,原因比较特殊,因为我们这里用代码来屏蔽用户选择对另一侧的影响的(我觉得应该有更好的解决方案,例如通过属性指定在视图侧树形控件不可被用户点击选择,但目前还没找到这个方案);还有一个是规定绘制颜色的Brush,用来设置节点文本的背景色以指示节点的属性。IsExpanded默认设置为True,这样树形视图控件默认展开,于是在设计期就能查看效果(如前面图1所示)。ViewModel中主要完成将一个元素绑定之后递归绑定所有子元素的逻辑,由其静态函数Create()肇始。
namespace WpfTreeViewer.ViewModel
{
public class WpfTreeNodeViewModel : ViewModelBase<object>
{
#region Enumerations
public enum RelationsWithParent
{
Logical = 0,
Visual,
LogicalAndVisual
};
#endregion
#region Constants
private readonly Color[] _reltionColorMap = new[] { Colors.Blue, Colors.Orange, Colors.Chartreuse };
#endregion
#region Properties
#region Exposed as ViewModel
public bool IsSelected
{
get { return _isSelected; }
set { }
}
private bool IsSelectedInternal
{
set
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
public ObservableCollection<WpfTreeNodeViewModel> Children
{
get { return _children ?? (_children = new ObservableCollection<WpfTreeNodeViewModel>()); }
}
public Brush RelationBrush
{
get { return new SolidColorBrush(RelationColor); }
}
public Color RelationColor
{
get { return _relationColor; }
set
{
if (value == _relationColor) return;
_relationColor = value;
OnPropertyChanged("RelationColor");
OnPropertyChanged("RelationBrush");
}
}
#endregion
#region Internal use
public RelationsWithParent RelationWithParent
{
get { return _relationWithParent; }
set
{
if (value == _relationWithParent) return;
_relationWithParent = value;
RelationColor = _reltionColorMap[(int)value];
}
}
#endregion
#endregion
#region Construcotrs
private WpfTreeNodeViewModel(object model)
: base(model)
{
_relationWithParent = RelationsWithParent.Logical;
_relationColor = _reltionColorMap[(int)RelationWithParent];
IsSelected = false;
if (Model is FrameworkElement)
{
((FrameworkElement)Model).PreviewMouseDown += ModelOnPreviewMouseDown;
}
}
#endregion
#region Methods
public static WpfTreeNodeViewModel Create(DependencyObject model)
{
var viewModel = new WpfTreeNodeViewModel(model);
MapNode(viewModel, model);
return viewModel;
}
private static void MapNode(WpfTreeNodeViewModel viewModel, object model)
{
var dobj = model as DependencyObject;
if (dobj == null)
{
// TODO generate a suitable name
viewModel.DisplayName = model.ToString();
return;
}
var mergedChildren = new HashSet<object>();
var mergedChildrenDesc = new Dictionary<object, RelationsWithParent>();
var logicChildren = LogicalTreeHelper.GetChildren(dobj);
foreach (var logicChild in logicChildren)
{
mergedChildren.Add(logicChild);
mergedChildrenDesc[logicChild] = RelationsWithParent.Logical;
}
if (dobj is Visual || dobj is Visual3D)
{
var visualChildrenCount = VisualTreeHelper.GetChildrenCount(dobj);
for (var i = 0; i < visualChildrenCount; i++)
{
var visualChild = VisualTreeHelper.GetChild(dobj, i);
if (!mergedChildren.Contains(visualChild))
{
mergedChildren.Add(visualChild);
mergedChildrenDesc[visualChild] = RelationsWithParent.Visual;
}
else if (mergedChildrenDesc[visualChild] == RelationsWithParent.Logical)
{
mergedChildrenDesc[visualChild] = RelationsWithParent.LogicalAndVisual;
}
}
}
// TODO generate a suitable name
viewModel.DisplayName = dobj.GetType().ToString();
foreach (var child in mergedChildren)
{
var childViewModel = new WpfTreeNodeViewModel(child)
{
RelationWithParent = mergedChildrenDesc[child]
};
viewModel.Children.Add(childViewModel);
MapNode(childViewModel, child);
}
}
private void ModelOnPreviewMouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
if (_lastSelected != null)
{
_lastSelected.IsSelectedInternal = false;
}
IsSelectedInternal = true;
_lastSelected = this;
}
#endregion
#region Fields
private ObservableCollection<WpfTreeNodeViewModel> _children;
private RelationsWithParent _relationWithParent;
private Color _relationColor;
private static WpfTreeNodeViewModel _lastSelected;
private bool _isSelected;
#endregion
}
}
如此在这个用户控件的主体Code Behind中主要只需暴露一个Root属性,用于外部调用者绑定容器控件即可:
namespace WpfTreeViewer
{
/// <summary>
/// Interaction logic for WpfTreeViewerControl.xaml
/// </summary>
public partial class WpfTreeViewerControl
{
#region Fields
public static DependencyProperty RootProperty = DependencyProperty.Register("Root", typeof (DependencyObject),
typeof (WpfTreeViewerControl),
new PropertyMetadata(null,
PropertyChangedCallback));
#endregion
#region Properties
public DependencyObject Root
{
get { return (DependencyObject)GetValue(RootProperty); }
set { SetValue(RootProperty, value); }
}
#endregion
#region Constructors
public WpfTreeViewerControl()
{
InitializeComponent();
}
#endregion
#region Methods
private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var control = dependencyObject as WpfTreeViewerControl;
var rootViewModel = WpfTreeNodeViewModel.Create((DependencyObject)e.NewValue);
System.Diagnostics.Trace.Assert(control != null);
control.WpfTreeView.ItemsSource = new List<object> { rootViewModel };
}
#endregion
}
}
主窗体的XAML(包括容器定义)大致如图1中XAML文本编辑器中所示。
执行期就如下图所示(树节点选中高亮还存在一些微小的问题,但貌似不算Bug):
图2 运行截图
托WPF-MVVM的福,程序显得过于简单,就不代码维护了,但肯定可以增加不少特性用于进一步WPF学习和分析。源码下载(用VS2010和VS2012打开应该没有任何问题),欢迎拍砖:
代码托管在:
https://github.com/lincolnyu/WpfTreeViewer