跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C#

WPF 中使用 TreeView 封装组合控件 TreeView+DataGrid

综述由AI生成WPF 开发中,原生控件无法直接满足 TreeView 与 DataGrid 组合的需求,需通过自定义封装实现。基于 WPF 原生控件,通过继承 TreeView 并重写容器生成逻辑,结合 GridView 列定义,实现了支持动态列映射、隔行变色及选中高亮的树形表格组件。代码包含核心 C# 类定义与 XAML 样式模板,展示了如何移除默认 Margin 干扰并绑定数据上下文,为开发者提供无需付费第三方库的替代方案。

道系青年发布于 2025/2/5更新于 2026/6/616 浏览
WPF 中使用 TreeView 封装组合控件 TreeView+DataGrid

WPF 原生控件封装:TreeView 与 DataGrid 组合实现

WPF 的功能非常强大,许多控件都是原生的。但在实际开发中,有时候需要同时使用 TreeView 和 DataGrid 的组合效果(例如树形表格),这就需要我们去封装实现。

第三方控件虽然方便,但往往收费且功能受限。利用原生控件进行封装,既能满足需求又能保持轻量。本文演示如何使用 TreeView 来实现这一组合效果。

实现思路

实现上述效果主要有三种技术路径:

  1. TreeView:本文重点演示的方案,适合层级数据展示。
  2. DataGrid:适合纯表格数据。
  3. 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 风格。

目录

  1. WPF 原生控件封装:TreeView 与 DataGrid 组合实现
  2. 实现思路
  3. 1. 创建 WPF 项目
  4. 2. 封装 TreeGrid 核心类
  5. 3. 定义 XAML 样式
  6. 4. 使用示例
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • KIMI 与文心一言、通义千问大模型能力对比评测
  • AI 大模型微调与推理实战指南
  • 基于 Spring Cloud 的电商系统设计与实现:用户与商品模块
  • 使用 Bright Data Web Scraper API + Python 抓取 Glassdoor 数据实战
  • 当前主流大模型盘点及国内企业选型指南
  • LoRA 微调 LLaMA 类模型:原理与实战指南
  • 2024 大模型落地应用案例集:娱乐、视频与游戏行业精选
  • 网络安全工程师岗位需求分析:市场前景与技能方向
  • IDEA AI 编程插件实测对比:Copilot、灵码与 TRAE
  • Linux System V 共享内存:原理、实操与避坑指南
  • 企业电子招投标采购系统功能简介
  • Gossip 协议解读
  • C/C++ 错误信息捕获与处理实战指南
  • llama-cpp-python 安装配置与性能优化指南
  • AI 产品经理的 5 点核心认知与实践指南
  • 网络安全核心基础知识详解
  • 第五届长城杯 2025 Web 初赛 Writeup
  • 五大生成模型全方位对比
  • 混沌工程开源平台解析与测试实践指南
  • 大型语言模型微调入门指南

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online