WPF多表頭表格實現

趋时软件發表於2024-04-19

前言

多表頭表格是一個常見的業務需求,然而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庫的效果。

相關文章