DataGridView 控件分页
在使用Winform开发桌面应用时,工具箱预先提供了丰富的基础控件,利用这些基础控件可以开展各类项目的开发。但是或多或少都会出现既有控件无法满足功能需求的情况,或者在开发类似项目时,我们希望将具有相同功能的模板封装成一个标准控件等,在这些场景下,winform自带的控件就有些乏力了,需要我们自己开发一些控件。
本篇开篇于DataGridView控件的分页效果,当数据量大的时候,分页是必要的,但是控件本身是没有分页功能的,所以需要自己实现。
我不是专业的控件开发人员,所以写下这篇文章作为学习过程中的记录。
前言
.NET提供了丰富的控件创作技术,自定义控件主要分为三类 - Windows Forms Control Development Basics:
- 复合控件:将现有控件组合成一个新的控件
- 扩展控件:在现有控件的基础上修改原有控件功能或添加新的功能
- 自定义控件:从头到尾开发一个全新的控件。继承
System.Windows.Forms.Control类,添加和重写基类的属性、方法和事件。winform的控件都是直接或间接从System.Windows.Forms.Control派生的类,基类Control提供了控件进行可视化所需要的所有功能,包括窗口的句柄、消息路由、鼠标和键盘事件以及许多其他用户界面事件。自定义控件是最灵活也最为强大的方法,同时对开发者的要求也比较高,你需要处理更为底层的Windows消息,需要了解GDI+技术以及Windows API
由易到难,我们从最简单的复合控件一步一步来,自定义控件作为我们的终极目标哈
通过MSND上的 ctlClockLib 示例学一下怎样开发复合控件以及扩展现有控件:
复合控件 - 示例
来看看怎样创建和调试自定义控件项目,以MSND上的ctlClockLib 中的 ctlClock为例:
创建
Windows 窗体控件库
之后其实和开发
Winform项目差不多,在设计时里拖入想要组合的控件,在后台代码实现相应的内容。具体代码,不做赘述,和文档相同。这个教程只要是完成一个可以自定义底色以及时间字体颜色的以及时钟控件,由一个Label和一个Timer组成,暴露出一个ClockBackColor属性和ClockBackColor分别控制背景色以及字体颜色:using System;
using System.Drawing;
using System.Windows.Forms; namespace ctlClockLib
{
public partial class ctlClock : UserControl
{
private Color colFColor;
private Color colBColor; public Color ClockBackColor
{
get => colBColor;
set
{
colBColor = value;
lblDisplay.BackColor = colBColor;
}
}
public Color ClockBackColor
{
get => colFColor;
set
{
colFColor = value;
lblDisplay.ForeColor = colFColor;
}
}
public ctlClock()
{
InitializeComponent();
}
protected virtual void timer1_Tick(object sender, EventArgs e)
{
lblDisplay.Text = DateTime.Now.ToLongTimeString();
}
}
}
运行以后是一个类似设计器的页面,右侧为控件属性,左侧为控件内容:

这样一个简单的复合控件 - ctlClock就完成了,怎么在实际项目中使用就和调用第三方控件是相似的:
新建一个新的
Winform工程:
在工具箱新建一个选项卡,然后选择项添加上面时钟控件生成的
DLL文件,或者直接将文件拖入选项卡中:


- 然后就和正常控件一样用就可以了,这个时钟控件,你拖入可以发现他在设计器里也是会正常走时间的,之后调整自定义的时钟控件就可以在使用控件的窗体中显现出来。

