自定义 TreeView 以允许多选

发布于 2024-07-12 07:04:43 字数 78 浏览 4 评论 0原文

内置的 WPF TreeView 控件不允许像 ListBox 那样进行多项选择。 如何自定义 TreeView 以允许多重选择而不重写它。

The built-in WPF TreeView control does not allow for multi selection, like a ListBox does. How can I customize the TreeView to allow for multi selection without rewriting it.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

瞳孔里扚悲伤 2024-07-19 07:04:43

我有一个 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);
    }

希望这可以帮助。

I have a variation on SoMoS implementation that uses an attached property declared on a derivation of the base TreeView control to track the selection state of the TreeViewItems. This keeps the selection tracking on the TreeViewItem element itself, and off of the model object being presented by the tree-view.

This is the new TreeView class derivation.

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
    }
}

And here's the XAML. Make note that the salient part is the replacement of the two triggers that use the singular 'IsSelected' property with the new 'IsItemSelected' attached property in the MultiSelectTreeViewItemStyle to achieve the visual state.

Also note I'm not aggregating the new TreeView control into a 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>

And here's a cheesy view-model to drive it (for demo purposes).

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);
        }
    }
}

And here's the button click-handler on the MainWindow code-behind that shows the selections in a 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);
    }

Hope this helps.

紫轩蝶泪 2024-07-19 07:04:43

当我考虑重写控件(如树视图)的基本行为时,我总是喜欢考虑与我的决定相关的可用性和工作量。

在树视图的具体情况下,我发现切换到列表视图并结合零个、一个或多个控件可以形成更可用的解决方案,而且通常更容易实现。

作为示例,请考虑常见的“打开”对话框或 Windows 资源管理器应用程序。

When I consider overriding the fundamental behavior of a control, like a treeview, I always like to consider the usability and effort associated with my decision.

In the specific case of a treeview I find that switching to a listview in combination with zero, one, or more controls makes for a more usable solution that often is easier to implement.

As an example, consider the common Open dialog, or Windows Explorer application.

z祗昰~ 2024-07-19 07:04:43

我简化了此任务,在每个树视图项的文本前添加一个复选框。

因此,我创建了一个包含 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];
}

如果您不需要任何复杂的内容,这是一种简单的方法。

I've simplified this task adding a checkbox before the text for each treeviewitem.

So, I've created a dockpanel with 2 items inside: checkbox + textblock.

So...

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);

And then you can access checkbox value:

void item_Selected(object sender, RoutedEventArgs e)
{
  selectedTVI = ((TreeViewItem)sender);

  CheckBox cb = (Checkbox)((DockPanel)selectedTVI.Header).Children[0];
}

This is a simple way to do if you don't need anything complex.

鸠书 2024-07-19 07:04:43

我终于结束了自己的自定义控件的编码,其中包含一个 TreeView 。 基于其他人的工作,功能的关键在于使 TreeView 的模型的所有项都继承接口 ISelectable:

public interface ISelectable
{
    public bool IsSelected {get; set}
}

这样我们将拥有一个新的“IsSelected”属性,该属性与 TreeViewItem IsSelected 无关。 我们只需要设置树的样式,以便它处理模型的 IsSelected 属性。 这里是代码(它使用 http://code.html 上提供的拖放库。 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
}
}

I finally ended coding my own CustomControl containing a TreeView inside. Based on the work of others the key of the functionality resides on making all the items of the Model of the TreeView inherit the interface ISelectable:

public interface ISelectable
{
    public bool IsSelected {get; set}
}

This way we will have a new 'IsSelected' property that has nothing to do with the TreeViewItem IsSelected. We just need to style our tree so it handles the model IsSelected property. Here the code (it's using the Drag & drop libraries available at 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
}
}
隔纱相望 2024-07-19 07:04:43

@Nishan Hossepian 答案很好,但最好重写 SelectedItemChangedInternal 方法,如下所示:

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);
            }
        }
        // Otherwise, individual selection
        else
        {
            SetIsItemSelected(tvItem, !GetIsItemSelected(tvItem));
            _lastItemSelected = tvItem;
        }
    }

@Nishan Hossepian answer is good, but it is better to rewrite the SelectedItemChangedInternal method like this:

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);
            }
        }
        // Otherwise, individual selection
        else
        {
            SetIsItemSelected(tvItem, !GetIsItemSelected(tvItem));
            _lastItemSelected = tvItem;
        }
    }
天邊彩虹 2024-07-19 07:04:43

我使用 PropertyTools.WPF 包中的多选 TreeListBox 控件。

效果很好。 Examples 文件夹中有一个 TreeListBox 的示例。

github 链接

发布以防有人仍在寻找此问题的答案。

I use the multi-select TreeListBox control from the PropertyTools.WPF package.

Works great. There is an example of TreeListBox in the Examples folder.

Link to github

Posting in case someone is still looking for an answer to this question.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文