news 2026/9/8 13:37:56

WinForm DataGridView分页控件封装实战:从SQL到UI的完整方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WinForm DataGridView分页控件封装实战:从SQL到UI的完整方案

简介:这是一份面向WinForm开发者的DataGridView分页控件源码资源,适合需要在C#桌面应用中快速实现表格分页展示的初中级开发者,也可作为自定义控件开发的学习范例。控件将DataGridView与分页逻辑封装为一体,调用简洁,配合示例工程即可直接运行。压缩包共45个文件,体积仅96KB,主要包含10个CS源码文件(含核心分页控件、测试窗体及Designer设计器代码)、4个RESX资源文件、5个DLL与EXE编译输出及PDB调试符号,并附有VS解决方案与测试工程,目录结构清晰。目前已有222人学习下载,足以说明该控件在实际开发中有一定参考价值。借助源码和示例工程,读者既能快速移植分页逻辑,也能理解WinForm自定义控件的封装与调试思路,遇到显示异常或翻页问题时还可结合PDB符号对照定位。 WinForm里做DataGridView分页,网上能搜到的方案五花八门,有直接用第三方库的,有自己写SQL分页的,也有把分页逻辑硬塞进窗体代码里的。我自己的项目经历过从“一次性加载全部数据”到“自己封装分页控件”的完整演变过程,踩了不少坑,也总结了一套还算成熟的方案。这篇文章就把完整的思路、代码骨架和遇到的问题都整理出来,给正在做类似需求的朋友一个参考。

1. 分页控件的整体设计与技术选型

1.1 为什么选择自己封装而不是用第三方库

先说结论:如果你的项目里有多处列表展示需求,并且你希望分页行为完全可控,自己封装一个轻量级的分页控件,性价比远高于引入第三方UI库。

我当时的项目是基于.NET Framework 4.5的WinForms系统,数据量虽然不算恐怖,但单表几十万条记录是有的。最开始团队图省事,直接一次性SELECT *把数据全查出来绑定到DataGridView上,结果就是页面打开卡顿,滚动条拖动像幻灯片,数据量一上来程序直接假死。后来决定做分页,市面上找了一圈控件,要么太重(集成了一堆用不上的功能),要么UI风格和现有系统不搭,要么有授权风险。权衡之下,我决定自己封装一个。

自己封装分页控件有几个实实在在的好处:

  • 完全定制UI布局,不用迁就第三方控件的固定样式。
  • 不引入额外依赖,避免了版本冲突和潜在的商业授权问题。
  • 核心翻页逻辑完全可控,遇到问题可以直接定位、修改,不用担心黑盒行为。

当然,如果你的项目预算充足、时间紧张,直接用DevExpress或者ComponentOne这类成熟的商业控件库,确实能省不少事。但分页控件本身逻辑不算复杂,自己封装反而能加深对整个分页机制的理解。

1.2 数据访问层的选型:ADO.NET + ROW_NUMBER()

数据访问层我选了ADO.NET,没有引入Entity Framework或Dapper。很多人可能会觉得奇怪,都什么年代了还用ADO.NET?但我的理由很实际:这个项目的数据库访问逻辑相对直接,表结构也不算复杂,团队对ADO.NET的掌握程度极深,引入ORM反而增加了学习和维护成本。

分页查询的SQL我用了ROW_NUMBER()窗口函数,这是SQL Server 2005及以上版本支持的特性。核心SQL长这样:

WITH PagedData AS ( SELECT *, ROW_NUMBER() OVER (ORDER BY CreateTime DESC) AS RowNum FROM Orders WHERE (@WhereClause) ) SELECT * FROM PagedData WHERE RowNum BETWEEN (@PageIndex - 1) * @PageSize + 1 AND @PageIndex * @PageSize

