自定义 TreeView 以允许多选
Posted
技术标签:
【中文标题】自定义 TreeView 以允许多选【英文标题】:Customizing the TreeView to allow multi select 【发布时间】:2010-10-02 08:13:40 【问题描述】:内置的 WPF TreeView 控件不允许多选,就像 ListBox 一样。如何而不重写它。
【问题讨论】:
我强烈推荐 Josh Smiths 关于 codeproject 的文章:Simplifying the WPF TreeView by Using the ViewModel Pattern 虽然他的文章没有涉及多选功能,但它提供了一种在 WPF 中处理 TreeView 的好方法。似乎还有另一篇文章讨论了多选 TreeView 的方法here @Govert 那篇文章中的代码写得很糟糕。我不会向任何人推荐它。就好像作者花在为他的代码辩护而不是编码上的时间一样。 你可以看看TreeViewEx的例子。 @Anders 更不用说样式都是以编程方式应用的,哎呀! 本文描述了一种可能的实现:chrigas.blogspot.de/2014/08/… 其他一些人的工作基于本文:* github.com/cmyksvoll/MultiSelectTreeView * github.com/codecadwallader/codemaid/blob/master/CodeMaid/UI/… * nuget.org/packages/MultiSelectTreeView 【参考方案1】:我有一个 SoMoS 实现的变体,它使用在基本 TreeView 控件的派生上声明的附加属性来跟踪 TreeViewItems 的选择状态。这样可以保持对 TreeViewItem 元素本身的选择跟踪,而不是树视图呈现的模型对象。
这是新的 TreeView 类派生。
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Controls;
using System.Collections;
using System.Collections.Generic;
namespace MultiSelectTreeViewDemo
public sealed class MultiSelectTreeView : TreeView
#region Fields
// Used in shift selections
private TreeViewItem _lastItemSelected;
#endregion Fields
#region Dependency Properties
public static readonly DependencyProperty IsItemSelectedProperty =
DependencyProperty.RegisterAttached("IsItemSelected", typeof(bool), typeof(MultiSelectTreeView));
public static void SetIsItemSelected(UIElement element, bool value)
element.SetValue(IsItemSelectedProperty, value);
public static bool GetIsItemSelected(UIElement element)
return (bool)element.GetValue(IsItemSelectedProperty);
#endregion Dependency Properties
#region Properties
private static bool IsCtrlPressed
get return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
private static bool IsShiftPressed
get return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
public IList SelectedItems
get
var selectedTreeViewItems = GetTreeViewItems(this, true).Where(GetIsItemSelected);
var selectedModelItems = selectedTreeViewItems.Select(treeViewItem => treeViewItem.Header);
return selectedModelItems.ToList();
#endregion Properties
#region Event Handlers
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
base.OnPreviewMouseDown(e);
// If clicking on a tree branch expander...
if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border)
return;
var item = GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null) SelectedItemChangedInternal(item);
#endregion Event Handlers
#region Utility Methods
private void SelectedItemChangedInternal(TreeViewItem tvItem)
// Clear all previous selected item states if ctrl is NOT being held down
if (!IsCtrlPressed)
var items = GetTreeViewItems(this, true);
foreach (var treeViewItem in items)
SetIsItemSelected(treeViewItem, false);
// Is this an item range selection?
if (IsShiftPressed && _lastItemSelected != null)
var items = GetTreeViewItemRange(_lastItemSelected, tvItem);
if (items.Count > 0)
foreach (var treeViewItem in items)
SetIsItemSelected(treeViewItem, true);
_lastItemSelected = items.Last();
// Otherwise, individual selection
else
SetIsItemSelected(tvItem, true);
_lastItemSelected = tvItem;
private static TreeViewItem GetTreeViewItemClicked(DependencyObject sender)
while (sender != null && !(sender is TreeViewItem))
sender = VisualTreeHelper.GetParent(sender);
return sender as TreeViewItem;
private static List<TreeViewItem> GetTreeViewItems(ItemsControl parentItem, bool includeCollapsedItems, List<TreeViewItem> itemList = null)
if (itemList == null)
itemList = new List<TreeViewItem>();
for (var index = 0; index < parentItem.Items.Count; index++)
var tvItem = parentItem.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (tvItem == null) continue;
itemList.Add(tvItem);
if (includeCollapsedItems || tvItem.IsExpanded)
GetTreeViewItems(tvItem, includeCollapsedItems, itemList);
return itemList;
private List<TreeViewItem> GetTreeViewItemRange(TreeViewItem start, TreeViewItem end)
var items = GetTreeViewItems(this, false);
var startIndex = items.IndexOf(start);
var endIndex = items.IndexOf(end);
var rangeStart = startIndex > endIndex || startIndex == -1 ? endIndex : startIndex;
var rangeCount = startIndex > endIndex ? startIndex - endIndex + 1 : endIndex - startIndex + 1;
if (startIndex == -1 && endIndex == -1)
rangeCount = 0;
else if (startIndex == -1 || endIndex == -1)
rangeCount = 1;
return rangeCount > 0 ? items.GetRange(rangeStart, rangeCount) : new List<TreeViewItem>();
#endregion Utility Methods
这里是 XAML。请注意,突出的部分是使用 MultiSelectTreeViewItemStyle 中的新的 'IsItemSelected' 附加属性替换使用单数 'IsSelected' 属性的两个触发器以实现视觉状态。
另外请注意,我没有将新的 TreeView 控件聚合到 UserControl 中。
<Window
x:Class="MultiSelectTreeViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MultiSelectTreeViewDemo"
Title="MultiSelect TreeView Demo" Height="350" Width="525">
<Window.Resources>
<local:DemoViewModel x:Key="ViewModel"/>
<Style x:Key="TreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Fill" Color="#FF595959"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Stroke" Color="#FF262626"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Stroke" Color="#FF1BBBFA"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Fill" Color="Transparent"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Stroke" Color="#FF262626"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Fill" Color="#FF595959"/>
<PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Fill" Color="Transparent"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Stroke" Color="#FF989898"/>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="x:Type ToggleButton">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type ToggleButton">
<Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
<Path x:Name="ExpandPath" Data="StaticResource TreeArrow" Fill="StaticResource TreeViewItem.TreeArrow.Static.Fill" Stroke="StaticResource TreeViewItem.TreeArrow.Static.Stroke">
<Path.RenderTransform>
<RotateTransform Angle="135" CenterY="3" CenterX="3"/>
</Path.RenderTransform>
</Path>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="RenderTransform" TargetName="ExpandPath">
<Setter.Value>
<RotateTransform Angle="180" CenterY="3" CenterX="3"/>
</Setter.Value>
</Setter>
<Setter Property="Fill" TargetName="ExpandPath" Value="StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill"/>
<Setter Property="Stroke" TargetName="ExpandPath" Value="StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="ExpandPath" Value="StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="StaticResource TreeViewItem.TreeArrow.MouseOver.Fill"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Stroke" TargetName="ExpandPath" Value="StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MultiSelectTreeViewItemStyle" TargetType="x:Type TreeViewItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Binding HorizontalContentAlignment, RelativeSource=RelativeSource AncestorType=x:Type ItemsControl"/>
<Setter Property="VerticalContentAlignment" Value="Binding VerticalContentAlignment, RelativeSource=RelativeSource AncestorType=x:Type ItemsControl"/>
<Setter Property="Padding" Value="1,0,0,0"/>
<Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.ControlTextBrushKey"/>
<Setter Property="FocusVisualStyle" Value="StaticResource TreeViewItemFocusVisual"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type TreeViewItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19" Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToggleButton
x:Name="Expander"
ClickMode="Press"
IsChecked="Binding IsExpanded, RelativeSource=RelativeSource TemplatedParent"
Style="StaticResource ExpandCollapseToggleStyle"/>
<Border
x:Name="Bd"
BorderBrush="TemplateBinding BorderBrush"
BorderThickness="TemplateBinding BorderThickness"
Background="TemplateBinding Background"
Grid.Column="1"
Padding="TemplateBinding Padding"
SnapsToDevicePixels="true">
<ContentPresenter
x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="TemplateBinding HorizontalContentAlignment"
SnapsToDevicePixels="TemplateBinding SnapsToDevicePixels"/>
</Border>
<ItemsPresenter
x:Name="ItemsHost"
Grid.ColumnSpan="2"
Grid.Column="1"
Grid.Row="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</Trigger>
<!--Trigger Property="IsSelected" Value="true"-->
<Trigger Property="local:MultiSelectTreeView.IsItemSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="DynamicResource x:Static SystemColors.HighlightBrushKey"/>
<Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.HighlightTextBrushKey"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<!--Condition Property="IsSelected" Value="true"/-->
<Condition Property="local:MultiSelectTreeView.IsItemSelected" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="DynamicResource x:Static SystemColors.InactiveSelectionHighlightBrushKey"/>
<Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.InactiveSelectionHighlightTextBrushKey"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.GrayTextBrushKey"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid
Background="WhiteSmoke"
DataContext="DynamicResource ViewModel">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:MultiSelectTreeView
x:Name="multiSelectTreeView"
ItemContainerStyle="StaticResource MultiSelectTreeViewItemStyle"
ItemsSource="Binding FoodGroups">
<local:MultiSelectTreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="Binding Children">
<Grid>
<TextBlock FontSize="14" Text="Binding Name"/>
</Grid>
</HierarchicalDataTemplate>
</local:MultiSelectTreeView.ItemTemplate>
</local:MultiSelectTreeView>
<Button
Grid.Row="1"
Margin="0,10"
Padding="20,2"
HorizontalAlignment="Center"
Content="Get Selections"
Click="GetSelectionsButton_OnClick"/>
</Grid>
</Window>
这里有一个俗气的视图模型来驱动它(用于演示目的)。
using System.Collections.ObjectModel;
namespace MultiSelectTreeViewDemo
public sealed class DemoViewModel
public ObservableCollection<FoodItem> FoodGroups get; set;
public DemoViewModel()
var redMeat = new FoodItem Name = "Reds" ;
redMeat.Add(new FoodItem Name = "Beef" );
redMeat.Add(new FoodItem Name = "Buffalo" );
redMeat.Add(new FoodItem Name = "Lamb" );
var whiteMeat = new FoodItem Name = "Whites" ;
whiteMeat.Add(new FoodItem Name = "Chicken" );
whiteMeat.Add(new FoodItem Name = "Duck" );
whiteMeat.Add(new FoodItem Name = "Pork" );
var meats = new FoodItem Name = "Meats", Children = redMeat, whiteMeat ;
var veggies = new FoodItem Name = "Vegetables" ;
veggies.Add(new FoodItem Name = "Potato" );
veggies.Add(new FoodItem Name = "Corn" );
veggies.Add(new FoodItem Name = "Spinach" );
var fruits = new FoodItem Name = "Fruits" ;
fruits.Add(new FoodItem Name = "Apple" );
fruits.Add(new FoodItem Name = "Orange" );
fruits.Add(new FoodItem Name = "Pear" );
FoodGroups = new ObservableCollection<FoodItem> meats, veggies, fruits ;
public sealed class FoodItem
public string Name get; set;
public ObservableCollection<FoodItem> Children get; set;
public FoodItem()
Children = new ObservableCollection<FoodItem>();
public void Add(FoodItem item)
Children.Add(item);
这是 MainWindow 代码隐藏中的按钮单击处理程序,它显示了 MessageBox 中的选择。
private void GetSelectionsButton_OnClick(object sender, RoutedEventArgs e)
var selectedMesg = "";
var selectedItems = multiSelectTreeView.SelectedItems;
if (selectedItems.Count > 0)
selectedMesg = selectedItems.Cast<FoodItem>()
.Where(modelItem => modelItem != null)
.Aggregate(selectedMesg, (current, modelItem) => current + modelItem.Name + Environment.NewLine);
else
selectedMesg = "No selected items!";
MessageBox.Show(selectedMesg, "MultiSelect TreeView Demo", MessageBoxButton.OK);
希望这会有所帮助。
【讨论】:
刚刚第二次使用这个,这次是树列表视图。但是,如果您删除if (IsShiftPressed && _lastItemSelected != null)
中的 _lastItemSelected = items.Last();
行,行为就像在资源管理器中一样,即第二个 Shift 选择会扩展选择,而不是从最后一个元素开始。
如果在注释的部分中带有“// 否则,单独选择”,您可以将true
更改为!GetIsItemSelected(tvItem)
以使 Ctrl-单击切换项目,而不仅仅是添加。【参考方案2】:
当我考虑覆盖控件的基本行为(例如树视图)时,我总是喜欢考虑与我的决定相关的可用性和工作量。
在树形视图的特定情况下,我发现切换到列表视图并结合零个、一个或多个控件可以提供更实用的解决方案,而且通常更易于实施。
例如,考虑常见的“打开”对话框或 Windows 资源管理器应用程序。
【讨论】:
+1 建议重新考虑设计 - 不要认为我已经准备好接受这一点 - 但有一个健全的检查是很好的。 我完全同意这一点 - 我使用 ListView 轻松实现了多选树视图,其中每个项目 = Expander + ListView。当然,这只是一个 1 级树视图,但它是我真正需要的。【参考方案3】:我已经简化了这个任务,在每个树视图项的文本前添加了一个复选框。
所以,我创建了一个包含 2 个项目的停靠面板:复选框 + 文本块。
所以...
XAML
<TreeView x:Name="treeViewProcesso" Margin="1,30.351,1,5" BorderBrush="x:Null" MinHeight="250" VerticalContentAlignment="Top" BorderThickness="0" >
<TreeViewItem Header="Documents" x:Name="treeView" IsExpanded="True" DisplayMemberPath="DocumentsId" >
</TreeViewItem>
</TreeView>
CS
TreeViewItem treeViewItem = new TreeViewItem();
DockPanel dp = new DockPanel();
CheckBox cb = new CheckBox();
TextBlock tb = new TextBlock();
tb.Text = "Item";
dp.Children.Add(cb);
dp.Children.Add(tb);
treeViewItem.Header = dp;
treeViewItem.Selected += new RoutedEventHandler(item_Selected);
treeView.Items.Add(treeViewItem);
然后您可以访问复选框值:
void item_Selected(object sender, RoutedEventArgs e)
selectedTVI = ((TreeViewItem)sender);
CheckBox cb = (Checkbox)((DockPanel)selectedTVI.Header).Children[0];
如果您不需要任何复杂的东西,这是一种简单的方法。
【讨论】:
【参考方案4】:我终于结束了我自己的自定义控件的编码,其中包含一个 TreeView。基于其他人的工作,功能的关键在于使TreeView的Model的所有项目都继承ISelectable接口:
public interface ISelectable
public bool IsSelected get; set
这样,我们将拥有一个与 TreeViewItem IsSelected 无关的新“IsSelected”属性。我们只需要设置树的样式,以便它处理模型 IsSelected 属性。这里是代码(它使用了http://code.google.com/p/gong-wpf-dragdrop/ 提供的拖放库):
XAML
<UserControl x:Class="Picis.Wpf.Framework.ExtendedControls.TreeViewEx.TreeViewEx"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:DragAndDrop="clr-namespace:Picis.Wpf.Framework.DragAndDrop">
<TreeView ItemsSource="Binding ItemsSource, RelativeSource=RelativeSource AncestorType=UserControl"
ItemTemplate="Binding ItemTemplate, RelativeSource=RelativeSource AncestorType=UserControl"
ItemContainerStyle="Binding ItemContainerStyle, RelativeSource=RelativeSource AncestorType=UserControl"
DragAndDrop:DragDrop.DropHandler ="Binding DropHandler, RelativeSource=RelativeSource AncestorType=UserControl"
PreviewMouseDown="TreeViewOnPreviewMouseDown"
PreviewMouseUp="TreeViewOnPreviewMouseUp"
x:FieldModifier="private" x:Name="InnerTreeView" >
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Style.Resources>
<SolidColorBrush x:Key="x:Static SystemColors.HighlightBrushKey" Color="White" />
<SolidColorBrush x:Key="x:Static SystemColors.HighlightTextBrushKey" Color="Black" />
<SolidColorBrush x:Key="x:Static SystemColors.ControlBrushKey" Color="White" />
</Style.Resources>
</Style>
</TreeView.Resources>
</TreeView>
C#:
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using GongSolutions.Wpf.DragDrop;
using DragDrop = GongSolutions.Wpf.DragDrop;
namespace <yournamespace>.TreeViewEx
public partial class TreeViewEx : UserControl
#region Attributes
private TreeViewItem _lastItemSelected; // Used in shift selections
private TreeViewItem _itemToCheck; // Used when clicking on a selected item to check if we want to deselect it or to drag the current selection
private bool _isDragEnabled;
private bool _isDropEnabled;
#endregion
#region Dependency Properties
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<ISelectable>), typeof(TreeViewEx));
public IEnumerable<ISelectable> ItemsSource
get
return (IEnumerable<ISelectable>)this.GetValue(TreeViewEx.ItemsSourceProperty);
set
this.SetValue(TreeViewEx.ItemsSourceProperty, value);
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(TreeViewEx));
public DataTemplate ItemTemplate
get
return (DataTemplate)GetValue(TreeViewEx.ItemTemplateProperty);
set
SetValue(TreeViewEx.ItemTemplateProperty, value);
public static readonly DependencyProperty ItemContainerStyleProperty = DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(TreeViewEx));
public Style ItemContainerStyle
get
return (Style)GetValue(TreeViewEx.ItemContainerStyleProperty);
set
SetValue(TreeViewEx.ItemContainerStyleProperty, value);
public static readonly DependencyProperty DropHandlerProperty = DependencyProperty.Register("DropHandler", typeof(IDropTarget), typeof(TreeViewEx));
public IDropTarget DropHandler
get
return (IDropTarget)GetValue(TreeViewEx.DropHandlerProperty);
set
SetValue(TreeViewEx.DropHandlerProperty, value);
#endregion
#region Properties
public bool IsDragEnabled
get
return _isDragEnabled;
set
if (_isDragEnabled != value)
_isDragEnabled = value;
DragDrop.SetIsDragSource(this.InnerTreeView, _isDragEnabled);
public bool IsDropEnabled
get
return _isDropEnabled;
set
if (_isDropEnabled != value)
_isDropEnabled = value;
DragDrop.SetIsDropTarget(this.InnerTreeView, _isDropEnabled);
#endregion
#region Public Methods
public TreeViewEx()
InitializeComponent();
#endregion
#region Event Handlers
private void TreeViewOnPreviewMouseDown(object sender, MouseButtonEventArgs e)
if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border) // If clicking on the + of the tree
return;
TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null && item.Header != null)
this.SelectedItemChangedHandler(item);
// Check done to avoid deselecting everything when clicking to drag
private void TreeViewOnPreviewMouseUp(object sender, MouseButtonEventArgs e)
if (_itemToCheck != null)
TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null && item.Header != null)
if (!TreeViewEx.IsCtrlPressed)
GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
((ISelectable)_itemToCheck.Header).IsSelected = true;
_lastItemSelected = _itemToCheck;
else
((ISelectable)_itemToCheck.Header).IsSelected = false;
_lastItemSelected = null;
#endregion
#region Private Methods
private void SelectedItemChangedHandler(TreeViewItem item)
ISelectable content = (ISelectable)item.Header;
_itemToCheck = null;
if (content.IsSelected)
// Check it at the mouse up event to avoid deselecting everything when clicking to drag
_itemToCheck = item;
else
if (!TreeViewEx.IsCtrlPressed)
GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
if (TreeViewEx.IsShiftPressed && _lastItemSelected != null)
foreach (TreeViewItem tempItem in GetTreeViewItemsBetween(_lastItemSelected, item))
((ISelectable)tempItem.Header).IsSelected = true;
_lastItemSelected = tempItem;
else
content.IsSelected = true;
_lastItemSelected = item;
private static bool IsCtrlPressed
get
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
private static bool IsShiftPressed
get
return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
private TreeViewItem GetTreeViewItemClicked(UIElement sender)
Point point = sender.TranslatePoint(new Point(0, 0), this.InnerTreeView);
DependencyObject visualItem = this.InnerTreeView.InputHitTest(point) as DependencyObject;
while (visualItem != null && !(visualItem is TreeViewItem))
visualItem = VisualTreeHelper.GetParent(visualItem);
return visualItem as TreeViewItem;
private IEnumerable<TreeViewItem> GetTreeViewItemsBetween(TreeViewItem start, TreeViewItem end)
List<TreeViewItem> items = this.GetTreeViewItems(false);
int startIndex = items.IndexOf(start);
int endIndex = items.IndexOf(end);
// It's possible that the start element has been removed after it was selected,
// I don't find a way to happen on the end but I add the code to handle the situation just in case
if (startIndex == -1 && endIndex == -1)
return new List<TreeViewItem>();
else if (startIndex == -1)
return new List<TreeViewItem>() end;
else if (endIndex == -1)
return new List<TreeViewItem>() start ;
else
return startIndex > endIndex ? items.GetRange(endIndex, startIndex - endIndex + 1) : items.GetRange(startIndex, endIndex - startIndex + 1);
private List<TreeViewItem> GetTreeViewItems(bool includeCollapsedItems)
List<TreeViewItem> returnItems = new List<TreeViewItem>();
for (int index = 0; index < this.InnerTreeView.Items.Count; index++)
TreeViewItem item = (TreeViewItem)this.InnerTreeView.ItemContainerGenerator.ContainerFromIndex(index);
returnItems.Add(item);
if (includeCollapsedItems || item.IsExpanded)
returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
return returnItems;
private static IEnumerable<TreeViewItem> GetTreeViewItemItems(TreeViewItem treeViewItem, bool includeCollapsedItems)
List<TreeViewItem> returnItems = new List<TreeViewItem>();
for (int index = 0; index < treeViewItem.Items.Count; index++)
TreeViewItem item = (TreeViewItem)treeViewItem.ItemContainerGenerator.ContainerFromIndex(index);
if (item != null)
returnItems.Add(item);
if (includeCollapsedItems || item.IsExpanded)
returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
return returnItems;
#endregion
【讨论】:
建设性批评?将 TreeView 包装在 UserControl 中是不好的做法。最好做一个继承TreeView的控件并为其定义一个新的模板。包装它的问题是 TreeView 的属性没有暴露,有些(像我一样)不能使用它。不过很好的解决方案。 @ThyArtIsCode:我同意。这是我的 WPF 有点受限的时候,现在我会像你说的那样做。无论如何,您总是可以公开包装它们的属性,这不好笑但有效。以上是关于自定义 TreeView 以允许多选的主要内容,如果未能解决你的问题,请参考以下文章
WPF 自定义列表筛选 自定义TreeView模板 自定义ListBox模板