DataGridView右键菜单自定义显示及隐藏列

    WinForm程序中表单的列可自定义显示及隐藏,是一种常见的功能,对于用户体验来讲是很是好的。笔者通过一段时间的摸索,终于实现了本身想要的功能及效果,现记录一下过程:数据库

    一、新建一个自定义控件,命名为:PopupMenuControl。ide

    二、在PopupMenuControl.Designet文件中的InitializeComponent()方法下面,注册如下事件:函数

    this.Paint += new System.Windows.Forms.PaintEventHandler(this.PopupMenuControl_Paint);
    this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.PopupMenuControl_MouseDown);
    this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.PopupMenuControl_MouseMove);

    三、PopupMenuControl的代码:布局

    public partial class PopupMenuControl : UserControl
    {
        public delegate void CheckedChanged(int hitIndex, bool isChecked);  //勾选改变委托
        public event CheckedChanged CheckedChangedEvent;                    //勾选改变事件
        PopupMenuHelper popupMenuHelper = null;                             //菜单帮助类,主要负责菜单绘制。

        public PopupMenuControl()
        {
            InitializeComponent();
        }

        public void Initialize(DataGridView dgvTarget)
        {
            //菜单帮助类实例化
            popupMenuHelper = new PopupMenuHelper();
            //将列标题添加到items
            foreach (DataGridViewColumn column in dgvTarget.Columns)
            {
                popupMenuHelper.AddItem(column.HeaderText, column.Visible);
            }
            //菜单绘制
            popupMenuHelper.Prepare(CreateGraphics());
            Width = popupMenuHelper.Width;
            Height = popupMenuHelper.Height;
        }

        /// <summary>
        /// 绘制
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PopupMenuControl_Paint(object sender, PaintEventArgs e)
        {
            popupMenuHelper.Draw(e.Graphics);
        }

        /// <summary>
        /// 鼠标移过
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PopupMenuControl_MouseMove(object sender, MouseEventArgs e)
        {
            if (popupMenuHelper.IsMouseMove(e.X, e.Y))
            {
                popupMenuHelper.Draw(CreateGraphics());
            }
        }

        /// <summary>
        /// 鼠标按下
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PopupMenuControl_MouseDown(object sender, MouseEventArgs e)
        {
            if (popupMenuHelper.IsMouseDown(e.X, e.Y))
            {
                int hitIndex = popupMenuHelper.HitIndex;
                if (hitIndex != -1)
                {
                    bool isChecked = popupMenuHelper.IsCheckedChange(hitIndex, CreateGraphics());
                    OnCheckedChanged(hitIndex, isChecked);
                }
            }
        }

        /// <summary>
        /// 勾选改变
        /// </summary>
        /// <param name="iIndex"></param>
        /// <param name="bChecked"></param>
        public virtual void OnCheckedChanged(int hitIndex, bool isChecked)
        {
            CheckedChangedEvent?.Invoke(hitIndex, isChecked);
        }
    }

    四、这上面涉及到一个PopupMenuHelper的帮助类,此帮助类主要是为PopupMenuControl控件实现菜单绘制的功能,其代码以下:测试

    class PopupMenuHelper
    {
        //变量
        private PopupMenuItem hotItem = null;                           //当前Item
        private List<PopupMenuItem> items = new List<PopupMenuItem>();  //Item集合
        private Bitmap bitmap;                                          //位图
        private Graphics graphics;                                      //图像
        private static readonly int BasicConst = 24;                    //Item:高度、Image宽度、2倍Checkbox边长
        private static readonly int BasicGap = 3;                       //四周间距
        private static readonly int BasicRows = 3;                      //最大行数
        private static readonly int BasicSide = 10;                     //Item:CheckBox边长(建议用偶数)
        private int totality = 1;                                       //分割总数
        private int[] eachWidth = null;                                 //各个宽度

        //属性
        public int Width { get { return bitmap.Width; } }               //宽度
        public int Height { get { return bitmap.Height; } }             //高度

        //PopupMenuItem类
        private class PopupMenuItem
        {
            //属性
            public string ItemText { get; set; }                        //Item文本
            public bool IsChecked { get; set; }                         //勾选状态

            //构造函数
            public PopupMenuItem(string itemText) : this(itemText, false)
            {
            }
            public PopupMenuItem(string itemText, bool isChecked)
            {
                ItemText = itemText;
                IsChecked = isChecked;
            }
        }

        //无参构造函数
        public PopupMenuHelper()
        {
        }

        /// <summary>
        /// 被点击Item的Index
        /// </summary>
        public int HitIndex
        {
            get
            {
                return items.IndexOf(hotItem);
            }
        }

        /// <summary>
        /// 勾选改变状态
        /// </summary>
        /// <param name="hitIndex">被点击Item的Index</param>
        /// <param name="g">图像</param>
        /// <returns></returns>
        public bool IsCheckedChange(int hitIndex, Graphics g)
        {
            items[hitIndex].IsChecked = !items[hitIndex].IsChecked;
            Draw(g);
            return items[hitIndex].IsChecked;
        }

        /// <summary>
        /// 添加Item
        /// </summary>
        /// <param name="itemText">Item文本</param>
        /// <param name="isChecked">Item勾选状态</param>
        public void AddItem(string itemText, bool isChecked)
        {
            items.Add(new PopupMenuItem(itemText, isChecked));
        }

        /// <summary>
        /// 绘制菜单准备
        /// </summary>
        /// <param name="g">图像</param>
        public void Prepare(Graphics g)
        {
            //获取菜单的宽度及高度
            totality = (int)Math.Ceiling((double)items.Count / BasicRows);
            eachWidth = new int[totality];
            int totalWidth = 0, totalHeight = 0;
            double maxTextWidth = 0;
            if (totality == 1)
            {
                totalHeight = items.Count * BasicConst + 2 * BasicGap;
                foreach (PopupMenuItem item in items)
                {
                    //SizeF:存储有序浮点数对,一般为矩形的宽度和高度。
                    SizeF sizeF = g.MeasureString(item.ItemText, SystemInformation.MenuFont);
                    maxTextWidth = Math.Max(maxTextWidth, sizeF.Width);
                }
                totalWidth = (int)Math.Ceiling((double)maxTextWidth) + BasicConst + 2 * BasicGap;
                eachWidth[0] = (int)Math.Ceiling((double)maxTextWidth) + BasicConst;
            }
            else
            {
                totalHeight = BasicRows * BasicConst + 2 * BasicGap;
                int rows = 0, cols = 1;
                foreach (PopupMenuItem item in items)
                {
                    rows++;
                    //SizeF:存储有序浮点数对,一般为矩形的宽度和高度。
                    SizeF sizeF = g.MeasureString(item.ItemText, SystemInformation.MenuFont);
                    maxTextWidth = Math.Max(maxTextWidth, sizeF.Width);
                    if (cols < totality)
                    {
                        //1..[totality-1]列
                        if (rows == BasicRows)
                        {
                            totalWidth += (int)Math.Ceiling((double)maxTextWidth) + BasicConst;
                            eachWidth[cols - 1] = (int)Math.Ceiling((double)maxTextWidth) + BasicConst;
                            maxTextWidth = 0;
                            cols++;
                            rows = 0;
                        }
                    }
                    else
                    {
                        //totality列
                        if ((cols - 1) * BasicRows + rows == items.Count)
                        {
                            totalWidth += (int)Math.Ceiling((double)maxTextWidth) + BasicConst + 2 * BasicGap;
                            eachWidth[cols - 1] = (int)Math.Ceiling((double)maxTextWidth) + BasicConst;
                        }
                    }
                }
            }
            //图像初始化
            bitmap = new Bitmap(totalWidth, totalHeight);
            graphics = Graphics.FromImage(bitmap);
        }

        /// <summary>
        /// 绘制菜单
        /// </summary>
        /// <param name="g"></param>
        public void Draw(Graphics g)
        {
            Rectangle area = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
            graphics.Clear(SystemColors.Menu);
            DrawBackground(graphics, area);
            DrawItems(graphics);
            g.DrawImage(bitmap, area, area, GraphicsUnit.Pixel);
        }

        /// <summary>
        /// 绘制菜单背景
        /// </summary>
        /// <param name="g"></param>
        /// <param name="area"></param>
        private void DrawBackground(Graphics g, Rectangle area)
        {
            //描边
            using (Pen borderPen = new Pen(Color.FromArgb(112, 112, 112)))
                g.DrawRectangle(borderPen, area);

            //Image及Text
            int left = BasicGap, top = BasicGap;
            if (totality == 1)
            {
                Rectangle imageArea = new Rectangle(left, top, BasicConst, items.Count * BasicConst);
                using (Brush backBrush = new SolidBrush(Color.FromArgb(240, 240, 240)))
                    g.FillRectangle(backBrush, imageArea);

                Rectangle textArea = new Rectangle(left + BasicConst, top, eachWidth[0], items.Count * BasicConst);
                using (Brush backBrush = new SolidBrush(Color.FromArgb(255, 255, 255)))
                    g.FillRectangle(backBrush, textArea);
            }
            else
            {
                for (int i = 0; i < totality; i++)
                {
                    Rectangle imageArea = new Rectangle(left, top, BasicConst, BasicRows * BasicConst);
                    using (Brush backBrush = new SolidBrush(Color.FromArgb(240, 240, 240)))
                        g.FillRectangle(backBrush, imageArea);

                    Rectangle textArea = new Rectangle(left + BasicConst, top, eachWidth[i], BasicRows * BasicConst);
                    using (Brush backBrush = new SolidBrush(Color.FromArgb(255, 255, 255)))
                        g.FillRectangle(backBrush, textArea);

                    left += eachWidth[i];
                }
            }
        }

        /// <summary>
        /// 绘制全部菜单Item
        /// </summary>
        /// <param name="g">图像</param>
        private void DrawItems(Graphics g)
        {
            int left = BasicGap, top = BasicGap;
            int rows = 0, cols = 1;
            foreach (PopupMenuItem item in items)
            {
                if (totality == 1)
                {
                    DrawSingleItem(g, left, ref top, eachWidth[0], item, item == hotItem);
                }
                else
                {
                    rows++;
                    DrawSingleItem(g, left, ref top, eachWidth[cols - 1], item, item == hotItem);
                    //1..[totality-1]列
                    if (rows % BasicRows == 0)
                    {
                        left += eachWidth[cols - 1];
                        top = BasicGap;
                        cols++;
                        rows = 0;
                    }
                }
            }
        }

        /// <summary>
        /// 绘制单个菜单Item
        /// </summary>
        /// <param name="g">图像</param>
        /// <param name="top">图像Top</param>
        /// <param name="item">菜单Item</param>
        /// <param name="isHotItem">是否为当前菜单Item</param>
        private void DrawSingleItem(Graphics g, int left, ref int top,int width, PopupMenuItem item, bool isHotItem)
        {
            //Item区域
            Rectangle drawRect = new Rectangle(left, top, width, BasicConst);
            top += BasicConst;

            //Text区域
            Rectangle itemTextArea = new Rectangle
                (
                    drawRect.Left + BasicConst,
                    drawRect.Top,
                    drawRect.Width - BasicConst,
                    drawRect.Height
                );

            //背景色及描边色
            if (isHotItem)
            {
                //HotItem
                Rectangle hotItemArea = new Rectangle(drawRect.Left, drawRect.Top, drawRect.Width, drawRect.Height);
                using (SolidBrush backBrush = new SolidBrush(Color.FromArgb(214, 235, 255)))
                    g.FillRectangle(backBrush, hotItemArea);
                using (Pen borderPen = new Pen(Color.FromArgb(51, 153, 255)))
                    g.DrawRectangle(borderPen, hotItemArea);
            }

            //Text处理
            StringFormat itemTextFormat = new StringFormat();
            //NoClip:容许显示字形符号的伸出部分和延伸到矩形外的未换行文本。
            //NoWrap:在矩形内设置格式时,禁用自动换行功能。
            itemTextFormat.FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.NoWrap;
            //Near:指定文本靠近布局对齐。
            itemTextFormat.Alignment = StringAlignment.Near;
            //Center:指定文本在布局矩形中居中对齐(呃,感受不是很垂直居中,偏上了一些)。
            itemTextFormat.LineAlignment = StringAlignment.Center;
            //Show:显示热键前缀。
            itemTextFormat.HotkeyPrefix = HotkeyPrefix.Show;

            SolidBrush textBrush = new SolidBrush(SystemColors.MenuText);
            g.DrawString(item.ItemText, SystemInformation.MenuFont, textBrush, itemTextArea, itemTextFormat);

            //Checkbox处理
            if (item.IsChecked)
            {
                int checkBoxGap = (int)((drawRect.Height - BasicSide) / 2);
                int checkBoxLeft = drawRect.Left + checkBoxGap;
                int checkBoxTop = drawRect.Top + checkBoxGap;

                //将checkBoxArea的Top减1,与文本的对齐效果稍微好一些。
                Rectangle checkBoxArea = new Rectangle(checkBoxLeft, checkBoxTop - 1, BasicSide, BasicSide);
                using (Brush checkBoxBrush = new SolidBrush(Color.FromArgb(214, 235, 255)))
                    g.FillRectangle(checkBoxBrush, checkBoxArea);
                using (Pen checkBoxPen = new Pen(Color.FromArgb(51, 153, 255)))
                    g.DrawRectangle(checkBoxPen, checkBoxArea);

                using (Pen checkBoxTick = new Pen(Color.FromArgb(51, 153, 255)))
                {
                    g.DrawLine(checkBoxTick, new Point(checkBoxLeft, checkBoxTop - 1 + (int)(BasicSide / 2)), new Point(checkBoxLeft + (int)(BasicSide / 2), checkBoxTop - 1 + BasicSide));
                    g.DrawLine(checkBoxTick, new Point(checkBoxLeft + (int)(BasicSide / 2), checkBoxTop - 1 + BasicSide), new Point(checkBoxLeft + BasicSide + BasicGap, checkBoxTop - 1 - BasicGap));
                }
            }
        }

        /// <summary>
        /// 点击测试
        /// </summary>
        /// <param name="X">X坐标</param>
        /// <param name="Y">Y坐标</param>
        /// <returns></returns>
        private PopupMenuItem HitTest(int X, int Y)
        {
            if (X < 0 || X > Width || Y < 0 || Y > Height)
            {
                return null;
            }

            int left = BasicGap, top = BasicGap;
            int rows = 0, cols = 1;
            foreach (PopupMenuItem item in items)
            {
                if (totality == 1)
                {
                    rows++;
                    if (X > left && X < left + eachWidth[0] && Y > top + (rows - 1) * BasicConst && Y < top + rows * BasicConst)
                    {
                        return item;
                    }
                }
                else
                {
                    rows++;
                    if (X > left && X < left + eachWidth[cols - 1] && Y > top + (rows - 1) * BasicConst && Y < top + rows * BasicConst)
                    {
                        return item;
                    }
                    //1..[totality-1]列
                    if (rows % BasicRows == 0)
                    {
                        left += eachWidth[cols - 1];
                        top = BasicGap;
                        cols++;
                        rows = 0;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// 是不是鼠标移过
        /// </summary>
        /// <param name="X">X坐标</param>
        /// <param name="Y">Y坐标</param>
        /// <returns></returns>
        public bool IsMouseMove(int X, int Y)
        {
            PopupMenuItem popupMenuItem = HitTest(X, Y);
            if (popupMenuItem != hotItem)
            {
                hotItem = popupMenuItem;
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// 是不是鼠标按下
        /// </summary>
        /// <param name="X">X坐标</param>
        /// <param name="Y">Y坐标</param>
        /// <returns></returns>
        public bool IsMouseDown(int X, int Y)
        {
            PopupMenuItem popupMenuItem = HitTest(X, Y);
            return popupMenuItem != null;
        }
    }

    这个类实现了多菜单页面的功能:即若是DataGridView字段很是的多,可经过产生多列菜单来显示,程序是经过BasicRows变量来控制。this

    五、新建一个DataGridViewColumnSelector类,此类的功能主要是衔接DataGridView与PopupMenuControl,其代码以下:spa

/// <summary>
    /// DataGridView右键菜单自定义显示及隐藏列
    /// </summary>
    class DataGridViewColumnSelector
    {
        private DataGridView dgvTarget = null;                      //待处理的DataGridView对象
        private ToolStripDropDown dropDown;                         //用于加载PopupMenu控件
        PopupMenuControl popupMenuControl = new PopupMenuControl(); //PopupMenu控件

        //无参构造函数
        public DataGridViewColumnSelector()
        {
            //注册PopupMenu控件事件
            popupMenuControl.CheckedChangedEvent += new PopupMenuControl.CheckedChanged(OnCheckedChanged);
            //使用容器承载PopupMenu控件(至关于容器类型的ToolStripItem)
            ToolStripControlHost controlHost = new ToolStripControlHost(popupMenuControl);
            controlHost.Padding = Padding.Empty;
            controlHost.Margin = Padding.Empty;
            controlHost.AutoSize = false;
            //加载PopupMenu控件
            dropDown = new ToolStripDropDown();
            dropDown.Padding = Padding.Empty;
            dropDown.AutoClose = true;
            dropDown.Items.Add(controlHost);
        }

        //有参构造函数
        public DataGridViewColumnSelector(DataGridView dataGridView) : this()
        {
            DataGridView = dataGridView;
        }

        //DataGridView属性
        public DataGridView DataGridView
        {
            get { return dgvTarget; }
            set
            {
                //去除单元格点击事件
                if (dgvTarget != null) { dgvTarget.CellMouseClick -= new DataGridViewCellMouseEventHandler(DataGridView_CellMouseClick); }
                dgvTarget = value;
                //注册单元格点击事件
                if (dgvTarget != null) { dgvTarget.CellMouseClick += new DataGridViewCellMouseEventHandler(DataGridView_CellMouseClick); }
            }
        }

        /// <summary>
        /// 右键点击标题栏弹出菜单
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DataGridView_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e)
        {
            if (e.Button == MouseButtons.Right && e.RowIndex == -1)
            {
                popupMenuControl.Initialize(dgvTarget);
                //将菜单显示在光标位置
                dropDown.Show(Cursor.Position);
            }
        }

        /// <summary>
        /// 勾选事件执行方法
        /// </summary>
        /// <param name="hitIndex"></param>
        /// <param name="isCheck"></param>
        private void OnCheckedChanged(int hitIndex, bool isChecked)
        {
            dgvTarget.Columns[hitIndex].Visible = isChecked;
        }
    }

    六、以上这些,已经实现了所有的功能。下面开始建一个WinForm程序来测试结果,为方便测试将DataGridView的数据源由xml文件读取。code

          从SQL Server数据库随便找张数据表生成XML,文件保存为Test.xml。(请将Test.xml文件拷贝到Debug文件夹下面)orm

SELECT TOP 10 MO_NO,MRP_NO,QTY,BIL_NO 
FROM MF_MO 
WHERE MO_DD='2019-11-07' 
ORDER BY MO_NO 
FOR XML PATH ('Category'),TYPE,ROOT('DocumentElement')

    七、新建一个WinForm程序,命名为Main,并拖入一个DataGridView控件,Main_Load方法以下:xml

        private void Test_Load(object sender, EventArgs e)
        {
            try
            {
                //xml文件路径
                string path = @"Test.xml";
                //读取文件
                DataSet ds = new DataSet();
                if (File.Exists(path))
                {
                    ds.ReadXml(path);
                }
                dataGridView1.DataSource = ds.Tables.Count > 0 ? ds.Tables[0] : null;
                //加工dataGridView1
                #region 加列标题测试
                dataGridView1.Columns[0].HeaderText = "制令单号";
                dataGridView1.Columns[1].HeaderText = "成品编号";
                dataGridView1.Columns[2].HeaderText = "生产数量";
                dataGridView1.Columns[3].HeaderText = "来源单号";
                #endregion
                DataGridViewColumnSelector columnSelector = new DataGridViewColumnSelector(dataGridView1);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

    八、执行程序,在任意DataGridView标题栏右击,便可弹出菜单:

 

    好了,分享就到此结束了,但愿对有此须要的人有一些帮助。