WPF多表头表格实现

  • WPF多表头表格实现已关闭评论
  • 60 次浏览
  • A+
所属分类:.NET技术
摘要

前言 多表头表格是一个常见的业务需求,然而WPF中却没有默认实现这个功能,得益于WPF强大的控件模板设计,我们可以通过修改控件模板的方式自己实现它。

前言

多表头表格是一个常见的业务需求,然而WPF中却没有默认实现这个功能,得益于WPF强大的控件模板设计,我们可以通过修改控件模板的方式自己实现它。

一、需求分析

   下图为一个典型的统计表格,统计1-12月的数据。

WPF多表头表格实现

      此时我们有一个需求,需要将月份按季度划分,以便能够直观地看到季度统计数据,以下为该需求的最终效果。

WPF多表头表格实现

      通过上图分析我们可以看出,我们需要在每个月份上设置一个值来标记它是属于哪一个季度,并且在列上面把它显示出来。

二、程序设计

     WPF所有控件中最贴近需求的控件是DataGrid和ListView,而DataGrid除了基本的表格功能外还有新增行、编辑行、删除行等功能,为了获得更高的性能,我们这里使用更加轻量级的ListView来实现多表头表格功能。

      下图为ListView控件的运行效果,我们可以分析一下ListView控件的模板,看看如何来添加多表头功能。

WPF多表头表格实现

       以下代码为ListView的控件模板。

<SolidColorBrush x:Key="ListBorder" Color="#828790"/> <Style x:Key="{x:Static GridView.GridViewScrollViewerStyleKey}" TargetType="{x:Type ScrollViewer}">     <Setter Property="Focusable" Value="false"/>     <Setter Property="Template">         <Setter.Value>             <ControlTemplate TargetType="{x:Type ScrollViewer}">                 <Grid SnapsToDevicePixels="true" Background="{TemplateBinding Background}">                     <Grid.RowDefinitions>                         <RowDefinition Height="*"/>                         <RowDefinition Height="Auto"/>                     </Grid.RowDefinitions>                     <Grid.ColumnDefinitions>                         <ColumnDefinition Width="*"/>                         <ColumnDefinition Width="Auto"/>                     </Grid.ColumnDefinitions>                     <DockPanel Margin="{TemplateBinding Padding}">                         <ScrollViewer VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" Focusable="false" DockPanel.Dock="Top">                             <GridViewHeaderRowPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Margin="2,0,2,0" ColumnHeaderTemplateSelector="{Binding TemplatedParent.View.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}" Columns="{Binding TemplatedParent.View.Columns, RelativeSource={RelativeSource TemplatedParent}}" ColumnHeaderTemplate="{Binding TemplatedParent.View.ColumnHeaderTemplate, RelativeSource={RelativeSource TemplatedParent}}" ColumnHeaderContextMenu="{Binding TemplatedParent.View.ColumnHeaderContextMenu, RelativeSource={RelativeSource TemplatedParent}}" ColumnHeaderStringFormat="{Binding TemplatedParent.View.ColumnHeaderStringFormat, RelativeSource={RelativeSource TemplatedParent}}" ColumnHeaderToolTip="{Binding TemplatedParent.View.ColumnHeaderToolTip, RelativeSource={RelativeSource TemplatedParent}}" ColumnHeaderContainerStyle="{Binding TemplatedParent.View.ColumnHeaderContainerStyle, RelativeSource={RelativeSource TemplatedParent}}" AllowsColumnReorder="{Binding TemplatedParent.View.AllowsColumnReorder, RelativeSource={RelativeSource TemplatedParent}}"/>                         </ScrollViewer>                         <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.DirectionalNavigation="Local" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" CanContentScroll="{TemplateBinding CanContentScroll}"/>                     </DockPanel>                     <ScrollBar x:Name="PART_HorizontalScrollBar" ViewportSize="{TemplateBinding ViewportWidth}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Grid.Row="1" Orientation="Horizontal" Minimum="0.0" Maximum="{TemplateBinding ScrollableWidth}" Cursor="Arrow"/>                     <ScrollBar x:Name="PART_VerticalScrollBar" ViewportSize="{TemplateBinding ViewportHeight}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Orientation="Vertical" Minimum="0.0" Maximum="{TemplateBinding ScrollableHeight}" Grid.Column="1" Cursor="Arrow"/>                     <DockPanel Grid.Row="1" LastChildFill="false" Grid.Column="1" Background="{Binding Background, ElementName=PART_VerticalScrollBar}">                         <Rectangle Width="1" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Fill="White" DockPanel.Dock="Left"/>                         <Rectangle Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Height="1" Fill="White" DockPanel.Dock="Top"/>                     </DockPanel>                 </Grid>             </ControlTemplate>         </Setter.Value>     </Setter> </Style> <Style x:Key="ListViewStyle1" TargetType="{x:Type ListView}">     <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>     <Setter Property="BorderBrush" Value="{StaticResource ListBorder}"/>     <Setter Property="BorderThickness" Value="1"/>     <Setter Property="Foreground" Value="#FF042271"/>     <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>     <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>     <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>     <Setter Property="ScrollViewer.PanningMode" Value="Both"/>     <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>     <Setter Property="VerticalContentAlignment" Value="Center"/>     <Setter Property="Template">         <Setter.Value>             <ControlTemplate TargetType="{x:Type ListView}">                 <Themes:ListBoxChrome x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderFocused="{TemplateBinding IsKeyboardFocusWithin}" SnapsToDevicePixels="true">                     <ScrollViewer Padding="{TemplateBinding Padding}" Style="{DynamicResource {x:Static GridView.GridViewScrollViewerStyleKey}}">                         <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>                     </ScrollViewer>                 </Themes:ListBoxChrome>                 <ControlTemplate.Triggers>                     <Trigger Property="IsEnabled" Value="false">                         <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>                     </Trigger>                     <MultiTrigger>                         <MultiTrigger.Conditions>                             <Condition Property="IsGrouping" Value="true"/>                             <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>                         </MultiTrigger.Conditions>                         <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>                     </MultiTrigger>                 </ControlTemplate.Triggers>             </ControlTemplate>         </Setter.Value>     </Setter> </Style>

 通过以上代码可以得知,ListView控件模板外层为一个ScrollViewer控件,ScrollViewer中包含了一个ItemsPresenter控件,而列头就在ScrollViewer控件模板中,GridViewHeaderRowPresenter就是列头的最终呈现控件,我们只需要在GridViewHeaderRowPresenter的上面再放一个呈现列头的控件就可以实现多列头功能。

