带有复选框的WPF TreeView

5
经过大量搜索,我没有找到以下问题的解决方案。我需要一个带有“复选框”的树形视图控件和CheckedItems属性,以便进行方便的数据绑定(例如,文件夹结构的树形视图,当用户选中文件夹时,在文本框中显示所选文件夹的大小)。顺便说一句,我已经阅读了«Working with Checkboxes in the WPF TreeView», Josh Smith,但是在我的情况下,“IsChecked”方法不合适,因为我需要将CheckedItems绑定为集合。我会感激任何帮助!

alt text

图片链接已附上。我希望列表框与 CheckTreeViewCheckedItems 属性数据绑定。有人知道如何实现泛型的 CheckTreeView 并可能绑定到 CheckedItems 集合吗?
1个回答

4

更新
终于添加了CheckBoxTreeView的缺失功能。可以从此处下载CheckBoxTreeViewLibrary源代码。

  • 添加了CheckedItems属性
  • CheckedItems是一个ObservableCollection<T>,其中T是ItemsSource的内部类型
  • CheckedItems支持双向绑定到源
  • 如果一个CheckBoxTreeViewItem还没有生成(未展开),那么它的源在生成之前不会出现在CheckedItems集合中

该控件可以像常规TreeView一样使用。要为IsChecked属性添加双向绑定,必须合并CheckBoxTreeViewItemStyle.xaml ResourceDictionary。例如:

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="/CheckBoxTreeViewLibrary;component/Themes/CheckBoxTreeViewItemStyle.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

然后,ItemContainerStyle 可以像这样使用:
<cbt:CheckBoxTreeView ...>
    <cbt:CheckBoxTreeView.ItemContainerStyle>
        <Style TargetType="{x:Type cbt:CheckBoxTreeViewItem}"
               BasedOn="{StaticResource {x:Type cbt:CheckBoxTreeViewItem}}">
            <Setter Property="IsChecked" Value="{Binding IsChecked}"/>
            <!-- additional Setters, Triggers etc. -->
        </Style>
    </cbt:CheckBoxTreeView.ItemContainerStyle>
</cbt:CheckBoxTreeView>

CheckBoxTreeView.cs

namespace CheckBoxTreeViewLibrary
{
    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CheckBoxTreeViewItem))]
    public class CheckBoxTreeView : TreeView
    {
        public static DependencyProperty CheckedItemsProperty =
            DependencyProperty.Register("CheckedItems",
                                        typeof(IList),
                                        typeof(CheckBoxTreeView));

        private RoutedEventHandler Checked_EventHandler;
        private RoutedEventHandler Unchecked_EventHandler;

        public CheckBoxTreeView()
            : base()
        {
            Checked_EventHandler = new RoutedEventHandler(checkBoxTreeViewItem_Checked);
            Unchecked_EventHandler = new RoutedEventHandler(checkBoxTreeViewItem_Unchecked);

            DependencyPropertyDescriptor dpd =
                DependencyPropertyDescriptor.FromProperty(CheckBoxTreeView.ItemsSourceProperty, typeof(CheckBoxTreeView));
            if (dpd != null)
            {
                dpd.AddValueChanged(this, ItemsSourceChanged);
            }
        }
        void ItemsSourceChanged(object sender, EventArgs e)
        {
            Type type = ItemsSource.GetType();
            if (ItemsSource is IList)
            {
                Type listType = typeof(ObservableCollection<>).MakeGenericType(type.GetGenericArguments()[0]);
                CheckedItems = (IList)Activator.CreateInstance(listType);
            }
        }

        internal void OnNewContainer(CheckBoxTreeViewItem newContainer)
        {
            newContainer.Checked -= Checked_EventHandler;
            newContainer.Unchecked -= Unchecked_EventHandler;
            newContainer.Checked += Checked_EventHandler;
            newContainer.Unchecked += Unchecked_EventHandler;
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = new CheckBoxTreeViewItem();
            OnNewContainer(checkBoxTreeViewItem);
            return checkBoxTreeViewItem;
        }

        void checkBoxTreeViewItem_Checked(object sender, RoutedEventArgs e)
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = sender as CheckBoxTreeViewItem;

            Action action = () =>
            {
                var checkedItem = checkBoxTreeViewItem.Header;
                CheckedItems.Add(checkedItem);
            };
            this.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
        }

        void checkBoxTreeViewItem_Unchecked(object sender, RoutedEventArgs e)
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = sender as CheckBoxTreeViewItem;
            Action action = () =>
            {
                var uncheckedItem = checkBoxTreeViewItem.Header;
                CheckedItems.Remove(uncheckedItem);
            };
            this.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
        }

        public IList CheckedItems
        {
            get { return (IList)base.GetValue(CheckedItemsProperty); }
            set { base.SetValue(CheckedItemsProperty, value); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

CheckBoxTreeViewItem.cs

namespace CheckBoxTreeViewLibrary
{
    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CheckBoxTreeViewItem))]
    public class CheckBoxTreeViewItem : TreeViewItem
    {
        public static readonly RoutedEvent CheckedEvent = EventManager.RegisterRoutedEvent("Checked",
            RoutingStrategy.Direct,
            typeof(RoutedEventHandler),
            typeof(CheckBoxTreeViewItem));

        public static readonly RoutedEvent UncheckedEvent = EventManager.RegisterRoutedEvent("Unchecked",
            RoutingStrategy.Direct,
            typeof(RoutedEventHandler),
            typeof(CheckBoxTreeViewItem));

        public static readonly DependencyProperty IsCheckedProperty =
            DependencyProperty.Register("IsChecked",
                                        typeof(bool),
                                        typeof(CheckBoxTreeViewItem),
                                        new FrameworkPropertyMetadata(false,
                                                                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                      CheckedPropertyChanged));

        private static void CheckedPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = (CheckBoxTreeViewItem)source;
            if (checkBoxTreeViewItem.IsChecked == true)
            {
                checkBoxTreeViewItem.OnChecked(new RoutedEventArgs(CheckedEvent, checkBoxTreeViewItem));
            }
            else
            {
                checkBoxTreeViewItem.OnUnchecked(new RoutedEventArgs(UncheckedEvent, checkBoxTreeViewItem));
            }
        }

        public CheckBoxTreeViewItem()
            : base()
        {
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            PropertyInfo parentTreeViewPi = typeof(TreeViewItem).GetProperty("ParentTreeView", BindingFlags.Instance | BindingFlags.NonPublic);
            CheckBoxTreeView parentCheckBoxTreeView = parentTreeViewPi.GetValue(this, null) as CheckBoxTreeView;
            CheckBoxTreeViewItem checkBoxTreeViewItem = new CheckBoxTreeViewItem();
            parentCheckBoxTreeView.OnNewContainer(checkBoxTreeViewItem);
            return checkBoxTreeViewItem;
        }

        [Category("Behavior")]
        public event RoutedEventHandler Checked
        {
            add
            {
                AddHandler(CheckedEvent, value);
            }
            remove
            {
                RemoveHandler(CheckedEvent, value);
            }
        }
        [Category("Behavior")]
        public event RoutedEventHandler Unchecked
        {
            add
            {
                AddHandler(UncheckedEvent, value);
            }
            remove
            {
                RemoveHandler(UncheckedEvent, value);
            }
        }

        public bool IsChecked
        {
            get { return (bool)base.GetValue(IsCheckedProperty); }
            set { base.SetValue(IsCheckedProperty, value); }
        }

        protected virtual void OnChecked(RoutedEventArgs e)
        {
            base.RaiseEvent(e);
        }
        protected virtual void OnUnchecked(RoutedEventArgs e)
        {
            base.RaiseEvent(e);
        }
    }
}

