C# WinForm中向DataGridView添加PictureBox控件报错解决方法
问题原因
DataGridViewCell类型没有Controls属性,控件无法直接挂载到单元格对象上,原代码中dataGrid.Rows[i].Cells[3].Controls.Add()的写法本身不成立。除此之外原代码还有两个会导致运行异常的隐性问题:
- 仅实例化了1个查看按钮、1个删除按钮,WinForm控件不能同时归属多个父容器,就算挂载路径正确,最终按钮也只会显示在最后一行
- for循环边界写为
i <= dataGrid.Rows.Count,会触发索引越界:行索引从0开始,最大有效索引为Rows.Count - 1;如果DataGridView开启了AllowUserToAddRows,末尾自动生成的空白新行也不需要添加操作按钮。
正确实现方案
不要往单元格上挂载控件,直接把控件添加到DataGridView本身的控件集合中,在数据绑定完成后根据单元格的实际显示位置,计算每个按钮的坐标和大小,逐行生成独立的按钮实例即可。同时需要处理滚动、控件大小变化场景下的按钮位置刷新,避免错位。
修正后的完整代码如下:
private void InitializeComponent() { dataGrid = new DataGridView(); dataGrid.DataSource = this._lstWorklet; // 绑定事件避免滚动、调整控件尺寸时按钮错位 dataGrid.Scroll += (s, ev) => { RefreshActionButtonsPosition(); }; dataGrid.SizeChanged += (s, ev) => { RefreshActionButtonsPosition(); }; dataGrid.ColumnWidthChanged += (s, ev) => { RefreshActionButtonsPosition(); }; dataGrid.DataBindingComplete += new System.Windows.Forms.DataGridViewBindingCompleteEventHandler(dataGrid_DataBindingComplete); } // 统一刷新操作按钮位置 private void RefreshActionButtonsPosition() { // 先清理之前生成的操作按钮,避免重复生成 foreach (Control ctl in dataGrid.Controls.OfType<PictureBox>().Where(p => p.Tag?.ToString() == "GridActionBtn").ToList()) { dataGrid.Controls.Remove(ctl); ctl.Dispose(); } // 逐行生成按钮,跳过未提交的空白新行 for (int i = 0; i < dataGrid.Rows.Count; i++) { DataGridViewRow row = dataGrid.Rows[i]; if (row.IsNewRow) continue; DataGridViewCell actionCell = row.Cells[3]; Rectangle cellRect = dataGrid.GetCellDisplayRectangle(3, i, true); // 不在可视区域的单元格跳过,优化性能 if (!actionCell.Displayed || cellRect.IsEmpty) continue; // 为当前行生成独立的查看按钮 PictureBox btnView = new PictureBox(); btnView.Tag = (row.DataBoundItem, "View"); btnView.Size = new Size(24, 24); btnView.BackgroundImage = ProcessingDataModel._viewImg; btnView.BackgroundImageLayout = ImageLayout.Center; btnView.Cursor = Cursors.Hand; btnView.Click += btnViewRecording_click; // 计算位置:单元格内水平边距10px,垂直居中 btnView.Location = new Point(cellRect.X + 10, cellRect.Y + (cellRect.Height - btnView.Height)/2); new ToolTip().SetToolTip(btnView, "查看录音"); // 为当前行生成独立的删除按钮 PictureBox btnDelete = new PictureBox(); btnDelete.Tag = (row.DataBoundItem, "Delete"); btnDelete.Size = new Size(24, 24); btnDelete.BackgroundImage = ProcessingDataModel._deleteImg; btnDelete.BackgroundImageLayout = ImageLayout.Center; btnDelete.Cursor = Cursors.Hand; btnDelete.Click += btnDeleteRecording_click; // 放在查看按钮右侧,留8px间距 btnDelete.Location = new Point(btnView.Right + 8, cellRect.Y + (cellRect.Height - btnDelete.Height)/2); new ToolTip().SetToolTip(btnDelete, "删除录音"); // 按钮添加到DataGridView的控件集合,而非单元格 dataGrid.Controls.Add(btnView); dataGrid.Controls.Add(btnDelete); } } private void dataGrid_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e) { // 配置操作列基础属性 dataGrid.Columns[3].Name = "Action"; dataGrid.Columns[3].HeaderText = "操作"; dataGrid.Columns[3].DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; // 生成操作按钮 RefreshActionButtonsPosition(); }
点击事件取值示例
按钮的Tag中已经存储了当前行绑定的业务对象,点击时不需要额外计算行索引,直接取数据即可:
private void btnViewRecording_click(object sender, EventArgs e) { if (sender is PictureBox btn && btn.Tag is (WorkletModel item, string action) && action == "View") { // 此处编写查看录音逻辑,item为当前行绑定的业务实体 MessageBox.Show($"查看对应录音:{item.RecordPath}"); } } private void btnDeleteRecording_click(object sender, EventArgs e) { if (sender is PictureBox btn && btn.Tag is (WorkletModel item, string action) && action == "Delete") { if(MessageBox.Show("确认删除该条录音?", "操作提示", MessageBoxButtons.YesNo) == DialogResult.Yes) { // 此处编写删除录音逻辑,操作完成后重新绑定数据源即可 } } }
注:如果项目使用4.7以下版本的.NET Framework,不支持值元组语法,可以将行索引存在按钮Tag中,点击时通过
dataGrid.Rows[索引].DataBoundItem获取对应行数据即可。
内容的提问来源于stack exchange,提问作者Dhruvi Parikh
相关产品推荐
相关产品推荐