这里ROW_NUMBER()配合索引,在大表上的执行计划通常比OFFSET FETCH更优。不过它有个明显的短板:SQL语句依赖特定数据库的语法,可移植性差。如果你的项目将来有换数据库的打算,建议直接用标准的OFFSET / FETCH NEXT语法,或者干脆用ORM自带的分页API,各有利弊。

1.3 分页控件的UI布局设计

分页控件的UI布局我设计了三个区域:

  • 左侧:显示“当前页/总页数”和“总记录数”,让人一眼看清当前浏览的位置。
  • 中间:翻页按钮(首页、上一页、下一页、末页)和页码按钮区。
  • 右侧:跳转到指定页的输入框和确定按钮。

这个布局符合国内用户的使用习惯,左边看状态,中间操作,右边跳转。页码按钮区域做了动态生成逻辑,总页数多的时候不会把按钮全部堆出来,而是显示首尾页、当前页附近的页码,中间多余的用省略号代替。这个思路有点像搜索引擎的分页策略,既保证了可访问性,又不会让界面过于拥挤。

页码动态生成的逻辑大概是:

private List<int> GetVisiblePageNumbers(int currentPage, int totalPages) { var pages = new List<int>(); pages.Add(1); // 首页 if (currentPage > 3) { pages.Add(-1); // 省略号 } for (int i = Math.Max(2, currentPage - 1); i <= Math.Min(totalPages - 1, currentPage + 1); i++) { pages.Add(i); } if (currentPage < totalPages - 2) { pages.Add(-2); // 省略号 } if (totalPages > 1) { pages.Add(totalPages); // 末页 } return pages; }

-1-2在这里作为省略号的标识,渲染的时候显示为...按钮,不可点击。

2. 分页控件的核心实现与代码骨架

2.1 控件对外暴露的属性与事件

分页控件的基础是一个UserControl,内部用TableLayoutPanel作为根容器,分几行放置状态标签、翻页按钮、页码按钮和跳转控件。对外暴露的关键成员有:

成员类型说明
PageSize属性每页显示的记录条数
CurrentPage属性当前页码
TotalCount属性总记录数
TotalPages属性总页数(只读)
PageChanged事件当前页变化时触发

外部窗体订阅PageChanged事件,在事件处理函数里调用数据访问层,获取当前页数据,绑定到DataGridView。分页控件本身不关心数据来源,只管翻页逻辑,符合单一职责原则。

2.2 翻页操作的核心逻辑

翻页的核心逻辑其实很简单,封装在一个ChangePage方法里:

public void ChangePage(int newPage) { if (newPage < 1 || newPage > TotalPages) return; CurrentPage = newPage; UpdateUI(); PageChanged?.Invoke(this, EventArgs.Empty); }

UpdateUI方法负责根据CurrentPageTotalPages刷新按钮状态(比如第一页时禁用“上一页”按钮)和页码显示。

跳转页的输入框需要做输入验证,只允许输入正整数,超出范围自动修正。用户输入非法值时不直接抛出异常,而是通过ErrorProvider给出友好提示,或者干脆在KeyPress事件里阻止非数字字符的输入。

2.3 数据访问方法的参数封装

分页参数我用了一个简单的查询参数类:

public class PageQueryParams { public int PageIndex { get; set; } public int PageSize { get; set; } public string WhereClause { get; set; } public string OrderBy { get; set; } }

数据访问层的方法签名大致如下:

public PageResult<T> GetPageData<T>(PageQueryParams queryParams)

返回的PageResult<T>包含两部分:Items(当前页的数据集合)和TotalCount(总记录数)。总记录数通过单独的COUNT(*)查询获取,这样分页控件可以正确计算总页数,并在最后一条数据被删除后自动调整页码。

2.4 DataGridView绑定的防闪烁处理

每次翻页都要重新绑定数据源,DataGridView会闪烁,视觉上很不舒服。解决方法是绑定前后挂起和恢复布局:

this.SuspendLayout(); this.dataGridView.SuspendLayout(); this.dataGridView.DataSource = pageResult.Items; this.labelTotal.Text = $"共 {pageResult.TotalCount} 条记录"; this.labelPage.Text = $"第 {CurrentPage} / {TotalPages} 页"; this.dataGridView.ResumeLayout(); this.ResumeLayout();

这个技巧虽然简单,但效果立竿见影,实测下来界面流畅很多。

2.5 CellFormatting事件处理数据格式化

DataGridView在绑定数据源时,如果列类型和数据类型不匹配,会抛出异常或显示难看的数据。我的做法是在CellFormatting事件里写专门的格式化逻辑:

private void dataGridView_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.Value == null || e.Value == DBNull.Value) return; // 日期格式化为 yyyy-MM-dd if (e.Value is DateTime dateTime) { e.Value = dateTime.ToString("yyyy-MM-dd"); e.FormattingApplied = true; } // 小数保留两位 if (e.Value is decimal decimalValue) { e.Value = decimalValue.ToString("F2"); e.FormattingApplied = true; } }

