本页以 Microsoft Access.mdb)为数据源,主讲传统控件 System.Windows.Forms.DataGridView(工具箱显示名:数据表格视图):连接与填充、绑定增删改查,以及按钮 / 图片 / 进度条 / 下拉四类自定义列。面向 .NET Framework 4.8;凡有代码均同时给出 C# 5.0 与 VB.NET。设计器文案以 LCode 中文界面为准。

与 Krypton:需要与整体主题一致的表格外观时,可改用 KryptonDataGridView(工具箱显示名:数据表格),API 与标准 DataGridView 兼容,绑定与自定义列写法基本相同。详见表控件手册 KryptonDataGridView
手册页内说明表:本页下方各 prop-table 使用 XP 灰表头/网格线,仅用于手册排版对照;不等于运行时 DataGridView 的系统主题外观。控件运行时样式由 Windows / Krypton 主题决定。

场景与前置

说明(以 LCode / 示例工程惯用为准)
网格控件 System.Windows.Forms.DataGridView(工具箱:数据表格视图)
数据访问程序集 System.Data(含 System.Data.OleDb)。窗体模板默认已引用,一般无需再添加 DLL
提供程序(.mdb) Microsoft.Jet.OLEDB.4.0(LCode 示例工程 OrderManage 等惯用)
提供程序(.accdb) Microsoft.ACE.OLEDB.12.0(须本机安装 Access Database Engine;位数与进程一致)
平台 Jet 4.0 为 32 位:工程宜 x86,或 AnyCPU 勾选「首选 32 位」(Prefer32Bit)
示例库文件 下文用相对 EXE 的 Data\demo.mdb 与表 Products(字段见下表);完整 Access 专题见 数据库 · Access

假定表 Products 字段:

字段 类型(示例) 用途
Id 自动编号 / 整型主键 行标识;删除/更新条件
Name 文本 名称
Category 文本 下拉列绑定值
Progress 整型 0–100 进度条列
ImagePath 文本 相对路径,供图片列加载

连接 Access 并填充 DataTable

在窗体上放置 DataGridView(名如 dgvProducts)与若干按钮。用 OleDbConnection + OleDbDataAdapter 填充 DataTable,再赋给 DataSource。连接串与查询可抽到辅助方法,便于增删改复用同一适配器。

C#
using System;
using System.Data;
using System.Data.OleDb;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

public partial class MainForm : Form
{
    private OleDbDataAdapter _adapter;
    private DataTable _table;
    private OleDbConnection _conn;

    private string GetConnectionString()
    {
        string path = Path.Combine(Application.StartupPath, "Data", "demo.mdb");
        return "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + path
            + ";Persist Security Info=False;";
    }

    private void LoadProducts()
    {
        if (_conn != null)
        {
            _conn.Dispose();
            _conn = null;
        }
        _conn = new OleDbConnection(GetConnectionString());
        _adapter = new OleDbDataAdapter("SELECT Id, Name, Category, Progress, ImagePath FROM Products", _conn);
        OleDbCommandBuilder builder = new OleDbCommandBuilder(_adapter);
        _adapter.InsertCommand = builder.GetInsertCommand();
        _adapter.UpdateCommand = builder.GetUpdateCommand();
        _adapter.DeleteCommand = builder.GetDeleteCommand();

        _table = new DataTable();
        _adapter.Fill(_table);
        this.dgvProducts.AutoGenerateColumns = true;
        this.dgvProducts.DataSource = _table;
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
        LoadProducts();
    }
}
VB.NET
Imports System
Imports System.Data
Imports System.Data.OleDb
Imports System.Drawing
Imports System.IO
Imports System.Windows.Forms

Public Class MainForm
    Inherits Form

    Private _adapter As OleDbDataAdapter
    Private _table As DataTable
    Private _conn As OleDbConnection

    Private Function GetConnectionString() As String
        Dim path As String = Path.Combine(Application.StartupPath, "Data", "demo.mdb")
        Return "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & path &
            ";Persist Security Info=False;"
    End Function

    Private Sub LoadProducts()
        If _conn IsNot Nothing Then
            _conn.Dispose()
            _conn = Nothing
        End If
        _conn = New OleDbConnection(GetConnectionString())
        _adapter = New OleDbDataAdapter(
            "SELECT Id, Name, Category, Progress, ImagePath FROM Products", _conn)
        Dim builder As New OleDbCommandBuilder(_adapter)
        _adapter.InsertCommand = builder.GetInsertCommand()
        _adapter.UpdateCommand = builder.GetUpdateCommand()
        _adapter.DeleteCommand = builder.GetDeleteCommand()

        _table = New DataTable()
        _adapter.Fill(_table)
        Me.dgvProducts.AutoGenerateColumns = True
        Me.dgvProducts.DataSource = _table
    End Sub

    Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        LoadProducts()
    End Sub
