效果:
实现主要逻辑:通过动态拼接XML生成表头样式,绑定到列上。
主要是动态拼接XML时要仔细核对对应的占位行,具体可以看代码,注释很详细
两个类一个接口
NTree<T>:定义表头树形结构
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Collections.ObjectModel; 5 6 namespace SLDGHeader 7 { 8 /// <summary> 9 /// 树结构 10 /// </summary> 11 /// <typeparam name="T">节点中的数据</typeparam> 12 public class NTree<T> 13 { 14 /// <summary> 15 /// 节点数据 16 /// </summary> 17 private readonly T data; 18 /// <summary> 19 /// 节点数据 20 /// </summary> 21 public T Data 22 { 23 get { return data; } 24 } 25 /// <summary> 26 /// 是否根节点 27 /// </summary> 28 public bool IsRoot { get { return Parent == null; } } 29 /// <summary> 30 /// 当前节点深度 31 /// 根节点为1 32 /// </summary> 33 public int Depth { get; private set; } 34 /// <summary> 35 /// 父节点 36 /// </summary> 37 public NTree<T> Parent 38 { 39 get; 40 private set; 41 } 42 /// <summary> 43 /// 子节点 44 /// </summary> 45 public ReadOnlyCollection<NTree<T>> Children 46 { 47 get { return children.AsReadOnly(); } 48 } 49 private List<NTree<T>> children; 50 /// <summary> 51 /// 实例化一个节点 52 /// </summary> 53 /// <param name="data">节点数据</param> 54 public NTree(T data) 55 { 56 this.Depth = 1; 57 this.data = data; 58 children = new List<NTree<T>>(); 59 } 60 /// <summary> 61 /// 在当前节点添加子节点 62 /// </summary> 63 /// <param name="data">节点数据</param> 64 /// <returns>当前节点</returns> 65 public NTree<T> AddChild(T data) 66 { 67 var node = new NTree<T>(data) { Parent = this, Depth = this.Depth + 1 }; 68 children.Add(node); 69 return this; 70 } 71 /// <summary> 72 /// 在当前节点子节点中插入子节点 73 /// </summary> 74 /// <param name="index">插入位置</param> 75 /// <param name="data">节点数据</param> 76 /// <returns>当前节点</returns> 77 public NTree<T> InsertChild(int index, T data) 78 { 79 var node = new NTree<T>(data) { Parent = this, Depth = this.Depth + 1 }; 80 children.Insert(index, node); 81 return this; 82 } 83 /// <summary> 84 /// 在当前节点添加子节点 85 /// </summary> 86 /// <param name="data">节点数据</param> 87 /// <returns>当前节点</returns> 88 public NTree<T> AddChilren(params T[] datas) 89 { 90 foreach (var data in datas) 91 { 92 AddChild(data); 93 } 94 return this; 95 } 96 /// <summary> 97 /// 移除当前节点下指定的子节点 98 /// </summary> 99 /// <param name="node">要移除的子节点</param> 100 /// <returns>当前节点</returns> 101 public NTree<T> RemoveChild(NTree<T> node) 102 { 103 children.Remove(node); 104 return this; 105 } 106 /// <summary> 107 /// 在当前节点添加兄弟节点 108 /// </summary> 109 /// <param name="data">节点数据</param> 110 /// <returns>当前节点</returns> 111 /// <exception cref="NullParentNodeException:当前节点没有父节点">当前节点没有父节点</exception> 112 public NTree<T> AddBrother(T data) 113 { 114 if (this.Parent == null) 115 { 116 throw new NullParentNodeException("有父节点的节点才能添加兄弟节点。"); 117 } 118 119 this.Parent.AddChild(data); 120 return this; 121 } 122 /// <summary> 123 /// 获取指定索引处的子节点 124 /// </summary> 125 /// <param name="i">子节点索引</param> 126 /// <returns>子节点</returns> 127 /// <exception cref="ArgumentOutOfRangeException:子节点索引超出范围">子节点索引超出范围</exception> 128 public NTree<T> GetChild(int i) 129 { 130 if (i >= children.Count || i < 0) 131 { 132 throw new ArgumentOutOfRangeException("子节点索引超出范围"); 133 } 134 return children[i]; 135 } 136 /// <summary> 137 /// 获取指定的子节点 138 /// </summary> 139 /// <param name="data">节点数据</param> 140 /// <returns>查找到的第一个子节点</returns> 141 public NTree<T> GetChild(T data) 142 { 143 return children.Where(i => i.data.Equals(data)).FirstOrDefault(); 144 } 145 /// <summary> 146 /// 获取指定子节点的索引 147 /// </summary> 148 /// <param name="data">节点数据</param> 149 /// <returns>查找到的第一个子节点的索引,没有找到返回-1</returns> 150 public int GetChildIndex(NTree<T> data) 151 { 152 var index = -1; 153 for (int i = 0; i < children.Count; i++) 154 { 155 if (children[i].Equals(data)) 156 { 157 index = i; 158 break; 159 } 160 } 161 return index; 162 } 163 /// <summary> 164 /// 遍历树节点 165 /// </summary> 166 /// <param name="node">起始节点</param> 167 /// <param name="action">遍历到每个节点的操作</param> 168 /// <exception cref="ArgumentNullException:参数action不可为空">参数action不可为空</exception> 169 public static void Traverse(NTree<T> node, Action<T> action) 170 { 171 if (action == null) 172 { 173 throw new ArgumentNullException("参数action不可为空"); 174 } 175 action(node.data); 176 foreach (var kid in node.children) 177 { 178 Traverse(kid, action); 179 } 180 } 181 /// <summary> 182 /// 从当前节点开始遍历树节点 183 /// </summary> 184 /// <param name="action">遍历到每个节点的操作</param> 185 /// <exception cref="ArgumentNullException:参数action不可为空">参数action不可为空</exception> 186 public void Traverse(Action<T> action) 187 { 188 Traverse(this, action); 189 } 190 /// <summary> 191 /// 遍历树节点 192 /// </summary> 193 /// <param name="node">起始节点</param> 194 /// <param name="action">遍历到每个节点的操作</param> 195 /// <exception cref="ArgumentNullException:参数action不可为空">参数action不可为空</exception> 196 public static void Traverse(NTree<T> node, Action<NTree<T>> action) 197 { 198 if (action == null) 199 { 200 throw new ArgumentNullException("参数action不可为空"); 201 } 202 action(node); 203 foreach (var kid in node.children) 204 { 205 Traverse(kid, action); 206 } 207 } 208 /// <summary> 209 /// 从当前节点开始遍历树节点 210 /// </summary> 211 /// <param name="action">遍历到每个节点的操作</param> 212 /// <exception cref="ArgumentNullException:参数action不可为空">参数action不可为空</exception> 213 public void Traverse(Action<NTree<T>> action) 214 { 215 if (action == null) 216 { 217 throw new ArgumentNullException("参数action不可为空"); 218 } 219 action(this); 220 foreach (var kid in this.children) 221 { 222 Traverse(kid, action); 223 } 224 } 225 /// <summary> 226 /// 获取当前节点开始的所有节点中数据 227 /// </summary> 228 /// <returns>节点数据列表</returns> 229 public IEnumerable<T> GetDatas() 230 { 231 return new[] { data }.Union(children.SelectMany(x => x.GetDatas())); 232 } 233 /// <summary> 234 /// 当前节点下叶节点的数量 235 /// </summary> 236 /// <returns></returns> 237 public int GetCount() 238 { 239 var count = 0; 240 Traverse((NTree<T> n) => 241 { 242 if (n.Children.Count == 0) 243 { 244 count++; 245 } 246 }); 247 return count; 248 } 249 /// <summary> 250 /// 获取当前节点所在树的深度 251 /// </summary> 252 /// <returns>当前节点所在树的深度</returns> 253 public int GetTreeDepth() 254 { 255 int Depth = 1; 256 var parent = this; 257 while (parent.Parent != null) 258 { 259 parent = parent.Parent; 260 } 261 Traverse((NTree<T> n) => 262 { 263 if (Depth < n.Depth) 264 { 265 Depth = n.Depth; 266 } 267 }); 268 269 return Depth; 270 } 271 } 272 /// <summary> 273 /// 父节点为空引用异常 274 /// </summary> 275 public class NullParentNodeException : Exception 276 { 277 public NullParentNodeException() 278 : base("父节点为空引用") 279 { 280 281 } 282 public NullParentNodeException(string message) 283 : base(message) 284 { 285 286 } 287 public NullParentNodeException(string message, Exception inner) 288 : base(message) 289 { 290 291 } 292 //public NullParentNodeException(SerializationInfo info, StreamingContext context) 293 //{ 294 295 //} 296 } 297 }
MultiHeadersColumn:多重表头和绑定的列
IDataGridHeader:定义生成表头和绑定列的数据接口
1 using System; 2 using System.Windows; 3 using System.Windows.Controls; 4 using System.Collections.Generic; 5 using System.Windows.Markup; 6 using System.Text; 7 8 namespace SLDGHeader 9 { 10 public class MultiHeadersColumn 11 { 12 #region 字段 13 /// <summary> 14 /// 单元格边框颜色 15 /// </summary> 16 string splitLineColor = "#ccc"; 17 /// <summary> 18 /// 数据行宽度 19 /// </summary> 20 string dataWidth = "30"; 21 /// <summary> 22 /// 表头行高度 23 /// </summary> 24 string dataHeight = "auto"; 25 /// <summary> 26 /// 分隔线线宽度 27 /// </summary> 28 string lineWidth = "1"; 29 /// <summary> 30 /// 分隔符线高度 31 /// </summary> 32 string lineHeight = "1"; 33 #endregion 34 #region 属性 35 /// <summary> 36 /// 单元格边框颜色 37 /// </summary> 38 public string SplitLineColor 39 { 40 get { return splitLineColor; } 41 set { splitLineColor = value; } 42 } 43 /// <summary> 44 /// 数据行宽度 45 /// </summary> 46 public string DataWidth 47 { 48 get { return dataWidth; } 49 set { dataWidth = value; } 50 } 51 /// <summary> 52 /// 表头行高度 53 /// </summary> 54 public string DataHeight 55 { 56 get { return dataHeight; } 57 set { dataHeight = value; } 58 } 59 /// <summary> 60 /// 分隔线线宽度 61 /// </summary> 62 public string LineWidth 63 { 64 get { return lineWidth; } 65 set { lineWidth = value; } 66 } 67 /// <summary> 68 /// 分隔符线高度 69 /// </summary> 70 public string LineHeight 71 { 72 get { return lineHeight; } 73 set { lineHeight = value; } 74 } 75 #endregion 76 public DataGridTemplateColumn GetMultiHeadersColumn(NTree<IDataGridHeader> node) 77 { 78 DataGridTemplateColumn col = GetTemplateColumn(node); 79 col.HeaderStyle = GetStyle("NameHeaderStyle", node); 80 return col; 81 } 82 DataGridTemplateColumn GetTemplateColumn(NTree<IDataGridHeader> node) 83 { 84 List<NTree<IDataGridHeader>> nodes = new List<NTree<IDataGridHeader>>(); 85 node.Traverse((NTree<IDataGridHeader> n) => 86 { 87 if (n.Children.Count == 0) 88 { 89 nodes.Add(n); 90 } 91 }); 92 string strtemplate = GetDataTemplate(nodes); 93 DataTemplate template = (DataTemplate)XamlReader.Load(strtemplate); 94 95 DataGridTemplateColumn colunm = new DataGridTemplateColumn(); 96 colunm.CellTemplate = template; 97 98 return colunm; 99 } 100 /// <summary> 101 /// 获取绑定列模板XML字符串 102 /// </summary> 103 /// <param name="nodes">列对应节点</param> 104 /// <returns>返回列模板XML字符串</returns> 105 string GetDataTemplate(List<NTree<IDataGridHeader>> nodes) 106 { 107 if (nodes == null) 108 { 109 throw new ArgumentNullException("参数nodes不能为空"); 110 } 111 StringBuilder sb = new StringBuilder(200); 112 113 sb.Append(@"<DataTemplate "); 114 sb.AppendLine("xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' "); 115 sb.AppendLine("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' "); 116 sb.AppendLine("xmlns:sdk='http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk'"); 117 sb.AppendLine(" >"); 118 119 sb.AppendLine("<StackPanel Orientation='Horizontal'> "); 120 for (int i = 0; i < nodes.Count * 2 - 1; i++) 121 { 122 if (i % 2 == 0) 123 { 124 sb.Append("<TextBlock VerticalAlignment='Center' "); 125 sb.Append(" Width='").Append(dataWidth).Append("' "); 126 sb.Append(" HorizontalAlignment='Center' TextWrapping='Wrap' Text='{Binding ").Append(nodes[(i + 1) / 2].Data.ColName).AppendLine("}' /> "); 127 } 128 else 129 { 130 sb.Append(" <Rectangle Fill='").Append(splitLineColor).Append("' VerticalAlignment='Stretch' "); 131 sb.Append(" Width='").Append(lineWidth).Append("' "); 132 sb.AppendLine(" /> "); 133 } 134 } 135 136 sb.AppendLine("</StackPanel> "); 137 sb.AppendLine("</DataTemplate> "); 138 return sb.ToString(); 139 } 140 141 Style GetStyle(string headerstyle, NTree<IDataGridHeader> node) 142 { 143 var depth = node.GetTreeDepth(); 144 string stylestr = GetStyleStr("NameHeaderStyle", depth, node); 145 Style ste = (Style)XamlReader.Load(stylestr); 146 147 return ste; 148 } 149 /// <summary> 150 /// 获取表头样式XML字符串 151 /// </summary> 152 /// <param name="headerstyle">样式名称</param> 153 /// <param name="depth">树的深度</param> 154 /// <param name="node">树的根节点</param> 155 /// <returns>返回表头样式XML字符串</returns> 156 string GetStyleStr(string headerstyle, int depth, NTree<IDataGridHeader> node) 157 { 158 159 StringBuilder sb = new StringBuilder(2000); 160 //计算数据列数量 161 int colCount = node.GetCount(); 162 163 AppendHeader(headerstyle, sb); 164 //构建表头行列 165 sb.AppendLine("<Grid HorizontalAlignment='{TemplateBinding HorizontalContentAlignment}' VerticalAlignment='{TemplateBinding VerticalContentAlignment}'>"); 166 //多少行 167 sb.AppendLine("<Grid.RowDefinitions>"); 168 //int rowCount = 3; 169 for (int i = 0; i < depth * 2; i++) 170 { 171 if (i % 2 == 0) 172 { 173 sb.AppendLine("<RowDefinition "); 174 sb.Append(" Height='").Append(dataHeight).Append("' "); 175 sb.AppendLine(" /> "); 176 } 177 else 178 { 179 sb.AppendLine("<RowDefinition "); 180 sb.Append(" Height='").Append(lineHeight).Append("' "); 181 sb.AppendLine("/>"); 182 } 183 } 184 sb.AppendLine("</Grid.RowDefinitions>"); 185 186 //多少列 187 sb.AppendLine("<Grid.ColumnDefinitions>"); 188 189 for (int i = 0; i < colCount * 2; i++) 190 { 191 if (i % 2 == 0) 192 { 193 sb.AppendLine("<ColumnDefinition "); 194 sb.Append("Width='").Append(dataWidth).Append("' "); 195 sb.AppendLine(" />"); 196 } 197 else 198 { 199 sb.AppendLine("<ColumnDefinition "); 200 sb.Append("Width='").Append(lineWidth).Append("' "); 201 sb.AppendLine(" />"); 202 } 203 204 } 205 sb.AppendLine("</Grid.ColumnDefinitions>"); 206 207 //开始构单元格 208 AppendCell(node, sb, depth, colCount); 209 210 211 AppendEnd(sb); 212 213 return sb.ToString(); 214 } 215 /// <summary> 216 /// 绘制头部和表头背景 217 /// </summary> 218 /// <param name="headerstyle">样式名称</param> 219 /// <param name="sb"></param> 220 private void AppendHeader(string headerstyle, StringBuilder sb) 221 { 222 sb.Append(@"<Style "); 223 sb.AppendLine("xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' "); 224 sb.AppendLine("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' "); 225 sb.AppendLine("xmlns:dataprimitives='clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data' "); 226 227 //列样式名称 228 sb.Append(@" x:Key='").Append(headerstyle).Append("' "); 229 230 sb.AppendLine(@"TargetType='dataprimitives:DataGridColumnHeader' >"); 231 sb.AppendLine("<Setter Property='Template'>"); 232 sb.AppendLine("<Setter.Value>"); 233 sb.AppendLine("<ControlTemplate>"); 234 sb.AppendLine("<Grid x:Name='Root'>"); 235 sb.AppendLine("<Grid.ColumnDefinitions>"); 236 sb.AppendLine("<ColumnDefinition/>"); 237 sb.AppendLine("<ColumnDefinition Width='Auto'/>"); 238 sb.AppendLine("</Grid.ColumnDefinitions>"); 239 sb.AppendLine("<Rectangle x:Name='BackgroundRectangle' Fill='#FF1F3B53' Stretch='Fill' Grid.ColumnSpan='2'/>"); 240 sb.AppendLine("<Rectangle x:Name='BackgroundGradient' Stretch='Fill' Grid.ColumnSpan='2'>"); 241 sb.AppendLine("<Rectangle.Fill>"); 242 //表头背景色 243 sb.AppendLine("<LinearGradientBrush EndPoint='.7,1' StartPoint='.7,0'>"); 244 sb.AppendLine(@"<GradientStop Color='#FCFFFFFF' Offset='0.015'/> 245 <GradientStop Color='#F7FFFFFF' Offset='0.375'/> 246 <GradientStop Color='#E5FFFFFF' Offset='0.6'/> 247 <GradientStop Color='#D1FFFFFF' Offset='1'/>"); 248 sb.AppendLine("</LinearGradientBrush>"); 249 sb.AppendLine("</Rectangle.Fill>"); 250 sb.AppendLine("</Rectangle>"); 251 } 252 /// <summary> 253 /// 添加结束XML部分 254 /// </summary> 255 /// <param name="sb"></param> 256 private void AppendEnd(StringBuilder sb) 257 { 258 sb.AppendLine("</Grid>"); 259 //绘制最后一列分割线 260 sb.AppendLine(@"<Rectangle x:Name='VerticalSeparator' Fill='") 261 .Append(splitLineColor) 262 .Append(@"' VerticalAlignment='Stretch'") 263 .Append(" Width='").Append(lineWidth).Append("' ") 264 .AppendLine(" Visibility='Visible' Grid.Row='1' Grid.Column='1' />"); 265 sb.AppendLine("</Grid>"); 266 sb.AppendLine("</ControlTemplate>"); 267 sb.AppendLine("</Setter.Value>"); 268 sb.AppendLine("</Setter>"); 269 sb.AppendLine("</Style>"); 270 } 271 /// <summary> 272 /// 构建行 273 /// 偶数行、偶数列为数据,奇数行、奇数列为分割线,以此计算单元格占位、合并行列 274 /// </summary> 275 /// <param name="node">构建行的树结构</param> 276 /// <param name="sb">字符串</param> 277 /// <param name="depth">树的深度</param> 278 private StringBuilder AppendCell(NTree<IDataGridHeader> node, StringBuilder sb, int depth, int totalCol) 279 { 280 //当前节点左兄弟节点的页节点数量,用于计算当前单元格列位置 281 var precolCount = 0; 282 if (!node.IsRoot) 283 { 284 precolCount = GetLeftCount(node); 285 } 286 287 //当前节点的页节点数量 288 var colCount = node.GetCount(); 289 290 //1、数据单元格 291 sb.Append(@"<ContentPresenter "); 292 sb.Append(@"Content='").Append(node.Data.DisplayColName).Append("' "); 293 sb.Append(@"VerticalAlignment='Center' HorizontalAlignment='Center' "); 294 sb.Append(" Grid.Row='").Append((node.Depth - 1) * 2).Append("' "); 295 //考虑表头行可能不一致(层级有深有浅),层级较少的需要合并列显示,所有合并在最后一行表头 296 var rowSpan = 1; 297 if (node.Children.Count == 0 && node.Depth != depth) 298 { 299 rowSpan = depth * 2 - 1 - (node.Depth - 1) * 2; 300 sb.Append(" Grid.RowSpan='").Append(rowSpan).Append("' "); 301 } 302 303 sb.Append(" Grid.Column='").Append(precolCount * 2).Append("' "); 304 sb.Append("Grid.ColumnSpan='").Append(colCount * 2 - 1).AppendLine("' />"); 305 306 //2、行分隔线单元格 307 sb.Append(@"<Rectangle Fill='").Append(splitLineColor).Append("' VerticalAlignment='Stretch' ") 308 .Append("Height='").Append(lineHeight).Append("' "); 309 310 //表头行合并后要调整分割线,否则会在合并单元格中间显示分割线 311 if (node.Children.Count == 0 && node.Depth != depth) 312 { 313 //如果有合并单元,则分割线要跳过合并单元格,合并数量为(rowSpan - 1) 314 sb.Append("Grid.Row='").Append(node.Depth * 2 - 1 + rowSpan - 1).Append("' "); 315 } 316 else 317 { 318 sb.Append("Grid.Row='").Append(node.Depth * 2 - 1).Append("' "); 319 } 320 321 sb.Append(" Grid.Column='").Append(precolCount * 2).Append("' "); 322 sb.Append("Grid.ColumnSpan='").Append(colCount * 2).AppendLine("' />"); 323 324 //4、列分隔线单元格 325 //最后一列分割线不绘制,否则在调整了后面列的列宽会出现类似空列的单元格 326 if ((precolCount + colCount) != totalCol) 327 { 328 sb.Append(@"<Rectangle Fill='").Append(splitLineColor).Append("' VerticalAlignment='Stretch' ") 329 .Append(" Width='").Append(lineWidth).Append("' "); 330 331 sb.Append("Grid.Row='").Append((node.Depth - 1) * 2).Append("' "); 332 sb.Append("Grid.RowSpan='").Append(rowSpan).Append("' "); 333 sb.Append("Grid.Column='").Append((precolCount + colCount) * 2 - 1).AppendLine("' />"); 334 } 335 336 337 //5、递归生成子节点单元格 338 if (node.Children.Count != 0) 339 { 340 foreach (var item1 in node.Children) 341 { 342 AppendCell(item1, sb, depth, totalCol); 343 } 344 } 345 346 347 return sb; 348 } 349 /// <summary> 350 /// 获取当前节点左边的叶节点数量 351 /// </summary> 352 /// <param name="node"></param> 353 /// <returns></returns> 354 private int GetLeftCount(NTree<IDataGridHeader> node) 355 { 356 var precolCount = 0; 357 var index = node.Parent.GetChildIndex(node); 358 for (int i = 0; i < index; i++) 359 { 360 precolCount += node.Parent.GetChild(i).GetCount(); 361 } 362 if (!node.Parent.IsRoot) 363 { 364 precolCount += GetLeftCount(node.Parent); 365 } 366 return precolCount; 367 } 368 } 369 public interface IDataGridHeader 370 { 371 /// <summary> 372 /// 绑定列名 373 /// </summary> 374 string ColName { get; set; } 375 /// <summary> 376 /// 显示名称 377 /// </summary> 378 string DisplayColName { get; set; } 379 } 380 }