WPF实现树形表格控件(TreeListView)
前言
本文将探讨如何利用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>