End Class
成员 / 项 说明
OleDbConnection 打开 Access;连接串须含 Provider 与 Data Source
OleDbDataAdapter Fill 填表;配合 OleDbCommandBuilder 生成增删改命令后可 Update
DataTable 内存行集;网格的 DataSource
AutoGenerateColumns true 时按字段自动生成文本列;自定义列见后文,常改为 false 并手工加列
注意:若提示找不到提供程序,请确认:(1) 使用 Jet 时进程为 32 位;(2) 使用 ACE 时已安装对应位数的 Access Database Engine;(3) .mdb / .accdb 路径存在且进程有读权限。更细的连接排错留给 数据库 · Access

增删改查操作流程

绑定模式下,网格编辑即改 DataTable 行状态;点「保存」时调用 _adapter.Update(_table) 写回 Access。「刷新」重新 Fill。「新增」可在网格末行输入(须允许用户添加行),或代码 Rows.Add。「删除」删除当前行后同样靠 Update 提交。

C#
private void btnAdd_Click(object sender, EventArgs e)
{
    DataRow row = _table.NewRow();
    row["Name"] = "新商品";
    row["Category"] = "未分类";
    row["Progress"] = 0;
    row["ImagePath"] = "";
    _table.Rows.Add(row);
}

private void btnDelete_Click(object sender, EventArgs e)
{
    if (this.dgvProducts.CurrentRow == null || this.dgvProducts.CurrentRow.IsNewRow)
    {
        return;
    }
    // 绑定 DataTable 时须标记行删除,Rows.Remove 不会产生 Deleted 状态,Update 无法持久化
    DataRowView view = this.dgvProducts.CurrentRow.DataBoundItem as DataRowView;
    if (view != null)
    {
        view.Row.Delete();
    }
}

private void btnSave_Click(object sender, EventArgs e)
{
    this.dgvProducts.EndEdit();
    BindingContext[_table].EndCurrentEdit();
    _adapter.Update(_table);
    _table.AcceptChanges();
    MessageBox.Show("已保存到 Access。", "保存", MessageBoxButtons.OK, MessageBoxIcon.Information);
}

private void btnRefresh_Click(object sender, EventArgs e)
{
    LoadProducts();
}
VB.NET
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
    Dim row As DataRow = _table.NewRow()
    row("Name") = "新商品"
    row("Category") = "未分类"
    row("Progress") = 0
    row("ImagePath") = ""
    _table.Rows.Add(row)
End Sub

Private Sub btnDelete_Click(sender As Object, e As EventArgs) Handles btnDelete.Click
    If Me.dgvProducts.CurrentRow Is Nothing OrElse Me.dgvProducts.CurrentRow.IsNewRow Then
        Return
    End If
    ' 绑定 DataTable 时须标记行删除,Rows.Remove 不会产生 Deleted 状态,Update 无法持久化
    Dim view As DataRowView = TryCast(Me.dgvProducts.CurrentRow.DataBoundItem, DataRowView)
    If view IsNot Nothing Then
        view.Row.Delete()
    End If