扩展控件 - 示例
上面示例中创建了一个名为ctlClock的时钟控件,它只有钟表功能,怎样让它带有报警的功能呢,给ctlClock添加报警功能的过程就是拓展控件的过程。这里需要我们有一些C# 面向对象 - 继承的基础,以MSDN上的 ctlAlarmClock为例。
简单说一下继承:一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数。在实现继承中,派生类型采用基类型的每个函数的实现代码,除非在派生类型的定义中指定重写某个函数的实现代码。一般在需要给现有类型添加功能时使用继承。
具体编码就不说了,MSDN上都有,在原有ctlClock基础上,添加了一个指示报警的Label:lblAlarm,并重写了ctlClock的timer1_Tick:
using System;
using System.Drawing;
namespace ctlClockLib
{
public partial class ctlAlarmClock : ctlClock
{
private DateTime dteAlarmTime;
private bool blnAlarmSet;
private bool blnColorTicker;
public ctlAlarmClock()
{
InitializeComponent();
}
public DateTime AlarmTime { get => dteAlarmTime; set => dteAlarmTime = value; }
public bool AlarmSet { get => blnAlarmSet; set => blnAlarmSet = value; }
protected override void timer1_Tick(object sender, EventArgs e)
{
base.timer1_Tick(sender, e);// 基类中的timer1_Tick功能正常运行
if (AlarmSet == false)
return;
else
{
if (AlarmTime.Date == DateTime.Now.Date && AlarmTime.Hour ==
DateTime.Now.Hour && AlarmTime.Minute == DateTime.Now.Minute)
{
lblAlarm.Visible = true;
if (blnColorTicker == false) // 根据blnColorTicker交替改变lblAlarm背景颜色
{
lblAlarm.BackColor = Color.Red;
blnColorTicker = true;
}
else
{
lblAlarm.BackColor = Color.Blue;
blnColorTicker = false;
}
}
else
{
lblAlarm.Visible = false;
}
}
}
private void lblAlarm_Click(object sender, EventArgs e)
{
AlarmSet = false;
lblAlarm.Visible = false;
}
}
}
项目结构:

ctlTestDemo设计器:

运行ctlTestDemo:

回到正题,有了上面例子的基础,来尝试一下通过复合控件实现DataGridView 分页功能。
SuperGridView
参照 C# datagridview分页功能 - 没事写个Bug - 非自定义控件 做了一些优化,可以自定义数据源,做了控件大小自适应处理(就是通过TableLayout做了下处理),控件名 - SuperGridView:

控件样式如上图所示,通过TableLayout做了自适应的处理:

暴露一个DataSource属性用于给DataGridView绑定数据源,一个PageSize属性可以调整DataGridView每页显示的数据量,控件代码:

