效果:
实现主要逻辑:通过动态拼接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 }