• 多维表头的DataGridView


    背景

    对于.NET 原本提供的DataGridView控件,制作成如下形式的表格是毫无压力的。

    但是如果把表格改了一下,变成如下形式

     

    传统的DataGridView就做不到了,如果扩展一下还是行的,有不少网友也扩展了DataGridView控件,不过有些也只能制作出二维的表头。或者使用第三方的控件,之前也用过DevExpress的BoundGridView。不过在没有可使用的第三方控件的情况下,做到下面的效果,就有点麻烦了。

     

    那得自己扩展了,不过最后还是用了一个控件库的报表控件,Telerik的Reporting。不过我自己还是扩展了DataGridView,使之能制作出上面的报表。

    准备

    学习了一些网友的代码,原来制作这个多维表头都是利用GDI+对DataGirdView的表头进行重绘。

    用到的方法包括

    Graphics.FillRectangle //填充一个矩形

    Graphics.DrawLine //画一条线

    Graphics.DrawString  //写字符串

     

    此外为了方便组织表头,本人还定义了一个表头的数据结构 HeaderItem 和 HeaderCollection 分别作为每个表头单元格的数据实体和整个表头的集合。

    HeaderItem的定义如下

    public class HeaderItem
        {
            private int _startX;//起始横坐标
            private int _startY;//起始纵坐标
            private int _endX; //终止横坐标
            private int _endY; //终止纵坐标
            private bool _baseHeader; //是否基础表头
    
            public HeaderItem(int startX, int endX, int startY, int endY, string content)
            {
                this._endX = endX;
                this._endY = endY;
                this._startX = startX;
                this._startY = startY;
                this.Content = content;
            }
    
            public HeaderItem(int x, int y, string content):this(x,x,y,y,content)
            {
    
            }
    
            public HeaderItem()
            {
    
            }
    
            public static HeaderItem CreateBaseHeader(int x,int y,string content)
            {
                HeaderItem header = new HeaderItem();
                header._endX= header._startX = x;
                header._endY= header._startY = y;
                header._baseHeader = true;
                header.Content = content;
                return header;
            }
    
            public int StartX
            {
                get { return _startX; }
                set
                {
                    if (value > _endX)
                    {
                        _startX = _endX;
                        return;
                    }
                    if (value < 0) _startX = 0;
                    else _startX = value;
                }
            }
    
            public int StartY
            {
                get { return _startY; }
                set
                {
                    if (_baseHeader)
                    {
                        _startY = 0;
                        return;
                    }
                    if (value > _endY)
                    {
                        _startY = _endY;
                        return;
                    }
                    if (value < 0) _startY = 0;
                    else _startY = value;
                }
            }
    
            public int EndX
            {
                get { return _endX; }
                set
                {
                    if (_baseHeader)
                    {
                        _endX = _startX;
                        return;
                    }
                    if (value < _startX)
                    {
                        _endX = _startX;
                        return;
                    }
                    _endX = value;
                }
            }
    
            public int EndY
            {
                get { return _endY; }
                set
                {
                    if (value < _startY)
                    {
                        _endY = _startY;
                        return;
                    }
                    _endY = value;
                }
            }
    
            public bool IsBaseHeader
            {get{ return _baseHeader;} }
    
            public string Content { get; set; }
        }

    设计思想是利用数学的直角坐标系,给每个表头单元格定位并划定其大小。与计算机显示的坐标定位不同,这里的原点是跟数学的一样放在左下角,X轴正方向是水平向右,Y轴正方向是垂直向上。如下图所示

    之所以要对GridView中原始的列头进行特别处理,是因为这里的起止坐标和终止坐标都可以设置,而原始列头的起始纵坐标(StartY)只能是0,终止横坐标(EndX)必须与起始横坐标(StartY)相等。

    另外所有列头单元格的集合HeaderCollection的定义如下

    public class HeaderCollection
        {
            private List<HeaderItem> _headerList;
            private bool _iniLock;
    
            public DataGridViewColumnCollection BindCollection{get;set;}
    
            public HeaderCollection(DataGridViewColumnCollection cols)
            {
                _headerList = new List<HeaderItem>();
                BindCollection=cols;
                _iniLock = false;
            }
    
            public int GetHeaderLevels()
            {
                int max = 0;
                foreach (HeaderItem item in _headerList)
                    if (item.EndY > max)
                        max = item.EndY;
    
                return max;
            }
    
            public List<HeaderItem> GetBaseHeaders()
            {
                List<HeaderItem> list = new List<HeaderItem>();
                foreach (HeaderItem item in _headerList)
                    if (item.IsBaseHeader) list.Add(item);
                return list;
            }
    
            public HeaderItem GetHeaderByLocation(int x, int y) //先进行X坐标遍历,再进行Y坐标遍历。查找出包含输入坐标的表头单元格实例
            {
                if (!_iniLock) InitHeader();
                HeaderItem result=null;
                List<HeaderItem> temp = new List<HeaderItem>();
                foreach (HeaderItem item in _headerList)
                    if (item.StartX <= x && item.EndX >= x)
                        temp.Add(item);
                foreach (HeaderItem item in temp)
                    if (item.StartY <= y && item.EndY >= y)
                        result = item;
    
                return result;
            }
    
            public IEnumerator GetHeaderEnumer()
            {
                return _headerList.GetEnumerator();
            }
    
            public void AddHeader(HeaderItem header)
            {
                this._headerList.Add(header);
            }
    
            public void AddHeader(int startX, int endX, int startY, int endY, string content)
            {
                this._headerList.Add(new HeaderItem(startX,endX,startY,endY,content));
            }
    
            public void AddHeader(int x, int y, string content)
            {
                this._headerList.Add(new HeaderItem(x, y, content));
            }
    
            public void RemoveHeader(HeaderItem header)
            {
                this._headerList.Remove(header);
            }
    
            public void RemoveHeader(int x, int y)
            {
               HeaderItem header= GetHeaderByLocation(x, y);
               if (header != null) RemoveHeader(header);
            }
    
            private void InitHeader()
            {
                _iniLock = true;
                for (int i = 0; i < this.BindCollection.Count; i++)
                    if(this.GetHeaderByLocation(i,0)==null)
                    this._headerList.Add(HeaderItem.CreateBaseHeader(i,0 , this.BindCollection[i].HeaderText));
                _iniLock = false;
            }
        }

    这里仿照了.NET Frameword的Collection那样定义了Add方法和Remove方法,此外说明一下那个 GetHeaderByLocation 方法,这个方法可以通过给定的坐标获取那个坐标的HeaderItem。这个坐标是忽略了整个表头合并单元格的情况,例如

     

    上面这幅图,如果输入0,0 返回的是灰色区域,输入2,1 或3,2 或 5,1返回的都是橙色的区域。

    扩展控件

    到真正扩展控件了,最核心的是重写 OnCellPainting 方法,这个其实是与表格单元格重绘时触发事件绑定的方法,通过参数 DataGridViewCellPaintingEventArgs 的 ColumnIndex 和 RowIndex 属性可以知道当前重绘的是哪个单元格,于是就通过HeaderCollection获取要绘制的表头单元格的信息进行重绘,对已经重绘的单元格会进行标记,以防重复绘制。

    protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
            {
                if (e.ColumnIndex == -1 || e.RowIndex != -1)
                {
                    base.OnCellPainting(e);
                    return;
                }
                int lev=this.Headers.GetHeaderLevels();
                this.ColumnHeadersHeight = (lev + 1) * _baseColumnHeadHeight;
                for (int i = 0; i <= lev; i++) //到达某一列后,遍历各行,查找出还没绘制的表头进行绘制
                {
                    HeaderItem tempHeader= this.Headers.GetHeaderByLocation(e.ColumnIndex, i);
                    if (tempHeader==null|| i != tempHeader.EndY || e.ColumnIndex != tempHeader.StartX) continue;
                    DrawHeader(tempHeader, e);
                }
                e.Handled = true;
            }

    上面的代码中,最初是先判断当前要重绘的单元格是不是表头部分,如果不是则调用原本的OnCellPainting方法。 e.Handled=true; 比较关键,有了这句代码,重绘才能生效。

    绘制单元格的过程封装在方法DrawHeader里面

    private void DrawHeader(HeaderItem item,DataGridViewCellPaintingEventArgs e)
            {
                if (this.ColumnHeadersHeightSizeMode != DataGridViewColumnHeadersHeightSizeMode.DisableResizing)
                    this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
                int lev=this.Headers.GetHeaderLevels();  //获取整个表头的总行数
                lev=(lev-item.EndY)*_baseColumnHeadHeight;   //重新设置表头的行高
    
                SolidBrush backgroundBrush = new SolidBrush(e.CellStyle.BackColor);
                SolidBrush lineBrush = new SolidBrush(this.GridColor);
                Pen linePen = new Pen(lineBrush);
                StringFormat foramt = new StringFormat();
                foramt.Alignment = StringAlignment.Center;
                foramt.LineAlignment = StringAlignment.Center;
    
                Rectangle headRec = new Rectangle(e.CellBounds.Left, lev, ComputeWidth(item.StartX, item.EndX)-1, ComputeHeight(item.StartY, item.EndY)-1);
                e.Graphics.FillRectangle(backgroundBrush, headRec);  //填充矩形
                e.Graphics.DrawLine(linePen, headRec.Left, headRec.Bottom, headRec.Right, headRec.Bottom); //画单元格的底线
                e.Graphics.DrawLine(linePen, headRec.Right, headRec.Top, headRec.Right, headRec.Bottom);  //画单元格的右边线
                e.Graphics.DrawString(item.Content, this.ColumnHeadersDefaultCellStyle.Font, Brushes.Black,headRec, foramt);  //填写表头标题
            }

    填充矩形时,记得要给矩形的常和宽减去一个像素,这样才不会与相邻的矩形重叠区域导致矩形的边线显示不出来。还有这里的要设置 ColumnHeadersHeightSizeMode 属性,如果不把它设成 DisableResizing ,那么表头的高度是改变不了的,这样即使设置了二维,三维,n维,最终只是一维。

     

    这里用到的一些辅助方法如下,分别是通过坐标计算出高度和宽度。

    private int ComputeWidth(int startX, int endX)
            {
                int width = 0;
                for (int i = startX; i <= endX; i++)
                    width+= this.Columns[i].Width;
                return width;
            }
    
            private int ComputeHeight(int startY, int endY)
            {
                return _baseColumnHeadHeight * (endY - startY+1);
            }

    给一段使用的实例代码,这里要预先给DataGridView每一列设好绑定的字段,否则自动添加的列是做不出效果来的。

    HeaderItem item= this.boundGridView1.Headers.GetHeaderByLocation(0, 0);  //获取包括坐标(0,0)的单元格
                item.EndY = 2;
                item = this.boundGridView1.Headers.GetHeaderByLocation(9,0 );
                item.EndY = 2;
                item = this.boundGridView1.Headers.GetHeaderByLocation(10, 0);
                item.EndY = 2;
                item = this.boundGridView1.Headers.GetHeaderByLocation(11, 0);
                item.EndY = 2;
    
                this.boundGridView1.Headers.AddHeader(1, 2, 1, 1, "语文"); //增加表头,起始坐标(1,1) ,终止坐标(2,1) 内容"语文"
                this.boundGridView1.Headers.AddHeader(3, 4, 1, 1, "数学");  //增加表头,起始坐标(3,1) ,终止坐标(4,1) 内容"数学"
                this.boundGridView1.Headers.AddHeader(5, 6, 1, 1, "英语");  //增加表头,起始坐标(5,1) ,终止坐标(6,1) 内容"英语"
                this.boundGridView1.Headers.AddHeader(7, 8, 1, 1, "X科");  //增加表头,起始坐标(7,1) ,终止坐标(8,1) 内容"X科"
                this.boundGridView1.Headers.AddHeader(1, 8, 2, 2, "成绩");  //增加表头,起始坐标(1,2) ,终止坐标(8,2) 内容"成绩"

    效果图如下所示

     

    总的来说自我感觉有点小题大做,但想不出有什么更好的办法,各位如果觉得以上说的有什么不好的,欢迎拍砖;如果发现以上有什么说错了,恳请批评指正;如果觉得好的,请支持一下。谢谢!最后附上整个控件的源码

         public class BoundGridView : DataGridView
         {
             private int _baseColumnHeadHeight;
    
             public BoundGridView():base()
             {
                 this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
                 _baseColumnHeadHeight = this.ColumnHeadersHeight;
                 this.Headers = new HeaderCollection(this.Columns);
             }
    
             public HeaderCollection Headers{ get;private set; }
    
             protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
             {
                 if (e.ColumnIndex == -1 || e.RowIndex != -1)
                 {
                     base.OnCellPainting(e);
                     return;
                 }
                 int lev=this.Headers.GetHeaderLevels();
                 this.ColumnHeadersHeight = (lev + 1) * _baseColumnHeadHeight;
                 for (int i = 0; i <= lev; i++)
                 {
                     HeaderItem tempHeader= this.Headers.GetHeaderByLocation(e.ColumnIndex, i);
                     if (tempHeader==null|| i != tempHeader.EndY || e.ColumnIndex != tempHeader.StartX) continue;
                     DrawHeader(tempHeader, e);
                 }
                 e.Handled = true;
             }
    
             private int ComputeWidth(int startX, int endX)
             {
                 int width = 0;
                 for (int i = startX; i <= endX; i++)
                     width+= this.Columns[i].Width;
                 return width;
             }
    
             private int ComputeHeight(int startY, int endY)
             {
                 return _baseColumnHeadHeight * (endY - startY+1);
             }
    
             private void DrawHeader(HeaderItem item,DataGridViewCellPaintingEventArgs e)
             {
                 if (this.ColumnHeadersHeightSizeMode != DataGridViewColumnHeadersHeightSizeMode.DisableResizing)
                     this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
                 int lev=this.Headers.GetHeaderLevels();
                 lev=(lev-item.EndY)*_baseColumnHeadHeight;
    
                 SolidBrush backgroundBrush = new SolidBrush(e.CellStyle.BackColor);
                 SolidBrush lineBrush = new SolidBrush(this.GridColor);
                 Pen linePen = new Pen(lineBrush);
                 StringFormat foramt = new StringFormat();
                 foramt.Alignment = StringAlignment.Center;
                 foramt.LineAlignment = StringAlignment.Center;
    
                 Rectangle headRec = new Rectangle(e.CellBounds.Left, lev, ComputeWidth(item.StartX, item.EndX)-1, ComputeHeight(item.StartY, item.EndY)-1);
                 e.Graphics.FillRectangle(backgroundBrush, headRec);
                 e.Graphics.DrawLine(linePen, headRec.Left, headRec.Bottom, headRec.Right, headRec.Bottom);
                 e.Graphics.DrawLine(linePen, headRec.Right, headRec.Top, headRec.Right, headRec.Bottom);
                 e.Graphics.DrawString(item.Content, this.ColumnHeadersDefaultCellStyle.Font, Brushes.Black,headRec, foramt);
             }
         }
    
         public class HeaderItem
         {
             private int _startX;
             private int _startY;
             private int _endX;
             private int _endY;
             private bool _baseHeader;
    
             public HeaderItem(int startX, int endX, int startY, int endY, string content)
             {
                 this._endX = endX;
                 this._endY = endY;
                 this._startX = startX;
                 this._startY = startY;
                 this.Content = content;
             }
    
             public HeaderItem(int x, int y, string content):this(x,x,y,y,content)
             { 
    
             }
    
             public HeaderItem()
             { 
    
             }
    
             public static HeaderItem CreateBaseHeader(int x,int y,string content)
             {
                 HeaderItem header = new HeaderItem();
                 header._endX= header._startX = x;
                 header._endY= header._startY = y;
                 header._baseHeader = true;
                 header.Content = content;
                 return header;
             }
    
             public int StartX
             {
                 get { return _startX; }
                 set 
                 {
                     if (value > _endX)
                     {
                         _startX = _endX;
                         return;
                     }
                     if (value < 0) _startX = 0;
                     else _startX = value;
                 }
             }
    
             public int StartY
             {
                 get { return _startY; }
                 set
                 {
                     if (_baseHeader)
                     {
                         _startY = 0;
                         return;
                     }
                     if (value > _endY)
                     {
                         _startY = _endY;
                         return;
                     }
                     if (value < 0) _startY = 0;
                     else _startY = value;
                 }
             }
    
             public int EndX
             {
                 get { return _endX; }
                 set 
                 {
                     if (_baseHeader)
                     {
                         _endX = _startX;
                         return;
                     }
                     if (value < _startX)
                     {
                         _endX = _startX;
                         return;
                     }
                     _endX = value; 
                 }
             }
    
             public int EndY
             {
                 get { return _endY; }
                 set 
                 {
                     if (value < _startY)
                     {
                         _endY = _startY;
                         return;
                     }
                     _endY = value; 
                 }
             }
    
             public bool IsBaseHeader
             {get{ return _baseHeader;} }
    
             public string Content { get; set; }
         }
    
         public class HeaderCollection
         {
             private List<HeaderItem> _headerList;
             private bool _iniLock;
    
             public DataGridViewColumnCollection BindCollection{get;set;}
    
             public HeaderCollection(DataGridViewColumnCollection cols)
             {
                 _headerList = new List<HeaderItem>();
                 BindCollection=cols;
                 _iniLock = false;
             }
    
             public int GetHeaderLevels()
             {
                 int max = 0;
                 foreach (HeaderItem item in _headerList)
                     if (item.EndY > max)
                         max = item.EndY;
    
                 return max;
             }
    
             public List<HeaderItem> GetBaseHeaders()
             {
                 List<HeaderItem> list = new List<HeaderItem>();
                 foreach (HeaderItem item in _headerList)
                     if (item.IsBaseHeader) list.Add(item);
                 return list;
             }
    
             public HeaderItem GetHeaderByLocation(int x, int y)
             {
                 if (!_iniLock) InitHeader();
                 HeaderItem result=null;
                 List<HeaderItem> temp = new List<HeaderItem>();
                 foreach (HeaderItem item in _headerList)
                     if (item.StartX <= x && item.EndX >= x)
                         temp.Add(item);
                 foreach (HeaderItem item in temp)
                     if (item.StartY <= y && item.EndY >= y)
                         result = item;
    
                 return result;
             }
    
             public IEnumerator GetHeaderEnumer()
             {
                 return _headerList.GetEnumerator();
             }
    
             public void AddHeader(HeaderItem header)
             {
                 this._headerList.Add(header);
             }
    
             public void AddHeader(int startX, int endX, int startY, int endY, string content)
             {
                 this._headerList.Add(new HeaderItem(startX,endX,startY,endY,content));
             }
    
             public void AddHeader(int x, int y, string content)
             {
                 this._headerList.Add(new HeaderItem(x, y, content));
             }
    
             public void RemoveHeader(HeaderItem header)
             {
                 this._headerList.Remove(header);
             }
    
             public void RemoveHeader(int x, int y)
             {
                HeaderItem header= GetHeaderByLocation(x, y);
                if (header != null) RemoveHeader(header);
             }
    
             private void InitHeader()
             {
                 _iniLock = true;
                 for (int i = 0; i < this.BindCollection.Count; i++)
                     if(this.GetHeaderByLocation(i,0)==null)
                     this._headerList.Add(HeaderItem.CreateBaseHeader(i,0 , this.BindCollection[i].HeaderText));
                 _iniLock = false;
             }
         }
    控件的完整代码

    转 : https://www.cnblogs.com/HopeGi/archive/2013/04/03/2982837.html

    https://www.cnblogs.com/jiasonglin/archive/2013/03/28/2986040.html

  • 相关阅读:
    .net 正在中止线程
    jQuery ajax
    jQuery对checkbox的各种操作
    oracle实验39:触发器
    oracle实验21:建立简单的表,并对表进行简单的DDL操作
    oracle实验20:子查询
    oracle实验18-19:表的连接查询
    oracle实验42:编写包package
    oracle实验25:Sequence序列
    oracle实验17:分组统计函数
  • 原文地址:https://www.cnblogs.com/fps2tao/p/15603633.html
Copyright © 2020-2023  润新知