前言
多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制元件模板設計,我們可以透過修改控制元件模板的方式自己實現它。
一、需求分析
下圖為一個典型的統計表格,統計1-12月的資料。
此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計資料,以下為該需求的最終效果。
透過上圖分析我們可以看出,我們需要在每個月份上設定一個值來標記它是屬於哪一個季度,並且在列上面把它顯示出來。
二、程式設計
WPF所有控制元件中最貼近需求的控制元件是DataGrid和ListView,而DataGrid除了基本的表格功能外還有新增行、編輯行、刪除行等功能,為了獲得更高的效能,我們這裡使用更加輕量級的ListView來實現多表頭表格功能。
下圖為ListView控制元件的執行效果,我們可以分析一下ListView控制元件的模板,看看如何來新增多表頭功能。
以下程式碼為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>
至此開發完成,以下為執行效果。
四、自定義外觀
該控制元件基於ListView標準模板開發,可以在模板中自由修改控制元件外觀,也可以使用第三方UI庫,以下為使用MaterialDesign庫的效果。