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