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为例:

  1. 创建Windows 窗体控件库 [图片上传失败...(image-286e58-1690352998239)]

  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. 运行以后是一个类似设计器的页面,右侧为控件属性,左侧为控件内容:

    [图片上传失败...(image-1bdd2f-1690352998239)]

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

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

    [图片上传失败...(image-ece497-1690352998239)]

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

    [图片上传失败...(image-53513f-1690352998239)]

[图片上传失败...(image-63665b-1690352998239)]

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

[图片上传失败...(image-cdbf9b-1690352998239)]

扩展控件 - 示例

上面示例中创建了一个名为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;
        }
    }
}

项目结构:

[图片上传失败...(image-f9c528-1690352998239)]

ctlTestDemo设计器:

[图片上传失败...(image-892001-1690352998239)]

运行ctlTestDemo:

[图片上传失败...(image-e33c67-1690352998239)]

回到正题,有了上面例子的基础,来尝试一下通过复合控件实现DataGridView分页功能。

SuperGridView

参照 C# datagridview分页功能 - 没事写个Bug - 非自定义控件 做了一些优化,可以自定义数据源,做了控件大小自适应处理(就是通过TableLayout做了下处理),控件名 - SuperGridView:

[图片上传失败...(image-9616db-1690352998239)]

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

[图片上传失败...(image-931f8-1690352998239)]

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

[图片上传失败...(image-612241-1690352998239)]

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😂,好在我的应用场景下数据量不大:

[图片上传失败...(image-b61fd3-1690352998239)]

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

PagerControl

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

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

[图片上传失败...(image-9a490c-1690352998239)]

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

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

[图片上传失败...(image-f09e89-1690352998239)]

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`类型,可以扩展其他类型数据:

[图片上传失败...(image-a65809-1690352998239)]

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
    }
}

[图片上传失败...(image-3ee85e-1690352998239)]

客户端使用:

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;

[图片上传失败...(image-c48224-1690352998239)]

大致上就这个样子,还是有很大的改进空间的🌎

Demo的代码上传到GitHub了,感兴趣的友友们可以参考一下:PagerControl

还有一件事,真的很讨厌维护N年前老师傅写的项目,太痛苦了😭😭😭
原文地址:DataGridView 控件分页 - 水煮养乐多 - 博客园

参考

MSDN:

技术博文:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容