End Sub

Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
    Me.dgvProducts.EndEdit()
    BindingContext(_table).EndCurrentEdit()
    _adapter.Update(_table)
    _table.AcceptChanges()
    MessageBox.Show("已保存到 Access。", "保存", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub

Private Sub btnRefresh_Click(sender As Object, e As EventArgs) Handles btnRefresh.Click
    LoadProducts()
End Sub
步骤 要点
FillDataSource;需要条件查询时改 SQL 或带 OleDbParameter
DataTable.NewRow + Rows.Add,或允许网格 AllowUserToAddRows
在单元格直接编辑;保存前 EndEdit / EndCurrentEdit
DataBoundItemDataRowView,对 DataRow.Delete() 标记删除;勿用 Rows.Remove(绑定表时 Update 无法持久化)
提交 OleDbDataAdapter.Update(DataTable);成功后可 AcceptChanges
提示:自动编号主键插入后若要立刻看到库生成的 Id,保存后建议再执行一次刷新(重新 Fill)。只读展示可把列 ReadOnly = true,或不用 CommandBuilder、仅查询。

自定义列

自定义列前建议:AutoGenerateColumns = false,先按字段加文本列,再追加特殊列;或保留自动列后按索引/名称调整。下列四类各给最小双语言片段与属性对照。

按钮列(DataGridViewButtonColumn)

行内操作(编辑、删除确认等)。点击在 CellContentClick(或 CellClick)中判断列名。

C#
private void AddButtonColumn()
{
    DataGridViewButtonColumn col = new DataGridViewButtonColumn();
    col.Name = "colAction";
    col.HeaderText = "操作";
    col.Text = "详情";
    col.UseColumnTextForButtonValue = true;
    col.Width = 70;
    this.dgvProducts.Columns.Add(col);
}

private void dgvProducts_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
    if (e.RowIndex < 0)
    {
        return;
    }
    if (this.dgvProducts.Columns[e.ColumnIndex].Name != "colAction")
    {
        return;
    }
    object idObj = this.dgvProducts.Rows[e.RowIndex].Cells["Id"].Value;
    MessageBox.Show("当前行 Id = " + Convert.ToString(idObj), "详情");
}
VB.NET
Private Sub AddButtonColumn()
    Dim col As New DataGridViewButtonColumn()
    col.Name = "colAction"
    col.HeaderText = "操作"
    col.Text = "详情"
    col.UseColumnTextForButtonValue = True
    col.Width = 70
    Me.dgvProducts.Columns.Add(col)
End Sub

Private Sub dgvProducts_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) _
    Handles dgvProducts.CellContentClick
    If e.RowIndex < 0 Then
        Return
    End If
    If Me.dgvProducts.Columns(e.ColumnIndex).Name <> "colAction" Then
        Return
    End If
    Dim idObj As Object = Me.dgvProducts.Rows(e.RowIndex).Cells("Id").Value
    MessageBox.Show("当前行 Id = " & Convert.ToString(idObj), "详情")
