前言

本文将探讨如何利用WPF框架实现树形表格控件,该控件不仅能够有效地展示复杂的层级数据,还能够提供丰富的个性化定制选项。我们将介绍如何使用WPF提供的控件、模板、布局、数据绑定等技术来构建这样一个树形表格。

一、运行效果

1.1默认样式

1.2 自定义样式

二、代码实现

2.1 创建自定义控件(TreeListView)

新建一个继承自TreeView的控件,并定义一个类型为ViewBase的View依赖属性,用于在代码中指定列。

public classTreeListView : TreeView
{
publicViewBase View
{
get { return(ViewBase)GetValue(ViewProperty); }set{ SetValue(ViewProperty, value); }
}
public static readonly DependencyProperty ViewProperty =DependencyProperty.Register("View", typeof(ViewBase), typeof(TreeListView));
}

2.2 在TreeListView控件模板中处理列头

为了在TreeListView中显示列头,需要在合适的位置添加GridViewHeaderRowPresenter控件,并在Columns属性上绑定我们之前定义的View.Columns属性。下面我们首先来分析TreeView控件模板的代码。

<ControlTemplateTargetType="{x:Type TreeView}">
    <Borderx:Name="Bd"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"SnapsToDevicePixels="true">
        <ScrollViewerx:Name="_tv_scrollviewer_"Background="{TemplateBinding Background}"CanContentScroll="false"Focusable="false"HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"Padding="{TemplateBinding Padding}"SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
            <ItemsPresenter/>
        </ScrollViewer>
    </Border>
    <ControlTemplate.Triggers>
        <TriggerProperty="IsEnabled"Value="false">
            <SetterProperty="Background"TargetName="Bd"Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
        </Trigger>
        <TriggerProperty="VirtualizingPanel.IsVirtualizing"Value="true">
            <SetterProperty="CanContentScroll"TargetName="_tv_scrollviewer_"Value="true"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

通过以上代码我们可以看出,只要将GridViewHeaderRowPresenter控件添加到ScrollViewer控件上面即可实现列头功能,但这样会有一个问题,那就是内容宽度超出控件宽度后,鼠标拖动横向滚动条时列头不会跟随下方的数据列表一起滚动。为解决这个问题我们需要将GridViewHeaderRowPresenter放置到ScrollViewer控件模板中,以下为完整代码。

