Winform之DataGridView的浅见

354 阅读8分钟

Winform之DataGridView

DataGridView其实说白了就是一个表格控件,前几天接触Winform总觉得DataGridView很难用,这里就当是做一个学习记录吧。

DataGridView 列

这里的列,我更愿意称之为表头,在拖入DataGridView后,可以通过添加列的方式确定表头(如果想要通过绑定的数据源自动生成,可以设置AutoGenerateColumns=true,当然这种方式并不推荐,因为自动生成方式具有局限性,所以我们一般设置AutoGenerateColumns=false,并手动绑定列的DataPropertyName)。

屏幕截图 2024-11-15 134321.png

image.png

(a) 基础设置(b) 添加列

当然,也可以通过编辑列来更加快捷方便地管理表头。

  • 启用添加,等同于DataGridView中的AllowUserToAddRows置为true,否则置为false

  • 启动编辑,等同于DataGridView中的ReadOnly置为false,否则置为true

  • 启用删除,等同于DataGridView中的AllowUserToDeleteRows置为true,否则置为false

  • 启用列重新排序,等同于DataGridView中的AllowUserToOrderColumns置为true,否则置为false

列类型

言归正传,既然知道了如何添加列,那么就来看一下列的类型,毕竟不是所有的列都是用于显示数据或更改数据的。

  • 如果我们想显示数据(设置该列的ReadOnlytrue)或后续编辑数据(设置该列的ReadOnlyfalse),那么就使用DataGridViewTextBoxColumn

  • 如果我们想在每行的某一列放置一个按钮,那么就使用DataGridViewButtonColumn

  • 如果我们想在每行的某一列放置一个复选框,那么就使用DataGridViewCheckBoxColumn

  • 如果我们想在每行的某一列放置一个下拉框,那么就使用DataGridViewComboBoxColumn

  • 如果我们想在每行的某一列放置一个链接,那么就使用DataGridViewLinkColumn

  • 如果我们想在每行的某一列放置一张图片或者一个Icon,那么就使用DataGridViewImageColumn

在设置列属性时,存在下列情形:

  • 该列的单元格可编辑,那么ReadOnly需要设置为true

  • 如果所有列的列宽相加超出了DataGridView的实际宽度,那么将会出现水平滚动条,想要固定某列(一般为ID列【唯一值】),可以将该列的Frozen设置为false

  • 如果想自动调整列的宽度,可以设置AutoSizeMode,可选类型有很多,具体哪一类型有怎么样的效果【我也不太清楚】,其中若设置为Fill,可以通过FillWeight调整自动列宽的比例,比如列A设置为100,列B设置为100,列C设置为100,列D设置为100,列E设置为100,那么列ABCDE分别各占20%,当列A设置为50时,列A占50/450,即1/9,其余列占2/9

  • 如果想要调整外观(例如对齐方式和字体),可以设置DefaultCellStyle,当然为了保证表头外观的一致性,可以直接设置DataGridView的RowHeadersDefaultCellStyle

  • 将数据源与该列绑定可以通过DataPropertyName进行绑定。

  • 【特别->复选框】有ThreeState属性,可以存在三个复选状态

  • 【特别->链接】存在链接的相关属性,这里不做叙述。

  • 【特别->下拉框】应该是所有类型中设置最繁琐的一个,可以为下拉数据设置items、DataSource等。

  • 【特别->图片】,这里需要注意,其余列在未赋值前都是null,而图片列未赋值前不是null,而是bitmap类型的数据(一张带方框的 × 图片)。

DataGridView 行

这里的行,我指的其实就是实际的数据了。

格式控制

行的格式控制可以通过DefaultCellStyle设置,奇数行可以通过AlternatingRowsDefaultCellStyle设置。

新行

  • 想要在表格中新增数据,可以启用添加AllowUserToAddRows=true
  • 新增数据行的默认填充可以通过编写事件DefaultValuesNeeded来实现。
  • 需要注意的一点是,任何一行作为新行的生命周期是非常短暂的,在用户对该行任意一列执行输入时,该行的新行属性就会被抹除(IsNewRow的值为false),但与此同时,新的一行也会产生,也被赋予短暂的新行的使命。在最后一行的最前面存在着一个*星号,表示该行为新行。

DataGridView 数据绑定

这几天的学习下来,我目前采用了两种绑定方式(不涉及数据库),一种是通过List,一种是BindingList。List是一种相对更灵活操作的数据类型,而BindingList的优势则在于可以双向绑定,但在一些复杂场景中想要实现一些特殊需求就显得不够灵活。【这里的所有实现仅是我个人的拙见,不一定对 :joy: 】

List 数据绑定

采用List这一数据类型时,为了灵活操作,我并没有将DataGridView的数据源进行配置,而是通过事件来更新List中的数据。以一种较为常见的数据输入场景举例,每行数据有五列,其中A列是序号,B列是名称,C列是值,D列是候选项(勾选),E列描述前四列中输入是否正确【可使用三种icon来表示,未输入使用info32,输入错误使用error32,输入正确且未提交使用warning32,输入正确且已提交使用success32】。

  • icon颜色记录:
