背景
对于.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 IsBaseHeader107 { 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_headerList; 4 private bool _iniLock; 5 6 public DataGridViewColumnCollection BindCollection{ get;set;} 7 8 public HeaderCollection(DataGridViewColumnCollection cols) 9 {10 _headerList = new List ();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 GetBaseHeaders()26 {27 List list = new List ();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 temp = new List ();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) 内容"成绩"
效果图如下所示
总的来说自我感觉有点小题大做,但想不出有什么更好的办法,各位如果觉得以上说的有什么不好的,欢迎拍砖;如果发现以上有什么说错了,恳请批评指正;如果觉得好的,请支持一下。谢谢!最后附上整个控件的源码
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 StartX105 {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 StartY120 {121 get { return _startY; }122 set123 {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 EndX140 {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 EndY159 {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 IsBaseHeader173 { get{ return _baseHeader;} }174 175 public string Content { get; set; }176 }177 178 public class HeaderCollection179 {180 private List_headerList;181 private bool _iniLock;182 183 public DataGridViewColumnCollection BindCollection{ get;set;}184 185 public HeaderCollection(DataGridViewColumnCollection cols)186 {187 _headerList = new List ();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 GetBaseHeaders()203 {204 List list = new List ();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 temp = new List ();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 }