DataGridView 控件分頁

在使用Winform開發(fā)桌面應用時,工具箱預先提供了豐富的基礎控件,利用這些基礎控件可以開展各類項目的開發(fā)。但是或多或少都會出現(xiàn)既有控件無法滿足功能需求的情況,或者在開發(fā)類似項目時,我們希望將具有相同功能的模板封裝成一個標準控件等,在這些場景下,winform自帶的控件就有些乏力了,需要我們自己開發(fā)一些控件。

本篇開篇于DataGridView控件的分頁效果,當數(shù)據(jù)量大的時候,分頁是必要的,但是控件本身是沒有分頁功能的,所以需要自己實現(xiàn)。

我不是專業(yè)的控件開發(fā)人員,所以寫下這篇文章作為學習過程中的記錄。

前言

.NET提供了豐富的控件創(chuàng)作技術,自定義控件主要分為三類 - Windows Forms Control Development Basics

  • 復合控件:將現(xiàn)有控件組合成一個新的控件
  • 擴展控件:在現(xiàn)有控件的基礎上修改原有控件功能或添加新的功能
  • 自定義控件:從頭到尾開發(fā)一個全新的控件。繼承System.Windows.Forms.Control類,添加和重寫基類的屬性、方法和事件。winform的控件都是直接或間接從System.Windows.Forms.Control派生的類,基類Control提供了控件進行可視化所需要的所有功能,包括窗口的句柄、消息路由、鼠標和鍵盤事件以及許多其他用戶界面事件。自定義控件是最靈活也最為強大的方法,同時對開發(fā)者的要求也比較高,你需要處理更為底層的Windows消息,需要了解GDI+技術以及Windows API

由易到難,我們從最簡單的復合控件一步一步來,自定義控件作為我們的終極目標哈??

通過MSND上的 ctlClockLib 示例學一下怎樣開發(fā)復合控件以及擴展現(xiàn)有控件:

復合控件 - 示例

來看看怎樣創(chuàng)建和調(diào)試自定義控件項目,以MSND上的ctlClockLib 中的 ctlClock為例:

  1. 創(chuàng)建Windows 窗體控件庫 [圖片上傳失敗...(image-286e58-1690352998239)]

  2. 之后其實和開發(fā)Winform項目差不多,在設計時里拖入想要組合的控件,在后臺代碼實現(xiàn)相應的內(nèi)容。具體代碼,不做贅述,和文檔相同。這個教程只要是完成一個可以自定義底色以及時間字體顏色的以及時鐘控件,由一個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. 運行以后是一個類似設計器的頁面,右側(cè)為控件屬性,左側(cè)為控件內(nèi)容:

    [圖片上傳失敗...(image-1bdd2f-1690352998239)]

這樣一個簡單的復合控件 - ctlClock就完成了,怎么在實際項目中使用就和調(diào)用第三方控件是相似的:

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

    [圖片上傳失敗...(image-ece497-1690352998239)]

  2. 在工具箱新建一個選項卡,然后選擇項添加上面時鐘控件生成的DLL文件,或者直接將文件拖入選項卡中:

    [圖片上傳失敗...(image-53513f-1690352998239)]

[圖片上傳失敗...(image-63665b-1690352998239)]

  1. 然后就和正常控件一樣用就可以了,這個時鐘控件,你拖入可以發(fā)現(xiàn)他在設計器里也是會正常走時間的,之后調(diào)整自定義的時鐘控件就可以在使用控件的窗體中顯現(xiàn)出來。

[圖片上傳失敗...(image-cdbf9b-1690352998239)]

擴展控件 - 示例

上面示例中創(chuàng)建了一個名為ctlClock的時鐘控件,它只有鐘表功能,怎樣讓它帶有報警的功能呢,給ctlClock添加報警功能的過程就是拓展控件的過程。這里需要我們有一些C# 面向?qū)ο?- 繼承的基礎,以MSDN上的 ctlAlarmClock為例。