// #52C41A 绿色(success)
// #FF3838 红色(error)
// #FAAD14 橙色(warning)
// #1677FF 蓝色(info)
.CellClick 与 .CellContentClick

CellClick在单元格的任何部分被单击时触发,而CellContentClick在单击单元格内容时触发。

这两种事件的应用场景是点击单元格产生某种响应。比如按钮、文字、复选框、下拉框、图片等引发后续其他操作。

那么为什么会有CellClickCellContentClick的区分呢?CellClick明显比CellContentClick的作用范围更大,只要是焦点处于单元格范围内都认定为触发有效,而CellContentClick在点击“空白部分”时会出现点击失效的情况,这也导致99%的业务场景下都会使用CellClick而非ceLLContentClick,除非你的确有非常精细的点击需求时才会用到CellContentClick

由于我目前没有接触到需要用到这两种事件的业务需求,所以这里不展开描述,感兴趣的可以参考DataGridView.CellContentClick 事件DataGridView.CellClick 事件

.CellValueChanged

CellValueChanged事件可以对单元格内容改变而做出响应。而这种响应是存在滞后性的(在使用CellClickCellContentClick点击复选框之类的事件中也存在),这种滞后性在点击复选框事件中更为严重,例如点击某一个尚未被勾选的复选框(false),其被勾选后值变为true,当我们在CellClick事件的响应中加入弹窗显示点击后的值时,弹窗显示的内容却是勾选前的值false,所以CellClick触发的CellValueChanged的提交是存在滞后的,同步的办法也很简单,只需要为MyGridView添加CurrentCellDirtyStateChanged事件。

private void MyGridView_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
    // 单元格未提交时,实时检测
    if (MyGridView.IsCurrentCellDirty)
    {
        MyGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
    }
}

以上是CellClick的局限,而这种局限在CellValueChanged同样存在,比如我们想要去监测一个单元格中输入的数据的正确性,如果不加CurrentCellDirtyStateChanged,我们只能在完成输入后单击其他单元格以完成提交并弹窗提示是否输入正确,如果利用CurrentCellDirtyStateChanged,我们可以在每行的最后一列中(也就是我们前面提到的E列)通过不同的icon来描述当前行的所有数据输入是否正确,考虑到数据与数据之间可能存在联系,所以实时对list进行数据修改是不够现实的,因此可以使用CellValueChanged描述整行数据的正确性,然后通过RowValidating来完成对整行数据的提交。

.RowValidating

行验证(RowValidating),如果没有加入CurrentCellDirtyStateChanged,行内所有列的单元格数据的合法性可以在RowValidating中实现,不过正如我们前面所说,这种方式对用户不够友好。所以这里我们仅用于做数据提交。数据提交可以分为两部分来完成,一部分是新增数据,一部分是编辑数据。新增数据就是List中并不存在数据,e.RowIndex应该与当前MyList.Count的值相等,当e.RowIndex比当前MyList.Count的值小时,证明该数据是已存在的数据,那么可以直接覆盖旧数据(或通过与旧数据对比,将不一致的数据进行更新)。

.DefaultValuesNeeded

上面所写的都是针对DataGridView中已经存在的行进行的操作,而List最开始是没有数据的,所以我们需要产生一个新行(带默认值),且每次行验证后也需要产生新行(将AllowUserToAddRows置为true),新行的产生也会伴随着CellValueChanged的触发,这时最后一列的所描述的输入状态也必然会随之发生更改,更改后又会触发CellValueChanged,不断重复陷入循环,因此对于新行填充前我们需要暂时取消对CellValueChanged事件的订阅,在填充完成后再次订阅。

  • 新行填充使用DefaultValuesNeeded
// 取消订阅CellValueChanged
MyGridView.CellValueChanged -= MyGridView_CellValueChanged;
// A列 int
e.Row.Cells["A"].Value = SpansCount + 1;
// B列 string
e.Row.Cells["B"].Value = (SpansCount + 1).ToString() + "号名称";
// C列 int/double······
e.Row.Cells["C"].Value = 0;
// D列 boolean
e.Row.Cells["D"].Value = true;
// E列 bitmap
e.Row.Cells["E"].Value = Properties.Resources.info32;
// 重新订阅CellValueChanged
MyGridView.CellValueChanged += MyGridView_CellValueChanged;

BindingList数据绑定

采用BindingList进行数据绑定就比较简单一些,对于BindingList,我仅用于显示一些固定的数据,特点是:不需要新增或依赖其他数据进行被动新增、不需要删除或依赖其他数据进行被动删除、允许修改(不允许修改的数据设置为只读)。

针对BindingList数据,只需要按照前面我们了解过的列绑定、数据源绑定就可以一步到位(无需绑定其他事件)。