简介本资源是一个面向WPF开发者的技术实践项目聚焦于使用Telerik UI for WPF控件库实现DataGrid多行多级表头的完整示例适用于中初级WPF开发人员学习复杂表格布局与数据展示方案尤其适合报表类、数据分析类桌面应用的界面开发参考。压缩包共53个文件包含13个C#源码文件如MainWindow.xaml.cs、Vehicle.cs等、2个XAML界面定义文件、8个DLL依赖库Telerik核心组件、1个SLN解决方案及CSProj工程文件辅以配置、缓存和调试相关文件整体大小为3.34MB结构清晰可直接编译运行。已有292人学习下载体现了该场景在实际开发中的高频需求。读者可获得一套可运行的Telerik RadGridView多级表头实现方案涵盖XAML列组嵌套配置、数据绑定逻辑、HeaderTemplate定制方法并附带完整VS工程目录与调试支持文件pdb、suo、config等便于快速复现、调试与二次开发。1. WPF 多行表头不是“加个RowDefinition”就能搞定Telerik RadGridView 实现复合表头的硬核落地路径你试过在 WPF 里让 DataGrid 显示「部门2023年Q1销售额」这种三层嵌套表头吗原生 DataGrid 的Column.HeaderTemplate塞进 Grid 布局后列宽自动计算崩了、水平滚动条错位、导出 Excel 时表头全压成一行——这不是玄学是 WPF 渲染层对 HeaderContentPresenter 的尺寸测量逻辑和 Telerik 内部虚拟化机制打架的真实现场。这份 Telerik RadGridView 多行表头 Demo 不是教你怎么拖控件而是用一个可运行的最小闭环含完整 XAML ViewModel 数据构造逻辑把「跨列合并」「纵向分组」「动态列生成」「导出保形」四个卡点全部打通。它适合正在做财务报表、生产看板、多维度统计分析界面的 WPF 开发者尤其当你被产品扔来一张带斜线表头的 PDF 需求图时——别急着翻 StackOverflow先跑通这个 Demo再改参数。2. 为什么非得用 Telerik RadGridView 而不是原生 DataGrid从渲染链路看多行表头的本质约束2.1 原生 DataGrid 的 Header 渲染瓶颈HeaderTemplate 不是万能胶WPF 原生 DataGrid 的DataGridColumn.Header属性类型是object表面看可以塞任意 UIElement。但实际渲染时它会被包装进DataGridHeaderBorder再由DataGridCellsPanel统一测量。问题出在测量阶段当 Header 是 Grid 时Grid.MeasureOverride()会尝试按子元素需求反推列宽而 DataGrid 列宽策略Width*/Auto又依赖 Header 宽度预估——形成死循环。结果就是表头显示正常但鼠标悬停列分隔线时拖不动或者ScrollViewer.HorizontalOffset手动设置后 Header 和内容区完全错位。这不是 Bug是 WPF 布局系统对“Header 参与列宽计算”这一隐式契约的刚性要求。提示你可以用 Snoop 工具抓取DataGridHeaderBorder的ActualWidth对比DataGridCell的ActualWidth90% 的错位问题都源于两者差值超过 2px。2.2 Telerik RadGridView 的破局点HeaderRow 与 ColumnGroup 的双层抽象Telerik RadGridView 把表头拆成两个独立层级HeaderRow负责整体表头容器支持HeaderRowHeight、HeaderRowBackground等全局控制ColumnGroup每个 Group 是一个逻辑单元可设置Header、Children子列、IsVisible、DisplayIndex。关键突破在于ColumnGroup 不参与列宽计算。它的宽度由其下挂载的GridViewColumn决定而 Group 本身只负责在 HeaderRow 中定位和绘制。这意味着你可以放心地在 Group.Header 里放 StackPanel、Grid 甚至自定义 UserControl只要不破坏GridViewColumn.Width的显式设定就不会触发测量死锁。!-- Telerik RadGridView 多行表头核心结构 -- telerik:RadGridView x:Namegrid AutoGenerateColumnsFalse !-- 第一层主表头行如“销售数据总览” -- telerik:RadGridView.HeaderRowStyle Style TargetTypetelerik:GridViewHeaderRow Setter PropertyHeight Value40/ /Style /telerik:RadGridView.HeaderRowStyle !-- 第二层ColumnGroup 定义复合表头 -- telerik:RadGridView.Columns !-- 分组1时间维度 -- telerik:GridViewColumnGroup Header时间维度 DisplayIndex0 telerik:GridViewColumnGroup.Children telerik:GridViewDataColumn DataMemberBinding{Binding Year} Header年份 Width80/ telerik:GridViewDataColumn DataMemberBinding{Binding Quarter} Header季度 Width80/ /telerik:GridViewColumnGroup.Children /telerik:GridViewColumnGroup !-- 分组2指标维度 -- telerik:GridViewColumnGroup Header业绩指标 DisplayIndex1 telerik:GridViewColumnGroup.Children telerik:GridViewDataColumn DataMemberBinding{Binding Sales} Header销售额(万元) Width120/ telerik:GridViewDataColumn DataMemberBinding{Binding Orders} Header订单数 Width100/ /telerik:GridViewColumnGroup.Children /telerik:GridViewColumnGroup /telerik:RadGridView.Columns /telerik:RadGridView这段 XAML 的执行逻辑是RadGridView 先创建两个GridViewColumnGroup实例每个 Group 的Header文本被渲染为独立的TextBlock放入 HeaderRowGroup 下的GridViewDataColumn则被注入到Columns集合中由GridViewColumnCollection统一管理列宽和排序。Group.Header 和 Column.Header 是解耦的——这正是多行表头能稳定存在的底层前提。2.3 选型验证对比原生 DataGrid 的三处不可绕过缺陷对比项原生 DataGridTelerik RadGridView是否影响多行表头列宽同步Header Grid 的列定义与 DataGrid.Columns 无绑定关系需手动同步Column.WidthGridViewColumnGroup的Children直接持有GridViewDataColumn宽度天然一致✅ RadGridView 胜出导出保形DataGrid.SelectAllCells()后Clipboard.Copy()导出为 CSV表头丢失所有分组信息RadGridView.ExportToExcelML()或ExportToPdf()自动识别ColumnGroup.Header并生成合并单元格✅ RadGridView 胜出虚拟化兼容Header 模板中嵌套复杂控件如 ComboBox会导致VirtualizingStackPanel测量异常滚动卡顿GridViewHeaderRow使用独立的UIElementCollection不参与ItemsPanel虚拟化链路✅ RadGridView 胜出结论很直接如果你的项目需要生产环境级的多行表头即必须支持导出、滚动、筛选、排序全功能Telerik RadGridView 不是“更方便”而是“唯一可行”。这份 Demo 就是基于这个确定性选择构建的最小可交付单元。3. 从零搭建可运行 DemoXAML 结构、ViewModel 数据构造与动态列生成实战3.1 XAML 核心布局用 GridViewColumnGroup 构建三层表头我们以「某制造企业月度产能报表」为场景实现三层表头第一行是大类标题如“2024年产能汇总”第二行是产线分组A线/B线/C线第三行是具体指标计划/实绩/达成率。XAML 关键结构如下telerik:RadGridView x:NamecapacityGrid ItemsSource{Binding CapacityData} AutoGenerateColumnsFalse ShowGroupPanelFalse CanUserFreezeColumnsFalse SelectionModeExtended RowIndicatorVisibilityCollapsed !-- 全局 HeaderRow 高度与样式 -- telerik:RadGridView.HeaderRowStyle Style TargetTypetelerik:GridViewHeaderRow Setter PropertyHeight Value60/ Setter PropertyBackground Value#F5F5F5/ /Style /telerik:RadGridView.HeaderRowStyle !-- 第一层主标题行跨所有列 -- telerik:RadGridView.Columns telerik:GridViewColumnGroup Header2024年产能汇总 DisplayIndex0 HorizontalAlignmentCenter VerticalAlignmentCenter !-- 第二层产线分组 -- telerik:GridViewColumnGroup.Children !-- A线分组 -- telerik:GridViewColumnGroup HeaderA线 DisplayIndex0 telerik:GridViewColumnGroup.Children telerik:GridViewDataColumn DataMemberBinding{Binding ALinePlan} Header计划(台) Width100 TextAlignmentCenter/ telerik:GridViewDataColumn DataMemberBinding{Binding ALineActual} Header实绩(台) Width100 TextAlignmentCenter/ telerik:GridViewDataColumn DataMemberBinding{Binding ALineRate} Header达成率(%) Width100 TextAlignmentCenter/ /telerik:GridViewColumnGroup.Children /telerik:GridViewColumnGroup !-- B线分组 -- telerik:GridViewColumnGroup HeaderB线 DisplayIndex1 telerik:GridViewColumnGroup.Children telerik:GridViewDataColumn DataMemberBinding{Binding BLinePlan} Header计划(台) Width100 TextAlignmentCenter/ telerik:GridViewDataColumn DataMemberBinding{Binding BLineActual} Header实绩(台) Width100 TextAlignmentCenter/ telerik:GridViewDataColumn DataMemberBinding{Binding BLineRate} Header达成率(%) Width100 TextAlignmentCenter/ /telerik:GridViewColumnGroup.Children /telerik:GridViewColumnGroup !-- C线分组 -- telerik:GridViewColumnGroup HeaderC线 DisplayIndex2 telerik:GridViewColumnGroup.Children telerik:GridViewDataColumn DataMemberBinding{Binding CLinePlan} Header计划(台) Width100 TextAlignmentCenter/ telerik:GridViewDataColumn DataMemberBinding{Binding CLineActual} Header实绩(台) Width100 TextAlignmentCenter/ telerik:GridViewDataColumn DataMemberBinding{Binding CLineRate} Header达成率(%) Width100 TextAlignmentCenter/ /telerik:GridViewColumnGroup.Children /telerik:GridViewColumnGroup /telerik:GridViewColumnGroup.Children /telerik:GridViewColumnGroup /telerik:RadGridView.Columns /telerik:RadGridView参数说明DisplayIndex决定 Group 在 HeaderRow 中的左右顺序必须从 0 开始连续编号否则会出现空白列HorizontalAlignment/VerticalAlignment仅对最外层 Group.Header 生效用于居中主标题TextAlignment设在GridViewDataColumn上控制单元格内文本对齐不影响表头Width必须为每个GridViewDataColumn显式指定这是避免测量冲突的铁律。3.2 ViewModel 数据构造用 ObservableCollection 支撑动态绑定多行表头的难点常不在 UI而在数据模型如何匹配嵌套结构。我们不采用“一行数据对应一个产线”的扁平设计而是用单行承载全部产线指标这样ItemsSource仍是标准集合无需改造 RadGridView 的数据绑定链路public class CapacityItem : INotifyPropertyChanged { private int _month; public int Month { get _month; set { _month value; OnPropertyChanged(); } } // A线数据 private decimal _aLinePlan; public decimal ALinePlan { get _aLinePlan; set { _aLinePlan value; OnPropertyChanged(); } } private decimal _aLineActual; public decimal ALineActual { get _aLineActual; set { _aLineActual value; OnPropertyChanged(); } } private decimal _aLineRate; public decimal ALineRate { get _aLineRate; set { _aLineRate value; OnPropertyChanged(); } } // B线数据同理 public decimal BLinePlan { get; set; } public decimal BLineActual { get; set; } public decimal BLineRate { get; set; } // C线数据同理 public decimal CLinePlan { get; set; } public decimal CLineActual { get; set; } public decimal CLineRate { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } // ViewModel 中构造数据 public class MainViewModel : INotifyPropertyChanged { private ObservableCollectionCapacityItem _capacityData; public ObservableCollectionCapacityItem CapacityData { get _capacityData; set { _capacityData value; OnPropertyChanged(); } } public MainViewModel() { CapacityData new ObservableCollectionCapacityItem { new CapacityItem { Month 1, ALinePlan 1200, ALineActual 1180, ALineRate 98.3m, BLinePlan 800, BLineActual 795, BLineRate 99.4m, CLinePlan 600, CLineActual 612, CLineRate 102.0m }, new CapacityItem { Month 2, ALinePlan 1250, ALineActual 1265, ALineRate 101.2m, BLinePlan 820, BLineActual 810, BLineRate 98.8m, CLinePlan 610, CLineActual 598, CLineRate 98.0m } }; } }关键设计点所有属性名ALinePlan,BLineActual必须与 XAML 中DataMemberBinding的路径严格一致大小写敏感ObservableCollection保证新增/删除行时 UI 自动刷新无需调用Refresh()INotifyPropertyChanged是必须的否则修改某个月份的实绩后UI 不更新。3.3 动态列生成用代码在运行时添加新产线分组当业务要求“随时新增 D 线”硬编码 XAML 就失效了。RadGridView 支持完全代码化构建 ColumnGroupprivate void AddNewLineGroup(string lineName, string planProp, string actualProp, string rateProp) { // 创建新产线分组 var newGroup new GridViewColumnGroup { Header lineName, DisplayIndex capacityGrid.Columns.Count // 插入到最后 }; // 添加三个指标列 newGroup.Children.Add(new GridViewDataColumn { DataMemberBinding new Binding(planProp) { Mode BindingMode.OneWay }, Header 计划(台), Width 100, TextAlignment TextAlignment.Center }); newGroup.Children.Add(new GridViewDataColumn { DataMemberBinding new Binding(actualProp) { Mode BindingMode.OneWay }, Header 实绩(台), Width 100, TextAlignment TextAlignment.Center }); newGroup.Children.Add(new GridViewDataColumn { DataMemberBinding new Binding(rateProp) { Mode BindingMode.OneWay }, Header 达成率(%), Width 100, TextAlignment TextAlignment.Center }); // 将分组插入到最外层 Group 的 Children 集合 var rootGroup capacityGrid.Columns[0] as GridViewColumnGroup; if (rootGroup ! null) { rootGroup.Children.Add(newGroup); } } // 调用示例添加 D 线 AddNewLineGroup(D线, DLinePlan, DLineActual, DLineRate);注意DisplayIndex设为capacityGrid.Columns.Count是为了确保新 Group 插入到最右侧若需插入到中间需遍历现有Children集合并调整DisplayIndex。4. 避坑指南多行表头开发中踩过的五个真实血泪坑4.1 现象表头文字被截断明明设置了Width120却只显示“销售额…”原因GridViewColumnGroup.Header默认使用TextBlock渲染其TextWrappingNoWrap且MaxWidth未设限导致长文本强制单行显示并截断。解决为 Group.Header 指定HeaderTemplate强制换行telerik:GridViewColumnGroup.HeaderTemplate DataTemplate TextBlock Text{Binding} TextWrappingWrap TextAlignmentCenter MaxWidth100/ /DataTemplate /telerik:GridViewColumnGroup.HeaderTemplate4.2 现象点击列标题排序时整个 Group 的 Header 全部高亮无法区分当前排序列原因RadGridView 默认对GridViewColumnGroup应用统一的SortIconTemplate未向下透传到子列。解决禁用 Group 的排序能力只对叶子GridViewDataColumn启用排序!-- 在 GridViewColumnGroup 上 -- telerik:GridViewColumnGroup CanUserSortFalse/ !-- 在每个 GridViewDataColumn 上 -- telerik:GridViewDataColumn CanUserSortTrue SortMemberPathALinePlan/4.3 现象导出 Excel 后A线/B线/C线三个分组 Header 变成三行普通文本未合并单元格原因ExportToExcelML()默认不识别GridViewColumnGroup需手动启用分组导出支持。解决在导出前设置ExportOptionsvar exportOptions new GridViewExportOptions { ExportDefaultStyles true, ExportVisualSettings true, ExportColumnGroups true // 关键必须设为 true }; capacityGrid.ExportToExcelML(产能报表.xls, exportOptions);4.4 现象启用CanUserFreezeColumnsTrue后冻结列右侧的 Group.Header 错位半行原因冻结列机制与GridViewColumnGroup的HeaderRow渲染坐标系存在像素级偏移。解决关闭冻结列或改用FrozenColumnCount属性精确控制推荐!-- 替代 CanUserFreezeColumns -- telerik:RadGridView FrozenColumnCount1/ !-- 此时第一列如“月份”被冻结其余 Group.Header 自动对齐 --4.5 现象切换主题如从 Windows8 切到 Fluent后Group.Header 背景变成纯白失去分组视觉区隔原因不同主题对GridViewColumnGroup的默认HeaderBackground覆盖规则不同Fluent 主题未定义该属性。解决在 XAML 中为每个 Group 显式设置HeaderBackgroundtelerik:GridViewColumnGroup HeaderA线 HeaderBackground#E8F4FC !-- 子列 -- /telerik:GridViewColumnGroup5. 进阶技巧用 Style 触发器实现表头动态着色与指标状态可视化5.1 用 DataTrigger 给达成率表头加状态色标单纯文字表头缺乏业务语义。我们可以让“达成率”列的 Header 根据数值范围自动变色——比如 ≥100% 为绿色90%~99.9% 为黄色90% 为红色。这需要将 Header 从字符串升级为绑定对象!-- 修改 XAMLHeader 绑定到 ViewModel 中的 RateHeaderInfo -- telerik:GridViewDataColumn DataMemberBinding{Binding ALineRate} Header{Binding RateHeaderInfo} Width100 TextAlignmentCenter telerik:GridViewDataColumn.HeaderTemplate DataTemplate StackPanel OrientationHorizontal HorizontalAlignmentCenter TextBlock Text{Binding Text} Margin0,0,4,0/ Rectangle Width8 Height8 RadiusX2 RadiusY2 Rectangle.Fill SolidColorBrush Color{Binding Color}/ /Rectangle.Fill /Rectangle /StackPanel /DataTemplate /telerik:GridViewDataColumn.HeaderTemplate /telerik:GridViewDataColumn对应的 ViewModel 中RateHeaderInfo是一个HeaderInfo类public class HeaderInfo : INotifyPropertyChanged { public string Text { get; set; } 达成率(%); private Color _color Colors.Green; public Color Color { get _color; set { _color value; OnPropertyChanged(); } } // 根据业务规则动态更新颜色 public void UpdateColor(decimal rate) { Color rate 100m ? Colors.Green : rate 90m ? Colors.Orange : Colors.Red; } }然后在数据加载后批量更新foreach (var item in CapacityData) { // 更新 A线达成率 Header 颜色 ((HeaderInfo)item.ALineRateHeader).UpdateColor(item.ALineRate); // 同理更新 B线、C线... }5.2 用 MultiBinding 实现跨列计算表头如“总产能 A线 B线 C线”有时产品会要求在表头显示汇总值比如在“业绩指标”Group.Header 旁加一个(总计: 2600台)。这不能靠后台计算必须用MultiBinding实时绑定telerik:GridViewColumnGroup.HeaderTemplate DataTemplate StackPanel OrientationHorizontal HorizontalAlignmentCenter TextBlock Text{Binding Header} Margin0,0,6,0/ TextBlock TextBlock.Text MultiBinding StringFormat{}(总计: {0}台) Binding PathDataContext.TotalCapacity RelativeSource{RelativeSource AncestorTypetelerik:RadGridView}/ /MultiBinding /TextBlock.Text /TextBlock /StackPanel /DataTemplate /telerik:GridViewColumnGroup.HeaderTemplate对应 ViewModel 中添加TotalCapacity属性private decimal _totalCapacity; public decimal TotalCapacity { get _totalCapacity; private set { _totalCapacity value; OnPropertyChanged(); } } // 在 CapacityData 集合变更后重新计算 private void RecalculateTotal() { TotalCapacity CapacityData.Sum(x x.ALineActual x.BLineActual x.CLineActual); }5.3 表头导出保形终极方案自定义 ExcelML 导出模板ExportToExcelML()虽支持ExportColumnGroups但对复杂样式如跨行合并、背景渐变支持有限。真正可靠的方案是接管导出过程用Telerik.Windows.Controls.GridView.Export命名空间下的GridViewExportHelperprivate void CustomExportToExcel() { var workbook new Workbook(); var worksheet workbook.Worksheets.Add(); // 手动写入表头第一行主标题合并所有列 var mainHeaderCell worksheet.Cells[0, 0]; mainHeaderCell.SetValue(2024年产能汇总); mainHeaderCell.SetFont(new Font(微软雅黑, 14, FontStyle.Bold)); mainHeaderCell.SetBackgroundColor(Colors.LightBlue); worksheet.Cells[0, 0].Merge(worksheet.Cells[0, 8]); // 合并 0 行 0~8 列 // 第二行产线分组A线/B线/C线每组占3列 worksheet.Cells[1, 0].SetValue(A线).SetBackgroundColor(Colors.LightGreen); worksheet.Cells[1, 3].SetValue(B线).SetBackgroundColor(Colors.LightYellow); worksheet.Cells[1, 6].SetValue(C线).SetBackgroundColor(Colors.LightPink); worksheet.Cells[1, 0].Merge(worksheet.Cells[1, 2]); worksheet.Cells[1, 3].Merge(worksheet.Cells[1, 5]); worksheet.Cells[1, 6].Merge(worksheet.Cells[1, 8]); // 第三行指标计划/实绩/达成率 string[] metrics { 计划(台), 实绩(台), 达成率(%) }; for (int i 0; i 3; i) { worksheet.Cells[2, i].SetValue(metrics[i]); worksheet.Cells[2, i 3].SetValue(metrics[i]); worksheet.Cells[2, i 6].SetValue(metrics[i]); } // 写入数据行略 // ... workbook.Save(CustomCapacityReport.xlsx); }这个方案彻底脱离 RadGridView 的导出引擎用WorkbookAPI 精确控制每一格是应对审计级报表输出的后悔药。从那以后我每次接到带斜线/多层表头的需求第一件事就是打开这个 Demo 工程把GridViewColumnGroup结构复制过去再根据字段名替换DataMemberBinding路径最后补上ExportOptions.ExportColumnGroups true—— 三步之内必见效果。那些花三天调DataGrid.ColumnHeaderStyle却始终错位的夜晚真的没必要重来一遍。希望帮到你。本文还有配套的精品资源点击获取