多维表头的DataGridView
背景
对于.NET 原本提供的DataGridView控件,制作成如下形式的表格是毫无压力的。
但是如果把表格改了一下,变成如下形式
传统的DataGridView就做不到了,如果扩展一下还是行的,有不少网友也扩展了DataGridView控件,不过有些也只能制作出二维的表头。或者使用第三方的控件,之前也用过DevExpress的BoundGridView。不过在没有可使用的第三方控件的情况下,做到下面的效果,就有点麻烦了。
那得自己扩展了,不过最后还是用了一个控件库的报表控件,Telerik的Reporting。不过我自己还是扩展了DataGridView,使之能制作出上面的报表。
准备
学习了一些网友的代码,原来制作这个多维表头都是利用GDI+对DataGirdView的表头进行重绘。
用到的方法包括
Graphics.FillRectangle //填充一个矩形
Graphics.DrawLine //画一条线
Graphics.DrawString //写字符串
此外为了方便组织表头,本人还定义了一个表头的数据结构 HeaderItem 和 HeaderCollection 分别作为每个表头单元格的数据实体和整个表头的集合。
HeaderItem的定义如下
1 public class HeaderItem 2 { 3 private int _startX;//起始横坐标 4 private int _startY;//起始纵坐标 5 private int _endX; //终止横坐标 6 private int _endY; //终止纵坐标 7 private bool _baseHeader; //是否基础表头 8 9 public HeaderItem(int startX, int endX, int startY, int endY, string content) 10 { 11 this._endX = endX; 12 this._endY = endY; 13 this._startX = startX; 14 this._startY = startY; 15 this.Content = content; 16 } 17 18 public HeaderItem(int x, int y, string content):this(x,x,y,y,content) 19 { 20 21 } 22 23 public HeaderItem() 24 { 25 26 } 27 28 public static HeaderItem CreateBaseHeader(int x,int y,string content) 29 { 30 HeaderItem header = new HeaderItem(); 31 header._endX= header._startX = x; 32 header._endY= header._startY = y; 33 header._baseHeader = true; 34 header.Content = content; 35 return header; 36 } 37 38 public int StartX 39 { 40 get { return _startX; } 41 set 42 { 43 if (value > _endX) 44 { 45 _startX = _endX; 46 return; 47 } 48 if (value < 0) _startX = 0; 49 else _startX = value; 50 } 51 } 52 53 public int StartY 54 { 55 get { return _startY; } 56 set 57 { 58 if (_baseHeader) 59 { 60 _startY = 0; 61 return; 62 } 63 if (value > _endY) 64 { 65 _startY = _endY; 66 return; 67 } 68 if (value < 0) _startY = 0; 69 else _startY = value; 70 } 71 } 72 73 public int EndX 74 { 75 get { return _endX; } 76 set 77 { 78 if (_baseHeader) 79 { 80 _endX = _startX; 81 return; 82 } 83 if (value < _startX) 84 { 85 _endX = _startX; 86 return; 87 } 88 _endX = value; 89 } 90 } 91 92 public int EndY 93 { 94 get { return _endY; } 95 set 96 { 97 if (value < _startY) 98 { 99 _endY = _startY; 100 return; 101 } 102 _endY = value; 103 } 104 } 105 106 public bool IsBaseHeader 107 {get{ return _baseHeader;} } 108 109 public string Content { get; set; } 110 }
设计思想是利用数学的直角坐标系,给每个表头单元格定位并划定其大小。与计算机显示的坐标定位不同,这里的原点是跟数学的一样放在左下角,X轴正方向是水平向右,Y轴正方向是垂直向上。如下图所示
之所以要对GridView中原始的列头进行特别处理,是因为这里的起止坐标和终止坐标都可以设置,而原始列头的起始纵坐标(StartY)只能是0,终止横坐标(EndX)必须与起始横坐标(StartY)相等。
另外所有列头单元格的集合HeaderCollection的定义如下
1 public class HeaderCollection 2 { 3 private List<HeaderItem> _headerList; 4 private bool _iniLock; 5 6 public DataGridViewColumnCollection BindCollection{get;set;} 7 8 public HeaderCollection(DataGridViewColumnCollection cols) 9 { 10 _headerList = new List<HeaderItem>(); 11 BindCollection=cols; 12 _iniLock = false; 13 } 14 15 public int GetHeaderLevels() 16 { 17 int max = 0; 18 foreach (HeaderItem item in _headerList) 19 if (item.EndY > max) 20 max = item.EndY; 21 22 return max; 23 } 24 25 public List<HeaderItem> GetBaseHeaders() 26 { 27 List<HeaderItem> list = new List<HeaderItem>(); 28 foreach (HeaderItem item in _headerList) 29 if (item.IsBaseHeader) list.Add(item); 30 return list; 31 } 32 33 public HeaderItem GetHeaderByLocation(int x, int y) //先进行X坐标遍历,再进行Y坐标遍历。查找出包含输入坐标的表头单元格实例 34 { 35 if (!_iniLock) InitHeader(); 36 HeaderItem result=null; 37 List<HeaderItem> temp = new List<HeaderItem>(); 38 foreach (HeaderItem item in _headerList) 39 if (item.StartX <= x && item.EndX >= x) 40 temp.Add(item); 41 foreach (HeaderItem item in temp) 42 if (item.StartY <= y && item.EndY >= y) 43 result = item; 44 45 return result; 46 } 47 48 public IEnumerator GetHeaderEnumer() 49 { 50 return _headerList.GetEnumerator(); 51 } 52 53 public void AddHeader(HeaderItem header) 54 { 55 this._headerList.Add(header); 56 } 57 58 public void AddHeader(int startX, int endX, int startY, int endY, string content) 59 { 60 this._headerList.Add(new HeaderItem(startX,endX,startY,endY,content)); 61 } 62 63 public void AddHeader(int x, int y, string content) 64 { 65 this._headerList.Add(new HeaderItem(x, y, content)); 66 } 67 68 public void RemoveHeader(HeaderItem header) 69 { 70 this._headerList.Remove(header); 71 } 72 73 public void RemoveHeader(int x, int y) 74 { 75 HeaderItem header= GetHeaderByLocation(x, y); 76 if (header != null) RemoveHeader(header); 77 } 78 79 private void InitHeader() 80 { 81 _iniLock = true; 82 for (int i = 0; i < this.BindCollection.Count; i++) 83 if(this.GetHeaderByLocation(i,0)==null) 84 this._headerList.Add(HeaderItem.CreateBaseHeader(i,0 , this.BindCollection[i].HeaderText)); 85 _iniLock = false; 86 } 87 }
这里仿照了.NET Frameword的Collection那样定义了Add方法和Remove方法,此外说明一下那个 GetHeaderByLocation 方法,这个方法可以通过给定的坐标获取那个坐标的HeaderItem。这个坐标是忽略了整个表头合并单元格的情况,例如
上面这幅图,如果输入0,0 返回的是灰色区域,输入2,1 或3,2 或 5,1返回的都是橙色的区域。
扩展控件
到真正扩展控件了,最核心的是重写 OnCellPainting 方法,这个其实是与表格单元格重绘时触发事件绑定的方法,通过参数 DataGridViewCellPaintingEventArgs 的 ColumnIndex 和 RowIndex 属性可以知道当前重绘的是哪个单元格,于是就通过HeaderCollection获取要绘制的表头单元格的信息进行重绘,对已经重绘的单元格会进行标记,以防重复绘制。
1 protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e) 2 { 3 if (e.ColumnIndex == -1 || e.RowIndex != -1) 4 { 5 base.OnCellPainting(e); 6 return; 7 } 8 int lev=this.Headers.GetHeaderLevels(); 9 this.ColumnHeadersHeight = (lev + 1) * _baseColumnHeadHeight; 10 for (int i = 0; i <= lev; i++) //到达某一列后,遍历各行,查找出还没绘制的表头进行绘制 11 { 12 HeaderItem tempHeader= this.Headers.GetHeaderByLocation(e.ColumnIndex, i); 13 if (tempHeader==null|| i != tempHeader.EndY || e.ColumnIndex != tempHeader.StartX) continue; 14 DrawHeader(tempHeader, e); 15 } 16 e.Handled = true; 17 }
上面的代码中,最初是先判断当前要重绘的单元格是不是表头部分,如果不是则调用原本的OnCellPainting方法。 e.Handled=true; 比较关键,有了这句代码,重绘才能生效。
绘制单元格的过程封装在方法DrawHeader里面
1 private void DrawHeader(HeaderItem item,DataGridViewCellPaintingEventArgs e) 2 { 3 if (this.ColumnHeadersHeightSizeMode != DataGridViewColumnHeadersHeightSizeMode.DisableResizing) 4 this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; 5 int lev=this.Headers.GetHeaderLevels(); //获取整个表头的总行数 6 lev=(lev-item.EndY)*_baseColumnHeadHeight; //重新设置表头的行高 7 8 SolidBrush backgroundBrush = new SolidBrush(e.CellStyle.BackColor); 9 SolidBrush lineBrush = new SolidBrush(this.GridColor); 10 Pen linePen = new Pen(lineBrush); 11 StringFormat foramt = new StringFormat(); 12 foramt.Alignment = StringAlignment.Center; 13 foramt.LineAlignment = StringAlignment.Center; 14 15 Rectangle headRec = new Rectangle(e.CellBounds.Left, lev, ComputeWidth(item.StartX, item.EndX)-1, ComputeHeight(item.StartY, item.EndY)-1); 16 e.Graphics.FillRectangle(backgroundBrush, headRec); //填充矩形 17 e.Graphics.DrawLine(linePen, headRec.Left, headRec.Bottom, headRec.Right, headRec.Bottom); //画单元格的底线 18 e.Graphics.DrawLine(linePen, headRec.Right, headRec.Top, headRec.Right, headRec.Bottom); //画单元格的右边线 19 e.Graphics.DrawString(item.Content, this.ColumnHeadersDefaultCellStyle.Font, Brushes.Black,headRec, foramt); //填写表头标题 20 }
填充矩形时,记得要给矩形的常和宽减去一个像素,这样才不会与相邻的矩形重叠区域导致矩形的边线显示不出来。还有这里的要设置 ColumnHeadersHeightSizeMode 属性,如果不把它设成 DisableResizing ,那么表头的高度是改变不了的,这样即使设置了二维,三维,n维,最终只是一维。
这里用到的一些辅助方法如下,分别是通过坐标计算出高度和宽度。
1 private int ComputeWidth(int startX, int endX) 2 { 3 int width = 0; 4 for (int i = startX; i <= endX; i++) 5 width+= this.Columns[i].Width; 6 return width; 7 } 8 9 private int ComputeHeight(int startY, int endY) 10 { 11 return _baseColumnHeadHeight * (endY - startY+1); 12 }
给一段使用的实例代码,这里要预先给DataGridView每一列设好绑定的字段,否则自动添加的列是做不出效果来的。
1 HeaderItem item= this.boundGridView1.Headers.GetHeaderByLocation(0, 0); //获取包括坐标(0,0)的单元格 2 item.EndY = 2; 3 item = this.boundGridView1.Headers.GetHeaderByLocation(9,0 ); 4 item.EndY = 2; 5 item = this.boundGridView1.Headers.GetHeaderByLocation(10, 0); 6 item.EndY = 2; 7 item = this.boundGridView1.Headers.GetHeaderByLocation(11, 0); 8 item.EndY = 2; 9 10 this.boundGridView1.Headers.AddHeader(1, 2, 1, 1, "语文"); //增加表头,起始坐标(1,1) ,终止坐标(2,1) 内容"语文" 11 this.boundGridView1.Headers.AddHeader(3, 4, 1, 1, "数学"); //增加表头,起始坐标(3,1) ,终止坐标(4,1) 内容"数学"
12 this.boundGridView1.Headers.AddHeader(5, 6, 1, 1, "英语"); //增加表头,起始坐标(5,1) ,终止坐标(6,1) 内容"英语"
13 this.boundGridView1.Headers.AddHeader(7, 8, 1, 1, "X科"); //增加表头,起始坐标(7,1) ,终止坐标(8,1) 内容"X科"
14 this.boundGridView1.Headers.AddHeader(1, 8, 2, 2, "成绩"); //增加表头,起始坐标(1,2) ,终止坐标(8,2) 内容"成绩"
效果图如下所示
总的来说自我感觉有点小题大做,但想不出有什么更好的办法,各位如果觉得以上说的有什么不好的,欢迎拍砖;如果发现以上有什么说错了,恳请批评指正;如果觉得好的,请支持一下。谢谢!最后附上整个控件的源码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class BoundGridView : DataGridView 2 { 3 private int _baseColumnHeadHeight; 4 5 public BoundGridView():base() 6 { 7 this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; 8 _baseColumnHeadHeight = this.ColumnHeadersHeight; 9 this.Headers = new HeaderCollection(this.Columns); 10 } 11 12 public HeaderCollection Headers{ get;private set; } 13 14 protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e) 15 { 16 if (e.ColumnIndex == -1 || e.RowIndex != -1) 17 { 18 base.OnCellPainting(e); 19 return; 20 } 21 int lev=this.Headers.GetHeaderLevels(); 22 this.ColumnHeadersHeight = (lev + 1) * _baseColumnHeadHeight; 23 for (int i = 0; i <= lev; i++) 24 { 25 HeaderItem tempHeader= this.Headers.GetHeaderByLocation(e.ColumnIndex, i); 26 if (tempHeader==null|| i != tempHeader.EndY || e.ColumnIndex != tempHeader.StartX) continue; 27 DrawHeader(tempHeader, e); 28 } 29 e.Handled = true; 30 } 31 32 private int ComputeWidth(int startX, int endX) 33 { 34 int width = 0; 35 for (int i = startX; i <= endX; i++) 36 width+= this.Columns[i].Width; 37 return width; 38 } 39 40 private int ComputeHeight(int startY, int endY) 41 { 42 return _baseColumnHeadHeight * (endY - startY+1); 43 } 44 45 private void DrawHeader(HeaderItem item,DataGridViewCellPaintingEventArgs e) 46 { 47 if (this.ColumnHeadersHeightSizeMode != DataGridViewColumnHeadersHeightSizeMode.DisableResizing) 48 this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; 49 int lev=this.Headers.GetHeaderLevels(); 50 lev=(lev-item.EndY)*_baseColumnHeadHeight; 51 52 SolidBrush backgroundBrush = new SolidBrush(e.CellStyle.BackColor); 53 SolidBrush lineBrush = new SolidBrush(this.GridColor); 54 Pen linePen = new Pen(lineBrush); 55 StringFormat foramt = new StringFormat(); 56 foramt.Alignment = StringAlignment.Center; 57 foramt.LineAlignment = StringAlignment.Center; 58 59 Rectangle headRec = new Rectangle(e.CellBounds.Left, lev, ComputeWidth(item.StartX, item.EndX)-1, ComputeHeight(item.StartY, item.EndY)-1); 60 e.Graphics.FillRectangle(backgroundBrush, headRec); 61 e.Graphics.DrawLine(linePen, headRec.Left, headRec.Bottom, headRec.Right, headRec.Bottom); 62 e.Graphics.DrawLine(linePen, headRec.Right, headRec.Top, headRec.Right, headRec.Bottom); 63 e.Graphics.DrawString(item.Content, this.ColumnHeadersDefaultCellStyle.Font, Brushes.Black,headRec, foramt); 64 } 65 } 66 67 public class HeaderItem 68 { 69 private int _startX; 70 private int _startY; 71 private int _endX; 72 private int _endY; 73 private bool _baseHeader; 74 75 public HeaderItem(int startX, int endX, int startY, int endY, string content) 76 { 77 this._endX = endX; 78 this._endY = endY; 79 this._startX = startX; 80 this._startY = startY; 81 this.Content = content; 82 } 83 84 public HeaderItem(int x, int y, string content):this(x,x,y,y,content) 85 { 86 87 } 88 89 public HeaderItem() 90 { 91 92 } 93 94 public static HeaderItem CreateBaseHeader(int x,int y,string content) 95 { 96 HeaderItem header = new HeaderItem(); 97 header._endX= header._startX = x; 98 header._endY= header._startY = y; 99 header._baseHeader = true; 100 header.Content = content; 101 return header; 102 } 103 104 public int StartX 105 { 106 get { return _startX; } 107 set 108 { 109 if (value > _endX) 110 { 111 _startX = _endX; 112 return; 113 } 114 if (value < 0) _startX = 0; 115 else _startX = value; 116 } 117 } 118 119 public int StartY 120 { 121 get { return _startY; } 122 set 123 { 124 if (_baseHeader) 125 { 126 _startY = 0; 127 return; 128 } 129 if (value > _endY) 130 { 131 _startY = _endY; 132 return; 133 } 134 if (value < 0) _startY = 0; 135 else _startY = value; 136 } 137 } 138 139 public int EndX 140 { 141 get { return _endX; } 142 set 143 { 144 if (_baseHeader) 145 { 146 _endX = _startX; 147 return; 148 } 149 if (value < _startX) 150 { 151 _endX = _startX; 152 return; 153 } 154 _endX = value; 155 } 156 } 157 158 public int EndY 159 { 160 get { return _endY; } 161 set 162 { 163 if (value < _startY) 164 { 165 _endY = _startY; 166 return; 167 } 168 _endY = value; 169 } 170 } 171 172 public bool IsBaseHeader 173 {get{ return _baseHeader;} } 174 175 public string Content { get; set; } 176 } 177 178 public class HeaderCollection 179 { 180 private List<HeaderItem> _headerList; 181 private bool _iniLock; 182 183 public DataGridViewColumnCollection BindCollection{get;set;} 184 185 public HeaderCollection(DataGridViewColumnCollection cols) 186 { 187 _headerList = new List<HeaderItem>(); 188 BindCollection=cols; 189 _iniLock = false; 190 } 191 192 public int GetHeaderLevels() 193 { 194 int max = 0; 195 foreach (HeaderItem item in _headerList) 196 if (item.EndY > max) 197 max = item.EndY; 198 199 return max; 200 } 201 202 public List<HeaderItem> GetBaseHeaders() 203 { 204 List<HeaderItem> list = new List<HeaderItem>(); 205 foreach (HeaderItem item in _headerList) 206 if (item.IsBaseHeader) list.Add(item); 207 return list; 208 } 209 210 public HeaderItem GetHeaderByLocation(int x, int y) 211 { 212 if (!_iniLock) InitHeader(); 213 HeaderItem result=null; 214 List<HeaderItem> temp = new List<HeaderItem>(); 215 foreach (HeaderItem item in _headerList) 216 if (item.StartX <= x && item.EndX >= x) 217 temp.Add(item); 218 foreach (HeaderItem item in temp) 219 if (item.StartY <= y && item.EndY >= y) 220 result = item; 221 222 return result; 223 } 224 225 public IEnumerator GetHeaderEnumer() 226 { 227 return _headerList.GetEnumerator(); 228 } 229 230 public void AddHeader(HeaderItem header) 231 { 232 this._headerList.Add(header); 233 } 234 235 public void AddHeader(int startX, int endX, int startY, int endY, string content) 236 { 237 this._headerList.Add(new HeaderItem(startX,endX,startY,endY,content)); 238 } 239 240 public void AddHeader(int x, int y, string content) 241 { 242 this._headerList.Add(new HeaderItem(x, y, content)); 243 } 244 245 public void RemoveHeader(HeaderItem header) 246 { 247 this._headerList.Remove(header); 248 } 249 250 public void RemoveHeader(int x, int y) 251 { 252 HeaderItem header= GetHeaderByLocation(x, y); 253 if (header != null) RemoveHeader(header); 254 } 255 256 private void InitHeader() 257 { 258 _iniLock = true; 259 for (int i = 0; i < this.BindCollection.Count; i++) 260 if(this.GetHeaderByLocation(i,0)==null) 261 this._headerList.Add(HeaderItem.CreateBaseHeader(i,0 , this.BindCollection[i].HeaderText)); 262 _iniLock = false; 263 } 264 }