<Stylex:Key="{x:Static GridView.GridViewScrollViewerStyleKey}"TargetType="{x:Type ScrollViewer}">
    <SetterProperty="Focusable"Value="false" />
    <SetterProperty="Template">
        <Setter.Value>
            <ControlTemplateTargetType="{x:Type ScrollViewer}">
                <GridBackground="{TemplateBinding Background}"SnapsToDevicePixels="true">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinitionWidth="*" />
                        <ColumnDefinitionWidth="Auto" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinitionHeight="*" />
                        <RowDefinitionHeight="Auto" />
                    </Grid.RowDefinitions>

                    <DockPanelMargin="{TemplateBinding Padding}">
                        <ScrollViewerDockPanel.Dock="Top"Focusable="false"HorizontalScrollBarVisibility="Hidden"VerticalScrollBarVisibility="Hidden">
                            <GridViewHeaderRowPresenterMargin="2,0,2,0"AllowsColumnReorder="{Binding TemplatedParent.View.AllowsColumnReorder, RelativeSource={RelativeSource TemplatedParent}}"ColumnHeaderContainerStyle="{Binding TemplatedParent.View.ColumnHeaderContainerStyle, RelativeSource={RelativeSource TemplatedParent}}"ColumnHeaderContextMenu="{Binding TemplatedParent.View.ColumnHeaderContextMenu, RelativeSource={RelativeSource TemplatedParent}}"ColumnHeaderStringFormat="{Binding TemplatedParent.View.ColumnHeaderStringFormat, RelativeSource={RelativeSource TemplatedParent}}"ColumnHeaderTemplate="{Binding TemplatedParent.View.ColumnHeaderTemplate, RelativeSource={RelativeSource TemplatedParent}}"ColumnHeaderTemplateSelector="{Binding TemplatedParent.View.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}"ColumnHeaderToolTip="{Binding TemplatedParent.View.ColumnHeaderToolTip, RelativeSource={RelativeSource TemplatedParent}}"Columns="{Binding TemplatedParent.View.Columns, RelativeSource={RelativeSource TemplatedParent}}"SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </ScrollViewer>
                        <ScrollContentPresenterx:Name="PART_ScrollContentPresenter"CanContentScroll="{TemplateBinding CanContentScroll}"Content="{TemplateBinding Content}"ContentTemplate="{TemplateBinding ContentTemplate}"KeyboardNavigation.DirectionalNavigation="Local"SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </DockPanel>

                    <ScrollBarx:Name="PART_HorizontalScrollBar"Grid.Row="1"Cursor="Arrow"Maximum="{TemplateBinding ScrollableWidth}"Minimum="0.0"Orientation="Horizontal"ViewportSize="{TemplateBinding ViewportWidth}"Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
                    <ScrollBarx:Name="PART_VerticalScrollBar"Grid.Column="1"Cursor="Arrow"Maximum="{TemplateBinding ScrollableHeight}"Minimum="0.0"Orientation="Vertical"ViewportSize="{TemplateBinding ViewportHeight}"Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
                    <DockPanelGrid.Row="1"Grid.Column="1"Background="{Binding Background, ElementName=PART_VerticalScrollBar}"LastChildFill="false">
                        <RectangleWidth="1"DockPanel.Dock="Left"Fill="White"Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" />
                        <RectangleHeight="1"DockPanel.Dock="Top"Fill="White"Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />
                    </DockPanel>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<StyleTargetType="{x:Type local:TreeListView}">
    <SetterProperty="ScrollViewer.HorizontalScrollBarVisibility"Value="Auto" />
    <SetterProperty="ScrollViewer.VerticalScrollBarVisibility"Value="Auto" />
    <SetterProperty="ScrollViewer.CanContentScroll"Value="true" />
    <SetterProperty="Template">
        <Setter.Value>
            <ControlTemplateTargetType="{x:Type local:TreeListView}">
                <BorderBorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewerPadding="{TemplateBinding Padding}"Style="{StaticResource {x:Static GridView.GridViewScrollViewerStyleKey}}">
                        <ItemsPresenter/>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

2.3 在TreeListViewItem模板中处理子项的展开和收缩

新建一个继承自TreeViewItem的类,命名为TreeListViewItem(如有个性化需求,可以在该类中处理),编辑控件模板,在模板中添加以下代码。

<StyleTargetType="{x:Type local:TreeListViewItem}">
    <SetterProperty="BorderThickness"Value="1" />
    <SetterProperty="Template">
        <Setter.Value>
            <ControlTemplateTargetType="{x:Type local:TreeListViewItem}">
                <StackPanel>
                    <BorderName="Bd"Padding="{TemplateBinding Padding}"Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}">
                        <GridViewRowPresenterx:Name="PART_Header"Columns="{Binding RelativeSource={RelativeSource AncestorType=local:TreeListView}, Path=View.Columns}"Content="{TemplateBinding Header}" />
                    </Border>
                    <ItemsPresenterx:Name="ItemsHost" />
                </StackPanel>
                <ControlTemplate.Triggers>
                    <TriggerProperty="IsExpanded"Value="false">
                        <SetterTargetName="ItemsHost"Property="Visibility"Value="Collapsed" />
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <ConditionProperty="HasHeader"Value="false" />
                            <ConditionProperty="Width"Value="Auto" />
                        </MultiTrigger.Conditions>
                        <SetterTargetName="PART_Header"Property="MinWidth"Value="75" />
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <ConditionProperty="HasHeader"Value="false" />
                            <ConditionProperty="Height"Value="Auto" />
                        </MultiTrigger.Conditions>
                        <SetterTargetName="PART_Header"Property="MinHeight"Value="19" />
                    </MultiTrigger>

                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <ConditionProperty="extensions:TreeViewItemExtensions.IsMouseDirectlyOverItem"Value="True" />
                        </MultiTrigger.Conditions>
                        <SetterTargetName="Bd"Property="Background"Value="{StaticResource Item.MouseOver.Background}" />
                        <SetterTargetName="Bd"Property="BorderBrush"Value="{StaticResource Item.MouseOver.Border}" />
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <ConditionProperty="Selector.IsSelectionActive"Value="False" />
                            <ConditionProperty="IsSelected"Value="True" />
                        </MultiTrigger.Conditions>
                        <SetterTargetName="Bd"Property="Background"Value="{StaticResource Item.SelectedInactive.Background}" />
                        <SetterTargetName="Bd"Property="BorderBrush"Value="{StaticResource Item.SelectedInactive.Border}" />
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <ConditionProperty="Selector.IsSelectionActive"Value="True" />
                            <ConditionProperty="IsSelected"Value="True" />
                        </MultiTrigger.Conditions>
                        <SetterTargetName="Bd"Property="Background"Value="{StaticResource Item.SelectedActive.Background}" />
                        <SetterTargetName="Bd"Property="BorderBrush"Value="{StaticResource Item.SelectedActive.Border}" />
                    </MultiTrigger>
                    <TriggerProperty="IsEnabled"Value="False">
                        <SetterProperty="Foreground"Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