End Sub
属性 / 事件 说明
Text 按钮上显示的文字(配合 UseColumnTextForButtonValue = true
UseColumnTextForButtonValue true 时各行按钮同文;false 时可按单元格 Value 不同文案
FlatStyle 按钮平面样式
CellContentClick 点击按钮内容时触发;务必判断 RowIndex 与列 Name

图片列(DataGridViewImageColumn)

把路径字段转成 Image 再绑定,或在绑定完成后按行填充。下面在加载后根据 ImagePath 写入图片列(不直接绑路径字符串)。

C#
private void SetupImageColumn()
{
    this.dgvProducts.AutoGenerateColumns = false;
    this.dgvProducts.Columns.Clear();

    this.dgvProducts.Columns.Add(new DataGridViewTextBoxColumn()
    {
        Name = "Id",
        DataPropertyName = "Id",
        HeaderText = "编号",
        Width = 60
    });
    this.dgvProducts.Columns.Add(new DataGridViewTextBoxColumn()
    {
        Name = "Name",
        DataPropertyName = "Name",
        HeaderText = "名称"
    });

    DataGridViewImageColumn imgCol = new DataGridViewImageColumn();
    imgCol.Name = "colThumb";
    imgCol.HeaderText = "图";
    imgCol.ImageLayout = DataGridViewImageCellLayout.Zoom;
    imgCol.Width = 64;
    this.dgvProducts.Columns.Add(imgCol);

    this.dgvProducts.DataSource = _table;
    FillThumbnails();
}

private void FillThumbnails()
{
    for (int i = 0; i < this.dgvProducts.Rows.Count; i++)
    {
        if (this.dgvProducts.Rows[i].IsNewRow)
        {
            continue;
        }
        string rel = Convert.ToString(_table.Rows[i]["ImagePath"]);
        string full = Path.Combine(Application.StartupPath, rel ?? "");
        if (!string.IsNullOrEmpty(rel) && File.Exists(full))
        {
            this.dgvProducts.Rows[i].Cells["colThumb"].Value = Image.FromFile(full);
        }
    }
}
VB.NET
Private Sub SetupImageColumn()
    Me.dgvProducts.AutoGenerateColumns = False
    Me.dgvProducts.Columns.Clear()

    Dim idCol As New DataGridViewTextBoxColumn()
    idCol.Name = "Id"
    idCol.DataPropertyName = "Id"
    idCol.HeaderText = "编号"
    idCol.Width = 60
    Me.dgvProducts.Columns.Add(idCol)

    Dim nameCol As New DataGridViewTextBoxColumn()
    nameCol.Name = "Name"
    nameCol.DataPropertyName = "Name"
    nameCol.HeaderText = "名称"
    Me.dgvProducts.Columns.Add(nameCol)

    Dim imgCol As New DataGridViewImageColumn()
    imgCol.Name = "colThumb"
    imgCol.HeaderText = "图"
    imgCol.ImageLayout = DataGridViewImageCellLayout.Zoom
    imgCol.Width = 64
    Me.dgvProducts.Columns.Add(imgCol)

    Me.dgvProducts.DataSource = _table
    FillThumbnails()
End Sub

Private Sub FillThumbnails()
    Dim i As Integer
    For i = 0 To Me.dgvProducts.Rows.Count - 1
        If Me.dgvProducts.Rows(i).IsNewRow Then
            Continue For
        End If
        Dim rel As String = Convert.ToString(_table.Rows(i)("ImagePath"))
        Dim full As String = Path.Combine(Application.StartupPath, If(rel, ""))
        If Not String.IsNullOrEmpty(rel) AndAlso File.Exists(full) Then
            Me.dgvProducts.Rows(i).Cells("colThumb").Value = Image.FromFile(full)
        End If
    Next
End Sub
属性 说明
ImageLayout Normal / Stretch / Zoom;缩略图常用 Zoom
ValuesAreIcons 单元格值为 Icon 时设 true
DefaultCellStyle.NullValue 无图时显示的占位图(可为 null
行高 显示缩略图时适当增大 RowTemplate.Height
提示:示例用 Image.FromFile;长期占用文件时可改为读入 MemoryStreamImage.FromStream。资源嵌入方式见 资源文件

进度条列(CellPainting)

标准库没有内置 DataGridViewProgressBarColumn。常用做法:绑定整型字段的文本列,在 CellPainting 中按 0–100 绘制进度条(亦可自行封装列类型,思路相同)。

C#
private void AddProgressColumn()
{
    DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn();
    col.Name = "Progress";
    col.DataPropertyName = "Progress";
    col.HeaderText = "进度";
    col.Width = 120;
    col.ReadOnly = true;
    this.dgvProducts.Columns.Add(col);
    this.dgvProducts.CellPainting += dgvProducts_CellPaintingProgress;
}

private void dgvProducts_CellPaintingProgress(object sender, DataGridViewCellPaintingEventArgs e)
{
    if (e.RowIndex < 0 || e.ColumnIndex < 0)
    {
        return;
    }
    if (this.dgvProducts.Columns[e.ColumnIndex].Name != "Progress")
    {
        return;
    }
    e.PaintBackground(e.ClipBounds, true);
    int value = 0;
    if (e.Value != null && e.Value != DBNull.Value)
    {
        value = Convert.ToInt32(e.Value);
    }
    if (value < 0) value = 0;
    if (value > 100) value = 100;

    Rectangle bar = e.CellBounds;
    bar.Inflate(-4, -6);
    bar.Width = Math.Max(1, (int)(bar.Width * value / 100.0));
    e.Graphics.FillRectangle(Brushes.SteelBlue, bar);
    TextRenderer.DrawText(
        e.Graphics,
        value.ToString() + "%",
        e.CellStyle.Font,
        e.CellBounds,
        e.CellStyle.ForeColor,
        TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
    e.Handled = true;
}
VB.NET
Private Sub AddProgressColumn()
    Dim col As New DataGridViewTextBoxColumn()
    col.Name = "Progress"
    col.DataPropertyName = "Progress"
    col.HeaderText = "进度"
    col.Width = 120
    col.ReadOnly = True
    Me.dgvProducts.Columns.Add(col)
    AddHandler Me.dgvProducts.CellPainting, AddressOf dgvProducts_CellPaintingProgress
End Sub

Private Sub dgvProducts_CellPaintingProgress(sender As Object, e As DataGridViewCellPaintingEventArgs)
    If e.RowIndex < 0 OrElse e.ColumnIndex < 0 Then
        Return
    End If
    If Me.dgvProducts.Columns(e.ColumnIndex).Name <> "Progress" Then
        Return
    End If
    e.PaintBackground(e.ClipBounds, True)
    Dim value As Integer = 0
    If e.Value IsNot Nothing AndAlso e.Value IsNot DBNull.Value Then
        value = Convert.ToInt32(e.Value)
    End If
    If value < 0 Then value = 0
    If value > 100 Then value = 100

    Dim bar As Rectangle = e.CellBounds
    bar.Inflate(-4, -6)
    bar.Width = Math.Max(1, CInt(bar.Width * value / 100.0))
    e.Graphics.FillRectangle(Brushes.SteelBlue, bar)
    TextRenderer.DrawText(
        e.Graphics,
        value.ToString() & "%",
        e.CellStyle.Font,
        e.CellBounds,
        e.CellStyle.ForeColor,
        TextFormatFlags.HorizontalCenter Or TextFormatFlags.VerticalCenter)
    e.Handled = True
End Sub
要点 说明
数据 DataPropertyName 指向 0–100 的数值字段
CellPainting 自绘背景条与百分比文字;处理完设 e.Handled = true
只读 进度由业务更新字段即可;列可 ReadOnly = true
刷新 改字段后对单元格或行 Invalidate 以重绘

下拉列(DataGridViewComboBoxColumn)

把类别等枚举值限制为下拉选项;DataPropertyName 对应数据字段,Items 为可选列表。

C#
private void AddComboColumn()
{
    DataGridViewComboBoxColumn col = new DataGridViewComboBoxColumn();
    col.Name = "Category";
    col.DataPropertyName = "Category";
    col.HeaderText = "类别";
    col.FlatStyle = FlatStyle.Flat;
    col.Items.AddRange(new object[] { "未分类", "原料", "成品", "耗材" });
    col.Width = 100;
    this.dgvProducts.Columns.Add(col);
}
VB.NET
Private Sub AddComboColumn()
    Dim col As New DataGridViewComboBoxColumn()
    col.Name = "Category"
    col.DataPropertyName = "Category"
    col.HeaderText = "类别"
    col.FlatStyle = FlatStyle.Flat
    col.Items.AddRange(New Object() {"未分类", "原料", "成品", "耗材"})
    col.Width = 100
    Me.dgvProducts.Columns.Add(col)
End Sub
属性 说明
Items 下拉可选值;须覆盖库中已有类别,否则绑定后可能显示异常
DataPropertyName 绑定到 DataTable 字段名
DisplayStyle 下拉箭头显示时机(如始终显示 / 编辑时显示)
FlatStyle 外观;与窗体风格统一时可设 Flat
DataSource(高级) 选项也可绑另一张表;此时用 DisplayMember / ValueMember

设计器中加列 vs 代码加列

方式 做法 适用
设计器 选中网格 → 属性面板「列」集合(或智能标记「编辑列」)→ 添加列,选类型(文本 / 按钮 / 图片 / 下拉等),设 NameHeaderTextDataPropertyName 列结构固定、希望所见即所得
代码 AutoGenerateColumns = falseColumns.Add(...);进度条等无设计器类型时用代码 + 事件 列随运行条件变化,或需自绘列
混合 设计器放基础列,在 Load 里追加按钮列并挂事件 常见业务窗体
绑定名:列的 DataPropertyName 必须与 DataTable 列名一致(如 NameCategory)。仅显示、不绑定的列(如按钮列)不要设 DataPropertyName,或设为空。

注意事项

主题 说明
手册表 vs 运行时网格 本手册 prop-table 的 XP 灰样式只用于文档说明表;不要把手册 CSS 色值当成必须给 DataGridView 设置的运行时皮肤
Jet / ACE 位数 Jet 4.0 → 32 位进程;ACE 须与进程同为 x86 或 x64
文件占用 Access 文件被其他进程独占时 Open / Update 会失败;开发期关闭 Access 客户端占用
CommandBuilder 要求 SELECT 含足够主键信息;复杂 SQL / 多表 Join 时请手写 Insert/Update/Delete 命令
UI 线程 填充与改网格在 UI 线程完成;耗时查询勿卡死界面时参见后续「异步、同步与多线程」专题

相关阅读