在使用Winform
開發(fā)桌面應(yīng)用時,工具箱預(yù)先提供了豐富的基礎(chǔ)控件,利用這些基礎(chǔ)控件可以開展各類項目的開發(fā)。但是或多或少都會出現(xiàn)既有控件無法滿足功能需求的情況,或者在開發(fā)類似項目時,我們希望將具有相同功能的模板封裝成一個標(biāo)準(zhǔn)控件等,在這些場景下,winform
自帶的控件就有些乏力了,需要我們自己開發(fā)一些控件。
本篇開篇于DataGridView
控件的分頁效果,當(dāng)數(shù)據(jù)量大的時候,分頁是必要的,但是控件本身是沒有分頁功能的,所以需要自己實現(xiàn)。
我不是專業(yè)的控件開發(fā)人員,所以寫下這篇文章作為學(xué)習(xí)過程中的記錄。
前言
.NET
提供了豐富的控件創(chuàng)作技術(shù),自定義控件主要分為三類 - Windows Forms Control Development Basics:
- 復(fù)合控件:將現(xiàn)有控件組合成一個新的控件
- 擴展控件:在現(xiàn)有控件的基礎(chǔ)上修改原有控件功能或添加新的功能
- 自定義控件:從頭到尾開發(fā)一個全新的控件。繼承
System.Windows.Forms.Control
類,添加和重寫基類的屬性、方法和事件。winform
的控件都是直接或間接從System.Windows.Forms.Control
派生的類,基類Control
提供了控件進行可視化所需要的所有功能,包括窗口的句柄、消息路由、鼠標(biāo)和鍵盤事件以及許多其他用戶界面事件。自定義控件是最靈活也最為強大的方法,同時對開發(fā)者的要求也比較高,你需要處理更為底層的Windows
消息,需要了解GDI+
技術(shù)以及Windows API
由易到難,我們從最簡單的復(fù)合控件一步一步來,自定義控件作為我們的終極目標(biāo)哈??
通過MSND
上的 ctlClockLib 示例學(xué)一下怎樣開發(fā)復(fù)合控件以及擴展現(xiàn)有控件:
復(fù)合控件 - 示例
來看看怎樣創(chuàng)建和調(diào)試自定義控件項目,以MSND
上的ctlClockLib 中的 ctlClock為例:
-
創(chuàng)建
Windows 窗體控件庫
-
之后其實和開發(fā)
Winform
項目差不多,在設(shè)計時里拖入想要組合的控件,在后臺代碼實現(xiàn)相應(yīng)的內(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(); } } }
-
運行以后是一個類似設(shè)計器的頁面,右側(cè)為控件屬性,左側(cè)為控件內(nèi)容:
這樣一個簡單的復(fù)合控件 - ctlClock
就完成了,怎么在實際項目中使用就和調(diào)用第三方控件是相似的:
-
新建一個新的
Winform
工程: -
在工具箱新建一個選項卡,然后選擇項添加上面時鐘控件生成的
DLL
文件,或者直接將文件拖入選項卡中:
?
- 然后就和正??丶粯佑镁涂梢粤?,這個時鐘控件,你拖入可以發(fā)現(xiàn)他在設(shè)計器里也是會正常走時間的,之后調(diào)整自定義的時鐘控件就可以在使用控件的窗體中顯現(xiàn)出來。
擴展控件 - 示例
上面示例中創(chuàng)建了一個名為ctlClock
的時鐘控件,它只有鐘表功能,怎樣讓它帶有報警的功能呢,給ctlClock
添加報警功能的過程就是拓展控件的過程。這里需要我們有一些C# 面向?qū)ο?- 繼承的基礎(chǔ),以MSDN
上的 ctlAlarmClock為例。
簡單說一下繼承:一個類型派生于一個基類型,它擁有該基類型的所有成員字段和函數(shù)。在實現(xiàn)繼承中,派生類型采用基類型的每個函數(shù)的實現(xiàn)代碼,除非在派生類型的定義中指定重寫某個函數(shù)的實現(xiàn)代碼。一般在需要給現(xiàn)有類型添加功能時使用繼承。
具體編碼就不說了,MSDN
上都有,在原有ctlClock
基礎(chǔ)上,添加了一個指示報警的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) // 根據(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):
ctlTestDemo
設(shè)計器:
運行ctlTestDemo
:
回到正題,有了上面例子的基礎(chǔ),來嘗試一下通過復(fù)合控件實現(xiàn)DataGridView
分頁功能。
SuperGridView
參照 C# datagridview分頁功能 - 沒事寫個Bug - 非自定義控件 做了一些優(yōu)化,可以自定義數(shù)據(jù)源,做了控件大小自適應(yīng)處理(就是通過TableLayout
做了下處理),控件名 - SuperGridView
:
控件樣式如上圖所示,通過TableLayout
做了自適應(yīng)的處理:
暴露一個DataSource
屬性用于給DataGridView
綁定數(shù)據(jù)源,一個PageSize
屬性可以調(diào)整DataGridView
每頁顯示的數(shù)據(jù)量,控件代碼:
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; // 當(dāng)前頁數(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++;
}
//默認(rèn)第一頁
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();
}
}
}
控件功能:
- 控件具有自定義的數(shù)據(jù)源綁定功能,通過
DataSource
屬性綁定DataTable
對象作為數(shù)據(jù)源。 - 控件支持分頁顯示,可以按照每頁固定的記錄數(shù)顯示數(shù)據(jù)。
- 控件的分頁功能包括跳轉(zhuǎn)到第一頁、上一頁、下一頁、最后一頁,以及顯示總記錄數(shù)等。
- 控件中的數(shù)據(jù)表格 (
DataGridView
) 可以自動生成列,表中內(nèi)容默認(rèn)居中顯示
實機演示 - 也還湊合,試了一下自造了十萬條數(shù)據(jù),但是在十萬條數(shù)據(jù)下可以明顯看到內(nèi)存暴漲,從最初的22MB
漲到了60MB
??,好在我的應(yīng)用場景下數(shù)據(jù)量不大:
這段代碼只實現(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)存占用問題等,還有就是在設(shè)計器里不能暴露出來DataGridView 任務(wù)
操作選項,需要通過后臺代碼完成數(shù)據(jù)顯示的綁定,我在想是不是可以不直接用DataGridView
呢,只用下方的操作欄呢?
PagerControl
用上面的思路試一試組合一個操作欄出來,為了好看一點,這次換成組合CSkin
的控件。
樣式和上面幾乎一致,沒有放每頁條數(shù)的配置項,這個打算作為一個屬性放出來:
我的思路是給控件一個數(shù)據(jù)源,用于綁定頁面中的DataGridView
,然后獲取到數(shù)據(jù)以后和之前一樣,因為使用場景下數(shù)據(jù)量不是特別大,所以就同樣沿用上面的思路。
這里需要暴露一個配置項用于綁定頁面上的DataGridView
需要用到設(shè)計時的一些特性(Attribute),這些設(shè)計時的特性(Attribute)在C#
和類似的語言中扮演著非常重要的角色,用于影響控件在設(shè)計時的表現(xiàn)和行為,提供更好的用戶體驗和開發(fā)者便利:
- Windows 窗體控件中的特性
OK,理想很豐滿,現(xiàn)實很骨感。通過綁定綁定頁面中的DataGridView
獲取數(shù)據(jù)會有一個問題,因為我控制分頁的方式是通過給DataGridView
更換處理之后的DataSource
數(shù)據(jù)表,這就導(dǎo)致有一個問題是我不知道DataGridView
什么時候會綁定數(shù)據(jù),解決這個問題我能想到的就是監(jiān)聽數(shù)據(jù)源的變化,也就是通過DataGridView
的DataSourceChanged
事件,但這就導(dǎo)致我在實現(xiàn)分頁效果的時候也會觸發(fā)該事件,邏輯會陷入一個死循環(huán)里面。。。
換一種方式,清空DataGridView
表中數(shù)據(jù)然后一行一行的加Clear()
方法又會報錯:
// 假設(shè)已經(jīng)有一個DataGridView控件名為dataGridViewToBind
// 假設(shè)已經(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)容,當(dāng)然也是通過更改它的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
改為CSkin
的SkinDataGridView
還有就是數(shù)據(jù)源,程序用的DataTable
這里也就用``DataTable了,但是數(shù)據(jù)源那里放的
object`類型,可以擴展其他類型數(shù)據(jù):
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è)置每頁顯示的數(shù)據(jù)量")]
public int PageSize
{
get => pageSize;
set
{
if (value <= 0)
{
pageSize = 50; // 默認(rèn)顯示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
}
}
客戶端使用:
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:
-
MSDN - Developing Custom Windows Forms Controls with the .NET Framework
-
MSDN - Develop Windows Forms controls at design time
技術(shù)博文:
-
Winform控件開發(fā)總結(jié)目錄 - 白話Programing
-
C# Winform開發(fā)以及控件開發(fā)的需要注意的,被人問怕了,都是基礎(chǔ)常識 - DSkin
-
鋒利的C# - 代碼迷途
-
淺談Winform控件開發(fā)(一):使用GDI+美化基礎(chǔ)窗口 - __Meow
-
C# datagridview分頁功能 - 沒事寫個Bug - 非自定義控件
-
WinForm輕松實現(xiàn)自定義分頁(轉(zhuǎn)載)文章來源:http://www.zghlxwxcb.cn/news/detail-599838.html
-
WinForm下編寫分頁控件,實現(xiàn)DataGridView的分頁效果 - 牛遷遷文章來源地址http://www.zghlxwxcb.cn/news/detail-599838.html
到了這里,關(guān)于DataGridView 控件分頁的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!