• [WPF 自定义控件]创建包含CheckBox的ListBoxItem


    1. 前言

    Xceed wpftoolkit提供了一个CheckListBox,效果如下:

    不过它用起来不怎么样,与其这样还不如参考UWP的ListView实现,而且动画效果也很好看:

    它的样式如下:

    <ListViewItemPresenter ContentTransitions="{TemplateBinding ContentTransitions}"
        x:Name="Root"
        Control.IsTemplateFocusTarget="True"
        FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
        SelectionCheckMarkVisualEnabled="{ThemeResource ListViewItemSelectionCheckMarkVisualEnabled}"
        CheckBrush="{ThemeResource ListViewItemCheckBrush}"
        CheckBoxBrush="{ThemeResource ListViewItemCheckBoxBrush}"
        DragBackground="{ThemeResource ListViewItemDragBackground}"
        DragForeground="{ThemeResource ListViewItemDragForeground}"
        FocusBorderBrush="{ThemeResource ListViewItemFocusBorderBrush}"
        FocusSecondaryBorderBrush="{ThemeResource ListViewItemFocusSecondaryBorderBrush}"
        PlaceholderBackground="{ThemeResource ListViewItemPlaceholderBackground}"
        PointerOverBackground="{ThemeResource ListViewItemBackgroundPointerOver}"
        PointerOverForeground="{ThemeResource ListViewItemForegroundPointerOver}"
        SelectedBackground="{ThemeResource ListViewItemBackgroundSelected}"
        SelectedForeground="{ThemeResource ListViewItemForegroundSelected}"
        SelectedPointerOverBackground="{ThemeResource ListViewItemBackgroundSelectedPointerOver}"
        PressedBackground="{ThemeResource ListViewItemBackgroundPressed}"
        SelectedPressedBackground="{ThemeResource ListViewItemBackgroundSelectedPressed}"
        DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
        DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
        ReorderHintOffset="{ThemeResource ListViewItemReorderHintThemeOffset}"
        HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
        VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
        ContentMargin="{TemplateBinding Padding}"
        CheckMode="{ThemeResource ListViewItemCheckMode}"
        RevealBackground="{ThemeResource ListViewItemRevealBackground}"
        RevealBorderThickness="{ThemeResource ListViewItemRevealBorderThemeThickness}"
        RevealBorderBrush="{ThemeResource ListViewItemRevealBorderBrush}">
    

    属性是很多了,但这里没有自定义CheckBox样式的方法,而且也没法参考它的动画如何实现。幸好UWP还提供了一个ListViewItemExpanded样式,里面有完整的布局、VisualState等,不过总共有差不多500行,只拿其中MultiSelectStates的部分也将近100行,这太过复杂了,这还是有些麻烦,在WPF中实现起来反而简单很多。

    2. 实现

    微软的文档中有介绍如何Create ListViewItems with a CheckBox,原理十分简单:

    <DataTemplate x:Key="FirstCell">
      <StackPanel Orientation="Horizontal">
        <CheckBox IsChecked="{Binding Path=IsSelected, 
          RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListViewItem}}}"/>
      </StackPanel>
    </DataTemplate>
    

    就是在控件模板中添加一个CheckBox并且这个CheckBox通过FindAncestor的Binding方式绑定到ListViewItem的IsSelected属性。虽然是ListView的方法,但它同样适用于ListBox。所以我使用这个方式封装了一个ListBox控件,目前基本上没什么功能,就只是在每个ListBoxItem前面加上一个CheckBox。以前介绍过如何自定义ItemsControl,要自定义一个ListBox控件,同样需要三部:

    1. 定义ListBox
    2. 关联ListBoxItem和ListBox
    3. 实现ListBox的逻辑
    public class ExtendedListBox : ListBox
    {
        public static readonly DependencyProperty IsMultiSelectCheckBoxEnabledProperty =
            DependencyProperty.Register(nameof(IsMultiSelectCheckBoxEnabled), typeof(bool), typeof(ExtendedListBox), new PropertyMetadata(true));
    
        public bool IsMultiSelectCheckBoxEnabled
        {
            get { return (bool)GetValue(IsMultiSelectCheckBoxEnabledProperty); }
            set { SetValue(IsMultiSelectCheckBoxEnabledProperty, value); }
        }
    
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new ExtendedListBoxItem();
        }
    }
    
    
    public class ExtendedListBoxItem : ListBoxItem
    {
        public ExtendedListBoxItem()
        {
            DefaultStyleKey = typeof(ExtendedListBoxItem);
        }
    }
    

    上面就是全部代码。定义了ExtendedListBoxExtendedListBoxItem两个类,然后重写GetContainerForItemOverride关联这两个类,最后在ExtendedListBox的代码里模仿UWP的ListView提供了IsMultiSelectCheckBoxEnabled属性,其他功能主要由XAML提供:

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Primitives:KinoResizer>
        <CheckBox Margin="{TemplateBinding Padding}"
                  IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                  VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                  IsTabStop="False"
                  x:Name="SelectionCheckMark"/>
    </Primitives:KinoResizer>
    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Grid.Column="1"
                      Margin="{TemplateBinding Padding}"/>
    

    ControlTemplate使用Resizer包装CheckBox,这是为了CheckBox隐藏或显示时有过渡动画。然后在ControlTemplate.Triggers里添加两个DataTrigger,根据所属的ListBox的IsMultiSelectCheckBoxEnabledSelectionMode显示或隐藏SelectionCheckMark:

    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListBox},Path=SelectionMode}"
                 Value="Single">
        <Setter Property="Visibility"
                TargetName="SelectionCheckMark"
                Value="Collapsed" />
    </DataTrigger>
    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListBox},Path=IsMultiSelectCheckBoxEnabled}"
                 Value="False">
        <Setter Property="Visibility"
                TargetName="SelectionCheckMark"
                Value="Collapsed" />
    </DataTrigger>
    
    

    最终效果如下:

    3. 添加VisualState

    WPF的Button的ControlTemplate没有使用VisualState,但Button支持VisualState,用户可以自定义使用VisualState的ControlTemplate。ExtendedListBoxItem也模仿UWP提供了MultiSelectEnabled和MultiSelectDisabled两个VisualState,因为ListBoxItem需要知道承载它的ListBox的IsMultiSelectCheckBoxEnabled和SelectionMode,所以需要给ListBoxItem添加一个Owner属性,并重载ListBox的PrepareContainerForItemOverride函数,在这个函数中为ListBoxItem的Owner赋值:

    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
        if (element is ExtendedListBoxItem listBoxItem)
            listBoxItem.Owner = this;
    }
    

    ListBoxItem中使用监视Owner的IsMultiSelectCheckBoxEnabled和SelectionMode的改变,并在这两个值改变时更新VisualState:

    protected virtual void OnOwnerChanged(ExtendedListBox oldValue, ExtendedListBox newValue)
    {
        if (oldValue != null)
        {
            var descriptor = DependencyPropertyDescriptor.FromProperty(ListBox.SelectionModeProperty, typeof(ExtendedListBox));
            descriptor.RemoveValueChanged(newValue, OnSelectionModeChanged);
    
            descriptor = DependencyPropertyDescriptor.FromProperty(ExtendedListBox.IsMultiSelectCheckBoxEnabledProperty, typeof(ExtendedListBox));
            descriptor.RemoveValueChanged(newValue, OnIsMultiSelectCheckBoxEnabledChanged);
        }
        if (newValue != null)
        {
            var descriptor = DependencyPropertyDescriptor.FromProperty(ListBox.SelectionModeProperty, typeof(ExtendedListBox));
            descriptor.AddValueChanged(newValue, OnSelectionModeChanged);
    
            descriptor = DependencyPropertyDescriptor.FromProperty(ExtendedListBox.IsMultiSelectCheckBoxEnabledProperty, typeof(ExtendedListBox));
            descriptor.AddValueChanged(newValue, OnIsMultiSelectCheckBoxEnabledChanged);
        }
    }
    
    private void OnSelectionModeChanged(object sender, EventArgs args)
    {
        UpdateVisualStates(true);
    }
    
    private void OnIsMultiSelectCheckBoxEnabledChanged(object sender, EventArgs args)
    {
        UpdateVisualStates(true);
    }
    

    为了使用VisualState我在ControlTemplate多写了80行代码,因为没有用上VisualTransition所以这个ControlTemplate有一些Bug,反正只是用来验证添加的两个VisualState是否有效。在ListBoxItem里用Trigger比使用VisualState更简洁有效。

    4. 使用同样的原理为DataGrid的行添加ChechBox

    DataGrid也可以用同样的原理为每一行添加CheckBox,只不过DataGrid的Template会负责很多。

    首先自定义一个DataGrid类:

    public class ExtendedDataGrid : DataGrid, IMultiSelector
    {
        // Using a DependencyProperty as the backing store for IsMultiSelectCheckBoxEnabled.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsMultiSelectCheckBoxEnabledProperty =
            DependencyProperty.Register(nameof(IsMultiSelectCheckBoxEnabled), typeof(bool), typeof(ExtendedDataGrid), new PropertyMetadata(true));
    
        public ExtendedDataGrid()
        {
            DefaultStyleKey = typeof(ExtendedDataGrid);
        }
    
        public bool IsMultiSelectCheckBoxEnabled
        {
            get { return (bool)GetValue(IsMultiSelectCheckBoxEnabledProperty); }
            set { SetValue(IsMultiSelectCheckBoxEnabledProperty, value); }
        }
    }
    

    然后定义一个RowHeaderTemplate

    <DataTemplate x:Key="DataGridRowHeaderTemplate">
        <Grid>
            <CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}"
                          x:Name="SelectionCheckBox"/>
        </Grid>
    </DataTemplate>
    

    在DataGrid的Style上应用这个RowHeaderTemplate。最后再DataGrid的Style的Triggers中添加两个DataTrigger:

    <Trigger Property="SelectionMode" Value="Single">
        <Setter Property="HeadersVisibility"  Value="Column" />
    </Trigger>
    <Trigger Property="IsMultiSelectCheckBoxEnabled" Value="False">
        <Setter Property="HeadersVisibility"  Value="Column"/>
    </Trigger>
    

    HeadersVisibility是个DataGridHeadersVisibility的属性,它用于控制DataGrid行和列的Header是否显示,因为我在每一行的开头放了CheckBox(就是使用上面定义的RowHeaderTempalte),所以定一只只显示Column的Header的话相当于隐藏了这个CheckBox,运行效果如下:

    5. 结语

    ListBox和DataGrid的自定义是个很大的话题,这里只实现最简单的功能,通常会根据业务需求逐渐增加更多需求。如果有更复杂的需求,我建议买商业的控件,毕竟DataGrid的自定义可以很复杂,花时间不如花钱。

    6. 参考

    How to_ Create ListViewItems with a CheckBox - WPF _ Microsoft Docs

    ListBox Class (System.Windows.Controls) _ Microsoft Docs

    DataGrid Class (System.Windows.Controls) _ Microsoft Docs

    7. 源码

    Kino.Toolkit.Wpf_ExtendedListBox.cs at master

    Kino.Toolkit.Wpf_ExtendedDataGrid.cs at master

  • 相关阅读:
    2014/11/25 函数
    2014/11/24 条件查询
    2、计算器
    1、winform数据库调用(基本方法)
    1、网页基础
    14、函数输出参数、递归
    13、C#简易版 推箱子游戏
    12、函数
    11、结构体、枚举
    10、特殊集合
  • 原文地址:https://www.cnblogs.com/dino623/p/Create_ListBoxIte_with_a_CheckBox.html
Copyright © 2020-2023  润新知