
简介面向使用C#在Windows Forms下进行数据展示开发的人员本资源提供了一个完整的ListView控件绑定数据库数据的源码示例。压缩包内共40个文件以.cs源代码、.resx资源文件、.csproj工程文件及.sln解决方案文件为主并附带可直接运行的.exe程序与示例数据库.accdb适合初学者对照学习或作为项目模板复用。示例以详细信息视图展示数据列完整演示了从ADO.NET连接数据库、执行SQL查询、填充DataTable到将数据绑定至ListView并处理选择事件的常见流程。学习过程中可以掌握列头与子项的对应设置、数据刷新思路同时了解包含备份在内的Visual Studio工程组织方式。案例体量紧凑约78KB便于快速上手。已有825人学习适合具备基础C#语法、希望快速掌握ListView数据绑定或进行数据管理界面开发的开发者参考。1. 为什么说 ListView 展示数据库数据要手写循环而不是绑定ListView 控件在 C# WinForms 上位机里几乎是绕不开的列表组件做设备管理、用户列表、日志查询这类数据库场景第一反应往往是像 DataGridView 那样拉一个 DataSource 绑定上去。但这个案例告诉你一个关键事实WinForms 的 ListView 没有 DataSource 属性也没有默认的数据绑定能力它只认 ListViewItem 和 SubItems。也就是说把 DataTable 直接塞给控件的写法在真实工程里编译都过不了。这套“C# ListView 控件显示数据库数据的源代码”用的就是最朴素的循环填充先查库拿 DataTable再按行构造 ListViewItem。适合刚跑通 C# 入门、打算用 WinForms 做数据展示和上位机日志的人把这个案例看懂后面加排序、分页、虚拟化才有谱。2. 先拆工程结构再看 ListView 的列和数据显示原理拿到压缩包别急着双击 .sln把文件结构扫一遍比盲目运行更有用。这套源码里没有自带数据库文件数据读取完全走 ADO.NET所以重点在代码路径不在数据库本身。2.1 解决方案文件清单读一遍就知道哪几个文件该改文件结构先看清后续改起来不迷路。这套源码的核心文件大概是这样分工的文件路径在工程里的作用Case05_4.sln解决方案入口VS 直接打开它Case05_4.csproj项目定义记录目标框架版本和引用Form1.cs主窗体逻辑填充和事件处理都在这Form1.Designer.csInitializeComponent 里的控件初始化Form1.resx窗体资源文件一般不用手改Program.csMain 入口函数启动窗体的地方MyMeans.cs公共方法类一般放数据库连接和通用操作DataClass/MyMeans.cs数据访问封装类Properties/*.cs程序集信息和 Settings 设置Backup/历史备份版本相当于源码自带的后悔药这个清单里最容易混淆的是 Form1.cs 和 Form1.Designer.cs。Designer 文件管初始化比如 ListView 控件的位置、列头、View 模式都在这里生成Form1.cs 才管业务比如按钮点击后怎么查数据库、怎么往列表里塞数据。初学 C# 的人经常把初始化代码写进 Form1.cs导致重复创建控件界面显示异常。.csproj 里的 TargetFrameworkVersion 决定你跑在哪个 .NET 版本上。这类教学案例如果是 VS2012 时代生成的多半是 .NET Framework 4.0 或 4.5用新版 VS 打开时会自动升级导入。另一个值得关注的是 Backup 文件夹很多源码包会把上一版能跑的代码留在这万一你改坏了主工程从 Backup 里把 Form1.cs 拷回来就能恢复这在调试阶段很实用。2.2 Details 视图为什么是默认选择Columns 与 SubItems 的工作关系ListView 控件一共提供五种视图模式差别在于布局和数据展示密度这个案例选择 Details 是有明确理由的。视图模式布局特点适用场景Details多列表格形态带列头数据库记录展示LargeIcon大图标平铺文件浏览、图标选择SmallIcon小图标紧凑排列轻量图标列表List单列垂直列表快速选择型交互Tile大图标加说明行缩略图场景做数据库展示选 Details 几乎没有争议因为数据库表天然是行、列二维结构Details 视图里的 ListViewItem 对应一行SubItems 对应该行的多个字段正好和 DataTable 的 Rows、Columns 一一对应。这种对应关系在很多 C# 高级编程相关的资料里反复出现但真正动手时还是容易绕晕。在 Details 视图下Columns 集合决定列头显示什么Items 集合里的每个 ListViewItem 第一列文本是主列SubItems 追加的是后面各列的文本。代码里通常把第一列放 ID 或主键再把姓名、部门这类字段按顺序压进 SubItems。这里有个容易踩的细节SubItems[0] 对应的是 Columns[1]不是 Columns[0]这个下标偏移是大量错位现场的根源后面避坑章会专门展开。2.3 数据从哪来DataClass 和 MyMeans 的封装套路这个案例的数据库读取思路是经典的 ADO.NET 三步走SqlConnection 建立连接SqlCommand 执行 SQLSqlDataAdapter 把结果填充进 DataTable。案例里我一般会把这几步封装成一个静态方法所有窗体共用一个入口避免每个窗体都写一遍连接逻辑。常见做法是这样public static class MyMeans { private static readonly string connStr server.;databaseStudentDB;uidsa;pwd123456;; public static DataTable GetDataTable(string sql) { using (SqlConnection conn new SqlConnection(connStr)) { SqlCommand cmd new SqlCommand(sql, conn); SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); return dt; } } }代码逻辑conn 是连接对象cmd 是 SQL 命令对象da 是适配器它负责把 cmd 返回的结果映射到内存里的 DataTable。Fill 方法执行真正的查询动作返回值就是内存表。方法用 static 修饰之后窗体里直接写 MyMeans.GetDataTable(sql) 就能拿表不用每次 new 一个类出来。连接串参数说明server.; 指本机 SQL Server点号代表本地databaseStudentDB 指定要连的库名uid 和 pwd 是 SQL Server 账号密码。如果是 Windows 身份验证改成 Data Source.;Initial CatalogStudentDB;Integrated SecurityTrue; 就可以了。如果数据库是 MySQL这套代码要换驱动MySqlConnection 配 MySqlDataAdapter连接串里还要带端口 3306 和字符集设置。这套封装有一个明显的边界它只负责取数据不负责写数据。如果你想在这个案例基础上做增删改查需要再封装一个 ExecuteNonQuery 方法把 INSERT、UPDATE、DELETE 语句传给 SqlCommand 后执行。这个案例定位在展示所以 GetDataTable 一个入口就够了。3. 填充与选中交互手写循环是这套代码的核心这一章解决两件事把 DataTable 变成屏幕上的列表以及让用户点击某一行时能读回这一行的完整数据。代码都在 Form1.cs 里顺着事件链路读就能跟完。3.1 DataTable 到 ListView 的填充循环窗体加载或者点击查询按钮之后先拿数据源再清空旧数据最后循环填这个顺序不能反过来否则会出现旧数据显示一瞬间再消失的效果。private void LoadListView() { string sql SELECT Id, Name, Dept, Phone FROM tb_User ORDER BY Id; DataTable dt MyMeans.GetDataTable(sql); listView.BeginUpdate(); listView.Items.Clear(); listView.Columns.Clear(); listView.Columns.Add(编号, 70); listView.Columns.Add(姓名, 100); listView.Columns.Add(部门, 130); listView.Columns.Add(电话, 150); foreach (DataRow row in dt.Rows) { ListViewItem item new ListViewItem(row[Id].ToString()); item.SubItems.Add(row[Name].ToString()); item.SubItems.Add(row[Dept].ToString()); item.SubItems.Add(row[Phone].ToString()); listView.Items.Add(item); } listView.EndUpdate(); }代码逻辑SQL 先按 Id 排序保证列表顺序稳定。GetDataTable 返回内存表之后BeginUpdate 到 EndUpdate 之间的代码块暂时关闭 ListView 的重绘避免每次 Add 都刷一遍屏幕。foreach 循环里第一列 Id 作为 ListViewItem 的 Text剩余字段依次塞进 SubItems顺序和上面对应。参数说明item.SubItems.Add 的顺序必须和上面 Columns.Add 的顺序一致否则列表里会出现表头和数据错位。列宽单位是像素70 到 150 是常规可读宽度中文列名建议从 100 起步。如果查询条件变了这个方法可以加参数比如传一个 whereSql 字符串进去就能实现过滤查询。这个循环写法在几百行数据时完全够用但数据库字段返回空值时 row[Name] 会得到 DBNull直接 ToString 在某些情况下会有异常风险。稳妥的写法是加一个判断object name row[Name]; item.SubItems.Add(name DBNull.Value ? : name.ToString());DBNull 是数据库空值的标准表示做数据开发时这条判断几乎是必备的。不写的话等到线上数据出现空值那天就得回来补。3.2 点击选中行ItemSelectionChanged 事件怎么接列表填完之后第二件事是让用户选中某一行把数据显示到文本框或标签控件上。ListView 里最常用的是 ItemSelectionChanged 事件它在选中状态变化时触发比 SelectedIndexChanged 事件给的信息更全。private void listView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) { if (!e.IsSelected) return; string id e.Item.SubItems[0].Text; string name e.Item.SubItems[1].Text; string dept e.Item.SubItems[2].Text; string phone e.Item.SubItems[3].Text; txtName.Text name; txtDept.Text dept; txtPhone.Text phone; }代码逻辑事件参数 e 里带这次变化的 Item 和 IsSelected 标记。IsSelected 为 false 表示用户取消选中这时候直接跳过为 true 时从 e.Item 里取 SubItems 做显示。使用 e.Item 比再查一次 SelectedItems 集合更直接因为当前变化的就是这一行。这段代码里有一个习惯把 Id 也读出来放在变量里。虽然当前没用它显示但后续做反查关联数据时会用到。很多上位机项目里选中一个设备之后要查它的历史记录靠的就是这个 Id。如果不提前留好后面加功能时还得改事件方法。SelectionMode 属性对事件行为影响很大。默认的 SelectMode 是 SelectItem点击整行都会触发选中如果设成 SelectOnlyOne和 ItemSelectionChanged 配合起来单选场景下的逻辑就简单很多。需要多选时再考虑 CheckBoxes 或者 Ctrl 配合选。3.3 从选中行反查业务表一个联动查询示例选中行之后更常见的落地场景是拿主键去查关联数据。比如选中一个用户下面显示这个用户的登录记录这是 ListView 显示数据库数据之外最典型的二次查询路径。private void listView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) { if (!e.IsSelected) return; string id e.Item.SubItems[0].Text; string sql SELECT LoginTime, IP FROM tb_LoginLog WHERE UserId id; DataTable dt MyMeans.GetDataTable(sql); listViewLog.BeginUpdate(); listViewLog.Items.Clear(); foreach (DataRow row in dt.Rows) { ListViewItem item new ListViewItem(row[LoginTime].ToString()); item.SubItems.Add(row[IP].ToString()); listViewLog.Items.Add(item); } listViewLog.EndUpdate(); }代码逻辑选中用户后立刻用主键去查登录日志表查询结果填充到第二个 ListView 里。两个列表之间的纽带是 SubItems[0] 里存的 ID。执行时机是每次选中变化都会触发如果日志表数据量大这个事件的触发频率会带来查询压力。参数说明这里 SQL 直接拼了字符串只适合本地小工具和教学场景。真实项目里建议改成参数化查询SqlCommand 里加 Parameters.AddWithValue防注入的同时也避免字符串拼接时数字类型转换出问题。两个列表的填充逻辑重复可以提取一个 FillListView(ListView lv, DataTable dt) 公共方法代码会干净很多。到这里这套案例的主干功能就通了连库、查数、填表、选中回显、联动查询。接下来是把主干代码搬到真实项目时的五个高频翻车点。4. 避坑记录这个 ListView 案例最容易翻车的五个点案例代码量不大但放到真实项目里踩出来的坑不少。这五条基本覆盖了搜索 C# ListView 时出现频率最高的报错现场每一条都按现象、原因、解决的顺序来。4.1 编译报错ListView 没有 DataSource 属性现象照着 DataGridView 的思路写 listView1.DataSource dataTable;编译直接报错提示 ListView 不包含 DataSource 的定义。原因ListView 不是数据感知控件没有实现数据绑定相关的接口。DataSource、DataMember 这两个属性在它身上根本不存在。网上一些教程用伪代码演示 ListView 绑定抄过来必然翻车因为那是 DataGridView 的写法。解决用循环填充把 DataTable 每一行转成 ListViewItem 再 Add 进 Items 集合也就是本案例核心代码做的事情。如果数据量极大而且想避免手动管理 ItemsListView 提供的是 VirtualMode 配合 RetrieveVirtualItem 事件和 DataSource 完全是两条路线后面进阶章会讲。4.2 列头全没显示数据却都在现象列表行数据正常显示但顶部没有列标题就像一串没有表头的表格用户根本分不清哪列是姓名哪个是部门。原因最快排查的是 View 没设成 Details其次检查 Columns 集合是否为空还有一个隐蔽原因是窗口加载顺序问题列头代码在 ListView 控件创建之前就执行了。解决在 InitializeComponent 或 Load 事件里显式写 listView.View View.Details;然后逐个 Columns.Add 列头。确认 Designer 文件里已经生成了这些初始化代码没有就手动补上。列头样式如果设置了 ColumnHeaderStyle.None也会导致列头不可见需要改回 Clickable 或 Nonclickable。4.3 数据一多就闪烁窗口拖动都卡现象数据量到几百行后每次刷新列表窗体肉眼可见地闪烁拖拽窗口时尤其明显严重的时候像在播放幻灯片。原因直接调 Items.Clear 和 Items.Add 时每次增删都会触发一次控件的重绘频繁增删就累积成闪烁。尤其是循环里几百次 Add等于连续刷了几百次屏。解决在填充循环前后包上 BeginUpdate 和 EndUpdate。这两行代码暂时关闭控件的实时重绘所有 Items 都改完之后一次性刷新屏幕。注意这两个方法必须成对出现只写 BeginUpdate 忘写 EndUpdate 的话ListView 会一直处于不重绘状态界面卡死半天像真死机。4.4 SubItems 下标错位取出来的字段对不上现象选中一行后取 SubItems[1]显示的不是姓名而是部门或者点击刷新后字段全部乱掉数据本身没错错在对应关系。原因ListView 的 SubItems 从 0 开始SubItems[0] 对应的是主列也就是第一列追加的那个字段和 Columns 的下标天然错开一位。填充时改了字段顺序读取时没同步改就会全线错位。解决把列索引写进项目注释或者用常量类定义列索引。比如 const int COL_ID 0; const int COL_NAME 1;读取时用常量名代替数字后面调列序只需要改一处。先保证 Columns.Add 的顺序和 SubItems.Add 的顺序严格一致再写读取方法不要凭记忆猜下标。4.5 刷新后选中行回到第一行现象点击刷新按钮重新加载列表原本滚动到第 60 行选中的那条记录刷新后列表回到第一行用户每次都要重新滚动查找体验很差。原因刷新时把 Items 整个清空了ListView 不会自动记忆浏览位置和选中项Items 集合重建后原 item 对象也全部失效。解决刷新前记录当前选中行的主键刷新完成后在循环里找到主键匹配的行重新选中并调用 EnsureVisible 把它滚动到可见区域。string lastId listView.FocusedItem null ? : listView.FocusedItem.SubItems[0].Text; LoadListView(); if (lastId ! ) { foreach (ListViewItem item in listView.Items) { if (item.SubItems[0].Text lastId) { item.Selected true; item.Focused true; item.EnsureVisible(); break; } } }代码逻辑先取当前焦点行的主键刷新后按主键重新定位。FocusedItem 在没有选中行时为 null加上判空避免空引用。选中后调用 EnsureVisible 让该行滚到可视区域兼顾选中和浏览位置两个维度。这五条坑没有一条是语法级难题但业务系统跑起来后每一条都会变成实际维护成本。尤其是第一条几乎每个从 DataGridView 转过来的人都要交一次学费。5. 进阶点列头排序、关键字定位和大量数据的 VirtualMode数据展示跑通之后再往前走一小步就能覆盖大多数业务需求。这里直接放三个可以落到真实项目里的技巧对应三个常见场景列头排序、关键字搜索、数据量过大。5.1 点击列头排序一个自定义比较器常见做法是在 ColumnClick 事件里给 ListView 设置 ListViewItemSorter也就是实现 IComparer 接口的比较器对象。public class ListViewItemComparer : IComparer { private int col; public ListViewItemComparer(int column) { col column; } public int Compare(object x, object y) { ListViewItem a (ListViewItem)x; ListViewItem b (ListViewItem)y; return string.Compare(a.SubItems[col].Text, b.SubItems[col].Text); } }在 ColumnClick 事件里切换升序降序时有一个关键约束不要在设置 ListViewItemSorter 的同时给 listView.Sorting 赋值Sorting 会创建内置比较器把自定义 Sorter 覆盖掉你的比较逻辑就白写了。比较器里如果遇到数字列string.Compare 会出 10 排在 2 前面的问题需要先把文本转成 int 再比较。5.2 输入关键字定位第一个匹配行用 TextBox 的 TextChanged 事件触发遍历找到匹配项后选中并滚动到可见区域。private void txtSearch_TextChanged(object sender, EventArgs e) { foreach (ListViewItem item in listView.Items) { if (item.SubItems[1].Text.Contains(txtSearch.Text)) { item.Selected true; item.Focused true; item.EnsureVisible(); return; } } }逻辑关键字在“姓名”列里模糊匹配命中后立即返回一次定位一个。想要精确定位可以把 Contains 换成 Equals。EnsureVisible 只负责滚动不会自动选中所以顺序必须是先 Selected 后 EnsureVisible。5.3 数据量上来后VirtualMode 是另一个思路循环填充在 2000 行以内问题不大超过这个量级每个 ListViewItem 的内存开销和刷新成本会肉眼可见地拉高。两个方向可选分页加载适合业务上本来就分页的场景SQL Server 2012 以上可以用 OFFSET FETCH。SELECT Id, Name, Dept, Phone FROM tb_User ORDER BY Id OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY;OFFSET 和 FETCH 是 SQL Server 2012 引入的语法旧版数据库要用 ROW_NUMBER() 子查询实现等价功能。虚拟模式则更适合本地全量展示场景核心是让 ListView 不再直接管理 Items改为按需从内存数据里取listView.VirtualMode true; listView.VirtualListSize dt.Rows.Count; listView.RetrieveVirtualItem listView_RetrieveVirtualItem; private void listView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { DataRow row dt.Rows[e.ItemIndex]; ListViewItem item new ListViewItem(row[Id].ToString()); item.SubItems.Add(row[Name].ToString()); item.SubItems.Add(row[Dept].ToString()); item.SubItems.Add(row[Phone].ToString()); e.Item item; }代码逻辑VirtualMode 开启后 ListView 通过 RetrieveVirtualItem 事件按索引向外部要数据e.ItemIndex 就是当前需要渲染的行号从内存表里按索引取这行构造 ListViewItem 再返回。因为只按需要创建性能明显更好。用虚拟模式有两个硬性限制不能开 CheckBoxes也不能使用分组这两类和按需取行机制冲突。从那以后我每写一个 ListView 展示模块都会在动手前先把数据量级估算清楚几百行用循环填充加 BeginUpdate数据可能上万就直接 VirtualMode列索引映射写进注释排序和搜索这类附加功能按需接入。这套思路看着简单连着做三四个项目之后就知道能省多少事。希望帮到你。本文还有配套的精品资源点击获取