ListView启用VirtualMode可解决几万条数据卡顿问题,需手动管理数据源、设置VirtualListSize、在RetrieveVirtualItem中按需提供项,并自行维护选中/编辑状态及键盘导航等行为。

ListView 用 VirtualMode 真正解决卡顿
直接绑定几万条数据到 ListView,界面必然卡死——不是渲染慢,是控件内部在反复计算、重绘、响应滚动。Windows Forms 的 ListView 原生支持虚拟模式(VirtualMode = true),它不存数据,只按需拉取当前可视行的内容。
关键点:你得自己管数据源、行数、以及每次滚动时的 RetrieveVirtualItem 回调。
-
VirtualMode = true后,Items和CheckedItems都失效,别再往里 Add - 必须设置
VirtualListSize为总记录数,否则滚动条位置错乱 - 在
RetrieveVirtualItem事件里,根据e.ItemIndex查数据库或内存列表,返回新ListViewItem;别缓存ListViewItem实例(会重复触发) - 如果要支持排序或筛选,改
VirtualListSize和重置TopItem即可,不用清空重绑
分页逻辑别写在 UI 层,用 IAsyncEnumerable 流式查库
所谓“分页显示”,不是把十万条全查出来再切片——那只是把卡顿从渲染阶段挪到查询阶段。真正有效的分页,是让数据库只吐当前页的几百条。
比如用 EF Core 查询第 3 页(每页 200 条):
var items = await dbContext.Products
.Skip(400)
.Take(200)
.AsNoTracking()
.ToListAsync();
但注意:Skip + Take 在大数据量下性能差(MySQL/SQL Server 都要扫前 N 行)。更稳的做法是用游标分页(WHERE id > @lastId ORDER BY id LIMIT 200),尤其配合索引。
- 别在
RetrieveVirtualItem里实时查数据库——单次滚动可能触发几十次回调,IO 会炸 - 推荐预加载 3–5 页数据进内存 List<T>,滚动接近边界时异步加载下一批,用
Task.Run+await避免阻塞 UI - 若数据实时性要求高,加个刷新按钮比自动后台加载更可控
选中状态和编辑状态必须自己维护
开启 VirtualMode 后,ListViewItem.Checked、Text、SubItems 全部只读。所有状态都要映射到你的业务对象上,否则一滚动就丢状态。
常见错误现象:listView1.CheckedIndices 返回空数组,勾选后松手就取消。
- 定义一个
List<bool>或HashSet<int>记录已选中的原始数据索引 - 在
ItemCheck事件里更新这个集合,而不是操作ListViewItem - 在
RetrieveVirtualItem中,根据当前e.ItemIndex查这个集合,设置e.Item.Checked = isItemSelected - 编辑某行?监听
BeforeLabelEdit,修改对应业务对象字段,再触发RedrawItems刷新局部区域
WinForms ListView 虚拟模式的隐藏代价
它快,但不免费。最常被忽略的是双缓冲失效和键盘导航断裂。
默认情况下,VirtualMode 下 DoubleBuffered 不生效(因为没走标准绘制路径),快速滚动会出现闪烁;而方向键、Home/End、Ctrl+A 这些原生行为全部退化成“仅移动焦点框”,不自动滚动到对应项。
- 手动开启双缓冲:反射调用
SetStyle(ControlStyles.OptimizedDoubleBuffer, true),或继承ListView重写构造函数 - 键盘导航要自己处理
KeyDown:捕获Keys.Down、Keys.PageDown等,算出目标索引,再调EnsureVisible+Focus() - 右键菜单定位不准?
ContextMenuStrip.Show()前先用HitTest拿到真实项索引,别信SelectedIndices[0]
虚拟模式不是银弹,它把渲染负担转嫁成了状态管理复杂度。数据量真超 5 万,且需要频繁交互,不如换 DataGrid 或第三方控件——ListView 的设计初衷本就不是大数据表格。










