【问题标题】:Customizing the TreeView to allow multi select自定义 TreeView 以允许多选
【发布时间】:2010-10-02 08:13:40
【问题描述】:

内置的 WPF TreeView 控件不允许多选,就像 ListBox 一样。如何自定义 TreeView 以允许多选而不重写它。

【问题讨论】:

标签: wpf treeview multi-select


【解决方案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 &amp;&amp; _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 有点受限的时候,现在我会像你说的那样做。无论如何,您总是可以公开包装它们的属性,这不好笑但有效。
    猜你喜欢
    • 2010-09-17
    • 2020-11-17
    • 1970-01-01
    • 2020-01-23
    • 1970-01-01
    • 1970-01-01
    • 2018-10-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多