示例程序效果演示动画:
点击播放按钮,可以重复播放演示动画。
.Net的DataGridView中虽然提供有原生的DataGridViewComboBoxColumn的支持,但是其下拉列表的数据源只能是以列(Column)为单位固定设置,很多时候无法满足实际需要。
问题描述
假设我们有一个系统,存在三个主表:公司,部门,员工,分别存放公司,部门和员工的ID及名称等关键信息。三个主表存在从属关系:每个公司下设几个部门,每个部门统辖一批员工。
有一个画面用来录入关于员工的某些信息,如下图:
在上面这个画面的DataGridView中,只能输入和显示公司的ID,很不直观。
为了方便使用者录入数据,我们可以将公司,部门,员工这三列的ColumnType设为DataGridViewComboBoxColumn,同时设置好三列的数据源之后,就可以直接从下拉列表中选择公司,部门和员工,使用者不再需要记住和手动输入ID了。如下图所示:
比过去方便多了,但是仔细观察,上图表格的第一行有一个问题:虽然【公司】这一栏中已经选择了“A公司”,但是【部门】的下拉列表中却把“A公司”“B公司”所有的部门都表示出来。
为什么呢,因为【部门】是以列为单位设置的数据源,所以无论这一行的【公司】选择了什么值,其下拉列表显示都一样。【员工】这一列也一样,无论这一行的【公司】和【部门】选择了什么值,下拉列表都只是把所有的员工显示出来,无法任意变化。
从使用者的角度,希望的应该是下面这种操作方式:
选择完【公司】之后,【部门】的下拉列表里只显示该公司的所有部门;【部门】也被选择之后,【员工】的下拉列表里面只显示该部门的所有员工。
这种灵活可变的ComboBox,显然不是DataGridViewComboBoxColumn能够实现的,需要用一些非常手段。
解决问题的思路:
我们可以这么做
1,鼠标选中某一个Cell的时候,在这个Cell的位置显示一个ComboBox,其下拉列表根据需要动态设置。
通过这个ComboBox选中的ID值,设置到相应的Cell里面去。
使用者看到的画面显示变成下面这个样子:
2,鼠标焦点离开这个Cell的时候,隐藏这个ComboBox。刚才被设定到Cell里ID值就显示出来了。
但我们不想看到ID,而想看到名称,所以要在这个Cell的位置描画一个矩形,显示名称,挡住这个Cell所显示的ID值。
使用者看到的画面显示变成下面这个样子
代码实现:
1,既然DataGridViewComboBoxColumn无法满足需要,我们可以自定义一个新的ColumnType,以和普通的列相区别。这种列起名字叫DataGridViewMaskTextColumn。
1 namespace MultiComboBoxSample
2 {
3 /// <summary>
4 /// 联动Combo的DataGridViewColumn
5 /// </summary>
6 public class DataGridViewMaskTextColumn : DataGridViewTextBoxColumn
7 {
8 public DataGridViewMaskTextColumn()
9 {
10 this.CellTemplate = new DataGridViewMaskTextCell();
11 }
12 }
13 }
代码只有几行,只需要关注两点:
1)这种列继承于普通的DataGridViewTextBoxColumn;
2)这种列的Cell类型也是我们自定义的,叫做DataGridViewMaskTextCell
2,我们看看DataGridViewMaskTextCell的代码实现
1 namespace MultiComboBoxSample
2 {
3 /// <summary>
4 /// 联动Combo的DataGridViewCell
5 /// </summary>
6 public class DataGridViewMaskTextCell : DataGridViewTextBoxCell
7 {
8 //这个Cell要显示的名称
9 private string mstrDisplayValue = "";
10 public string DisplayValue
11 {
12 set { mstrDisplayValue = value; this.DataGridView.InvalidateCell(this); }
13 get { return mstrDisplayValue; }
14 }
15
16 protected override void Paint(
17 Graphics graphics,
18 Rectangle clipBounds,
19 Rectangle cellBounds,
20 int rowIndex,
21 DataGridViewElementStates cellState,
22 object value,
23 object formattedValue,
24 string errorText,
25 DataGridViewCellStyle cellStyle,
26 DataGridViewAdvancedBorderStyle advancedBorderStyle,
27 DataGridViewPaintParts paintParts)
28 {
29 //调用基类的Paint
30 base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState,
31 value, formattedValue, errorText, cellStyle,
32 advancedBorderStyle, paintParts);
33
34 if (this.RowIndex < 0)
35 {
36 return;//表头的Cell不做任何处理
37 }
38
39 //下面这段代码描绘一个显示名称的矩形
40 {
41 Rectangle newRect = new Rectangle(cellBounds.X + 1,
42 cellBounds.Y + 1, cellBounds.Width - 4,
43 cellBounds.Height - 4);
44
45 StringFormat format = new StringFormat(StringFormatFlags.NoWrap);
46
47 if(this.DataGridView.Rows[this.RowIndex].Selected)
48 {
49 //这一行是选中状态的时候,颜色要变成蓝底白色
50 graphics.DrawRectangle(SystemPens.Highlight, newRect);
51 graphics.FillRectangle(SystemBrushes.Highlight, newRect);
52 graphics.DrawString(mstrDisplayValue, new Font("MS UI Gothic",9), new SolidBrush(Color.White), newRect, format);
53
54 }
55 else
56 {
57 if (this.ReadOnly)
58 {
59 //这个Cell是只读的时候,背景色设为浅灰色
60 graphics.DrawRectangle(SystemPens.ControlLight, newRect);
61 graphics.FillRectangle(SystemBrushes.ControlLight, newRect);
62 graphics.DrawString(mstrDisplayValue, new Font("MS UI Gothic", 9), new SolidBrush(Color.Black), newRect, format);
63 }
64 else
65 {
66 graphics.DrawRectangle(Pens.White, newRect);
67 graphics.FillRectangle(Brushes.White, newRect);
68 graphics.DrawString(mstrDisplayValue, new Font("MS UI Gothic", 9), new SolidBrush(Color.Black), newRect, format);
69 }
70 }
71 }
72 }
73 }
74 }
代码也不长,分析这个类,可以明白三件事
1)它继承于最普通的DataGridViewTextBoxCell;
2)和普通的DataGridViewTextBoxCell相比,它多了一个叫做DisplayValue的属性。普通的DataGridViewTextBoxCell已经有一个叫做Value的属性,存放这个Cell的真正的值,例如公司ID,部门ID,员工ID;而DisplayValue这个属性存放的是用来显示的值,例如公司名称,部门名称或者员工名。
3)在Paint()方法里面,也就是Cell被描画出来显示在屏幕上的时候,做了这样一件事:描画一个矩形,蒙在这个Cell上,并在这个矩形内显示DisplayValue的值。
这下明白为什么这些类的名字中含有“Mask(口罩,面具)”这个字眼了吧,因为这种Cell的上面都蒙了一个矩形,好像带着一个口罩一样。
将这两个类添加到我们的工程中,并进行编译之后,就可以更改DataGridView的列的ColumnType属性为DataGridViewMaskTextColumn类型了。
3,要做的事情远远不止这些,虽然我们已经定义了一个新的ColumnType,但是现在这个新列和ComboBox看起来还没有任何联系。我们还需要创造一个ComboBox,让它在需要的时候出现,不需要的时候消失,该变大的时候变大,该变小的时候变小,它要能够随时显示合适的下拉列表,能够把选择的值设到DataGridView的合适的Cell里面。这些复杂的控制和操作,我都把它们写在一个叫做CGridComboHelper.cs的工具类里。
1)这个类里面有一个唯一的ComboBox实例。
1 /// <summary>
2 /// 联动Combo的控制
3 /// </summary>
4 public class CGridComboHelper
5 {
6 #region 变量与常量
7 //辅助输入用的ComboBox
8 private ComboBox cmbHelp;
9
10 ///...
2)在这个类的构造函数里,我们把DataGridView的实例,ComboBox还有下拉列表的数据源紧密团结起来。
1 /// <summary>
2 /// ComboBoxHelper构造函数
3 /// </summary>
4 public CGridComboHelper(DataGridView grd,//DataGridView的实例
5 DataTable dtCompany, //公司下拉列表的数据源(全部)
6 DataTable dtStation, //部门下拉列表的数据源(全部)
7 DataTable dtEmployee) //员工下拉列表的数据源(全部)
8 {
9 mGrid = grd;//一个DataGridView的实例
10
11 cmbHelp = new ComboBox();//创造一个ComboBox的实例
12 cmbHelp.DropDownStyle = ComboBoxStyle.DropDownList;
13 cmbHelp.Visible = false;
14 mGrid.Controls.Add(cmbHelp);//将这个ComboBox的实例加入到DataGridView的Controls中
15
16 ///...
3)要让ComboBox乖乖听话,还需要在这个类中,为DataGridView的实例mGrid,和ComboBox的实例cmbHelp实现一些事件处理函数:
mGrid_CurrentCellChanged事件
1 /// <summary>
2 /// 选中的Cell发生变化时,重设该Cell的下拉列表
3 /// </summary>
4 private void mGrid_CurrentCellChanged(object sender, EventArgs e)
5
6 ///...
当光标选中Cell发生变化的时候,需要做一些判断,判断这个Cell是不是需要显示一个ComboBox,如果显示的话,要根据需要设置合适的下拉列表。
cmbHelp_SelectedIndexChanged事件
1 /// <summary>
2 /// 把ComboBox中选择的值,设置到Cell中
3 /// </summary>
4 private void cmbHelp_SelectedIndexChanged(object sender, EventArgs e)
5
6 ///...
通过ComboBox选择了一个值得时候,把这个值,以及相应的显示值(名称)设到相应的Cell中。
mGrid_Paint事件
1 /// <summary>
2 /// Cell的显示有任何变化时,辅助输入用的ComboBox也要跟着变化
3 /// </summary>
4 private void mGrid_Paint(object sender, PaintEventArgs e)
5
6 ///...
当ComboBox显示出来的时候,我让其大小恰好覆盖住相应的Cell,看起来和Cell浑然一体。但是画面的大小,列的宽度都可以被鼠标拖动而改变,Cell的位置和大小也有可能发生变化,所以在DataGridView的Paint事件里面也加了处理,只要Cell的位置和大小发生任何改变,ComboBox也随之变化,以保持浑然一体的状态。
4)在CGridComboHelper.cs这个类里,还准备了一些从外部调用的公开函数
ResetComboCellDisplay()函数:
DataGridViewMaskTextCell的真正的值(Value)和显示用的值(DisplayValue)是分别保存的,通过ComboBox设定的时候,这两个值会被同时更新;但如果不是通过画面,而是通过代码仅仅改变了这个Cell的Value,其画面显示值不会发生变化。这时候需要调用一下这个公开方法,刷新一下所有Cell的显示。
1 /// <summary>
2 /// 必要的时候,重新描画所有Cell的显示值
3 /// </summary>
4 public void ResetComboCellDisplay()
StopComboEvent()函数,StartComboEvent()函数:
上面我们定义了一些DataGridView相关事件处理函数,有时候需要暂时避免这些事件处理的打扰,例如更新一下DataGridView的所有数据,进行一下排序等等,可以利用这两个函数。
1 /// <summary>
2 /// 暂停事件处理(在后台设置数据的时候,不用触发ComboBox相关事件函数)
3 /// </summary>
4 public void StopComboEvent()
5 {
6 if (cmbHelp.Visible)
7 {
8 cmbHelp.Visible = false;
9 }
10 mGrid.CurrentCellChanged -= new EventHandler(mGrid_CurrentCellChanged);
11 mGrid.CellClick -= new DataGridViewCellEventHandler(mGrid_CellClick);
12 }
13
14 /// <summary>
15 /// 恢复事件处理
16 /// </summary>
17 public void StartComboEvent()
18 {
19 mGrid.CurrentCellChanged += new EventHandler(mGrid_CurrentCellChanged);
20 mGrid.CellClick += new DataGridViewCellEventHandler(mGrid_CellClick);
21 }
5)CGridComboHelper.cs这个类是如何被使用的?
参考示例代码,在主画面显示的时候进行了如下操作,就这么简单。
private void frmMain_Load(object sender, EventArgs e)
{
try
{
///...
//使用自定义ComboBox,初始化工具类的实例comboHelper
comboHelper = new CGridComboHelper(dataGridView6, dtCompany, dtStation, dtUser);
comboHelper.ResetComboCellDisplay();//如果有显示不正常的情况,再次调用这个方法以刷新显示值
comboHelper.StartComboEvent();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
结束语
思路很简单,但是实现过程中解决了很多细节问题。由于只是为了解决项目中碰到的具体问题,所以基本使用硬编码实现,没有进行更深层次的考虑,例如将其包装成一个组件什么的。(如果已经存在这种组件,请告诉我,以满足我又重复制造了一个车轱辘的成就感。)
有类似问题需要解决的朋友,请直接下载示例代码,随便拷贝,修改,以及以各种方式利用或将其完善。