WPF学习版Win7之TreeView_提高(1)
朋友们,注意了!从本文开始,将使用 Visual Studio 2010 , Blend 4 作为主要开发工具。
本文包括:
- Converter 绑定转换及修改模板完善 TreeViewItem 在 MouseOver 时呈现外观
- Behavior 附加拖拽行为
- Adorner 装饰器装饰拖拽效果
完善 TreeView 外观
- 新建一 WPF 应用程序。
(注意本节开始用 VS2010 和 Blend 4 开发,使用 VS 08 或 Blend 3 将无法正常打开工程项目。实在抱歉!但不影响查看代码学习!) - 新建一用户控件,参见上节用 Blend 创建基本 TreeView 外观。
(整个 Win7 学习版不小,接下来的章节也将会创建继承 UserControl 或 Control 的用户控件,以方便管理与复用) - TreeViewItem 的 ControlTemplate 实质布局如下图,一个两行三列的 Grid:
- 默认布局致使子节点(即 ItemHost)总会比父节点向右偏移第一列的宽度(值:19)。
为达到偏移效果并且使得 ItemHost 能横跨所有列(这样 Border 就可以填充整行了),现在修改如下图,两行一列的 Grid,StackPanel 左边被 Margin 填充(值:19):
- XAML 中 StackPanel 的 Margin 绑定如下(绑定对象是 TreeViewItem):
<StackPanel x:Name="stackPanel" Orientation="Horizontal" Margin="{Binding RelativeSource=
{RelativeSource FindAncestor, AncestorType=TreeViewItem, AncestorLevel=1}, Converter={StaticResource ConverterLoginMarginLeft}}"> - ConverterLoginMarginLeft类定义如下(利用 VisualTreeHelper):
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double columnWidth = 19.0; double left = 0.0; UIElement element = value as TreeViewItem; while (element.GetType() != typeof(TreeView)) { element = (UIElement)VisualTreeHelper.GetParent(element); if (element.GetType() == typeof(TreeViewItem)) left += columnWidth; } return new Thickness(left,0,0,0); }
- 最初做法是绑定到到父节点的 StackPanel,然后获取其 Margin 并在原基础上加一值(如 19),但受 WPF 布局系统的布局顺序影响(如果用 Mode=OneWaytoSource)以及 Converter 在 OneWay 模式下目标数据的更新并没有导致数据源更改,以致查找上级 StackPanel 时 Margin 值没增加(如果 Model=Default),所以Border 都没能成功填充整行。
附加行为,装饰器
- 为实现附加行为,首先添加引用:System.Windows.Interactivity.dll (还需要引用相应命名空间,建议先敲上代码,再按 Shitf+Alt+F10 显示提示添加命名空间)
- 新建一类并继承 Behavior<T> 接口。
注意这里是直接把行为应用在 ItemsControl 控件(TreeView)而不是 TreeViewItem 上。
因为像后面将要介绍的主显示控件将会有多种显示视图,如果每个视图都增加拖拽行为不灵活;而且如果在拖拽的对象上附加行为,当按 Ctrl 多选时将较难模拟 Win7 装饰拖拽的多个对象。class ItemsControlDragDropBehavior:Behavior<ItemsControl>
- 首先方法 OnAttached,OnDetaching,并增加相应委托事件:
protected override void OnAttached() { base.OnAttached(); this.AssociatedObject.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(AssociatedObject_PreviewMouseLeftButtonDown); ...... } protected override void OnDetaching() { //同理......; }
- 鼠标按下时:_isDown = true;
鼠标移动时需要判断:(关键逻辑)
注意:一调用_adornerlayer.CaptureMouse(); 将会再次执行AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e)事件。
void AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e) { if (_isDown == false) return; if (_isDragging == true) { DragMoved(e); } else if (OverMiniDistance(e)) { DragStarted(e); } }
private void DragStarted(MouseEventArgs e) { _isDragging = true; _adornerlayer.CaptureMouse(); _startPoint = e.GetPosition(_adornerlayer); _adornerlayer = this.AssociatedObject as ItemsControl; Object data = Utilities.GetDataContext(_adornerlayer, _startPoint); _adorner = new TreeViewItemAdorner(_adornerlayer, data); AdornerLayer.GetAdornerLayer(_adornerlayer).Add(_adorner); }
- 上面的 GetDataContext 方法(这样就避免了要查找特定的控件类型,静态方法定义在 Utilities.cs 工具类里):
public static object GetDataContext(ItemsControl itemsControl, Point p) { FrameworkElement element = itemsControl.InputHitTest(p) as FrameworkElement; var data = element.DataContext; return data; }
- 最后处理放开鼠标。
- 这里的鼠标移动只有(实际是通过改变 TreeViewItemAdorner 装饰器的属性来实现移动):
private void DragMoved(MouseEventArgs e) { Point CurrentPosition = e.GetPosition(_adornerlayer); _adorner.LeftOffset = CurrentPosition.X - _startPoint.X; _adorner.TopOffset = CurrentPosition.Y - _startPoint.Y; }
- 创建一个继承 Adorner 的类,并且要调用基类的构造函数,这样才能知道在哪个对象上应用装饰:
class TreeViewItemAdorner : Adorner public TreeViewItemAdorner(UIElement adornedElement, object data) : base(adornedElement)
- 如果想获取被装饰元素的副本,可以在构造函数中:
VisualBrush _brush = new VisualBrush(adornedElement);
例如可以在 Rectangle 中填充 _brush。
当然了,这个示例里我传入的 UIElement adornerElement 参数类型为 ItemsControl(即整个 TreeView 控件),如果想获取选中项的 TreeViewItem 可以用 ItemContainerGenerator 或者 VisualTreeHelper 递归查找。
如果只是在被装饰元素中加些装饰物,则一般重载 OnRender 方法并在方法:
protected override void OnRender(DrawingContext drawingContext){......};
如:
drawingContext.DrawImage(bi, adornedElementRect);
可以加动画,透明度等,具体 API 可以查询 MSDN 的 Adorner 或下载本示例代码。 - 最后编译程序(Ctrl+Shift+B),可以在 Blend 中的 Assets 面板中拖拽 ItemsControlDragDropBehavior 行为(在 Behaviors 中)。
注意,这个行为稍加修改还可以复用到后面将要介绍的主显示控件中。
所以 class ItemsControlDragDropBehavior : Behavior<ItemsControl> 中的 T 类型是 ItemsControl。
代码中也尽量免去具体的类型(如 TreeView)。 - 为了避免不必要的判断逻辑,注意 ItemsControlDragDropBehavior 行为只处理 Item 的拖拽,所以可以把红矩形的去掉,或直接 ''Create Empty'' 创建一个只有 ItemsPresenter 的 TreeView 的 ControlTemplate,或设置 ScrollViewer 控件 HorizontalScrollBarVisibility="Disabled",主要目的是使 TreeView 控件出现水平滚动条,防止拖拽滚动条时出错,而且如果目录树有多个 TreeView 时出现多个滚动条也不合适。还有,有时因为ScollViewer 可能会占无限空间,致使拖拽不能移出 ScollViewer范围。
当然也可以添加逻辑,判断拖拽的目标类型。
简单的实现方法是让水平与竖直方向的滚动条放在整个目录树的外面。
- 最后把新建的用户控件拖拽到 MainWindow 上,并加个 GridSpitter。
- 附图:
小结
- 通过修改基本控件的模板样式,可以定制外观,注意利用 Blend 的可视化界面学习默认控件的布局与模板样式。
- VisualTreeHelper 与 ItemContainerGenerator 有几个常用的静态方法方法查找控件。
例如可以查找样式模板中的元素控件。 - 附加行为松耦合并灵活复用。
与附加属性不同,可以用附加行为处理行为;当涉及绑定时可以考虑用附加属性。 - 整个应用程序都常用到的方法可以建个工具类,方法为静态方法。如获取鼠标坐标等。