- A+
在使用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为例:
-
创建
Windows 窗体控件库
-
之后其实和开发
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(); } } }
-
运行以后是一个类似设计器的页面,右侧为控件属性,左侧为控件内容:
这样一个简单的复合控件 - ctlClock
就完成了,怎么在实际项目中使用就和调用第三方控件是相似的:
-
新建一个新的
Winform
工程: -
在工具箱新建一个选项卡,然后选择项添加上面时钟控件生成的
DLL
文件,或者直接将文件拖入选项卡中:
- 然后就和正常控件一样用就可以了,这个时钟控件,你拖入可以发现他在设计器里也是会正常走时间的,之后调整自定义的时钟控件就可以在使用控件的窗体中显现出来。
扩展控件 - 示例
上面示例中创建了一个名为ctlClock
的时钟控件,它只有钟表功能,怎样让它带有报警的功能呢,给ctlClock
添加报警功能的过程就是拓展控件的过程。这里需要我们有一些C# 面向对象 - 继承的基础,以MSDN
上的 ctlAlarmClock为例。
简单说一下继承:一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数。在实现继承中,派生类型采用基类型的每个函数的实现代码,除非在派生类型的定义中指定重写某个函数的实现代码。一般在需要给现有类型添加功能时使用继承。
具体编码就不说了,MSDN
上都有,在原有ctlClock
基础上,添加了一个指示报警的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) // 根据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(); } } }
控件功能:
- 控件具有自定义的数据源绑定功能,通过
DataSource
属性绑定DataTable
对象作为数据源。 - 控件支持分页显示,可以按照每页固定的记录数显示数据。
- 控件的分页功能包括跳转到第一页、上一页、下一页、最后一页,以及显示总记录数等。
- 控件中的数据表格 (
DataGridView
) 可以自动生成列,表中内容默认居中显示
实机演示 - 也还凑合,试了一下自造了十万条数据,但是在十万条数据下可以明显看到内存暴涨,从最初的22MB
涨到了60MB
?,好在我的应用场景下数据量不大:
这段代码只实现了一个简单的分页数据表格控件,适合处理中小规模的数据。它的主要优点是简化数据绑定和提供分页显示,但仍有改进空间,尤其在处理大数据集和功能扩展方面。如果只是在项目中使用,且数据量不大,这个控件可能已经足够。然而,如果需要更多功能和性能优化,可能需要进一步开发和优化,比如可以加上页面,页码自动跳转之类的,还有内存占用问题等,还有就是在设计器里不能暴露出来DataGridView 任务
操作选项,需要通过后台代码完成数据显示的绑定,我在想是不是可以不直接用DataGridView
呢,只用下方的操作栏呢?
PagerControl
用上面的思路试一试组合一个操作栏出来,为了好看一点,这次换成组合CSkin
的控件。
样式和上面几乎一致,没有放每页条数的配置项,这个打算作为一个属性放出来:
我的思路是给控件一个数据源,用于绑定页面中的DataGridView
,然后获取到数据以后和之前一样,因为使用场景下数据量不是特别大,所以就同样沿用上面的思路。
这里需要暴露一个配置项用于绑定页面上的DataGridView
需要用到设计时的一些特性(Attribute),这些设计时的特性(Attribute)在C#
和类似的语言中扮演着非常重要的角色,用于影响控件在设计时的表现和行为,提供更好的用户体验和开发者便利:
OK,理想很丰满,现实很骨感。通过绑定绑定页面中的DataGridView
获取数据会有一个问题,因为我控制分页的方式是通过给DataGridView
更换处理之后的DataSource
数据表,这就导致有一个问题是我不知道DataGridView
什么时候会绑定数据,解决这个问题我能想到的就是监听数据源的变化,也就是通过DataGridView
的DataSourceChanged
事件,但这就导致我在实现分页效果的时候也会触发该事件,逻辑会陷入一个死循环里面。。。
换一种方式,清空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
改为CSkin
的SkinDataGridView
还有就是数据源,程序用的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:
技术博文: