尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C# ListView显示数据库数据:完整源码实现与避坑指南

发布时间:2026/9/28 15:47:10

资讯中心
01
ARTICLE

C# ListView显示数据库数据:完整源码实现与避坑指南

C# ListView显示数据库数据:完整源码实现与避坑指南
简介这是面向C#初学者的ListView数据库显示示例解决WinForms开发中用列表控件直观展示数据库内容的常见需求。资源讲解基于ADO.NET建立数据库连接、执行SQL查询、填充DataTable并配合ListView的列头、项与子项实现字段对应同时涉及选择事件处理适合具有C#基础、正在学习数据驱动界面开发的读者参考实践。压缩包共40个文件大小仅78KB核心包括.cs源码、.csproj工程文件、.sln解决方案文件另含.accdb数据库文件、.resx资源文件、编译生成的.exe与配置备份结构紧凑完整。示例附带Visual Studio解决方案与数据库样本可打开工程直接查看代码结构并运行调试便于理解ListView数据展示的完整流程并可基于示例继续扩展排序、过滤、搜索等功能也可作为课程设计或日常开发排错的参考。目前已有825人学习下载对想在WinForms中快速上手数据列表展示的开发者颇具参考价值。1. 用 ListView 控件显示数据库数据一个数据开发界面里的高频需求做内部资产管理系统的时候我发现团队里不少同事第一反应是“数据库数据展示直接用 DataGridView 不就行了”。但在只读查询、状态列表、日志回放这类场景下ListView 控件反而更合适它轻、快、样式可控也方便后续绑定右键菜单和现场编辑。这里说的“ListView 显示数据库数据的 C# 源代码”就是把这个场景的完整实现拆开怎么读库、怎么构造行、怎么避免踩坑最后做成能维护的代码。适合谁看正在写 WinForms 管理端、上位机监控界面或者想用轻量方式做数据开发工具界面的 .NET 工程师。2. 选型与绑定为什么 ListView 加载数据库数据不能靠 DataSource2.1 ListView 没有 DataSource 属性先放弃“拖拖拽拽绑定”的念头ListView 控件和 DataGridView 在数据绑定上完全是两条路线。DataGridView 可以直接接收 DataTable然后自动生成列、自动排版甚至内置单元格编辑ListView 没有 DataSource 属性只有 Items 集合。这尤其让人不习惯因为我们用惯了 ComboBox、ListBox、DataGridView总觉得 WinForms 控件默认都应该有数据绑定。结果一编译报错信息就明明白白告诉你这个属性不存在。这不是遗漏。ListView 的定位是“资源管理器式”的列表一个 Item 可以带图标、分组、子项、状态每个 ListViewItem 都自带一套显示逻辑。如果把 DataTable 直接塞进去反而把它自己那套显示能力废掉了。所以在数据开发里常见做法是手写一段映射代码把数据库行转成 ListViewItem。这个映射虽然只有几行但最坑的细节都藏在里面。另外一个选型判断在动手之前就要做如果需求是“单元格级编辑、公式、图表联动、复杂排序”用 DataGridView 是正确的需求只是“把数据库结果一口气端出来用户看一眼、筛选一下、偶尔改一下”那 ListView 是更好的选择。尤其是在 C# 上位机项目里设备状态列表、报警日志、点位表展示很多都是拿 ListView 做理由就是轻、可定制、不喧宾夺主。这一章先把模型讲透等真开始敲代码时你会少走至少两次弯路。2.2 ListViewItem 与 SubItems 的映射关系第一列是个特例把数据库行塞进 ListView所有理解成本都在 ListViewItem 的结构上。一个 ListViewItem 对象代表一行它自己的 Text 属性就是这一行的第一列SubItems 是后续列的集合。注意 SubItems 集合里下标为 0 的那一项和 Item.Text 是同一列真正从第二列开始的是 SubItems[1]。写代码时的对应关系是这样的new ListViewItem(dr[0].ToString())dr[0] 进了第一列item.SubItems.Add(dr[1].ToString())dr[1] 进第二列再Add一次就是第三列以此类推。如果你在new ListViewItem()的时候留空然后连着Add五个字段第一列就会空着后续所有列整体错位。这在界面上看起来就像“数据被吃了”实际上只是起点错了。另一个容易被忽略的点是因为 SubItems[0] 和 Item.Text 是同一列所以循环填充时不要用下标去重排 SubItems 集合否则你在修改已存在行的某一列时可能改的是第一列的引用。我一般习惯用数据库字段名而不是列序号去取值row[asset_code]比row[1]可读性高得多而且一旦 SQL 里调整了列顺序前者不会悄悄错位后者会直接把数据显示到错误的列上排查成本极高。2.3 加载策略三选一DataTable 全量、DataReader 流式、VirtualMode 虚拟数据开发里另一个常见问题是“到底用哪种方式把数据库数据喂给 ListView”。我通常按数据量分三档。第一档几百到几千行用 DataTable 全量加载。SQL 查出结果DataAdapter.Fill 到一个 DataTable然后 foreach 遍历 Rows 构造 ListViewItem。优点是代码短、好调试空值和类型转换都好处理缺点是内存里有一份 DataTableListView 自己也创建了一套对象数据量过万以后内存和界面刷新都会变慢。第二档一次只取少量字段且想省内存用 DataReader 边读边填。SqlDataReader 或 MySqlDataReader 配合while (reader.Read())循环每读一行立刻 Add 一个 ListViewItem。这样省掉了 DataTable 的中间缓存但要特别注意用using释放 reader否则连接会被一直占住。第三档超过十万行就不要硬塞了。把 ListView 的VirtualMode打开配合RetrieveVirtualItem事件按需取行。屏幕滚到哪一行ListView 才找你拿那一行数据界面不会因为行数多而卡死。代价是排序、列宽自动调整、全选这些行为都要自己处理复杂度明显上升。我的判断标准很朴素管理后台的数据大多数在几万行以内第一档完全够用如果查询结果动辄几十万行项目里应该先做分页查询而不是寄希望于把四十万行一次塞进 ListView。虚拟模式是在分页方案不合适时的兜底策略不是默认选择。3. 把数据库表刷进 ListView 的 C# 源码连接、读取、填充三步拆解3.1 项目准备与连接字符串MySql.Data 引用和字符集先假设一个常见的资产登记表结构如下字段类型说明idint主键asset_codevarchar资产编号asset_namevarchar资产名称reg_datedatetime登记日期ownervarchar管理人我的连接字符串一般写成这样string connStr Server127.0.0.1;Port3306;Databaseasset_db;Uidroot;Pwdyour_password;CharSetutf8mb4;SslModeNone;Connection Timeout10;Poolingtrue;;这段配置里有三个参数值得说明。CharSetutf8mb4是 MySQL 中文乱码的常见解药你后面如果发现 ListView 里中文显示成问号先检查这里。Poolingtrue表示启用数据库连接池频繁开关连接时减少 TCP 握手开销这是 C# 连 MySQL 时很容易忽略的默认行为。Connection Timeout10防止数据库假死时界面长时间无响应数据开发工具尤其要设这个值。项目引用方面用 MySQL 就在 NuGet 里装 MySql.Data用 SQL Server 就把 MySqlConnection 换成 SqlConnection。后面所有代码结构都不变数据库客户端只影响连接对象的类型。3.2 读库方法DataAdapter 把结果放进 DataTable数据访问我习惯封装成一个独立方法不直接写在 Form 事件里private DataTable ExecuteQuery(string sql) { DataTable dt new DataTable(); string connStr Server127.0.0.1;Port3306;Databaseasset_db;Uidroot;Pwdyour_password;CharSetutf8mb4;; using (MySqlConnection conn new MySqlConnection(connStr)) using (MySqlCommand cmd new MySqlCommand(sql, conn)) { conn.Open(); using (MySqlDataAdapter da new MySqlDataAdapter(cmd)) { da.Fill(dt); } } return dt; }逻辑说明两层using保证连接和命令在方法结束时一定被释放这是数据开发代码的基本卫生习惯。MySqlDataAdapter.Fill会在内部完成查询和填充并且不会长时间占用数据库连接适合查询结果一次性展示的场景。如果改用 DataReader 边读边填就需要你自己维护循环和连接关闭时机代码会多出不少边界分支。调用方式很简单string sql SELECT id, asset_code, asset_name, reg_date, owner FROM tb_asset ORDER BY reg_date DESC;; DataTable dt ExecuteQuery(sql);我把这段放进Form_Load还是Shown事件取决于窗体的初始化顺序。如果窗体上有需要先加载的权限配置就用 Shown如果只是无脑拉数据Form_Load 足够。这个选择不影响 ListView 本身。3.3 核心填充源码列头配置 ListViewItem 循环这是整个标题里最核心的一段代码直接解决“ListView 显示数据库数据”这个诉求。分两步先配置列头private void ConfigListView(ListView lv) { lv.View View.Details; // 只有 Details 模式下列头才可见 lv.FullRowSelect true; // 点击任意列选中整行 lv.GridLines true; // 加网格线视觉上更像数据表 lv.MultiSelect false; lv.Columns.Clear(); lv.Columns.Add(ID, 60, HorizontalAlignment.Center); lv.Columns.Add(资产编号, 110, HorizontalAlignment.Left); lv.Columns.Add(资产名称, 160, HorizontalAlignment.Left); lv.Columns.Add(登记日期, 90, HorizontalAlignment.Center); lv.Columns.Add(管理人, 80, HorizontalAlignment.Center); }再写填充方法private void FillListView(ListView lv, DataTable dt) { lv.BeginUpdate(); try { lv.Items.Clear(); foreach (DataRow row in dt.Rows) { ListViewItem item new ListViewItem(row[id].ToString()); item.SubItems.Add(row[asset_code].ToString()); item.SubItems.Add(row[asset_name].ToString()); // 日期不直接 ToString统一格式化为短日期 item.SubItems.Add(Convert.ToDateTime(row[reg_date]).ToString(yyyy-MM-dd)); // 空值显示为占位符避免列表里出现空白格 string owner row[owner] DBNull.Value ? - : row[owner].ToString(); item.SubItems.Add(owner); // 主键藏到 Tag后续编辑、排序定位都靠它 item.Tag row[id]; lv.Items.Add(item); } } finally { lv.EndUpdate(); } }逻辑说明BeginUpdate和EndUpdate是 ListView 填充的“后悔药”。如果没有这两行每次Items.Add都会触发一次界面重绘几千行数据时窗体直接变白屏循环结束后才一次性恢复。try/finally保证循环哪怕中途抛异常EndUpdate也一定被执行否则 ListView 会一直停留在“不刷新”状态界面怎么操作都不响应。参数说明new ListViewItem(row[id].ToString())这个构造参数就是第一列的内容对应 Columns 里的“ID”列后面的SubItems.Add依次对应“资产编号”“资产名称”“登记日期”“管理人”。item.Tag row[id]这行特别重要它把数据库主键挂到每一行的 Tag 上之后不管用户怎么排序我们用 Tag 里的主键去回写数据库都不会指错行。调用代码ConfigListView(listView1); DataTable dt ExecuteQuery(SELECT id, asset_code, asset_name, reg_date, owner FROM tb_asset); FillListView(listView1, dt);3.4 空值、日期格式与列宽填充时的三个细节处理细节一空值判断。row[owner]可能返回DBNull.Value这时候直接调用ToString()不会崩溃但会得到一个空字符串界面上看起来像缺了一块。我在代码里做了三元判断用-占位用户扫一眼就知道这个字段本身是空的而不是程序漏填了。细节二日期格式。row[reg_date]是 datetime 类型直接ToString()会输出“2024-01-01 00:00:00”这类完整时间。数据列表里通常只需要日期所以先Convert.ToDateTime(...)再格式化成yyyy-MM-dd。这里千万不要在 SQL 里写DATE(reg_date)去截那样索引会失效数据量一大查询就慢。细节三列宽。Columns.Add的第二个参数是像素宽度你可以手工给每列定宽。想偷懒时用-2表示按内容自动调整宽度但有代价行数一多自动列宽会反复计算反而拖慢界面。我一般在数据量小于 500 行时用-2数据量大就固定宽度宁可让横向滚动条工作也不要让界面卡顿。4. ListView 数据库的避坑清单5 个最容易翻车的现场与修复方法4.1 现象第一列空了后面的字段整体右移数据库字段明明全查出来了但 ListView 第一列是空白第二列开始显示的内容正好是从数据库第二个字段开始的。整个行看起来像被什么东西挤偏了一位。原因构造 ListViewItem 时用了new ListViewItem()没有把数据库第一列放进去然后又把五个字段全部SubItems.Add进去了。ListView 把 Item.Text 当成第一列你的第一列始终是空的子项数量又比列头多了一个于是所有列错位。解决改成new ListViewItem(row[id].ToString())把第一列写进构造参数然后从第二列开始Add。我在填完数据后还会加一个断言让问题在开发期就暴露System.Diagnostics.Debug.Assert( lv.Columns.Count item.SubItems.Count, 列头数量必须等于 SubItems 数量);这个断言在 Release 环境不生效Debug 环境一旦列映射不一致就会立刻弹出来比人眼盯屏幕找错快得多。4.2 现象几千行数据填充时界面假死甚至白屏一次往 ListView 里塞五千行窗体现在能活活卡住好几秒期间拖动窗口都拖不动像死了一样。原因循环里每次Items.Add都会触发一次失效和重绘行数越多失效区域越大重绘开销成倍上涨。这是 ListView 的机制不是你的机器配置低。解决用第 3 章代码里的BeginUpdate()/EndUpdate()把整个填充过程包裹起来并且一定把EndUpdate()放进finally。如果还不够快就在填充前临时把lv.Scrollable false填充结束后恢复这会减少滚动条布局的计算。数据量超过一万行还卡就不要死磕这个控件了改用分页加载或者虚拟模式。4.3 现象双击格子改了文字重新打开数据库发现没有保存界面上明明能改文字也变了但程序重启后数据还是原来的值数据库里一点变化都没有。原因ListView 本身没有内置的编辑机制。很多人的“双击编辑”是在MouseDoubleClick事件里直接改了item.SubItems[index].Text只动了界面没有任何 SQL 去更新数据库。数据库是数据的唯一来源UI 只改内存对象自然保存不了。解决把编辑链路补完整。先用HitTest定位到具体 SubItem从item.Tag里取出主键再通过参数化 SQL 执行UPDATE。注意不要用字符串拼接把用户输入直接塞进 SQL否则字段里带个单引号就会翻车。详细的完整方案在下一章这里先记住一个原则所有界面修改最终都要落到一次参数化的数据库写操作。4.4 现象中文显示成“??”或者一串乱码别的数据库查询工具显示都正常偏偏自己的 ListView 里中文全是问号英文和数字倒没问题。原因这是典型的连接字符集不一致。MySQL 默认连接字符集可能是 latin1而表里的数据是 utf8mb4两端编码对不上中文就变成了问号。这不是 ListView 的锅是数据链路在数据库访问层就断掉了。解决在连接字符串里加CharSetutf8mb4;同时确认数据库表本身的字符集也是 utf8mb4。如果改完连接串还乱码在查询前手动执行一次初始化命令using (MySqlCommand init new MySqlCommand(SET NAMES utf8mb4, conn)) { init.ExecuteNonQuery(); }这一步能解决绝大部分 MySQL 中文乱码问题。代码里所有涉及中文的列名、SQL 注释也尽量保持统一编码别把 GBK 的字符串混进 utf8mb4 的连接里。4.5 现象排序或滚动后点选的行不是刚才看到的那条给 ListView 加了排序功能结果排序后点某一行程序拿到的数据却是另一行的或者虚拟模式滚动后点击事件定位错了行。原因ListView 的排序只改变 Items 在集合里的显示顺序不会同步更新你代码里保存的“当前行”变量。如果用列表索引去记住业务主键排序一发生索引指向的就不是原来的记录了。解决永远不要用Items.IndexOf或者行号当业务主键。从最开始填充数据时就把数据库主键存进item.Tag。事件处理时只从Tag读取主键再通过主键去数据库查明细。想恢复排序前的选中状态就在排序前记录主键值排序完成后遍历Items找到Tag相等的行再选中。这是我在真实项目里改过一次血泪代码后固化下来的习惯。5. 双击单元格现场编辑并写回数据库把只读 ListView 变成数据维护工具5.1 用 MouseDoubleClick 定位单元格并弹出编辑框现场编辑是数据开发工具里最实在的一个进阶功能。常见做法是准备一个隐藏的 TextBox用户双击单元格时把它移动到对应格子上方显示原值让用户改。先放编辑框的代码private TextBox txtEditor; private ListViewItem editItem; private int editSubIndex -1; // 列名白名单只允许这几种字段被编辑防止任意列名拼接 SQL private string[] EditColumns { , asset_code, asset_name, reg_date, owner }; private void lvMain_MouseDoubleClick(object sender, MouseEventArgs e) { ListViewHitTestInfo hit lvMain.HitTest(e.Location); if (hit.Item null || hit.SubItem null) return; // 第一列是主键列不允许编辑 int subIndex hit.Item.SubItems.IndexOf(hit.SubItem); if (subIndex 0) return; SaveEditingValue(); // 先保存上一个编辑框里的值防止丢数据 editItem hit.Item; editSubIndex subIndex; Rectangle r hit.SubItem.Bounds; txtEditor.Visible true; txtEditor.Text hit.SubItem.Text; txtEditor.BringToFront(); txtEditor.Focus(); txtEditor.SelectAll(); // 把 ListView 的客户区坐标转成窗体的客户区坐标再定位 Point globalPt lvMain.PointToScreen(r.Location); txtEditor.Location this.PointToClient(globalPt); txtEditor.Size new Size(r.Width 40, r.Height 2); }逻辑说明HitTest的作用是根据鼠标坐标算出命中的 ListViewItem 和 SubItem这是现场编辑的定位基础。IndexOf(hit.SubItem)拿到列下标通过EditColumns数组找到数据库字段名。这里的关键点是字段名数组写死在代码里绝不从界面输入或用户数据里取列名否则就等于把 SQL 注入漏洞留在生产环境里。参数说明txtEditor.Location需要经过坐标转换。ListView 的SubItem.Bounds返回值是 ListView 的客户区坐标而编辑框是窗体的子控件所以先用PointToScreen转成屏幕坐标再用窗体的PointToClient转回窗体坐标。DPI 缩放和窗体边框存在时这一步不能省。5.2 回车保存与参数化 UPDATE 写回数据库编辑器能定位了接下来是保存逻辑private void txtEditor_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) SaveEditingValue(); if (e.KeyCode Keys.Escape) CancelEditing(); } private void SaveEditingValue() { if (editItem null || editSubIndex 0) return; if (editSubIndex EditColumns.Length) return; string newText txtEditor.Text.Trim(); string fieldName EditColumns[editSubIndex]; object id editItem.Tag; string sql $UPDATE tb_asset SET {fieldName} val WHERE id id; using (MySqlConnection conn new MySqlConnection(connStr)) using (MySqlCommand cmd new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(val, newText); cmd.Parameters.AddWithValue(id, id); conn.Open(); cmd.ExecuteNonQuery(); } // 数据库写成功后再把界面上的文字同步成新值 editItem.SubItems[editSubIndex].Text newText; editItem null; txtEditor.Visible false; } private void CancelEditing() { txtEditor.Visible false; editItem null; }逻辑说明fieldName虽然用了字符串拼进 SQL但它的值来自EditColumns这个固定数组不是用户输入所以不会引入注入风险。真正传给数据库的值全部走Parameters.AddWithValue。这里的editItem.Tag就是第 3 章填充时存进去的主键它让 UPDATE 语句永远定位到正确的行不管用户在前台怎么排序滚动都不会指错。保存成功后再刷新界面行文字是一个容易漏掉的顺序问题。我见过不少实现是先改界面再执行 SQL结果 SQL 失败界面上的假数据还留着造成“看起来保存了其实没有”的假象。先写库、后刷界面两者不一致时数据库永远是权威。这一套逻辑本质上是把 ListView 变成了一个轻量级的数据维护工具每次回车都是一次小型的数据库同步界面和库表保持一致。这也是我做这一类功能后总结下来的习惯主键进 Tag列名进白名单数组提交走参数化 SQL界面永远等数据库确认成功后再更新。ListView 显示数据库数据的源码看起来不难但把这些边界都守住了它才真正能交给用户用而不是只在看起来正常的演示数据上跑通。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。