前言
经常使用表格控件DataGridView的行关联快捷菜单(也称为上下文弹出菜单)ContextMenuStrip,基本步骤如下:
- 在窗体上设计ContextMenuStrip快捷菜单控件;
- 设置DataGridView.RowTemplate.ContextMenuStrip属性为指定的快捷菜单;
- 在菜单弹出前捕获关联事件DataGridView.RowContextMenuStripNeeded,获得当前行与快捷菜单,并做适当处理。
但是,使用其关联事件DataGridView.RowContextMenuStripNeeded有一个重要的前提:“RowContextMenuStripNeeded 事件仅在设置了DataGridView控件的DataSource属性或者该控件的VirtualMode属性为 true 时发生。”(参考MSDN:RowContextMenuStripNeeded 事件)。
此外,上述方法还有一个不足之处:在非数据行的地方(如:表格列头或行头)不能使用RowTemplate.ContextMenuStrip快捷菜单,也捕获不到事件DataGridView.RowContextMenuStripNeeded事件。当然,还可以使用其它类似事件,如:CellContextMenuStripNeeded,等等。但它们均受到同样的约束。
事实上,DataGridView.ContextMenuStrip是控件本身的快捷菜单。本文介绍的定制DataGridView控件,就是直接应用其ContextMenuStrip属性,定制一个快捷菜单关联事件,实现RowTemplate.ContextMenuStrip类似功能。基本思路如下:
- 重写DataGridView.MouseDown(MouseEventArgs e)方法,捕获鼠标右击事件;
- 根据事件参数MouseEventArgs的鼠标位置,计算DataGridView当前位置的行号与列号;
- 定制关联事件ContextMenuStripNeeded,在快捷菜单弹出前获取行号、列号与快捷菜单对象对象。
关键技术
捕获鼠标右击位置(坐标),根据该位置计算当前行号与列号,并引发自定义关联事件。如下代码是捕获鼠标右击事件(定制DataGridView控件中的代码):
- protected override void OnMouseDown(MouseEventArgs e)
- {
- base.OnMouseDown(e);
- if (e.Button == MouseButtons.Right)
- {
- if (this.ContextMenuStrip != null && this.ContextMenuStripNeeded != null)
- {
- int rowIndex = this.GetRowIndexAt(e.Location);
- int colIndex = this.GetColIndexAt(e.Location);
- ContextMenuStripNeededEventArgs ee;
- ee = new ContextMenuStripNeededEventArgs(this.ContextMenuStrip, rowIndex, colIndex);
- this.OnContextMenuStripNeeded(ee);
- }
- }
- }
只有在ContextMenuStrip属性对象非空,以及定制关联事件ContextMenuStripNeeded非空(即有事件注册者)时,才需要计算行列坐标,并由OnContextMenuStripNeeded引发调用事件处理方法。当前鼠标位置的行/列编号计算方法如下:
- private int GetColIndexAt(int mouseLocation_X)
- {
- if (this.FirstDisplayedScrollingColumnIndex < 0)
- {
- return -1;
- }
- if (this.RowHeadersVisible == true && mouseLocation_X <= this.RowHeadersWidth)
- {
- return -1;
- }
- int columnCount = this.Columns.Count;
- for(int index = 0; index < columnCount; index++)
- {
- if(this.Columns[index].Displayed == true)
- {
- Rectangle rect = this.GetColumnDisplayRectangle(index, true);
- if (rect.Left <= mouseLocation_X && mouseLocation_X < rect.Right)
- {
- return index;
- }
- }
- }
- return -1;
- }
- private int GetRowIndexAt(int mouseLocation_Y)
- {
- if (this.FirstDisplayedScrollingRowIndex < 0)
- {
- return -1;
- }
- if (this.ColumnHeadersVisible == true && mouseLocation_Y <= this.ColumnHeadersHeight)
- {
- return -1;
- }
- int index = this.FirstDisplayedScrollingRowIndex;
- int displayedCount = this.DisplayedRowCount(true);
- for (int k = 1; k <= displayedCount; )
- {
- if (this.Rows[index].Visible == true)
- {
- Rectangle rect = this.GetRowDisplayRectangle(index, true);
- if (rect.Top <= mouseLocation_Y && mouseLocation_Y < rect.Bottom)
- {
- return index;
- }
- k++;
- }
- index++;
- }
- return -1;
- }
代码中,参数mouseLocation来自MouseEventArgs的Location属性,this.FirstDisplayedScrollingRowIndex表示当前显示的第一行的行号,this.DisplayedRowCount(true)获取显示的全部行数,参数true表示要包括最后部分显示的行。
按照惯例,-1表示当前鼠标位置位于所有行或列之外,如:表的列头、行头等地方。
源码与演示程序
自定义关联事件的参数类ContextMenuStripNeededEventArgs、完整的定制CustomDataGridView及演示窗体,请参考 全部源码与示例(C#2005)。需要说明,为便于测试比较当前行号与列号,示例代码中的快捷菜单在其Opening事件中设置e.Cancel = true,具体测试时可以去掉该设置。另外,可以不定制DataGridView控件,直接在窗体和DataGridView上应用本文介绍的方法,此时只需要稍稍修改代码即可。
补记:一个更好的解决方案
上述方法的核心是重写DataGraidView的鼠标事件处理函数OnMouseDown(),更好的处理方法如下:
- 重写WndProc(ref m)消息处理函数,根据消息参数m.LParam计算出鼠标坐标位置,再计算该位置的DataGridView的行号与列号;
- 根据消息(WM_CONTEXTMENU=0x007b)可以屏幕快捷菜单,完成ContextMenuStrip.Opening事件中设置e.Cancel=true的类似功能。
参考如下代码:
- protected override void WndProc(ref Message m)
- {
- if (m.Msg == WM_CONTEXTMENU)
- {
- if (m_cancelPopup == true)
- {
- m_cancelPopup = false;
- }
- else
- {
- base.WndProc(ref m);
- }
- }
- else
- {
- base.WndProc(ref m);
-
- if (m.Msg == WM_RBUTTONDOWN || m.Msg == WM_RBUTTONDBLCLK)
- {
- if (this.ContextMenuStrip != null && this.ContextMenuStripNeeded != null)
- {
- int v = m.LParam.ToInt32();
- int x = v & 0xffff;
- int y = (v >> 16) & 0xffff;
- int rowIndex = this.GetRowIndexAt(y);
- int colIndex = this.GetColIndexAt(x);
- ContextMenuStripNeededEventArgs e =
- new ContextMenuStripNeededEventArgs(this.ContextMenuStrip, rowIndex, colIndex);
- this.OnContextMenuStripNeeded(e);
- m_cancelPopup = e.Cancel;
- }
- }
- }
- }
在消息处理函数WndProc(ref m)中,首先捕获鼠标右击消息(WM_RBUTTONDOWN=0x0204),计算鼠标当前位置,从而算出DataGridView当前的行号与列号,最后处理定制的关联事件ContextMenuStripNeeded。关联事件处理程序中可以设置参数Cancel。当Cancel=true时表示禁止快捷菜单消息,从而屏蔽了该菜单弹出事件。显然,还需要修改相应的行号和列号计算函数GetRowIndexAt()与GetColIndexAt()的参数类型为int。
下面是定制关联事件的参数类ContextMenuStripNeededEventArgs:
- public class ContextMenuStripNeededEventArgs : EventArgs
- {
- private bool m_cancel = false;
- private int m_rowIndex = -1;
- private int m_colIndex = -1;
- private ContextMenuStrip m_contextMenuStrip;
- public ContextMenuStripNeededEventArgs(ContextMenuStrip cms, int rowIndex, int colIndex)
- {
- m_contextMenuStrip = cms;
- m_rowIndex = rowIndex;
- m_colIndex = colIndex;
- }
- public ContextMenuStrip ContextMenuStrip
- {
- get { return m_contextMenuStrip; }
- }
- public int RowIndex
- {
- get { return m_rowIndex; }
- }
- public int ColIndex
- {
- get { return m_colIndex; }
- }
- public bool Cancel
- {
- get { return m_cancel; }
- set { m_cancel = value; }
- }
- }
参数类ContextMenuStripNeededEventArgs的属性Cancel是可以读写的,其它参数则只能读。如果在事件处理方法中设置Cancel=true,表示取消快捷菜单弹出。
为保证完整性,下面给出修改后的定制控件CustomDataGridView的全部代码,也可到前面链接下载更新后的源码与示例:
更正说明
测试中还发现如下一些问题,本文的代码已做了修改。全新的源码(有部分变化)和示例请参考 全部源码与示例(C#2005,2009-2-14),特声明。
- 不仅需要捕获鼠标右单击消息,还必须捕获鼠标右双击消息(WM_RBUTTONDBLCLK = 0x0206);
- 计算列号时,必须考虑第一列有部分隐藏的宽度,累加时必须减去这部分;
- 计算行/列号前,必须判断当前显示的行/列号是否为-1。这是DataGridView无行/列定义的情况;
- 直接应用GetColumnDisplayRectangle(index, true)、GetRowDisplayRectangle(index, true)获取行或列单元的坐标;
前言
经常使用表格控件DataGridView的行关联快捷菜单(也称为上下文弹出菜单)ContextMenuStrip,基本步骤如下:
- 在窗体上设计ContextMenuStrip快捷菜单控件;
- 设置DataGridView.RowTemplate.ContextMenuStrip属性为指定的快捷菜单;
- 在菜单弹出前捕获关联事件DataGridView.RowContextMenuStripNeeded,获得当前行与快捷菜单,并做适当处理。
但是,使用其关联事件DataGridView.RowContextMenuStripNeeded有一个重要的前提:“RowContextMenuStripNeeded 事件仅在设置了DataGridView控件的DataSource属性或者该控件的VirtualMode属性为 true 时发生。”(参考MSDN:RowContextMenuStripNeeded 事件)。
此外,上述方法还有一个不足之处:在非数据行的地方(如:表格列头或行头)不能使用RowTemplate.ContextMenuStrip快捷菜单,也捕获不到事件DataGridView.RowContextMenuStripNeeded事件。当然,还可以使用其它类似事件,如:CellContextMenuStripNeeded,等等。但它们均受到同样的约束。
事实上,DataGridView.ContextMenuStrip是控件本身的快捷菜单。本文介绍的定制DataGridView控件,就是直接应用其ContextMenuStrip属性,定制一个快捷菜单关联事件,实现RowTemplate.ContextMenuStrip类似功能。基本思路如下:
- 重写DataGridView.MouseDown(MouseEventArgs e)方法,捕获鼠标右击事件;
- 根据事件参数MouseEventArgs的鼠标位置,计算DataGridView当前位置的行号与列号;
- 定制关联事件ContextMenuStripNeeded,在快捷菜单弹出前获取行号、列号与快捷菜单对象对象。
关键技术
捕获鼠标右击位置(坐标),根据该位置计算当前行号与列号,并引发自定义关联事件。如下代码是捕获鼠标右击事件(定制DataGridView控件中的代码):
- protected override void OnMouseDown(MouseEventArgs e)
- {
- base.OnMouseDown(e);
- if (e.Button == MouseButtons.Right)
- {
- if (this.ContextMenuStrip != null && this.ContextMenuStripNeeded != null)
- {
- int rowIndex = this.GetRowIndexAt(e.Location);
- int colIndex = this.GetColIndexAt(e.Location);
- ContextMenuStripNeededEventArgs ee;
- ee = new ContextMenuStripNeededEventArgs(this.ContextMenuStrip, rowIndex, colIndex);
- this.OnContextMenuStripNeeded(ee);
- }
- }
- }
只有在ContextMenuStrip属性对象非空,以及定制关联事件ContextMenuStripNeeded非空(即有事件注册者)时,才需要计算行列坐标,并由OnContextMenuStripNeeded引发调用事件处理方法。当前鼠标位置的行/列编号计算方法如下:
- private int GetColIndexAt(int mouseLocation_X)
- {
- if (this.FirstDisplayedScrollingColumnIndex < 0)
- {
- return -1;
- }
- if (this.RowHeadersVisible == true && mouseLocation_X <= this.RowHeadersWidth)
- {
- return -1;
- }
- int columnCount = this.Columns.Count;
- for(int index = 0; index < columnCount; index++)
- {
- if(this.Columns[index].Displayed == true)
- {
- Rectangle rect = this.GetColumnDisplayRectangle(index, true);
- if (rect.Left <= mouseLocation_X && mouseLocation_X < rect.Right)
- {
- return index;
- }
- }
- }
- return -1;
- }
- private int GetRowIndexAt(int mouseLocation_Y)
- {
- if (this.FirstDisplayedScrollingRowIndex < 0)
- {
- return -1;
- }
- if (this.ColumnHeadersVisible == true && mouseLocation_Y <= this.ColumnHeadersHeight)
- {
- return -1;
- }
- int index = this.FirstDisplayedScrollingRowIndex;
- int displayedCount = this.DisplayedRowCount(true);
- for (int k = 1; k <= displayedCount; )
- {
- if (this.Rows[index].Visible == true)
- {
- Rectangle rect = this.GetRowDisplayRectangle(index, true);
- if (rect.Top <= mouseLocation_Y && mouseLocation_Y < rect.Bottom)
- {
- return index;
- }
- k++;
- }
- index++;
- }
- return -1;
- }
代码中,参数mouseLocation来自MouseEventArgs的Location属性,this.FirstDisplayedScrollingRowIndex表示当前显示的第一行的行号,this.DisplayedRowCount(true)获取显示的全部行数,参数true表示要包括最后部分显示的行。
按照惯例,-1表示当前鼠标位置位于所有行或列之外,如:表的列头、行头等地方。
源码与演示程序
自定义关联事件的参数类ContextMenuStripNeededEventArgs、完整的定制CustomDataGridView及演示窗体,请参考 全部源码与示例(C#2005)。需要说明,为便于测试比较当前行号与列号,示例代码中的快捷菜单在其Opening事件中设置e.Cancel = true,具体测试时可以去掉该设置。另外,可以不定制DataGridView控件,直接在窗体和DataGridView上应用本文介绍的方法,此时只需要稍稍修改代码即可。
补记:一个更好的解决方案
上述方法的核心是重写DataGraidView的鼠标事件处理函数OnMouseDown(),更好的处理方法如下:
- 重写WndProc(ref m)消息处理函数,根据消息参数m.LParam计算出鼠标坐标位置,再计算该位置的DataGridView的行号与列号;
- 根据消息(WM_CONTEXTMENU=0x007b)可以屏幕快捷菜单,完成ContextMenuStrip.Opening事件中设置e.Cancel=true的类似功能。
参考如下代码:
- protected override void WndProc(ref Message m)
- {
- if (m.Msg == WM_CONTEXTMENU)
- {
- if (m_cancelPopup == true)
- {
- m_cancelPopup = false;
- }
- else
- {
- base.WndProc(ref m);
- }
- }
- else
- {
- base.WndProc(ref m);
-
- if (m.Msg == WM_RBUTTONDOWN || m.Msg == WM_RBUTTONDBLCLK)
- {
- if (this.ContextMenuStrip != null && this.ContextMenuStripNeeded != null)
- {
- int v = m.LParam.ToInt32();
- int x = v & 0xffff;
- int y = (v >> 16) & 0xffff;
- int rowIndex = this.GetRowIndexAt(y);
- int colIndex = this.GetColIndexAt(x);
- ContextMenuStripNeededEventArgs e =
- new ContextMenuStripNeededEventArgs(this.ContextMenuStrip, rowIndex, colIndex);
- this.OnContextMenuStripNeeded(e);
- m_cancelPopup = e.Cancel;
- }
- }
- }
- }
在消息处理函数WndProc(ref m)中,首先捕获鼠标右击消息(WM_RBUTTONDOWN=0x0204),计算鼠标当前位置,从而算出DataGridView当前的行号与列号,最后处理定制的关联事件ContextMenuStripNeeded。关联事件处理程序中可以设置参数Cancel。当Cancel=true时表示禁止快捷菜单消息,从而屏蔽了该菜单弹出事件。显然,还需要修改相应的行号和列号计算函数GetRowIndexAt()与GetColIndexAt()的参数类型为int。
下面是定制关联事件的参数类ContextMenuStripNeededEventArgs:
- public class ContextMenuStripNeededEventArgs : EventArgs
- {
- private bool m_cancel = false;
- private int m_rowIndex = -1;
- private int m_colIndex = -1;
- private ContextMenuStrip m_contextMenuStrip;
- public ContextMenuStripNeededEventArgs(ContextMenuStrip cms, int rowIndex, int colIndex)
- {
- m_contextMenuStrip = cms;
- m_rowIndex = rowIndex;
- m_colIndex = colIndex;
- }
- public ContextMenuStrip ContextMenuStrip
- {
- get { return m_contextMenuStrip; }
- }
- public int RowIndex
- {
- get { return m_rowIndex; }
- }
- public int ColIndex
- {
- get { return m_colIndex; }
- }
- public bool Cancel
- {
- get { return m_cancel; }
- set { m_cancel = value; }
- }
- }
参数类ContextMenuStripNeededEventArgs的属性Cancel是可以读写的,其它参数则只能读。如果在事件处理方法中设置Cancel=true,表示取消快捷菜单弹出。
为保证完整性,下面给出修改后的定制控件CustomDataGridView的全部代码,也可到前面链接下载更新后的源码与示例:
更正说明
测试中还发现如下一些问题,本文的代码已做了修改。全新的源码(有部分变化)和示例请参考 全部源码与示例(C#2005,2009-2-14),特声明。
- 不仅需要捕获鼠标右单击消息,还必须捕获鼠标右双击消息(WM_RBUTTONDBLCLK = 0x0206);
- 计算列号时,必须考虑第一列有部分隐藏的宽度,累加时必须减去这部分;
- 计算行/列号前,必须判断当前显示的行/列号是否为-1。这是DataGridView无行/列定义的情况;
- 直接应用GetColumnDisplayRectangle(index, true)、GetRowDisplayRectangle(index, true)获取行或列单元的坐标;