WPF 原生控件封装:TreeView 与 DataGrid 组合实现
WPF 的功能非常强大,许多控件都是原生的。但在实际开发中,有时候需要同时使用 TreeView 和 DataGrid 的组合效果(例如树形表格),这就需要我们去封装实现。
第三方控件虽然方便,但往往收费且功能受限。利用原生控件进行封装,既能满足需求又能保持轻量。本文演示如何使用 TreeView 来实现这一组合效果。
实现思路
实现上述效果主要有三种技术路径:
- TreeView:本文重点演示的方案,适合层级数据展示。
- DataGrid:适合纯表格数据。
- ListView:灵活性高但配置较繁琐。
我们选择第一种方案,通过继承 TreeView 并重写相关属性,模拟出类似 DataGrid 的列显示效果。
1. 创建 WPF 项目
首先建立一个标准的 WPF 程序项目,确保引用了必要的命名空间。
2. 封装 TreeGrid 核心类
我们需要创建一个自定义控件 TreeGrid,它继承自 TreeView。核心在于定义列映射、行高以及单元格样式。
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace TreeView.TreeDataGrid.Controls
{
public class TreeGrid : TreeView
{
static TreeGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TreeGrid),
new FrameworkPropertyMetadata(typeof(TreeGrid)));
}
#region ColumnMappings DependencyProperty
/// <summary>
/// 列映射字符串,格式如 "标题:字段名;标题:字段名"
/// </summary>
public string ColumnMappings
{
get { return (string)GetValue(ColumnMappingsProperty); }
set { SetValue(ColumnMappingsProperty, value); }
}
public static readonly DependencyProperty ColumnMappingsProperty =
DependencyProperty.Register("ColumnMappings", typeof(string), typeof(TreeGrid),
new PropertyMetadata("", new PropertyChangedCallback(OnColumnMappingsPropertyChanged)));
private static void OnColumnMappingsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is TreeGrid grid)
{
grid.OnColumnMappingsValueChanged();
}
}
protected void OnColumnMappingsValueChanged()
{
if (!string.IsNullOrEmpty(ColumnMappings))
{
ResetMappingColumns(ColumnMappings);
}
}
private void ResetMappingColumns(string mapping)
{
var items = new GridViewColumnCollection();
var columns = mapping.Split(new[] { ';', '|' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var c in columns)
{
var index = c.IndexOf(':');
var title = "";
var name = "";
if (index > 0)
{
title = c.Substring(0, index);
name = c.Substring(index + 1);
}
else
{
title = c;
name = c;
}
DataTemplate temp = null;
// 尝试查找资源中的 DataTemplate
var res = this.FindTreeResource<DataTemplate>(name);
if (res != null && res is DataTemplate template)
{
temp = template;
}
else
{
temp = new DataTemplate();
FrameworkElementFactory element = null;
// 第一列通常使用特殊内容控制
if (items.Count == 0)
{
element = new FrameworkElementFactory(typeof(TreeItemContentControl));
element.SetValue(ContentControl.ContentProperty, new Binding(name));
}
else
{
element = new FrameworkElementFactory(typeof(TreeGridCell));
element.SetValue(ContentControl.ContentProperty, new Binding(name));
}
temp.VisualTree = element;
}
var col = new GridViewColumn
{
Width = 200,
Header = title,
CellTemplate = temp,
};
items.Add(col);
}
Columns = items;
}
#endregion
#region Columns DependencyProperty
public GridViewColumnCollection Columns
{
get { return (GridViewColumnCollection)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(GridViewColumnCollection), typeof(TreeGrid),
new PropertyMetadata(null, new PropertyChangedCallback(OnColumnsPropertyChanged)));
private static void OnColumnsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is TreeGrid grid)
{
grid.OnColumnsValueChanged();
}
}
protected void OnColumnsValueChanged() { }
#endregion
#region RowHeight DependencyProperty
public double RowHeight
{
get { return (double)GetValue(RowHeightProperty); }
set { SetValue(RowHeightProperty, value); }
}
public static readonly DependencyProperty RowHeightProperty =
DependencyProperty.Register("RowHeight", typeof(double), typeof(TreeGrid),
new PropertyMetadata(30.0, new PropertyChangedCallback(OnRowHeightPropertyChanged)));
private static void OnRowHeightPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is TreeGrid grid) grid.OnRowHeightValueChanged();
}
protected void OnRowHeightValueChanged() { }
#endregion
#region ShowCellBorder DependencyProperty
public bool ShowCellBorder
{
get { return (bool)GetValue(ShowCellBorderProperty); }
set { SetValue(ShowCellBorderProperty, value); }
}
public static readonly DependencyProperty ShowCellBorderProperty =
DependencyProperty.Register("ShowCellBorder", typeof(bool), typeof(TreeGrid),
new PropertyMetadata(false, new PropertyChangedCallback(OnShowCellBorderPropertyChanged)));
private static void OnShowCellBorderPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is TreeGrid grid) grid.OnShowCellBorderValueChanged();
}
protected void OnShowCellBorderValueChanged() { }
#endregion
#region IconStroke DependencyProperty
public Brush IconStroke
{
get { return (Brush)GetValue(IconStrokeProperty); }
set { SetValue(IconStrokeProperty, value); }
}
public static readonly DependencyProperty IconStrokeProperty =
DependencyProperty.Register("IconStroke", typeof(Brush), typeof(TreeGrid),
new PropertyMetadata(new SolidColorBrush(Colors.LightGray), new PropertyChangedCallback(OnIconStrokePropertyChanged)));
private static void OnIconStrokePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is TreeGrid grid) grid.OnIconStrokeValueChanged();
}
protected void OnIconStrokeValueChanged() { }
#endregion
#region CellBorderBrush DependencyProperty
public Brush CellBorderBrush
{
get { return (Brush)GetValue(CellBorderBrushProperty); }
set { SetValue(CellBorderBrushProperty, value); }
}
public static readonly DependencyProperty CellBorderBrushProperty =
DependencyProperty.Register("CellBorderBrush", typeof(Brush), typeof(TreeGrid),
new PropertyMetadata(new SolidColorBrush(Colors.LightGray), new PropertyChangedCallback(OnCellBorderBrushPropertyChanged)));
private static void OnCellBorderBrushPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is TreeGrid grid) grid.OnCellBorderBrushValueChanged();
}
protected void OnCellBorderBrushValueChanged() { }
#endregion
protected override DependencyObject GetContainerForItemOverride()
{
return new TreeGridItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TreeGridItem;
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
}
}
public class TreeGridItem : TreeViewItem
{
public event EventHandler IconStateChanged;
static TreeGridItem()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TreeGridItem),
new FrameworkPropertyMetadata(typeof(TreeGridItem)));
}
public TreeGridItem()
{
this.DataContextChanged += TreeGridItem_DataContextChanged;
}
private void TreeGridItem_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext != null && DataContext is TreeItemData treeData)
{
this.SetBinding(IsExpandedProperty, new Binding("IsExpanded") { Source = treeData, Mode = BindingMode.TwoWay });
}
}
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
base.OnVisualParentChanged(oldParent);
}
protected override DependencyObject GetContainerForItemOverride()
{
return new TreeGridItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TreeGridItem;
}
}
/*
* GridViewRowPresenter 里的每个元素默认有 Margin,设置边框时会有间隙,这里在加载时移除
*/
public class TreeGridCell : ContentControl
{
static TreeGridCell()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TreeGridCell),
new FrameworkPropertyMetadata(typeof(TreeGridCell)));
}
public TreeGridCell()
{
Loaded += TreeGridCell_Loaded;
}
private void TreeGridCell_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= TreeGridCell_Loaded;
var p = VisualTreeHelper.GetParent(this);
if (p != null && p is FrameworkElement f && f.Margin.Left > 0)
{
f.Margin = new Thickness(0);
}
}
}
public static class TreeHelper
{
public static T FindParent<T>(this DependencyObject obj)
{
var p = VisualTreeHelper.GetParent(obj);
if (p == null) return default(T);
if (p is T tt) return tt;
return FindParent<T>(p);
}
public static T FindTreeResource<T>(this FrameworkElement obj, string key)
{
if (obj == null) return default(T);
var r = obj.TryFindResource(key);
if (r == null) r = Application.Current.TryFindResource(key);
if (r != null && r is T t) return t;
var p = FindParent<FrameworkElement>(obj);
if (p != null) return FindTreeResource<T>(p, key);
return default(T);
}
}
}
3. 定义 XAML 样式
为了让控件看起来像表格,我们需要重写模板。主要涉及 TreeGridItem 和 TreeGridCell 的样式。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TreeView.TreeDataGrid.Controls">
<SolidColorBrush x:Key="TreeIconStroke" Color="GreenYellow" />
<!-- 树项样式 -->
<Style x:Key="TreeGridItemStyle" TargetType="{x:Type local:TreeGridItem}">
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="IsExpanded" Value="True"/>
<Setter Property="BorderBrush" Value="Wheat"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TreeGridItem}">
<StackPanel>
<Border Name="Bd"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
Padding="{TemplateBinding Padding}">
<!-- 关键:使用 GridViewRowPresenter 渲染列头和内容 -->
<GridViewRowPresenter x:Name="PART_Header"
Content="{TemplateBinding Header}"
Columns="{Binding Path=Columns, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TreeGrid}}" />
</Border>
<ItemsPresenter x:Name="ItemsHost" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="false"/>
<Condition Property="Width" Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_Header" Property="MinWidth" Value="75"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="false"/>
<Condition Property="Height" Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_Header" Property="MinHeight" Value="19"/>
</MultiTrigger>
<MultiTrigger>
<!-- 鼠标悬停变色 -->
<MultiTrigger.Conditions>
<Condition Property="IsFocused" Value="False"/>
<Condition SourceName="Bd" Property="IsMouseOver" Value="true"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="Red" TargetName="Bd"/>
</MultiTrigger>
<Trigger Property="IsSelected" Value="true">
<!-- 选中背景颜色 -->
<Setter TargetName="Bd" Property="Background" Value="YellowGreen"/>
<Setter Property="Foreground" Value="Red"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<!-- 隔行换色 -->
<Trigger Property="AlternationIndex" Value="0">
<Setter Property="Background" Value="#e7e7e7" />
</Trigger>
<Trigger Property="AlternationIndex" Value="1">
<Setter Property="Background" Value="#f2f2f2" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type local:TreeGridItem}" BasedOn="{StaticResource TreeGridItemStyle}"/>
<!-- 单元格样式 -->
<Style TargetType="{x:Type local:TreeGridCell}">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TreeGridCell}">
<Border x:Name="CellBorder"
Margin="0,0,-0.5,0"
Background="{TemplateBinding Background}"
BorderBrush="Red"
BorderThickness="0,0,0,1">
<ContentControl Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="True"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 主控件样式 -->
<Style TargetType="{x:Type local:TreeGrid}">
<Setter Property="IconStroke" Value="{StaticResource TreeIconStroke}"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource {x:Type local:TreeGridItem}}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TreeGrid}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="0">
<DockPanel>
<!-- 标题栏 -->
<GridViewHeaderRowPresenter IsHitTestVisible="False"
Columns="{TemplateBinding Columns}"
Height="{TemplateBinding RowHeight}"
DockPanel.Dock="Top" />
<ItemsPresenter />
</DockPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
4. 使用示例
将上述代码整合到项目中后,只需在 XAML 中声明命名空间并设置 ColumnMappings 即可快速生成树形表格。
<local:TreeGrid ItemsSource="{Binding TreeNodes}"
ColumnMappings="名称:Name;时间:Time;状态:Status"
RowHeight="30" />
这样我们就实现了无需依赖第三方付费控件的原生 TreeView+DataGrid 组合方案。开发者可以根据实际需求修改样式和资源字典,灵活适配不同的 UI 风格。