此处的核心在于模板中添加了GridViewRowPresenter控件,并在Columns属性上绑定了我们之前定义的View.Columns属性,这样就可以在每一行上面显示列数据。还有一个关键点是ItemsPresenter,它用于显示子项数据,此处命名为ItemsHost,它由属性触发器中的代码来控件展开和收起。以下是属性触发器代码。

<TriggerProperty="IsExpanded"Value="false">
    <SetterTargetName="ItemsHost"Property="Visibility"Value="Collapsed" />
</Trigger>

2.4 在单元格模板中控件子项的展开与收起

为了达到展开和收起的效果,需要在首列的单元格中控制TreeListViewItem的IsExpanded属性。以下为完整代码。

<DataTemplatex:Key="ExpandCellTemplate">
    <DockPanel>
        <ToggleButtonx:Name="Expander"Margin="{Binding Path=Level, Converter={StaticResource LevelIndentConverter}, RelativeSource={RelativeSource AncestorType={x:Type TreeListViewItem}}}"ClickMode="Press"IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type TreeListViewItem}}}"Style="{StaticResource ExpandCollapseToggleStyle}" />
        <TextBlockText="{Binding Property1}" />
    </DockPanel>
    <DataTemplate.Triggers>
        <DataTriggerBinding="{Binding Path=HasItems, RelativeSource={RelativeSource AncestorType={x:Type TreeListViewItem}}}"Value="False">
            <SetterTargetName="Expander"Property="Visibility"Value="Hidden" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

其关键代码为

IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type TreeListViewItem}}}"

2.5 控件使用

<TreeListViewItemsSource="{Binding Collection}">
    <TreeListView.ItemTemplate>
        <HierarchicalDataTemplateItemsSource="{Binding Collection, IsAsync=True}" />
    </TreeListView.ItemTemplate>
    <TreeListView.View>
        <GridView>
            <GridViewColumnCellTemplate="{StaticResource ExpandCellTemplate}"Header="Property1" />
            <GridViewColumnDisplayMemberBinding="{Binding Property2}"Header="Property2" />
            <GridViewColumnDisplayMemberBinding="{Binding Property3}"Header="Property3" />
            <GridViewColumnDisplayMemberBinding="{Binding Property4}"Header="Property4" />
            <GridViewColumnDisplayMemberBinding="{Binding Property5}"Header="Property5" />
            <GridViewColumnDisplayMemberBinding="{Binding Property6}"Header="Property6" />
            <GridViewColumnDisplayMemberBinding="{Binding Property7}"Header="Property7" />
            <GridViewColumnDisplayMemberBinding="{Binding Property8}"Header="Property8" />
            <GridViewColumnDisplayMemberBinding="{Binding Property9}"Header="Property9" />
            <GridViewColumnDisplayMemberBinding="{Binding Property10}"Header="Property10" />
            <GridViewColumnDisplayMemberBinding="{Binding Property11}"Header="Property11" />
            <GridViewColumnDisplayMemberBinding="{Binding Property12}"Header="Property12" />
        </GridView>
    </TreeListView.View>
</TreeListView>

标签: none

添加新评论