
简介一套演示C# WinForms中ListView控件绑定数据库数据的完整源码包面向.NET初学者及需要在数据展示界面中快速落地的开发者。案例基于ADO.NET连接Access数据库含.accdb数据文件清晰展示从建立连接、执行SQL查询、通过DataAdapter填充DataTable到配置ListView列头并完成数据绑定的全过程同时覆盖项选中事件的处理思路可快速理解ListView在详细信息视图下的列、项与子项映射关系。压缩包共40个文件核心为14个.cs源码文件另含.sln解决方案、.resx界面资源、.exe可运行程序及Access数据库文件便于直接打开工程调试或对照界面设计整体仅78KB轻量易用。已有825人学习下载。通过学习该工程可梳理DataClass、MyMeans.cs等辅助类的职责并借助Backup备份目录对比不同版本改动适合作为数据管理类WinForms项目的起步参考模板。1. 数据开发工具里为什么偏偏用 ListView 显示数据库数据做数据开发免不了要写查询小工具。字段可能变、表可能换今天查订单明天查流水界面要的就是“一把梭”——把 SQL 查出来的结果直接平铺在列表里。很多人第一反应是拖一个 DataGridView 上去但数据开发工具里 ListView 往往更顺手它原生不支持绑定数据源反而让你老老实实把 DataTable 转成行代码不绕1 万行数据也能快速滚起来不容易把界面卡死。这个标题要解决的就是这样一件事怎么写一段 C# 源代码让 ListView 控件稳定、高效地显示数据库查询结果。适合 WinForms 下写数据开发小工具、报表预览和上位机数据展示的开发者如果你还惦记着单元格级编辑看完第 4 章再决定要不要换 DataGridView 也不迟。2. 选 ListView 而不是 DataGridViewView 模式、SubItems 结构与性能边界ListView 在 WinForms 里常被当成“老控件”但数据开发恰恰是它的主场。它不是黑匣子数据怎么进去、怎么显示每一行都能自己控制。这一章先把三个核心概念讲透View 模式、Items/SubItems 结构、VirtualMode 的边界。理解了这三样后面写绑定代码就是流水线作业。2.1 ViewDetails数据表展示的唯一正确姿势ListView 有 5 种 View 模式但只有 Details 会显示列头和网格线。很多人把 ListView 拖到窗体上不设置 View默认是 LargeIcon然后问为什么没有列——因为这个控件默认是图标模式列头只在 Details 下存在。设置 View.Details 是第一步而且要在添加 ColumnHeader 之前做。View 模式特点数据表场景Details多列、列头、网格线可排序适用LargeIcon大图标适合文件/图片选择不适用SmallIcon小图标流式排列不适用List单列列表无列头不适用Tile大图标带多行说明依赖系统主题不适用我一般固定用 View.Details FullRowSelect GridLines 三件套。FullRowSelect 决定鼠标点任意单元格时选中整行做只读查询时这是默认选项GridLines 决定是否显示行间横线查出来的数据带网格线更容易对齐看。另一个容易被忽略的是 HideSelection设为 false 后焦点离开列表选中行仍然高亮否则用户查完数据一不留意就以为没点中。2.2 Items 与 SubItems把数据库“一行一列”映射到 ListViewListView 的行叫 ListViewItem列叫 ListViewSubItem但有个反直觉的细节SubItems 集合里的第 0 项就是 Item 本身。所以第一列的值要传给 ListViewItem 构造函数第二列起才 Add 到 SubItems。我第一次写绑定代码就在这翻车从 0 开始循环 Add结果第一列重复、最后一列被吞。正确写法是列循环从 j1 开始。这个结构直接影响绑定逻辑Columns 定义表头Items 决定行数SubItems 决定每行的第 1..n 列。还有 ListViewItem.Tag这是一个万能口袋我会把主键值塞进去双击行时取出来做下钻查询比去 SubItems 里找 ID 列快得多。注意 Tag 是 object 类型取出来时记得转成原来的类型。2.3 和 DataGridView 的一次认真对比内存、速度与适用边界并不是说 DataGridView 不好而是它更重。做数据开发工具时如果只做“查出来看看、复制几行、导出 Excel”ListView 的加载速度和滚动体验要舒服不少。对比项ListViewDataGridView绑定 DataSource不支持手动转 ListViewItem原生支持1 万行加载与滚动顺畅内存占用低默认较卡需开 VirtualMode 或关自动列宽单元格编辑需自己实现原生支持点列头排序需要写 IComparer内置单元格合并与样式弱强适合场景查询展示、工具界面、上位机表格编辑、复杂报表如果你的界面里全是只读查询和下载ListView 是更稳的选择如果要做成 Excel 那种可编辑表格别硬上 ListView第 5 章给的双击编辑只是一个轻量妥协方案复杂编辑直接换 DataGridView。2.4 VirtualMode 的适用边界几万行该选谁普通模式下 5 万行数据构造 5 万个 ListViewItem 对象要花一两秒滚动也不跟手。这时候有两种路SQL 分页一次取 500 行或者开 VirtualMode。VirtualModetrue 时ListView 只向你要当前可视区域的行通过 RetrieveVirtualItem 事件按需提供10 万行也能秒开。代价是代码复杂度上来了不能再直接操作 Items 集合Sort、Find 等很多方法也会受限制。数据开发工具里我更推荐先做分页因为 SQL 端 OFFSET 一次只取几百行内存和滚动都轻松VirtualMode 更适合结果集必须整体留在客户端的场景比如读取一个同步接口返回的固定数据。判断标准很简单数据能分页就分页不能分页再上 VirtualMode。3. 把 DataTable 绑进 ListView从连接串到通用绑定方法的完整代码这一章把标题里的“源代码”落到能编译的工程里。我按写查询工具的常规顺序来先搭 ListView UI再写数据库查询最后写通用绑定方法。以下代码基于 SQL Server 的 SqlClient换 MySQL 或 Oracle 时把命名空间和驱动换掉绑定 ListView 这段逻辑完全不用动。3.1 先定 ListView 属性FullRowSelect、GridLines 与列头在窗体 Load 事件里先做这些设置listView1.View View.Details; // 必须切到表格视图 listView1.FullRowSelect true; // 点任意单元格选整行 listView1.GridLines true; // 显示表格线 listView1.MultiSelect false; // 查询工具单行选中更清晰 listView1.HideSelection false; // 失焦后仍保留选中色 listView1.Scrollable true; // 行多时保证滚动条存在View.Details 不设置列头永远不出现这是最常见的“空列表”原因。MultiSelect 看场景如果允许用户复制多行数据再导出设 true只做下钻查询就设 false避免选中状态混乱。HideSelection 必须设 false否则列表失去焦点时选中行变灰白用户会误以为刚才没点中。3.2 查询数据库SqlDataAdapter.Fill 还是 DataReader 逐行查询部分我直接用 SqlDataAdapter因为它能一次把结果装进 DataTable方便后续绑定和二次筛选。只读展示场景下这是最小可行写法private DataTable QueryData(string sql) { string connStr Server127.0.0.1;DatabaseDemoDB;User Idsa;Password123456; PoolingTrue;Min Pool Size1;Max Pool Size50;Connection Timeout5;; using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { DataTable dt new DataTable(); adapter.Fill(dt); return dt; } }参数说明连接字符串不要写死在代码里放到 App.config 或配置中心换环境时只改配置不动代码。PoolingTrue 表示启用连接池Min Pool Size1 让进程启动时预建一个连接Max Pool Size50 防止连接数暴涨。Connection Timeout5 很关键数据库假死时界面最多等 5 秒而不是默认的 15 秒。这里有个很多人不知道的细节Fill 方法如果检测到连接是关闭状态会自己 Open执行完再恢复关闭状态所以 using 块结束后连接必然释放。如果数据量到了几十万行且只需要逐条流式处理就改用 SqlDataReader 配合 while (reader.Read())省内存但 ListView 本身扛不住几十万行 Item所以数据开发工具里 DataAdapter 基本够用。3.3 核心绑定循环ListViewItem 与 SubItems 的逐列填充绑定是全文的核心看懂这一段其余都是变体。我通常写成独立方法方便在多个窗口复用private void BindList(ListView lv, DataTable dt) { lv.BeginUpdate(); try { lv.Items.Clear(); for (int i 0; i dt.Rows.Count; i) { DataRow row dt.Rows[i]; ListViewItem item new ListViewItem(Convert.ToString(row[0])); for (int j 1; j dt.Columns.Count; j) { item.SubItems.Add(Convert.ToString(row[j])); } item.Tag row[id]; // 存主键双击行时取出来下钻 lv.Items.Add(item); } } finally { lv.EndUpdate(); } }关键参数说明列循环从 j1 开始因为第 0 列已经传给 ListViewItem 构造函数了再 Add 一次会让列错位。所有值都包一层 Convert.ToString 而不是直接传 object是为了让 DBNull 变成空字符串而不是类型转换异常。ListViewItem 和 SubItems 的 Text 都是 string 类型直接传 object 数组进去会被隐式 ToString但遇到复杂的自定义类型可能输出类名统一转换最保险。BeginUpdate 和 EndUpdate 必须成对而且 EndUpdate 要放在 finally 里。如果绑定中途抛出异常EndUpdate 不执行控件会一直处于挂起重绘的状态表现为怎么刷新界面都不重绘只能关窗体重新开。这个坑我在 4.1 还会细说。3.4 通用化改造动态列头、列宽与 SQL 别名上面的版本要求窗体上已经手动建好 Columns。数据开发工具里字段经常变更实用的是根据 DataTable 自动生成列头。改造后的方法长这样private void BindListAuto(ListView lv, DataTable dt) { lv.BeginUpdate(); try { lv.Items.Clear(); lv.Columns.Clear(); for (int c 0; c dt.Columns.Count; c) { lv.Columns.Add(dt.Columns[c].ColumnName, 120); } foreach (DataRow row in dt.Rows) { ListViewItem item new ListViewItem(Convert.ToString(row[0])); for (int c 1; c dt.Columns.Count; c) { item.SubItems.Add(Convert.ToString(row[c])); } lv.Items.Add(item); } if (lv.Columns.Count 0) lv.Columns[lv.Columns.Count - 1].Width -2; } finally { lv.EndUpdate(); } }列宽统一定 120 只是起步最后一列设成 -2 表示自动填充 ListView 剩余宽度窗口拉宽时最后一列跟着变宽不会留一大片空白。-1 是自动按内容调整宽度但内容很多时性能差只读查询不建议用。这里有个实用技巧数据库字段名通常是 id、user_name 这种直接当列头不好看。在 SQL 里用别名解决SELECT id AS 编号, user_name AS 用户名DataTable 的 ColumnName 就变成了中文列头自动显示中文。别在绑定代码里做中文映射维护起来太痛苦。3.5 异步加载数据慢的时候给用户一个台阶如果 SQL 本身要跑几秒界面会直接卡死。WinForms 里的标准解法是 async/await 加 Task.Run把数据库查询移到线程池UI 线程不阻塞。代码很简单private async void btnLoad_Click(object sender, EventArgs e) { btnLoad.Enabled false; listView1.Items.Clear(); listView1.Items.Add(new ListViewItem(查询中...)); try { DataTable dt await Task.Run(() QueryData(CurrentSql)); BindListAuto(listView1, dt); } catch (Exception ex) { MessageBox.Show(查询失败 ex.Message); } finally { btnLoad.Enabled true; } }await Task.Run 执行完后代码会自动回到 UI 线程上下文所以 BindListAuto 可以直接操作 ListView不会触发跨线程异常。CurrentSql 是界面上一个 TextBox 里的查询语句让用户自己输入 SQL这是数据开发工具最常见的形态。加载提示先显示“查询中...”BindListAuto 里的 Items.Clear 会把它清掉不用额外处理。数据库查询本质是 IO 密集型任务较真的话应该用 ExecuteReaderAsync但 Task.Run 在这个场景下简单可靠不值得为了理论最优增加复杂度。4. 避坑排查绑定数据后翻车的 5 个典型场景下面 5 个问题按出现频率排序都是我实际踩过或帮别人排查过的。每一条都按“现象 → 原因 → 解决”写对照检查就行。4.1 现象一加载就闪屏行数越多越严重数据到 2000 行以上添加行时界面像放幻灯片上下滚动也是肉眼可见的闪烁。核心原因有两个一是每 Add 一个 ListViewItem控件就触发一次重绘循环里加几百次就闪几百次二是 ListView 的 DoubleBuffered 属性是 protected默认没开双缓冲WinForms 控件没有自动双缓冲能力。解决就是 BeginUpdate/EndUpdate 配对再加一段反射强制开双缓冲typeof(ListView).GetProperty(DoubleBuffered, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) ?.SetValue(listView1, true);这段代码在窗体 Load 时执行一次。注意 BeginUpdate 要在 Items.Clear 之前调用EndUpdate 放 finally保证任何异常情况下都能恢复重绘。如果开了双缓冲还闪检查是不是每次循环里都改了 Font 或 BackColor 属性这类属性修改也会触发重绘。4.2 现象二查出了数据却显示空白SQL 执行有返回但 ListView 一片空白或者只有第一列、后面全是空白。按顺序查四个地方View 是不是 Details有没有 Add ColumnHeaderColumns 数量是不是少于 DataTable 的列数绑定循环是不是用了列名而列名拼错了。前两个是最常见的把 ListView 拖到窗体上就直接写绑定代码忘记设置 View 或者忘记建列。第三个问题的现象很有迷惑性数据其实是进去了只是 SubItems 比 Columns 多时多出来的列没有表头、显示为空白。第四个别忽视row[user_name]这类取值如果列名写错运行时抛 ArgumentException如果异常没被捕获你会看到程序卡住但界面还是空的。解决很简单统一用列索引取值或者确认 SQL 列名与代码一致。4.3 现象三日期、金额、NULL 显示得乱七八糟日期显示成2024/1/5 0:00:00金额没有千分位NULL 显示成空字符串分不清是“没数据”还是“没取到”。原因是 SubItems.Add 直接拿 object 调用 ToString日期格式取决于系统区域设置数字不带格式DBNull.ToString() 返回空串导致所有 NULL 看起来一样。解决思路是绑定前对常见类型做格式化不要在 SQL 里拼字符串格式化保持数据层干净object val row[c]; if (val is DBNull) item.SubItems.Add(); else if (val is DateTime) item.SubItems.Add(((DateTime)val).ToString(yyyy-MM-dd HH:mm)); else if (val is decimal || val is double || val is float) item.SubItems.Add(Convert.ToDecimal(val).ToString(N2)); else item.SubItems.Add(Convert.ToString(val));日期按业务需要选择yyyy-MM-dd还是带时间金额用 N2 补两位小数。如果你的查询结果里还有 bool 类型建议加一层判断把 True/False 转成“是/否”直接显示英文看着不像数据工具该有的样子。4.4 现象四后台线程碰 ListView直接抛异常用了 Task 或 BackgroundWorker 之后在后台线程里调用listView1.Items.Add抛 InvalidOperationException“线程间操作无效”。原因很简单控件只能在创建它的 UI 线程上访问后台线程改集合属于非法跨线程操作ListView 没做线程安全。两个解法。优先用 async/await第 3.5 节的写法让 UI 线程自己继续执行绑定根本不跨线程。如果代码结构必须用 BackgroundWorker 或 Timer就用 BeginInvoke 把操作调度回 UI 线程listView1.BeginInvoke(new Action(() { BindListAuto(listView1, dt); }));BeginInvoke 是异步投递不会阻塞后台线程Invoke 是同步等待需要拿返回值才用。接受不到返回值时一律用 BeginInvoke否则后台线程会被 UI 操作拖慢数据库查询反而成了排队等界面。4.5 现象五查询越跑越慢最后报连接池已满连续查询几十次后响应越来越慢最终报“连接池已满”或超时。这属于最常见的“看着像 SQL 问题其实是连接没释放”。SqlConnection 没有被 Dispose连接池里的物理连接一直被占用池满后新请求只能排队等空闲连接表现就是越来越慢直到超时。排查方法把连接字符串临时改成PoolingFalse如果问题消失确认是连接对象泄漏。解决就是所有 SqlConnection、SqlCommand、SqlDataReader 全部用 using 包裹任何一层都不放过。SqlDataAdapter 内部会释放自己的 Command但外部传入的 Connection 必须自己负责。另一个善后手段string connStr Server127.0.0.1;DatabaseDemoDB;User Idsa;Password123456; PoolingTrue;Min Pool Size1;Max Pool Size100;Connection Timeout5;;把 Max Pool Size 调到 100Min Pool Size 设为 1。想确认当前连接数SQL Server 执行select * from sys.dm_exec_connectionsMySQL 看show processlist如果连接数只增不减代码里一定有没释放的连接。这个坑在“数据库连接池”这类场景下特别典型很多团队调了半天 SQL 才发现是连接泄漏。5. 进阶验证耗时、双击现场编辑与最后的收尾习惯工具能跑起来只是第一步。这一章给两个收尾动作用 Stopwatch 量化列表绑定耗时以及实现双击单元格现场编辑。做完这两件事这个查询工具才算真正顺手。5.1 每次改动先用 Stopwatch 量一次耗时绑定代码改完先别急着宣布优化成功用数字说话var sw System.Diagnostics.Stopwatch.StartNew(); BindListAuto(listView1, dt); sw.Stop(); this.Text $加载 {dt.Rows.Count} 行耗时 {sw.ElapsedMilliseconds} ms;把耗时直接写进窗体标题栏每次改动后点一次查询就能对比。1 万行数据在 100ms 以内说明代码没问题超过 500ms先检查有没有漏掉 BeginUpdate/EndUpdate超过 3 秒说明页面还在创建大量 ListViewItem这时候别折腾控件了去 SQL 里做分页或者考虑 VirtualMode。5.2 双击现场编辑TextBox 覆盖方案的最简实现数据开发工具偶尔要改个别字段。ListView 的 LabelEdit 只能改第一列文本要任意列都能改常见做法是放一个隐藏的 TextBox 盖到单元格上private void listView1_MouseDoubleClick(object sender, MouseEventArgs e) { ListViewItem item listView1.GetItemAt(e.X, e.Y); if (item null) return; int colIndex GetColumnAt(e.X); if (colIndex 0) return; Rectangle r item.SubItems[colIndex].Bounds; txtEditor.Bounds r; txtEditor.Text item.SubItems[colIndex].Text; txtEditor.Visible true; txtEditor.Focus(); }GetColumnAt 遍历 ColumnHeader 的 Left 和 Width判断鼠标横坐标落在哪一列。TextBox 的 Leave 事件里把修改写回 ListView再写 UPDATE 语句回数据库。这个方案能应付轻量修改但排序、格式校验都得自己写。如果编辑需求超过两列我的建议是别硬扛换 DataGridView 更划算——这不算打脸是场景决定控件。做数据开发工具这几年最大的心得是优先选轻控件克制加功能。ListView 显示数据库数据这条路适合 80% 的只读查询场景代码可控、性能直观、出了问题容易排查。希望你照着这套代码搭出来的第一个工具就能少踩几个坑。本文还有配套的精品资源点击获取