• 多维表头的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     }
  • 相关阅读:
    2018-09-13 代码翻译尝试-使用Roaster解析和生成Java源码
    2018-09-10 使用现有在线翻译服务进行代码翻译的体验
    2018-09-06 Java实现英汉词典API初版发布在Maven
    2018-08-29 浏览器插件实现GitHub代码翻译原型演示
    2018-08-27 使用JDT核心库解析JDK源码后初步分析API命名
    2018-08-11 中文代码示例之Spring Boot 2.0.3问好
    2018-08-24 中文代码之Spring Boot对H2数据库简单查询
    2018-08-22 为中文API的简繁转换库添加迟到的持续集成
    2018-08-21 中文关键词替换体验页面原型
    vim打开不同的文件
  • 原文地址:https://www.cnblogs.com/HopeGi/p/2982837.html
Copyright © 2020-2023  润新知