ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C# WinForms DataGridView下拉列表实现:从静态列到动态编辑与性能优化

C# WinForms DataGridView下拉列表实现:从静态列到动态编辑与性能优化 简介面向具有一定C#基础、正在开发WinForm数据录入界面的开发者这套示例工程专门解决DataGridView列需要下拉选择而非手动输入的常见需求。压缩包仅58KB共22个文件主体为6个.cs源码文件配合.csproj/.sln工程文件可直接编译同时含可运行的.exe程序方便查看效果另有.resx资源、.Designer.cs设计文件与.pdb调试符号便于对照学习与排错已有2390人学习下载。源码演示了DataGridViewComboBoxColumn从创建、添加选项Items、设置列宽和显示样式到绑定外部DataSource、书写CellValueChanged事件取值、设置ReadOnly禁止编辑的完整流程并附有完整WindowsApplication1项目。读者既能直接运行观察交互效果也可将关键代码迁移至实际WinForm模块中实现带下拉列表的数据录入、选项一致性校验和最小化键盘输入有效降低数据输入错误率为刚接触DataGridView进阶特性的开发者提供了清晰、可直接复用的实现范例。 做C# WinForms开发尤其是搞上位机、MES、设备调试工具这类项目的人早晚会碰到一个需求DataGridView里某一列不要用户随便输入而是像Excel的数据验证一样点击后弹出一个下拉列表只能从预设选项里选。我当年做一个设备参数配置界面时就为了这个“DataGridView里加下拉列表”折腾了不少时间网上资料零零散散踩了几个坑才把逻辑理顺。这篇就把我自己的做法和踩过的坑完整写出来覆盖静态下拉列、动态下拉列、提交编辑、性能优化几个层面适合正在做C# WinForms界面开发、或者刚入门想搞懂DataGridView编辑机制的读者参考。1. 需求场景与两条实现路线1.1 典型业务场景为什么不能直接用文本框先看一个最常见的场景。你在做一个工业上位机软件某个配置表里有“通信方式”“波特率”“停止位”这些字段波特率无非就是9600、19200、115200这几个值。如果用TextBox让用户手输先不说打字慢光是“9600”和“9600Bps”“9600, 8N1”这种五花八门的写法就够你清洗半天数据。更麻烦的是有些字段对应的是一组枚举值比如设备状态只有“运行”“停止”“故障”三种一旦用户手输了一个“停止 ”带空格的值后续程序解析时直接崩。这种场景下下拉列表几乎是唯一合理的交互方案。它能保证写入DataGridView的单元格值一定来自你预设的合法集合从源头杜绝脏数据。另一个常见场景是分类选择比如不同设备类型关联不同的参数模板选择完类型后让用户在一组固定模板里挑一个这同样适合用下拉列表。1.2 两条路线DataGridViewComboBoxColumn与动态编辑控件实现下拉列表DataGridView本身提供了两种思路方案适用场景优点缺点DataGridViewComboBoxColumn列类型整列选项固定所有行共享同一组数据代码量少设计器可配置类型语义清晰不同行需要不同选项时实现起来别扭DataGridViewTextBoxColumn EditingControlShowing事件不同行需要不同选项、选项随其他列联动灵活可临时替换编辑控件需要自己处理事件绑定、数据源切换、清理工作这两种方案不是互斥的。实际项目中我经常先用方案一快速搭一个静态下拉列遇到“不同行可选项不一样”的需求再切成方案二。后面我分别讲清楚每种方案的具体写法以及为什么有些场景必须用第二种。2. 先搞懂DataGridView的编辑机制2.1 单元格里的ComboBox是从哪来的很多人用DataGridView用了一两年还不太清楚它内部为什么能“变出”各种编辑控件。其实DataGridView的每一列都有一个CellTemplate单元格模板这个模板决定了单元格在进入编辑状态时使用哪个控件。DataGridViewComboBoxColumn的模板是DataGridViewComboBoxCell所以进入编辑状态时DataGridView会创建并显示一个ComboBox作为编辑控件。这背后有一个很关键的机制单元格的显示状态和编辑状态是两回事。未进入编辑状态时DataGridView显示的是当前值对应的文本用户双击或按F2进入编辑状态后DataGridView会把编辑控件覆盖在单元格上让用户操作。这个“覆盖”过程是由EditingControlShowing事件触发的事件参数DataGridViewEditingControlShowingEventArgs里的Control属性就是当前正在使用的编辑控件。理解这个机制后很多问题就通了。比如你改ComboBox列的数据源但界面没反应很可能是数据源改了但单元格显示层没刷新再比如你动态替换了编辑控件退出编辑时又要确保不影响下次进入编辑的状态。2.2 三个容易混淆的属性DataPropertyName、DisplayMember、ValueMember这是新手最容易翻车的地方。DataGridViewComboBoxColumn有三个重要属性DataPropertyName告诉DataGridView这一列绑定数据源的哪个属性字段写入Grid行数据时也用这个属性。DisplayMember下拉列表里显示给用户看的文本来自选项对象的哪个属性。ValueMember选中后单元格实际保存的值来自选项对象的哪个属性。用一个简单例子说明。假设有一个选项类public class ComboOption { public string Display { get; set; } // 界面上看到的 public int Value { get; set; } // 实际存到单元格里的 }如果把DisplayMember设为“Display”ValueMember设为“Value”那你看到的列表项是中文描述但DataGridView单元格存的是对应的int值。这种“显示文本和实际值分离”的设计在正式项目里非常重要因为程序后续逻辑通常只认ID或枚举数字不认字符串。DataPropertyName则是绑定行数据用的。假如你的DataGridView通过DataSource绑定了一个DataTableDataTable中包含“Status”列你想让这一列显示下拉框就需要把列的DataPropertyName设为“Status”。这样加载数据时DataGridView会从每行的Status字段取初始值编辑结束后又会把选中的Value写回这一行的Status字段。3. 实操一静态下拉列——直接用DataGridViewComboBoxColumn3.1 设计器拖拽的局限与代码创建如果只是在设计器里拖一个DataGridViewComboBoxColumn再设置几个属性操作确实快。但实际项目里列往往需要动态生成或者在窗体Load时根据后台配置决定显示哪些列所以能用代码创建列还是很重要的基本功。我一般这样创建一列下拉列表// 准备选项数据源 ListComboOption statusOptions new ListComboOption { new ComboOption { Display 启用, Value 1 }, new ComboOption { Display 停用, Value 0 } }; // 创建下拉列 DataGridViewComboBoxColumn colStatus new DataGridViewComboBoxColumn(); colStatus.HeaderText 状态; colStatus.Name StatusColumn; colStatus.DataPropertyName Status; // 绑定行数据的字段 colStatus.DataSource statusOptions; // 下拉选项集合 colStatus.DisplayMember Display; // 显示文本 colStatus.ValueMember Value; // 实际值 // 加入DataGridView dataGridView1.Columns.Add(colStatus);这里有个顺序问题必须先设置DataSource、DisplayMember、ValueMember再设置DataPropertyName。如果你先给DataPropertyName赋值再设置DataSource某些情况下DataGridView已经按旧数据源解析过一次列结构后面再改可能不生效。我习惯把DataPropertyName放在最后设置。3.2 选项数据源推荐用对象列表或DataTable有人喜欢直接给DataSource赋值一个字符串数组colStatus.DataSource new string[] { 启用, 停用 };这样写虽然能显示下拉列表但单元格里保存的值和显示文本完全相同。一旦后续数据结构调整比如状态从字符串改成int枚举就必须大范围改代码。所以我更推荐用对象列表或者DataTable加两列的方式DataTable dtOptions new DataTable(); dtOptions.Columns.Add(Display, typeof(string)); dtOptions.Columns.Add(Value, typeof(int)); dtOptions.Rows.Add(启用, 1); dtOptions.Rows.Add(停用, 0); colStatus.DataSource dtOptions; colStatus.DisplayMember Display; colStatus.ValueMember Value;有人可能会问用DataTable会不会比List性能差在选项只有几十个的情况下差别完全可以忽略。DataTable反而有个好处它本身就支持“值查找”后期做数据显示时用DataTable的Select方法很方便。3.3 让下拉框“看起来”正常DisplayStyle与提交编辑如果只是设置了上述代码运行时你会注意到一个细节列一开始显示的数据可能是空白或者需要用户点两下才出现下拉框。这里有两个必调的属性。第一DisplayStyle。默认情况下DataGridViewComboBoxCell的DisplayStyle是DropDownButton但有些版本或皮肤下你可能想让它始终显示一个下拉按钮colStatus.DisplayStyle DataGridViewComboBoxDisplayStyle.DropDownButton;第二编辑提交。这是很多人忽略的重点用下拉列表选了值之后DataGridView不一定马上更新单元格显示。尤其当你的DataGridView绑定的是DataTable默认编辑模式是“在单元格级别编辑”时选择完选项后焦点不离开底层数据源不会收到新值。要解决这个问题需要监听CurrentCellDirtyStateChanged事件在值变脏时主动提交private void dataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e) { if (dataGridView1.IsCurrentCellDirty) { dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit); } }这个方法是我在项目里必挂的事件。不挂它你会遇到“下拉选了但保存时还是旧值”的灵异现象。4. 实操二动态下拉列——不同行不同选项4.1 真正解决业务问题的EditingControlShowing事件静态下拉列能覆盖的需求比较有限。做上位机时我经常遇到的场景是表格中不同设备类型的“参数模板”不同比如温控设备有PID选项运动控制设备有脉冲模式选项这些选项彼此独立静态列塞不进去。这时就需要用DataGridViewTextBoxColumn加动态编辑控件的方案。核心思路是列本身还是文本列但进入编辑状态时通过EditingControlShowing事件把编辑控件替换成ComboBox并根据当前行数据设置ComboBox的选项。直接上代码// 窗体构造函数或Load事件中注册事件 dataGridView1.EditingControlShowing dataGridView1_EditingControlShowing; private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) { // 只处理目标列避免影响其他列 if (dataGridView1.CurrentCell null || dataGridView1.CurrentCell.ColumnIndex ! colTemplate.Index) return; ComboBox combo e.Control as ComboBox; if (combo null) return; // 先解绑再绑定防止同一个编辑控件重复触发DataBinding事件 combo.SelectedIndexChanged - combo_SelectedIndexChanged; combo.SelectedIndexChanged combo_SelectedIndexChanged; // 根据当前行获取选项集合 int rowIndex dataGridView1.CurrentCell.RowIndex; string deviceType dataGridView1.Rows[rowIndex].Cells[DeviceTypeColumn].Value.ToString(); ListComboOption options GetTemplateOptions(deviceType); // 绑定数据源 combo.DataSource options; combo.DisplayMember Display; combo.ValueMember Value; }这里有个很关键的细节每次进入编辑状态都先解绑SelectedIndexChanged再重新绑定。因为DataGridView内部复用了编辑控件老的事件委托如果不清理当选过一次后再次编辑同一行事件会触发两次导致逻辑重复执行。4.2 动态数据源绑定时的取舍与注意事项动态方案虽然灵活但容易踩一个性能坑。如果EditingControlShowing事件里每次都去数据库查询选项而用户恰好每一行都点开下拉框那界面上就能明显感觉到“卡一下”。我做过一个设备列表总共500行每行点开都要等200ms才能弹出下拉选项用户直接说“卡死了”。正确的做法是预先一次性把所有可能用到的选项集合加载到内存按一个key存到Dictionary里。事件触发时只做内存里的查询快到无感。Dictionarystring, ListComboOption _optionCache new Dictionarystring, ListComboOption(); private void LoadOptionCache() { // 启动时遍历所有设备类型一次性加载 foreach (var type in GetDeviceTypes()) { _optionCache[type] GetTemplateOptions(type); } } private ListComboOption GetOptionsFromCache(string deviceType) { if (_optionCache.ContainsKey(deviceType)) return _optionCache[deviceType]; return new ListComboOption(); }顺便说一句EditingControlShowing里给ComboBox设置DataSource还有一个体验问题每次进入编辑都会重建下拉项如果用户正开着下拉框用鼠标滚动焦点切换回来后选项会重置。所以如果你在一个会话中只需要绑定一次可以加个判断只有选项集合引用变化时才重新赋值DataSource。这一点比较进阶但做体验优化时很实用。4.3 退出编辑状态时的清理动态方案最后一个容易忽略的点是清理工作。DataGridView的编辑控件是复用的如果你在某次编辑中改了ComboBox的数据源但没有在退出编辑时还原下次其他列进入编辑状态时可能也会拿到这个ComboBox。虽然事件里已经做了列号判断但为了稳妥我通常在CellEndEdit事件里把编辑控件内的回调解绑并清空DataSourceprivate void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) { DataGridView grid dataGridView1; if (e.ColumnIndex colTemplate.Index) { ComboBox combo grid.EditingControl as ComboBox; if (combo ! null) { combo.SelectedIndexChanged - combo_SelectedIndexChanged; combo.DataSource null; } } }这一步会避免很多“换了列之后下拉框数据变成了上一列选项”的诡异问题。5. 常见坑与排查实录5.1 下拉列表不显示点击没反应这种情况大概率是单元格进入不了编辑模式。检查三件事第一列是不是被设置了ReadOnly注意DataGridView列默认ReadOnly为false但如果你在绑定数据时手动对列做了只读限制就会挡掉编辑。第二DataGridView本身的ReadOnly和EditMode设置EditMode如果是EditProgrammatically那用户操作不会触发编辑。第三检查你的EditingControlShowing里是否有判断条件把你“误杀”了比如列Index判断写错。还有一个容易被忽略的列宽太小。当列宽小于下拉按钮所需宽度时下拉箭头可能被挤得看不见用户以为没有下拉功能。把列的AutoSizeMode设为AllCells或Fill就能解决。5.2 单元格显示的是数字而不是文本这个问题一看就是DisplayMember和ValueMember没配置对或者配置反了。数据源如果是对象列表DataGridView通过反射拿属性值属性名拼错会导致显示的是ToString()的结果。检查有没有把DisplayMember拼写成“display”或者“DisplayName”之类的不匹配值。另一种情况是单元格内容本身已经是Value数值但列Set了DataPropertyName后单元格没有刷新显示。可以调用dataGridView1.Refresh()或重新给该行的Cell赋值必要时把列的DisplayStyle和值文本刷新一下。5.3 下拉选了新值但底层数据源没变这就是前面说到的提交问题。DataGridView默认不会在单元格值变脏的瞬间把新值推给数据源必须调用CommitEdit。如果设置了DataSource绑定DataTable还需要注意在CellValueChanged事件里读取新值因为CommitEdit之后才会触发CellValueChanged事件。private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex 0 || e.ColumnIndex ! colStatus.Index) return; DataGridViewRow row dataGridView1.Rows[e.RowIndex]; int newValue Convert.ToInt32(row.Cells[e.ColumnIndex].Value); // 根据新值做后续逻辑 }记住CurrentCellDirtyStateChanged负责触发提交CellValueChanged负责响应提交后的值变化两个事件配在一起才完整。5.4 大数据量下编辑卡顿的排查方向如果你有几千行、且每行动态生成选项最容易卡的地方就是编辑控件每次绑定数据源。除了前面说的缓存方案外还可以考虑把DataGridView切换到虚拟模式只在你需要的列上使用ComboBoxCell这样能显著降低创建控件的开销。另一个性能隐患是绑定数据的List频繁被修改。比如你每行都创建一个新的List实例给DataSourceComboBox内部会重建ComboBoxItem列表这个动作非常慢。正确做法是直接把行内可能用到的选项实例复用在DataTable或对象集合中靠ValueMember区分选择结果而不是每次编辑都创建新的选项对象。5.5 下拉列表事件被同事改挂的“复活”问题这个属于经验里的经验。DataGridView的EditingControl是内部复用的同一个ComboBox可能在N次编辑会话中反复使用。如果代码里用匿名委托或者 lambda 绑定事件你会发现即使调用了-也解不掉。建议所有事件处理器都写成命名方法不要用 lambda 直接在事件里挂。这样你才能在CellEndEdit里精确地解绑。我遇到过最惨的一次是团队里有人为了省事在EditingControlShowing里写了一个lambdacombo.SelectedIndexChanged (s, args) { ... };结果运行一个下午后内存占用肉眼可见地飙高因为每次进入编辑都挂了一个新匿名委托退出时根本解不掉。最后我改成命名方法才把这个内存泄漏清零。6. 进阶级联下拉与自定义搜索过滤6.1 两个下拉列的联动级联下拉也是很常见的需求。比如A列选择“PLC类型”B列的可选项根据A列的选择动态变化。实现思路其实不难A列用DataGridViewComboBoxColumnB列也用DataGridViewComboBoxColumn选项集合根据A列值变化重新设置。private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex 0 || e.ColumnIndex ! colPlcType.Index) return; DataGridViewComboBoxColumn colProtocol (DataGridViewComboBoxColumn)dataGridView1.Columns[colProtocol.Index]; string plcType dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString(); colProtocol.DataSource GetProtocolsByPlcType(plcType); }注意这里有个细节DataGridViewComboBoxColumn的DataSource是列级共享的如果你只修改某一行的B列DataSource会影响到所有行。所以在级联场景中如果每一行的B列选项都不一样建议方案还是回到Dynamic方案把B列做成文本列在EditingControlShowing里动态设置ComboBox选项。6.2 让下拉列表支持输入过滤有时候选项过多用户希望输入关键字快速定位。标准ComboBox默认支持打开后输入第一个字母跳转但不支持模糊过滤。要实现“输入即过滤”可以自定义编辑控件继承DataGridViewTextBoxEditingControl接上ComboBox的TextChanged逻辑或者在ComboBox的DropDown事件里用后台线程筛选并按新的DataSource重新绑定。自定义控件写起来不短但思路不算难public class FilterComboBox : ComboBox { public FilterComboBox() { this.DropDownStyle ComboBoxStyle.DropDown; this.AutoCompleteMode AutoCompleteMode.SuggestAppend; this.AutoCompleteSource AutoCompleteSource.ListItems; } protected override void OnTextChanged(EventArgs e) { base.OnTextChanged(e); // 在这里对DataSource做过滤 } }实际项目中如果行数量不多我用标准的AutoComplete属性就够了。当数据量上万才考虑自绘和过滤控件。做之前先想清楚自己的实时数据量级别一开始就上重武器。6.3 一个值得深思的设计建议讲完技术实现最后提一个我自己的体会。DataGridView里放下拉列表的初衷是约束数据输入但很多时候我们忽略了另一个问题用户需要知道“为什么这一项不能手输、这一列又必须从列表选”。如果界面上没有明显的视觉暗示比如下拉按钮不明显、选项范围不加说明用户会以为程序坏了。我现在的习惯是所有下拉列都会把列头的ToolTip写清楚说明该列取值范围对于有联动关系的列在CellToolTipTextNeeded事件里根据当前行状态动态给提示。这些看起来不起眼的小事对交付项目的用户体验影响其实很大。DataGridView的下拉列表功能说难不难但牵扯到编辑控件机制、数据绑定、事件提交、性能优化几个层面。我写这篇时尽量把每一步的“为什么”讲透了你照着上面的代码走一遍再结合自己的业务调整选项集合基本能覆盖绝大多数需求。以后遇到“DataGridView加下拉列表”的需求不用再满世界翻帖子了。本文还有配套的精品资源点击获取
返回列表