using System;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
namespace cassControl
{
public partial class SuperGridView : UserControl
{
private int pageSize = 30; // 每页记录数
private int recordCount = 0; // 总记录数
private int pageCount = 0; // 总页数
private int currentPage = 0; // 当前页数
private DataTable originalTable = new DataTable(); // 数据源表
private DataTable schemaTable = new DataTable(); // 虚拟表
public SuperGridView()
{
InitializeComponent();
InitializeDataGridzview();
}
private void InitializeDataGridzview()
{
dgv.AutoGenerateColumns = true;
dgv.AllowUserToAddRows = false;
dgv.AllowUserToResizeRows = false;
dgv.ReadOnly = true;
dgv.RowHeadersVisible = true;
dgv.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
dgv.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
}
[Category("DataSource"), Description("指示 DataGridView 控件的数据源。")]
public object DataSource
{
get { return OriginalTable; }
set
{
if (value is DataTable dt)
{
OriginalTable = dt;
dgv.DataSource = dt;
PageSorter();
}
else
{
throw new ArgumentException("Only DataTable is supported as DataSource.");
}
}
}
[Category("PageSize"), Description("指示 DataGridView 控件每页数据量。")]
public int PageSize { get => pageSize; set => pageSize = value; }
private int RecordCount { get => recordCount; set => recordCount = value; }
private int PageCount { get => pageCount; set => pageCount = value; }
private int CurrentPage { get => currentPage; set => currentPage = value; }
private DataTable OriginalTable { get => originalTable; set => originalTable = value; }
private DataTable SchemaTable { get => schemaTable; set => schemaTable = value; }
private void PageSorter()
{
RecordCount = OriginalTable.Rows.Count;
this.lblCount.Text = RecordCount.ToString();
PageCount = (RecordCount / PageSize);
if ((RecordCount % PageSize) > 0)
{
PageCount++;
}
//默认第一页
CurrentPage = 1;
LoadPage();
}
private void LoadPage()
{
if (CurrentPage < 1) CurrentPage = 1;
if (CurrentPage > PageCount) CurrentPage = PageCount;
SchemaTable = OriginalTable.Clone();
int beginRecord;
int endRecord;
beginRecord = PageSize * (CurrentPage - 1);
if (CurrentPage == 1) beginRecord = 0;
endRecord = PageSize * CurrentPage - 1;
if (CurrentPage == PageCount) endRecord = RecordCount - 1;
int startIndex = beginRecord;
int endIndex = endRecord;
for (int i = startIndex; i <= endIndex; i++)
{
DataRow row = OriginalTable.Rows[i];
SchemaTable.ImportRow(row);
}
dgv.DataSource = SchemaTable;
}
private void btnNext_Click(object sender, EventArgs e)
{
if (CurrentPage == PageCount)
{ return; }
CurrentPage++;
LoadPage();
}
private void btnBegain_Click(object sender, EventArgs e)
{
if (CurrentPage == 1)
{ return; }
CurrentPage = 1;
LoadPage();
}
private void btnEnd_Click(object sender, EventArgs e)
{
if (CurrentPage == PageCount)
{ return; }
CurrentPage = PageCount;
LoadPage();
}
private void btnPre_Click(object sender, EventArgs e)
{
if (CurrentPage == 1)
{ return; }
CurrentPage--;
LoadPage();
}
}
}
控件功能:
- 控件具有自定义的数据源绑定功能,通过
DataSource属性绑定DataTable对象作为数据源。 - 控件支持分页显示,可以按照每页固定的记录数显示数据。
- 控件的分页功能包括跳转到第一页、上一页、下一页、最后一页,以及显示总记录数等。
- 控件中的数据表格 (
DataGridView) 可以自动生成列,表中内容默认居中显示
实机演示 - 也还凑合,试了一下自造了十万条数据,但是在十万条数据下可以明显看到内存暴涨,从最初的22MB涨到了60MB,好在我的应用场景下数据量不大:

这段代码只实现了一个简单的分页数据表格控件,适合处理中小规模的数据。它的主要优点是简化数据绑定和提供分页显示,但仍有改进空间,尤其在处理大数据集和功能扩展方面。如果只是在项目中使用,且数据量不大,这个控件可能已经足够。然而,如果需要更多功能和性能优化,可能需要进一步开发和优化,比如可以加上页面,页码自动跳转之类的,还有内存占用问题等,还有就是在设计器里不能暴露出来DataGridView 任务操作选项,需要通过后台代码完成数据显示的绑定,我在想是不是可以不直接用DataGridView呢,只用下方的操作栏呢?
PagerControl
用上面的思路试一试组合一个操作栏出来,为了好看一点,这次换成组合CSkin的控件。
样式和上面几乎一致,没有放每页条数的配置项,这个打算作为一个属性放出来:

我的思路是给控件一个数据源,用于绑定页面中的DataGridView,然后获取到数据以后和之前一样,因为使用场景下数据量不是特别大,所以就同样沿用上面的思路。
这里需要暴露一个配置项用于绑定页面上的DataGridView需要用到设计时的一些特性(Attribute),这些设计时的特性(Attribute)在C#和类似的语言中扮演着非常重要的角色,用于影响控件在设计时的表现和行为,提供更好的用户体验和开发者便利:

OK,理想很丰满,现实很骨感。通过绑定绑定页面中的DataGridView获取数据会有一个问题,因为我控制分页的方式是通过给DataGridView更换处理之后的DataSource数据表,这就导致有一个问题是我不知道DataGridView什么时候会绑定数据,解决这个问题我能想到的就是监听数据源的变化,也就是通过DataGridView的DataSourceChanged事件,但这就导致我在实现分页效果的时候也会触发该事件,逻辑会陷入一个死循环里面。。。
换一种方式,清空DataGridView表中数据然后一行一行的加Clear()方法又会报错:
// 假设已经有一个DataGridView控件名为dataGridViewToBind
// 假设已经有一个DataTable名为newDataTable
// 清空表格中的内容
dataGridView1.Rows.Clear();
dataGridView1.Refresh();
// 添加新的DataTable数据
foreach (DataRow row in newDataTable.Rows)
{
dataGridViewToBind.Rows.Add(row.ItemArray);
}
一通抓耳挠腮之后,我觉得换一种思路:只操作DataGridView上显示的内容,当然也是通过更改它的DataSource来完成,获取DataGridView的数据源采用之前的思路,控件给一个数据源属性,每次更改DataGridView的数据源的时候也顺路操作一下控件的数据源,这样就不用在控件内部监听DataGridView数据源的变化了,也就不会出现我在操作DataGridView的时候程序陷入死循环的问题。
All Right。来说说怎么搞的,更之前那个相比有点不一样,因为是给一个n年前的winform项目做的,所以这里DataGridView改为CSkin的SkinDataGridView还有就是数据源,程序用的DataTable这里也就用``DataTable了,但是数据源那里放的object`类型,可以扩展其他类型数据:
using CCWin.SkinControl;
using System;
using System.ComponentModel;
using System.Data;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace cassControl
{
public partial class PagerControl : UserControl
{
public PagerControl()
{
InitializeComponent();
}
#region fields, properties
private int pageCount;
private int dataCount;
private int pageSize = 50;
private int currentPage;
private DataTable dataSourceTable;
private DataTable tempTable;
private SkinDataGridView dataGridViewToBind;
[Browsable(true)]
[Category("PagerControl")]
[Description("为 PagerControl 绑定 DataGridView 数据项")]
public SkinDataGridView DataGridView
{
get { return dataGridViewToBind; }
set
{
dataGridViewToBind = value;
}
}
[Browsable(false)]
public object DataSource // 数据类型可以扩展
{
get { return dataSourceTable; }
set
{
if (value is DataTable dt)
{
dataSourceTable = dt;
PageSorter();
}
else
{
return;
}
}
}
[Browsable(false)]
public int CurrentPage { get => currentPage; set => currentPage = value; }
[Browsable(false)]
public int PageCount { get => pageCount; set => pageCount = value; }
[Browsable(false)]
public int DataCount { get => dataCount; set => dataCount = value; }
[Browsable(true)]
[Category("PagerControl")]
[Description("设置每页显示的数据量")]
public int PageSize
{
get => pageSize;
set
{
if (value <= 0)
{
pageSize = 50; // 默认显示50条数据
}
else { pageSize = value; }
}
}
#endregion fields, properties
#region methods
private void PageSorter()
{
DataCount = dataSourceTable.Rows.Count;
lblDataCount.Text = DataCount.ToString();
PageCount = (DataCount / PageSize);
if ((DataCount % PageSize) > 0)
{
PageCount++;
}
lblPageCount.Text = PageCount.ToString();
CurrentPage = 1;
lblCurrentPage.Text = CurrentPage.ToString();
SetCtlEnabled(true);
LoadPage();
}
private void LoadPage()
{
if (CurrentPage < 1) CurrentPage = 1;
if (CurrentPage > PageCount) CurrentPage = pageCount;
tempTable = dataSourceTable.Clone();
int beginIndex, endIndex;
if (CurrentPage == 1)
{
beginIndex = 0;
}
else { beginIndex = PageSize * (CurrentPage - 1); }
if (CurrentPage == PageCount)
{
endIndex = DataCount - 1;
}
else { endIndex = PageSize * CurrentPage; }
lblCurrentPage.Text = CurrentPage.ToString();
txtTargetPage.Text = CurrentPage.ToString();
for (int i = beginIndex; i < endIndex; i++)
{
DataRow row = dataSourceTable.Rows[i];
tempTable.ImportRow(row);
}
dataGridViewToBind.DataSource = tempTable;
}
private void SetCtlEnabled(bool status)
{
btnFirstpage.Enabled = status;
btnNextpage.Enabled = status;
btnPreviouspage.Enabled = status;
btnLastpage.Enabled = status;
txtTargetPage.Enabled = status;
btnSwitchPage.Enabled = status;
}
#endregion methods
#region events
private void btnFirstpage_Click(object sender, EventArgs e)
{
if (CurrentPage == 1)
{ return; }
CurrentPage = 1;
LoadPage();
}
private void btnPreviouspage_Click(object sender, EventArgs e)
{
if (CurrentPage == 1)
{ return; }
CurrentPage--;
LoadPage();
}
private void btnNextpage_Click(object sender, EventArgs e)
{
if (CurrentPage == PageCount)
{ return; }
CurrentPage++;
LoadPage();
}
private void btnLastpage_Click(object sender, EventArgs e)
{
if (CurrentPage == PageCount)
{ return; }
CurrentPage = PageCount;
LoadPage();
}
private void btnSwitchPage_Click(object sender, EventArgs e)
{
int num = 0;
int.TryParse(txtTargetPage.Text.Trim(), out num);
CurrentPage = num;
LoadPage();
}
private void txtTargetPage_KeyPress(object sender, KeyPressEventArgs e)
{
string pattern = @"[0-9]";
Regex regex = new Regex(pattern);
if (!regex.IsMatch(e.KeyChar.ToString()) && !char.IsControl(e.KeyChar))
{
e.Handled = true;
}
}
#endregion events
}
}

客户端使用:
DataTable dataTable = new DataTable();
dataTable.Columns.Add("ID", typeof(int));
dataTable.Columns.Add("Name", typeof(string));
dataTable.Columns.Add("Age", typeof(string));
dataTable.Columns.Add("Age1", typeof(string));
......
dataTable.Columns.Add("Age15", typeof(string));
for (int i = 1; i <= 100000; i++)
{
DataRow newRow = dataTable.NewRow();
newRow["ID"] = i;
newRow["Name"] = "Name_" + i;
newRow["Age"] = i * 1.2;
dataTable.Rows.Add(newRow);
}
superGridView1.DataSource = dataTable;
skinDataGridView1.DataSource = dataTable;
pagerControl1.DataSource = dataTable;

大致上就这个样子,还是有很大的改进空间的
Demo的代码上传到GitHub了,感兴趣的友友们可以参考一下:PagerControl
还有一件事,真的很讨厌维护
N年前老师傅写的项目,太痛苦了
参考
MSDN:
技术博文:
DataGridView 控件分页的更多相关文章
- Datagridview控件实现分页功能
可以进行sql语句进行设置: 1.先新建一个窗体,一个DataGridView控件.两个label控件.两个Button控件 2.代码如下: using System; using Sy ...
- C#实现WinForm DataGridView控件支持叠加数据绑定
我们都知道WinForm DataGridView控件支持数据绑定,使用方法很简单,只需将DataSource属性指定到相应的数据源即可,但需注意数据源必须支持IListSource类型,这里说的是支 ...
- 实现虚拟模式的动态数据加载Windows窗体DataGridView控件 .net 4.5 (一)
实现虚拟模式的即时数据加载Windows窗体DataGridView控件 .net 4.5 原文地址 :http://msdn.microsoft.com/en-us/library/ms171624 ...
- Visual Basic 2012 借助DataGridView控件将SQL server2012 数据导入到Excel 2010
摘 要: SQL Server 2012 数据和Excel 2010之间的连接和数据的传输,本篇文章主要针对的是SQL Server 2012 数据导入到Excel 2010文件中.Excel软件对 ...
- Visual Basic 2012 借助DataGridView控件将Excel 2010数据导入到SQL server 2012
(注:注释的颜色原本为绿色,在这里变为黑色,有点不便,但不会造成阅读影响.放入Visual Basic2012代码编辑器后会还原成绿色.) 摘 要:DataGridView控件作为数据传输的中介,只 ...
- Winform DataGridView控件添加行号
有很多种方法,这里介绍三种: A: 控件的RowStateChanged事件中添加,RowStateChanged事件是在行的状态更改(例如,失去或获得输入焦点)时发生的事件: e.Row.Hea ...
- Winform 中DataGridView控件添加行标题
有很多种方法. 1.可以在DataGridView控件中的RowStateChanged事件改变行标题单元格的值(Row.HeaderCell.Value) /// <summary> / ...
- C# DataGridView控件清空数据完美解决方法
C# DataGridView控件绑定数据后清空数据在清除DataGridview的数据时: 1.DataSource为NULL(DataGridView.DataSource= null;)这样会将 ...
- 实现winform DataGridView控件判断滚动条是否滚动到当前已加载的数据行底部
判断 DataGridView控件滚动条是否滚动到当前已加载的数据行底部,其实方法很简单,就是为DataGridView控件添加Scroll事件,然后写入以下代码就可以了,应用范围:可实现分部加载数据 ...
- 在DataGridView控件中加入ComboBox下拉列表框的实现
在DataGridView控件中加入ComboBox下拉列表框的实现 转自:http://www.cnblogs.com/luqingfei/archive/2007/03/28/691372.htm ...
随机推荐
- ai问答:使用vite如何配置多入口页面
Vite 是一个 web 开发构建工具,它可以用于开发单页应用和多页应用.要在 Vite 中配置多入口,可以: 在 vite.config.js 中定义多个 entry 入口: export defa ...
- 【Ubuntu】3.配置下载源与更新
在 Ubuntu 中,更改下载源可以加快下载速度.以下是更改 Ubuntu 下载源的步骤: 方法一: 备份之前的 sources.list 文件: sudo cp /etc/apt/sources.l ...
- Git&GitHub简介与入手(一)
一.Git版本控制 1.集中式版本控制工具:SVN(版本控制集中在服务器端,会有单点故障风险): 2.分布式版本控制工具:Git: 3.Git简史 Talk is cheap, show me the ...
- 2022-03-07:K 个关闭的灯泡。 N 个灯泡排成一行,编号从 1 到 N 。最初,所有灯泡都关闭。每天只打开一个灯泡,直到 N 天后所有灯泡都打开。 给你一个长度为 N 的灯泡数组 blubs
2022-03-07:K 个关闭的灯泡. N 个灯泡排成一行,编号从 1 到 N .最初,所有灯泡都关闭.每天只打开一个灯泡,直到 N 天后所有灯泡都打开. 给你一个长度为 N 的灯泡数组 blubs ...
- Casdoor 开始
Casdoor 是一个基于 OAuth 2.0 / OIDC 的中心化的单点登录(SSO)身份验证平台,简单来说,就是 Casdoor 可以帮你解决用户管理的难题,你无需开发用户登录.注册等与用户鉴权 ...
- Vue选日期滚动条自动定位到选定的日期位置
html 这里的关键点就是 :id="'scroll'+index" 以及 :scroll-into-view="intoIndex" <view c ...
- proto中service 作用的理解
转载请注明出处: 在 proto 文件中,service 用于定义一组 RPC 方法,在服务端实现这些方法,并在客户端调用这些方法进行远程过程调用. service 的定义方式如下: service ...
- drf——反序列化校验源码(了解)、断言、drf之请求和响应、视图之两个视图基类
1.模块与包 # 模块与包 模块:一个py文件 被别的py文件导入使用,这个py文件称之为模块,运行的这个py文件称之为脚本文件 包:一个文件夹下有__init__.py # 模块与包的导入问题 '' ...
- k8s calico网络
- 代码随想录算法训练营Day16二叉树|104.二叉树的最大深度 559.n叉树的最大深度 111.二叉树的最小深度 222.完全二叉树的节点个数
代码随想录算法训练营 代码随想录算法训练营Day16二叉树|104.二叉树的最大深度 559.n叉树的最大深度 111.二叉树的最小深度 222.完全二叉树的节点个数 104.二叉树的最大深度 题目 ...