3. 分页控件的扩展:排序、筛选、权限控制

3.1 筛选功能的接入方式

实际项目中,分页往往和筛选功能紧密结合。我的做法是在DataGridView外层加一排查询条件控件,点击查询按钮后,根据条件重新拼接WhereClause,再从第一页开始加载数据。

这里需要特别注意的是WhereClause的拼装,一定要用参数化查询,不要拼接字符串,否则SQL注入的风险非常大。比如:

string whereClause = "1=1"; if (!string.IsNullOrEmpty(txtOrderNo.Text)) { whereClause += " AND OrderNo LIKE @OrderNo"; parameters.Add("@OrderNo", $"%{txtOrderNo.Text.Trim()}%"); } if (dtpStart.Value != dtpStart.MinDate) { whereClause += " AND CreateTime >= @StartTime"; parameters.Add("@StartTime", dtpStart.Value.Date); }

3.2 排序功能的实现

排序功能利用DataGridView的ColumnHeaderMouseClick事件,点击列头时记录排序列名和排序方向,重新生成OrderBy语句,再次查询。基本逻辑是:

private void dataGridView_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) { string columnName = dataGridView.Columns[e.ColumnIndex].DataPropertyName; if (string.IsNullOrEmpty(columnName)) return; // 切换排序方向 if (_sortColumn == columnName) { _sortDirection = _sortDirection == "ASC" ? "DESC" : "ASC"; } else { _sortColumn = columnName; _sortDirection = "ASC"; } // 从第一页重新加载 CurrentPage = 1; LoadPageData(); }

需要注意,DataPropertyName务必要和数据实体类的属性名一致,否则取不到正确的排序列名。

3.3 权限控制与列/按钮级别的处理

权限控制的粒度我控制在列级别和按钮级别。管理员能看到的列,普通用户看不到;管理员能执行的删除操作,普通用户只能查看。具体做法是,登录成功后根据用户角色,动态设置DataGridView列的Visible属性和操作按钮的Enabled属性:

private void ApplyPermission(DataGridView dgv, UserRole role) { if (role == UserRole.Admin) { dgv.Columns["DeleteColumn"].Visible = true; btnAdd.Enabled = true; btnDelete.Enabled = true; } else { dgv.Columns["DeleteColumn"].Visible = false; btnAdd.Enabled = false; btnDelete.Enabled = false; } }

这些功能组合在一起,分页控件才真正融入了业务系统,而不是一个孤零零的翻页工具。

4. 实操中踩过的坑与排查技巧

4.1 删除操作后PageIndex越界的问题

这是我在实际项目里遇到的最典型的坑。场景是这样的:用户在最后一页,当前页只有一条数据,删除这条数据后,数据库总记录数减少了,但PageIndex还停留在原来的值,导致下一页查询返回空数据,界面上一片空白。

解决方法:删除操作后重新查询总记录数,如果当前PageIndex大于新的总页数,就把PageIndex调整为新的总页数。这个逻辑放在分页控件的RefreshData方法里做自动处理,用户无感知。

public void RefreshData() { int totalCount = _dataAccess.GetTotalCount(_whereClause); int totalPages = CalculateTotalPages(totalCount, PageSize); if (CurrentPage > totalPages) { CurrentPage = Math.Max(1, totalPages); } TotalCount = totalCount; LoadPageData(); }

4.2 页码跳转输入的边界条件处理

跳转页面的输入框,是另一个容易出问题的地方。用户输入了0、负数、超出总页数的数字、甚至非数字字符,如果处理不好,程序就会抛出异常或者出现不可预期的行为。我的处理方式是:

  • KeyPress事件里只允许输入数字和控制键。
  • 点击确定按钮时,解析输入值并做范围校验,超出范围自动修正为边界值。
  • 如果输入框为空,不做任何操作,而不是报错。

边界条件的处理一定要做全,否则上线后迟早出事。

4.3 大数据量下的异步加载优化

当数据量特别大的时候,COUNT(*)查询和分页查询都需要耗费一定时间,如果每次都同步执行,界面会卡顿。我把查询放到了Task.Run里异步执行,查询期间在分页控件的状态栏显示“加载中...”提示,查询完成后通过Invoke回到UI线程更新DataGridView:

private async void LoadPageData() { btnPrev.Enabled = false; btnNext.Enabled = false; labelStatus.Text = "加载中..."; var result = await Task.Run(() => _dataAccess.GetPageData(_queryParams)); dataGridView.DataSource = result.Items; labelStatus.Text = $"共 {result.TotalCount} 条记录"; UpdatePaginationButtons(); }

这个改动虽然不大,但对用户体验的提升是实实在在的。

4.4 常见问题速查表

问题可能原因解决方案
翻页后DataGridView空白PageIndex越界删除/筛选后重新查询总记录数,修正PageIndex
表格闪烁严重频繁绑定DataSource绑定前后调用SuspendLayout/ResumeLayout
日期显示为带时间格式列类型格式未设置CellFormatting事件统一格式化
跳转页码输入无反应输入验证拦截了事件检查KeyPress事件是否禁用了数字输入
大数据量翻页卡顿同步查询阻塞UI线程用Task.Run异步查询,配合“加载中”提示
安装到新机器报“未能加载程序集”缺少依赖DLL或.NET Framework版本不对打包时一并集成依赖和框架安装程序

4.5 分页控件的单元测试思路

可能很多人觉得UI控件不好测试,但这恰恰是我认为最需要测试的地方。我写了一个简单的测试项目,不依赖任何测试框架,直接模拟不同的PageIndexPageSize组合,验证总页数计算是否正确、最后一页的数据条数是否正确、删除后PageIndex调整是否正确。

测试代码重点关注这些边界条件:

  • PageSize为0
  • PageIndex为负数或0
  • 总记录数为0
  • 总记录数为1
  • 总记录数刚好是PageSize的整数倍

这些边界条件如果不在开发阶段测试好,上线后出了问题,排查的代价会非常大。

5. 程序打包与部署的经验总结

5.1 Inno Setup打包与.NET Framework依赖

WinForms程序打包,最传统的方式是Visual Studio自带的InstallShield,但这个方案配置繁琐,生成的安装包也比较大。我后来改用了Inno Setup,脚本语言简单,生成的安装包小,还支持中文界面。

打包时最容易踩的坑是依赖项缺失。项目的目标框架如果是.NET Framework 4.5,目标机器上必须安装对应版本的.NET Framework,否则程序无法运行。建议打包时把.NET Framework的安装程序也集成到安装包,或者在安装脚本里检测目标机器是否已安装,如果没有就引导用户安装。

5.2 配置文件的路径与权限问题

程序在开发机上运行正常,部署到别的机器就报错找不到配置文件,这通常是安装目录和配置文件路径不一致导致的。还有权限问题,如果程序需要写文件或者访问数据库,目标机器的用户权限不够,也会导致运行时错误。

排查思路基本上是:先看事件查看器里的错误日志,然后检查依赖项,最后检查权限设置。

5.3 连接字符串的安全管理

连接字符串一般写在App.config的connectionStrings节点里,这样修改数据库地址时不需要重新编译。但有个坑是,配置文件里的密码是明文存储的,如果项目对安全性要求比较高,建议使用DPAPI或其他加密方式对连接字符串进行加密。加密的代价是配置变得复杂,所以要根据项目的实际情况决定,不能一概而论。

6. 几个容易被忽略的细节

6.1 WinForms窗体缩放与控件布局

WinForms窗体默认不支持无级缩放,子控件的大小和位置都是固定的,当用户拉大窗体时,界面不会自动适应。解决这个问题,我的经验是在窗体上使用TableLayoutPanel作为容器,让子控件随窗体大小变化而自动调整,同时设置好AnchorDock属性。具体到DataGridView和分页控件的组合场景,DataGridView的Anchor属性设为Top, Bottom, Left, Right,分页控件Panel的Anchor设为Bottom, Left, Right,这样窗体缩放时,表格会跟着撑大,分页栏会保持在底部。

6.2 DataGridView的列宽控制

DataGridView的AutoSizeColumnsMode属性设置为Fill时,列宽会自动填充整个表格宽度,但如果表格有横向滚动条,这个设置可能导致列宽计算异常,出现多余的空白列。我的处理方式是,根据实际列数和数据内容,手动设置每一列的宽度,或者使用DisplayedCells模式让列宽自适应内容,这样更可控。另外,行头默认显示行号,如果想去掉,将RowHeadersVisible设置为false,否则会浪费一部分宽度,看起来也不够整洁。

6.3 WinForms界面美化的几个方向

WinForms本身的美化能力有限,要让界面看起来现代一点,可以从几个方面入手:统一字体和颜色、合理使用布局控件、在必要的地方使用自定义绘制、或者考虑使用第三方UI库。我做分页控件时没有直接用第三方UI库,因为这个场景自己封装完全够用,没有必要引入额外的依赖和授权成本。

6.4 PictureBox显示SVG图片的补充说明

WinForms的PictureBox原生不支持SVG格式,因为SVG是矢量图,而PictureBox是基于GDI+的,只支持位图。要在PictureBox中显示SVG,通常的方法是引入第三方库,比如SVG.NET,先把SVG渲染成Bitmap,再赋值给PictureBox的Image属性。SVG的尺寸和PictureBox的尺寸要匹配,不然图片会被裁切或变形。

回到分页控件本身,如果让我总结一句核心经验,那就是:分页控件看似简单,但要做到好用、可靠、可维护,需要把数据访问、UI交互、异常处理、性能优化这些环节全部揉碎了想清楚。这套方案里踩过的坑,基本都集中在边界条件处理和用户体验细节上,写出来给大家做个参考。下次你接手类似的WinForms项目,如果也需要一个分页控件,不妨也考虑自己动手封装一个,你会发现,这个过程中的收获,远比想象中要多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:37:03

AI日报热词解读:Agent、Spring AI与内容生产落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:36:56

默纳克电梯控制器测试平台搭建:语音播报与故障模拟实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:36:53

经纬度坐标与XY坐标转换:从原理到实用工具全解析

简介&#xff1a;面向GIS开发、测绘与地图制图人员的坐标转换工具源码包&#xff0c;用于解决经纬度坐标与XY平面坐标之间的互转问题&#xff0c;尤其适合需要处理GPS数据或地图投影的初学者。压缩包共28个文件&#xff0c;以C#工程为主&#xff0c;包含9个.cs源码、3个.exe可执…

作者头像 李华
网站建设 2026/9/8 13:36:33

Agent指令集:从RISC-V到智能体开发的关键设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:33:12

《红色沙漠》v1.10.01性能优化验证与新坐骑实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华