在使用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. 这可能是最全面的MySQL面试八股文了

    什么是MySQL MySQL是一个关系型数据库,它采用表的形式来存储数据.你可以理解成是Excel表格,既然是表的形式存储数据,就有表结构(行和列).行代表每一行数据,列代表该行中的每个值.列上的值是 ...

  2. RTSP&IGMP详解

                                                                                 RTSP协议 一.概述 1)RTSP(Real ...

  3. vue本地开发配置及项目部署

    一,  二,本地模拟配置代理,请求qq音乐的接口数据  三,axios请求头封装               参考http://www.axios-js.com/zh-cn/docs/#%E4%BB% ...

  4. C# 模拟界面点击/UI自动化测试

    有一些UI自动化测试框架,能够实现自动化测试. 本文介绍Peer(微软的TAF技术),也可以实现自动化测试,或是对其他进程进行UI操作.下面是案例~ 在界面上添加俩个按钮: 并处理相应的点击事件: 1 ...

  5. 文心一言 VS chatgpt (9)-- 算法导论2.3 7题 3.1 1题

    七.描述一个运行时间为O(nlgn)的算法,给定n个整数的集合S和另一个整数,该算法能确定 S中是否存在两个其和刚好为工的元素. 文心一言: 这里是一个运行时间为O(nlgn)的算法,可以用来确定集合 ...

  6. 2020-10-17:谈一谈DDD面向领域编程。

    福哥答案2020-10-17:#福大大架构师每日一题# [答案来自此链接](https://www.jianshu.com/p/fb319d7674ff) 一个通用领域驱动设计的架构性解决方案包含4 ...

  7. 2021-12-14:根据身高重建队列。 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个

    2021-12-14:根据身高重建队列. 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序).每个 people[i] = [hi, ki] 表示第 i 个 ...

  8. 在windows下安装elk

    一.下载elasticsearch-5.1.1 cd D:\bigdata\elasticsearch-5.1.1\bin elasticsearch-service.bat cmd 运行 servi ...

  9. react中useRef的应用

    何为useRef useRef是随着react函数式组件发展而来的,是react众多官方hook中的一个,调用useRef可以返回一个伴随这组件整个声明周期不发生改变的对象,这个对象常见的用途有两个: ...

  10. 人工智能导论——机器人自动走迷宫&强化学习

    一.问题重述 强化学习是机器学习中重要的学习方法之一,与监督学习和非监督学习不同,强化学习并不依赖于数据,并不是数据驱动的学习方法,其旨在与发挥智能体(Agent)的主观能动性,在当前的状态(stat ...