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

2021-03-19 20:26

阅读:354

标签:emd   prepare   hive   hub   包装   ima   stop   roo   turn   

原文:[WPF 自定义控件]创建包含CheckBox的ListBoxItem

1. 前言#

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

技术图片

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

技术图片

它的样式如下:

Copy
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,原理十分简单:

Copy
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的逻辑
Copy
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提供:

Copy
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:

Copy
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赋值:

Copy
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:

Copy
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类:

Copy
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

Copy
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:

Copy
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

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

标签:emd   prepare   hive   hub   包装   ima   stop   roo   turn   

原文地址:https://www.cnblogs.com/lonelyxmas/p/12324047.html


评论


亲,登录后才可以留言!