在使用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为例:

  1. 创建Windows 窗体控件库

  2. 之后其实和开发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();
    }
    }
    }
  3. 运行以后是一个类似设计器的页面,右侧为控件属性,左侧为控件内容:

这样一个简单的复合控件 - ctlClock就完成了,怎么在实际项目中使用就和调用第三方控件是相似的:

  1. 新建一个新的Winform工程:

  2. 在工具箱新建一个选项卡,然后选择项添加上面时钟控件生成的DLL文件,或者直接将文件拖入选项卡中:

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

扩展控件 - 示例

上面示例中创建了一个名为ctlClock的时钟控件,它只有钟表功能,怎样让它带有报警的功能呢,给ctlClock添加报警功能的过程就是拓展控件的过程。这里需要我们有一些C# 面向对象 - 继承的基础,以MSDN上的 ctlAlarmClock为例。

简单说一下继承:一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数。在实现继承中,派生类型采用基类型的每个函数的实现代码,除非在派生类型的定义中指定重写某个函数的实现代码。一般在需要给现有类型添加功能时使用继承。

具体编码就不说了,MSDN上都有,在原有ctlClock基础上,添加了一个指示报警的Label:lblAlarm,并重写了ctlClocktimer1_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();
}
}
}

控件功能:

  1. 控件具有自定义的数据源绑定功能,通过 DataSource 属性绑定 DataTable 对象作为数据源。
  2. 控件支持分页显示,可以按照每页固定的记录数显示数据。
  3. 控件的分页功能包括跳转到第一页、上一页、下一页、最后一页,以及显示总记录数等。
  4. 控件中的数据表格 (DataGridView) 可以自动生成列,表中内容默认居中显示

实机演示 - 也还凑合,试了一下自造了十万条数据,但是在十万条数据下可以明显看到内存暴涨,从最初的22MB涨到了60MB,好在我的应用场景下数据量不大:

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

PagerControl

用上面的思路试一试组合一个操作栏出来,为了好看一点,这次换成组合CSkin的控件。

样式和上面几乎一致,没有放每页条数的配置项,这个打算作为一个属性放出来:

我的思路是给控件一个数据源,用于绑定页面中的DataGridView,然后获取到数据以后和之前一样,因为使用场景下数据量不是特别大,所以就同样沿用上面的思路。

这里需要暴露一个配置项用于绑定页面上的DataGridView需要用到设计时的一些特性(Attribute),这些设计时的特性(Attribute)在C#和类似的语言中扮演着非常重要的角色,用于影响控件在设计时的表现和行为,提供更好的用户体验和开发者便利:

OK,理想很丰满,现实很骨感。通过绑定绑定页面中的DataGridView获取数据会有一个问题,因为我控制分页的方式是通过给DataGridView更换处理之后的DataSource数据表,这就导致有一个问题是我不知道DataGridView什么时候会绑定数据,解决这个问题我能想到的就是监听数据源的变化,也就是通过DataGridViewDataSourceChanged事件,但这就导致我在实现分页效果的时候也会触发该事件,逻辑会陷入一个死循环里面。。。