簡單說一下繼承:一個類型派生于一個基類型,它擁有該基類型的所有成員字段和函數(shù)。在實現(xiàn)繼承中,派生類型采用基類型的每個函數(shù)的實現(xiàn)代碼,除非在派生類型的定義中指定重寫某個函數(shù)的實現(xiàn)代碼。一般在需要給現(xiàn)有類型添加功能時使用繼承。

具體編碼就不說了,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)    // 根據(jù)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;
        }
    }
}

項目結(jié)構(gòu):

[圖片上傳失敗...(image-f9c528-1690352998239)]

ctlTestDemo設計器:

[圖片上傳失敗...(image-892001-1690352998239)]

運行ctlTestDemo:

[圖片上傳失敗...(image-e33c67-1690352998239)]

回到正題,有了上面例子的基礎,來嘗試一下通過復合控件實現(xiàn)DataGridView分頁功能。

SuperGridView

參照 C# datagridview分頁功能 - 沒事寫個Bug - 非自定義控件 做了一些優(yōu)化,可以自定義數(shù)據(jù)源,做了控件大小自適應處理(就是通過TableLayout做了下處理),控件名 - SuperGridView:

[圖片上傳失敗...(image-9616db-1690352998239)]

控件樣式如上圖所示,通過TableLayout做了自適應的處理:

[圖片上傳失敗...(image-931f8-1690352998239)]

暴露一個DataSource屬性用于給DataGridView綁定數(shù)據(jù)源,一個PageSize屬性可以調(diào)整DataGridView每頁顯示的數(shù)據(jù)量,控件代碼:

[圖片上傳失敗...(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;  // 每頁記錄數(shù)
        private int recordCount = 0;    // 總記錄數(shù)
        private int pageCount = 0;  // 總頁數(shù)
        private int currentPage = 0;    // 當前頁數(shù)
        private DataTable originalTable = new DataTable();  // 數(shù)據(jù)源表
        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 控件的數(shù)據(jù)源。")]
        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 控件每頁數(shù)據(jù)量。")]
        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. 控件具有自定義的數(shù)據(jù)源綁定功能,通過 DataSource 屬性綁定 DataTable 對象作為數(shù)據(jù)源。
  2. 控件支持分頁顯示,可以按照每頁固定的記錄數(shù)顯示數(shù)據(jù)。
  3. 控件的分頁功能包括跳轉(zhuǎn)到第一頁、上一頁、下一頁、最后一頁,以及顯示總記錄數(shù)等。
  4. 控件中的數(shù)據(jù)表格 (DataGridView) 可以自動生成列,表中內(nèi)容默認居中顯示

實機演示 - 也還湊合,試了一下自造了十萬條數(shù)據(jù),但是在十萬條數(shù)據(jù)下可以明顯看到內(nèi)存暴漲,從最初的22MB漲到了60MB??,好在我的應用場景下數(shù)據(jù)量不大:

[圖片上傳失敗...(image-b61fd3-1690352998239)]

這段代碼只實現(xiàn)了一個簡單的分頁數(shù)據(jù)表格控件,適合處理中小規(guī)模的數(shù)據(jù)。它的主要優(yōu)點是簡化數(shù)據(jù)綁定和提供分頁顯示,但仍有改進空間,尤其在處理大數(shù)據(jù)集和功能擴展方面。如果只是在項目中使用,且數(shù)據(jù)量不大,這個控件可能已經(jīng)足夠。然而,如果需要更多功能和性能優(yōu)化,可能需要進一步開發(fā)和優(yōu)化,比如可以加上頁面,頁碼自動跳轉(zhuǎn)之類的,還有內(nèi)存占用問題等,還有就是在設計器里不能暴露出來DataGridView 任務操作選項,需要通過后臺代碼完成數(shù)據(jù)顯示的綁定,我在想是不是可以不直接用DataGridView呢,只用下方的操作欄呢?

PagerControl

用上面的思路試一試組合一個操作欄出來,為了好看一點,這次換成組合CSkin的控件。

樣式和上面幾乎一致,沒有放每頁條數(shù)的配置項,這個打算作為一個屬性放出來:

[圖片上傳失敗...(image-9a490c-1690352998239)]

我的思路是給控件一個數(shù)據(jù)源,用于綁定頁面中的DataGridView,然后獲取到數(shù)據(jù)以后和之前一樣,因為使用場景下數(shù)據(jù)量不是特別大,所以就同樣沿用上面的思路。

這里需要暴露一個配置項用于綁定頁面上的DataGridView需要用到設計時的一些特性(Attribute),這些設計時的特性(Attribute)在C#和類似的語言中扮演著非常重要的角色,用于影響控件在設計時的表現(xiàn)和行為,提供更好的用戶體驗和開發(fā)者便利:

[圖片上傳失敗...(image-f09e89-1690352998239)]

OK,理想很豐滿,現(xiàn)實很骨感。通過綁定綁定頁面中的DataGridView獲取數(shù)據(jù)會有一個問題,因為我控制分頁的方式是通過給DataGridView更換處理之后的DataSource數(shù)據(jù)表,這就導致有一個問題是我不知道DataGridView什么時候會綁定數(shù)據(jù),解決這個問題我能想到的就是監(jiān)聽數(shù)據(jù)源的變化,也就是通過DataGridViewDataSourceChanged事件,但這就導致我在實現(xiàn)分頁效果的時候也會觸發(fā)該事件,邏輯會陷入一個死循環(huán)里面。。。

換一種方式,清空DataGridView表中數(shù)據(jù)然后一行一行的加Clear()方法又會報錯:

// 假設已經(jīng)有一個DataGridView控件名為dataGridViewToBind
// 假設已經(jīng)有一個DataTable名為newDataTable

// 清空表格中的內(nèi)容
dataGridView1.Rows.Clear(); 
dataGridView1.Refresh();

// 添加新的DataTable數(shù)據(jù)
foreach (DataRow row in newDataTable.Rows)
{
    dataGridViewToBind.Rows.Add(row.ItemArray);
}

一通抓耳撓腮之后,我覺得換一種思路:只操作DataGridView上顯示的內(nèi)容,當然也是通過更改它的DataSource來完成,獲取DataGridView的數(shù)據(jù)源采用之前的思路,控件給一個數(shù)據(jù)源屬性,每次更改DataGridView的數(shù)據(jù)源的時候也順路操作一下控件的數(shù)據(jù)源,這樣就不用在控件內(nèi)部監(jiān)聽DataGridView數(shù)據(jù)源的變化了,也就不會出現(xiàn)我在操作DataGridView的時候程序陷入死循環(huán)的問題。

All Right。來說說怎么搞的,更之前那個相比有點不一樣,因為是給一個n年前的winform項目做的,所以這里DataGridView改為CSkinSkinDataGridView還有就是數(shù)據(jù)源,程序用的DataTable這里也就用``DataTable了,但是數(shù)據(jù)源那里放的object`類型,可以擴展其他類型數(shù)據(jù):

[圖片上傳失敗...(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 數(shù)據(jù)項")]
        public SkinDataGridView DataGridView
        {
            get { return dataGridViewToBind; }
            set
            {
                dataGridViewToBind = value;
            }
        }

        [Browsable(false)]
        public object DataSource    // 數(shù)據(jù)類型可以擴展
        {
            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("設置每頁顯示的數(shù)據(jù)量")]
        public int PageSize
        {
            get => pageSize;
            set
            {
                if (value <= 0)
                {
                    pageSize = 50;  // 默認顯示50條數(shù)據(jù)
                }
                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 控件分頁 - 水煮養(yǎng)樂多 - 博客園

參考

MSDN:

技術博文:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容