三、代码实现

3.1 定义一个附加属性,用于设置列的分组。

public static class ListViewExtensions     {         #region Group         public static string GetGroup(DependencyObject obj)         {             return (string)obj.GetValue(GroupProperty);         }          public static void SetGroup(DependencyObject obj, string value)         {             obj.SetValue(GroupProperty, value);         }          public static readonly DependencyProperty GroupProperty =             DependencyProperty.RegisterAttached("Group", typeof(string), typeof(ListViewExtensions));          #endregion     }

3.2 设置列分组。

<ListView>     <ListView.View>         <GridView>             <GridViewColumn Extensions:ListViewExtensions.Group="Group1" DisplayMemberBinding="{Binding Property1}">                 <GridViewColumn.Header>                     <GridViewColumnHeader>Property1</GridViewColumnHeader>                 </GridViewColumn.Header>             </GridViewColumn>             <GridViewColumn Extensions:ListViewExtensions.Group="Group1" DisplayMemberBinding="{Binding Property2}">                 <GridViewColumn.Header>                     <GridViewColumnHeader>Property2</GridViewColumnHeader>                 </GridViewColumn.Header>             </GridViewColumn>             <GridViewColumn Extensions:ListViewExtensions.Group="Group2" DisplayMemberBinding="{Binding Property3}">                 <GridViewColumn.Header>                     <GridViewColumnHeader>Property3</GridViewColumnHeader>                 </GridViewColumn.Header>             </GridViewColumn>             <GridViewColumn Extensions:ListViewExtensions.Group="Group2" DisplayMemberBinding="{Binding Property4}">                 <GridViewColumn.Header>                     <GridViewColumnHeader>Property4</GridViewColumnHeader>                 </GridViewColumn.Header>             </GridViewColumn>             <GridViewColumn Extensions:ListViewExtensions.Group="Group2" DisplayMemberBinding="{Binding Property5}">                 <GridViewColumn.Header>                     <GridViewColumnHeader>Property5</GridViewColumnHeader>                 </GridViewColumn.Header>             </GridViewColumn>         </GridView>     </ListView.View> </ListView>

3.3 写一个继承自GridViewHeaderRowPresenter类的自定义控件(此处命名为GridViewGroupHeaderRowPresenter),用于处理分组列,该控件通过读取GridViewColumn设置的Extensions:ListViewExtensions.Group属性来创建分组列,并负责处理分组列与普通列的宽度分配和同步。

3.4 将GridViewGroupHeaderRowPresenter添加到ListView模板中,以下为关键代码。

<StackPanel Orientation="Vertical">    <local:GridViewGroupHeaderRowPresenter OriginalColumns="{Binding TemplatedParent.View.Columns, RelativeSource={RelativeSource Mode=TemplatedParent}}" />    <GridViewHeaderRowPresenter AllowsColumnReorder="{Binding TemplatedParent.View.AllowsColumnReorder, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         ColumnHeaderContainerStyle="{Binding TemplatedParent.View.ColumnHeaderContainerStyle, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         ColumnHeaderContextMenu="{Binding TemplatedParent.View.ColumnHeaderContextMenu, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         ColumnHeaderStringFormat="{Binding TemplatedParent.View.ColumnHeaderStringFormat, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         ColumnHeaderTemplate="{Binding TemplatedParent.View.ColumnHeaderTemplate, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         ColumnHeaderTemplateSelector="{Binding TemplatedParent.View.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         ColumnHeaderToolTip="{Binding TemplatedParent.View.ColumnHeaderToolTip, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         Columns="{Binding TemplatedParent.View.Columns, RelativeSource={RelativeSource Mode=TemplatedParent}}"                                         SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" /> </StackPanel>

      至此开发完成,以下为运行效果。

WPF多表头表格实现

四、自定义外观

      该控件基于ListView标准模板开发,可以在模板中自由修改控件外观,也可以使用第三方UI库,以下为使用MaterialDesign库的效果。

WPF多表头表格实现