换一种方式,清空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改为CSkinSkinDataGridView还有就是数据源,程序用的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 控件分页的更多相关文章

  1. Datagridview控件实现分页功能

    可以进行sql语句进行设置:      1.先新建一个窗体,一个DataGridView控件.两个label控件.两个Button控件   2.代码如下: using System; using Sy ...

  2. C#实现WinForm DataGridView控件支持叠加数据绑定

    我们都知道WinForm DataGridView控件支持数据绑定,使用方法很简单,只需将DataSource属性指定到相应的数据源即可,但需注意数据源必须支持IListSource类型,这里说的是支 ...

  3. 实现虚拟模式的动态数据加载Windows窗体DataGridView控件 .net 4.5 (一)

    实现虚拟模式的即时数据加载Windows窗体DataGridView控件 .net 4.5 原文地址 :http://msdn.microsoft.com/en-us/library/ms171624 ...

  4. Visual Basic 2012 借助DataGridView控件将SQL server2012 数据导入到Excel 2010

    摘  要: SQL Server 2012 数据和Excel 2010之间的连接和数据的传输,本篇文章主要针对的是SQL Server 2012 数据导入到Excel 2010文件中.Excel软件对 ...

  5. Visual Basic 2012 借助DataGridView控件将Excel 2010数据导入到SQL server 2012

    (注:注释的颜色原本为绿色,在这里变为黑色,有点不便,但不会造成阅读影响.放入Visual Basic2012代码编辑器后会还原成绿色.) 摘  要:DataGridView控件作为数据传输的中介,只 ...

  6. Winform DataGridView控件添加行号

      有很多种方法,这里介绍三种: A: 控件的RowStateChanged事件中添加,RowStateChanged事件是在行的状态更改(例如,失去或获得输入焦点)时发生的事件: e.Row.Hea ...

  7. Winform 中DataGridView控件添加行标题

    有很多种方法. 1.可以在DataGridView控件中的RowStateChanged事件改变行标题单元格的值(Row.HeaderCell.Value) /// <summary> / ...

  8. C# DataGridView控件清空数据完美解决方法

    C# DataGridView控件绑定数据后清空数据在清除DataGridview的数据时: 1.DataSource为NULL(DataGridView.DataSource= null;)这样会将 ...

  9. 实现winform DataGridView控件判断滚动条是否滚动到当前已加载的数据行底部

    判断 DataGridView控件滚动条是否滚动到当前已加载的数据行底部,其实方法很简单,就是为DataGridView控件添加Scroll事件,然后写入以下代码就可以了,应用范围:可实现分部加载数据 ...

  10. 在DataGridView控件中加入ComboBox下拉列表框的实现

    在DataGridView控件中加入ComboBox下拉列表框的实现 转自:http://www.cnblogs.com/luqingfei/archive/2007/03/28/691372.htm ...

随机推荐

  1. 深入理解python虚拟机:调试器实现原理与源码分析

    深入理解python虚拟机:调试器实现原理与源码分析 调试器是一个编程语言非常重要的部分,调试器是一种用于诊断和修复代码错误(或称为 bug)的工具,它允许开发者在程序执行时逐步查看和分析代码的状态和 ...

  2. B/S结构系统的会话机制(session)

    B/S结构系统的会话机制(session) 目录 B/S结构系统的会话机制(session) 每博一文案 1. session 会话机制的概述 2. 什么是 session 的会话 3. sessio ...

  3. Protobuf编码规则

    支持类型 该表显示了在 .proto 文件中指定的类型,以及自动生成的类中的相应类型: .proto Type Notes C++ Type Java/Kotlin Type[1] Java/Kotl ...

  4. 数据库定时备份winserver2012篇

    目录 1 序言 2 任务计划相关知识点介绍 2.1 任务计划 是什么? 2.2 批处理文件 2.2.1 批处理文件简介 2.2.2 批处理常用命令介绍 3 各个数据库备份脚本 3.1 Oracle数据 ...

  5. 2022-12-17:订单最多的客户。以下数据,结果输出3。请问sql语句如何写? DROP TABLE IF EXISTS `orders`; CREATE TABLE `orders` ( `

    2022-12-17:订单最多的客户.以下数据,结果输出3.请问sql语句如何写? DROP TABLE IF EXISTS `orders`; CREATE TABLE `orders` ( `or ...

  6. 2020-10-28:go中,好几个go程,其中一个go程panic,会产生什么问题?

    福哥答案2020-10-28: 1.运行时恐慌,当panic被抛出异常后,如果我们没有在程序中添加任何保护措施的话,程序就会打印出panic的详细情况之后,终止运行.2.有panic的子协程里的def ...

  7. 2022-06-10:薯队长从北向南穿过一片红薯地(南北长M,东西宽N),红薯地被划分为1x1的方格, 他可以从北边的任何一个格子出发,到达南边的任何一个格子, 但每一步只能走到东南、正南、西南方向的

    2022-06-10:薯队长从北向南穿过一片红薯地(南北长M,东西宽N),红薯地被划分为1x1的方格, 他可以从北边的任何一个格子出发,到达南边的任何一个格子, 但每一步只能走到东南.正南.西南方向的 ...

  8. GitLib详细使用手册(windows系统)

    Git是一个开源的分布式版本控制系统,可以有效.高速地处理从很小到非常大的项目版本管理. 对gitlab的常见的使用有建立仓库.提交代码.更新代码.回滚代码.显示/修改日志.拉取分支.解决冲突.设置比 ...

  9. harbor仓库主从同步

  10. 10 款 VS Code 插件神器,第 7 款超级实用!

    VS Code 是这两年非常热门的一款 IDE,它不仅有提升开发体验的界面.轻量化的编辑器,还有丰富而强大的插件,这些优秀的插件使得 VS Code 生态体系更加吸引人,让开发效率大大提升.本文来介绍 ...