背景
對於.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) 內容"成績"
效果圖如下所示
總的來說自我感覺有點小題大做,但想不出有什么更好的辦法,各位如果覺得以上說的有什么不好的,歡迎拍磚;如果發現以上有什么說錯了,懇請批評指正;如果覺得好的,請支持一下。謝謝!最后附上整個控件的源碼

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 }