CheckBoxTreeViewItemStyle.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:cti="clr-namespace:CheckBoxTreeViewLibrary">
    <Style x:Key="TreeViewItemFocusVisual">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate>
                    <Rectangle/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
    <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="Transparent" Stroke="#FF989898">
                            <Path.RenderTransform>
                                <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                            </Path.RenderTransform>
                        </Path>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF1BBBFA"/>
                            <Setter Property="Fill" TargetName="ExpandPath" Value="Transparent"/>
                        </Trigger>
                        <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="#FF595959"/>
                            <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF262626"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="{x:Type cti:CheckBoxTreeViewItem}">
        <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 cti:CheckBoxTreeViewItem}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition MinWidth="15" Width="Auto"/>
                            <!--<ColumnDefinition Width="Auto"/>-->
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" MinHeight="15"/>
                            <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">
                            <StackPanel Orientation="Horizontal">
                                <CheckBox Margin="0,2,4,0" x:Name="PART_CheckedCheckBox" IsChecked="{Binding IsChecked, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
                                <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </StackPanel>
                        </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">
                            <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="IsSelectionActive" Value="false"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                        </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="VirtualizingStackPanel.IsVirtualizing" Value="true">
                <Setter Property="ItemsPanel">
                    <Setter.Value>
                        <ItemsPanelTemplate>
                            <VirtualizingStackPanel/>
                        </ItemsPanelTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

@Serge:更新了我的答案,虽然比一个月晚了一点,但迟到总比错过好:) 我不知道你是否仍需要CheckBoxTreeView,但我的更新答案应该包括上一个答案缺少的功能。 - Fredrik Hedblad
这是一段非常有趣的代码和方法。我已经尝试了一下,几乎实现了预期效果。但是,我希望CheckedItems属性能够反映出在GUI中所做的选择,因此如果TreeViewItem被选中,它将被添加。我现在有4周WPF的经验,可能会错过一些显而易见的东西。我应该在标记中指定额外的内容吗? - buckley
你好。这个例子非常棒。我已经通过双向绑定将CheckedItems绑定到了TreeView和ListBox上。但是如果ListBox删除了CheckedItem中的一个条目,TreeView并不会反映这一变化。你知道我做错了什么吗? - Matt Fitzmaurice
<ListBox Grid.Column="0" ItemsSource="{Binding CheckedItems, Mode=TwoWay}" ItemTemplate="{StaticResource listBoxDataTemplate}" ItemsPanel="{StaticResource listBoxItemsPanel}"/> <controls:CheckBoxTreeView ItemsSource="{Binding FirstLevel}" CheckedItems="{Binding CheckedItems, Mode=TwoWay}" ItemTemplate="{StaticResource treeViewDataTemplate}" ItemContainerStyle="{StaticResource treeViewContainerStyle}"/> - Matt Fitzmaurice
搞定了。我在我的XAML中缺少了IsChecked Setter。 - Matt Fitzmaurice
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接