数据表单与自定义列
开发专题 · Access 数据源 + DataGridView 增删改查与自定义列
本页以 Microsoft Access(.mdb)为数据源,主讲传统控件 System.Windows.Forms.DataGridView(工具箱显示名:数据表格视图):连接与填充、绑定增删改查,以及按钮 / 图片 / 进度条 / 下拉四类自定义列。面向 .NET Framework 4.8;凡有代码均同时给出 C# 5.0 与 VB.NET。设计器文案以 LCode 中文界面为准。
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。连接串与查询可抽到辅助方法,便于增删改复用同一适配器。
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();
}
}
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 并手工加列 |
.mdb / .accdb 路径存在且进程有读权限。更细的连接排错留给 数据库 · Access。
增删改查操作流程
绑定模式下,网格编辑即改 DataTable 行状态;点「保存」时调用 _adapter.Update(_table) 写回 Access。「刷新」重新 Fill。「新增」可在网格末行输入(须允许用户添加行),或代码 Rows.Add。「删除」删除当前行后同样靠 Update 提交。
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();
}
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
| 步骤 | 要点 |
|---|---|
| 查 | Fill → DataSource;需要条件查询时改 SQL 或带 OleDbParameter |
| 增 | DataTable.NewRow + Rows.Add,或允许网格 AllowUserToAddRows |
| 改 | 在单元格直接编辑;保存前 EndEdit / EndCurrentEdit |
| 删 | 经 DataBoundItem 取 DataRowView,对 DataRow.Delete() 标记删除;勿用 Rows.Remove(绑定表时 Update 无法持久化) |
| 提交 | OleDbDataAdapter.Update(DataTable);成功后可 AcceptChanges |
Fill)。只读展示可把列 ReadOnly = true,或不用 CommandBuilder、仅查询。
自定义列
自定义列前建议:AutoGenerateColumns = false,先按字段加文本列,再追加特殊列;或保留自动列后按索引/名称调整。下列四类各给最小双语言片段与属性对照。
按钮列(DataGridViewButtonColumn)
行内操作(编辑、删除确认等)。点击在 CellContentClick(或 CellClick)中判断列名。
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), "详情");
}
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 写入图片列(不直接绑路径字符串)。
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);
}
}
}
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 |
进度条列(CellPainting)
标准库没有内置 DataGridViewProgressBarColumn。常用做法:绑定整型字段的文本列,在 CellPainting 中按 0–100 绘制进度条(亦可自行封装列类型,思路相同)。
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;
}
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 为可选列表。
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);
}
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 代码加列
| 方式 | 做法 | 适用 |
|---|---|---|
| 设计器 | 选中网格 → 属性面板「列」集合(或智能标记「编辑列」)→ 添加列,选类型(文本 / 按钮 / 图片 / 下拉等),设 Name、HeaderText、DataPropertyName |
列结构固定、希望所见即所得 |
| 代码 | AutoGenerateColumns = false,Columns.Add(...);进度条等无设计器类型时用代码 + 事件 |
列随运行条件变化,或需自绘列 |
| 混合 | 设计器放基础列,在 Load 里追加按钮列并挂事件 |
常见业务窗体 |
DataPropertyName 必须与 DataTable 列名一致(如 Name、Category)。仅显示、不绑定的列(如按钮列)不要设 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 线程完成;耗时查询勿卡死界面时参见后续「异步、同步与多线程」专题 |
相关阅读
- 数据库 · Access — 连接、提供程序与增删改查 API
- KryptonDataGridView — 现代化主题表格(控件手册)
- 资源文件 — 图片嵌入与落地读取
- C# / VB.NET 窗体应用入门 — 新建窗体项目
- 引用 DLL — 需额外程序集时的添加方式