多維表頭的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) 內容"成績"

效果圖如下所示

總的來說自我感覺有點小題大做,但想不出有什么更好的辦法,各位如果覺得以上說的有什么不好的,歡迎拍磚;如果發現以上有什么說錯了,懇請批評指正;如果覺得好的,請支持一下。謝謝!最后附上整個控件的源碼

控件的完整代碼
  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     }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM