热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[C#](原创)一步一步教你自定义控件——01,TrackBar

一、前言技术没有先进落后之分,只有合不合适。WinForm有着非常多的优点,在使用WinFo


一、前言

技术没有先进落后之分,只有合不合适。

WinForm有着非常多的优点,在使用WinForm久了之后,难免会觉得WinForm自带的某些控件外观上有些许朴素、或者功能上有些不如意,自然而然便想去美化这些控件,或者给控件添加一些额外功能,而这便是自定义控件的意义所在。

自定义控件的难度并不大,但是却处在一个比较尴尬的位置:

1,一般的教材不会讲——因为还是有难度的,而且一般用不上;

2,而网上或书上所找到的自定义控件相关知识教程里,大多都是给一个已完成的自定义控件,再附上源码,只有了了注释和说明。毕竟难度不大,懂的自然懂,而且对懂的人来说,看别人的自定义控件往往是为了看一下实现的思路或某个点的实现方法,因为很多都是一点就透。

对于初学者而言,要想掌握自定义控件,就需要花费不少的时间去学习那些源代码、去模仿、去练习、去摸索,最后一步步去归纳总结出适合自己的一条路。当掌握了之后,回头看去,会发现其实真的不难,耗费的时间与学习的难度并不成正比,这些额外的时间就花费在了摸索和总结上了。

我也是这样一步步走来的,所以不想让大家再花费这么多的时间去掌握一项并不太难的知识,便有了这篇文章。

在本文中,我会从零开始,带着大家一步一步去实现一个自定义控件,同时会分享一些我的经验之谈,相信看完的你,一定会有所收获。

本篇的自定义控件是:TrackBar

 


二、前期分析


(一)为什么需要去自定义控件?

我们来分析一下为什么要去自定义控件。

以本文要实现的TrackBar为例,最主要的原因便 是系统自带的TrackBar太过朴素,所以需要一款比较好看的TrackBar控件。

系统自带的TrackBar:


预想的TrackBar样式:



(二)实现目标

在实现一个自定义控件前,我们要确定一下我们要实现的目标,比如外观、功能、特点等。

1,外观

个人经验之谈

在设计预想样式时,可以何用任何方式,只要自己可以看明白就行,但是还是推荐使用绘图软件去做一个示意图,主要是因为在自定义控件时,往往会需要用到一些坐标、宽、高等值,特别是和GDI+有关时。使用绘图软件则可以去准确和清晰的标注出来这些信息,并进行相关的计算。

我想实现的TrackBar的外观样式如下:


2,功能

参考系统的TrackBar,可以将所需要的功能归为下面几点:

(1)支持鼠标点击。

(2)支持鼠标拖动。

(3)支持修改颜色。 

3,特点

既然全实现自己的TrackBar,肯定要有自己的特点。

(1)支持颜色调整,包括背景色和前景色。

(2)支持圆角显示,和直角显示。


(三)技术分析

在自定义控件的目标定好之后,接下来便是分析实现上述目标所需要的技术。


1,整体实现

 自定义的TrackBar从逻辑上可以分为两层:背景条(Bar)和滑块(Slider)。


在具体实现时也是按照这两层的思路去分层实现。


2,主要技术

通过上面的分析的示意,我们发现GDI+可以实现上述目标,所以我们的主要技术便是——GDI+。


3,圆角和直角的实现

直角可以使用GDI+中的Graphics.DrawLine去实现。那么圆角怎么实现呢?

其实也很简单,仍然使用Graphics.DrawLine实现,不过在创建Pen时,需要设置一下LineCap,通过LineCap可以实现多种样式,除了圆角外,还有菱形、箭头等等。

具体的设置后文会讲解,此处不再赘述。

MSDN中关于LineCap的说明如下:

指定可用线帽样式,Pen 对象以该线帽结束一段直线。


 

 


三、开始实现


(一)前期准备


1,创建自定义控件类库项目

个人经验之谈

建议创建自定义控件时,将自定义控件写在一个单独的类库里。主要的目的是提高复用性,同时也方便管理,以及方便控件间的相互调用。

关于控件间的相互调用:

因为控件除了单个的自定义控件外,还有用户控件(UserControl)——实现某些复杂功能的时候,往往就需要用到用户控件。用户控件往往是多个控件的组合,所以将控件放到一个类库中可以方便的调用,修改也方便。

启动VS(本文使用的VS2019),添加新的 类库(.NET Framework)项目,起好项目名称并选好位置,点击创建。

个人经验之谈

关于框架的选择。

在实际应用当中,框架版本要根据自定义控件所服务的项目去选择。因为是自定义控件,所以兼容性很高,往往.Net 2.0就可以实现绝大部分效果。所以,可以根据具体的项目去选择框架的版本,当然也可以选一个.Net 2.0,然后在实现完成之后编译成不同框架版本。


2,添加类

在项目名称上右击,选择添加-类,输入类名:LTrackBar.cs,确定。

个人经验之谈

关于类名

在起自定义控件的名称时,最好不要和系统控件名称一样,那样会导致二义性,平白增加代码量。

所以可以统一加一个前缀或后缀,如:TextBoxEx,PanelPlus。本文便是统一加上前缀”L“——LTrackBar


3,添加继承

在添加继承时,根据具体的需要去选择不同的继承。比如要对ComboBox的一拉选项添加不同的颜色,就继承ComboBox并进行重绘;比如要让TextBox支持透明,就继承TextBox进行重写等等。

在本例的LTrackBar中,通过前文的分析发现很简单,所以可以继承基础的Control类。

(1)添加继承

在类名后输入”:Control“


(2)添加引用

上一步里会发现”Control“显示代表错误的波浪线,我们将鼠标悬浮在上面,在弹出的提示按钮上点击,选择”将引用添加到System.Windows.Forms.dll”,然后”Control”下面的波浪线将会消失,并变为浅蓝色。



(3)修改可访问性。

由于是一个单独的类库,并且LTrackBar是一个独立的控件,所以我们需要将类的可访问性修改为Public。



4,添加自定义属性

个人经验之谈

关于参数命名

对于公共参数,个人建议添加一个统一的前缀。主要原因有两点:

1,在视图设计界面中的属性窗口中,无论是“按分类排序”还是“按字母排序”,都可以使控件所公开的自定义属性集中在一起。

按分类排序:


按字母排序:


2,在代码编辑界面,可以在输入统一的前缀后,将该控件的所以自定义属性都在代码提示窗口中显示在一起,方便选择。


(1)颜色相关

通过前文可知,我们涉及到的颜色有两个——背景条颜色和滑块颜色。所以我们添加两个属性,其中的“Invalidate()”是为了在修改该属性值后立刻使控件重绘。


(2)圆角相关


(3)最大值与最小值

如TrackBar一样,我们也需要有最大值和最小值,由于我的需要很简单,所以只支持整型(int)。

首先,最小值应该大于0,然后最小值要小于最大值,所以最小值如下:


其次,最大值也应该大于最小值。


(4)当前值 

用来获取或设置当前LTrackBar所代表的值。

当前值需要在最大值和最小值之间,同时我们需要知道值发生了变化,所以添加了一个委托事件LValueChanged,关于委托和事件此处不展开讲,因为不懂也不影响使用,就像固定公式一样往上套就行了。只需要知道其作用是让调用本控件的人知道当前的值发生了变化。


(5)方向

LTrackBar支持横向显示,也支持竖向显示。

在横向显示时,分为两种情况:1,左端为最小值(L_Minimum),右端为最大值(L_Maximum);2,左端为最大值(L_Maximum),右端为最小值(L_Minimum)。

在竖向显示时,分为两种情况:1,顶部为最小值(L_Minimum),底部为最大值(L_Maximum);2,顶部为最大值(L_Maximum),底部为最小值(L_Minimum)。

综上,共有4种情况,所以我们先创建一个枚举。

同样为了方便统一管理,新建一个类专门存放枚举信息。


之后,创建一个Orientation枚举类型的属性:


上面的那两个if语句的作用是为了实现在改变方向后,自动交换控件的宽和高。

(6)宽度/高度

像TrackBar只能在设计器中调整宽度一样,LTrackBar也只能调整宽度(横向显示时)或高度(竖向显示),所以需要一个属性来控制。


为了实现只能调整宽度/高度,需要重写SetBoundsCore方法,MSDN上关于SetBoundsCore的说明如下:


我们需要对其进行重写,以限制只能调整宽度或高度:


由于VS的强大,所以在重写时非常方便:

 

(7)增加描述信息

在公开属性上加入Catagory(分组),Description(描述)。之后便可以在属性窗口看到相应的分类和说明。



 


5,添加事件

为了获取LTrackBar的当前值,以及在值改变时执行某些操作,所以需要增加一个事件。事件数据则为当前值(L_Value)。

(1)新建类,继承自EventArgs。


(2)新建委托和事件



 6,重写方法

通过前文的分析,我们知道主要用到了GDI+,同时支持鼠标点击、拖动。所以我们需要重写以下这些方法。


其中,OnPaint事件是用来画显示界面的。Mouse相关的事件是与实现鼠标操作相关的。

为了知道当前鼠标的状态(进入、离开、按下、松开),需要定义一个枚举:


下面是每个重写方法的具体说明:

(1)OnMouseEnter方法

标识着鼠标进入,只需要设置一下鼠标状态即可。


(2)OnMouseLeave方法

同上


(3)OnMouseUp方法

同上


(4)OnMouseDown方法

当鼠标点击了控件时会触发本事件。在鼠标点击后,控件应该重绘界面,主要是滑块(Slider)的变化,同时滑块(Slider)所代表的值也应该发生变化,同时引发LValueChanged事件。


(5)OnMouseMove方法

当鼠标在控件上移动时触发本事件,在实际操作时都是在在按着鼠标左键并拖动,所以要判断鼠标的状态(mouseStatus)是否是按下(Down)。其他同上。


在OnMouseDown和OnMouseMove中,有一个方法:pPointToValue(),其作用便是将鼠标的坐标值转换为对应代表的值。其代码如下:


其代码很简单,就是计算鼠标落点占控件宽度/高度的比例,再乘以值的范围就得到了代表的值。在下文中有示意图讲解,本处不再赘述。

(6)OnPaint方法

本方法是控件实现的核心。几乎只要涉及控件重绘和自定义控件,都兔不了要重写OnPaint方法。

在OnPaint方法中,我们主要完成两部分的操作:

1)画背景条(Bar)

2)画滑块(Slider)

这便是OnPaint方法的完整代码:

protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
pValueToPoint();
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
Pen penBarBack = new Pen(_BarColor, _BarSize);
Pen penBarFore = new Pen(_SliderColor, _BarSize);
float fCapHalfWidth = 0;
float fCapWidth = 0;
if (_IsRound)
{
fCapWidth = _BarSize;
fCapHalfWidth = _BarSize / 2.0f;
penBarBack.StartCap = LineCap.Round;
penBarBack.EndCap = LineCap.Round;
penBarFore.StartCap = LineCap.Round;
penBarFore.EndCap = LineCap.Round;
}
float fPointValue = 0;
if (_Orientation == Orientation.Horizontal_LR || _Orientation == Orientation.Horizontal_RL)
{
e.Graphics.DrawLine(penBarBack, fCapHalfWidth, Height / 2f, Width - fCapHalfWidth, Height / 2f);
fPointValue = mousePoint.X;
if (fPointValue if (fPointValue > Width - fCapHalfWidth) fPointValue = Width - fCapHalfWidth;
}
else
{
e.Graphics.DrawLine(penBarBack, Width / 2f, fCapHalfWidth, Width / 2f, Height - fCapHalfWidth);
fPointValue = mousePoint.Y;
if (fPointValue if (fPointValue > Height - fCapHalfWidth) fPointValue = Height - fCapHalfWidth;
}
if (_Orientation == Orientation.Horizontal_LR)
{
e.Graphics.DrawLine(penBarFore, fCapHalfWidth, Height / 2f, fPointValue, Height / 2f);
}
else if (_Orientation == Orientation.Horizontal_RL)
{
e.Graphics.DrawLine(penBarFore, fPointValue, Height / 2f, Width - fCapHalfWidth, Height / 2f);
}
else if (_Orientation == Orientation.Vertical_TB)
{
e.Graphics.DrawLine(penBarFore, Width / 2f, fCapHalfWidth, Width / 2f, fPointValue);
}
else
{
e.Graphics.DrawLine(penBarFore, Width / 2f, fPointValue, Width / 2f, Height - fCapHalfWidth);
}
}
OnPaint

在OnPain方法用到了一个方法:pValueToPoint(),其作用是将值转换为相应坐标。代码如下:

private void pValueToPoint()
{
float fCapHalfWidth = 0;
float fCapWidth = 0;
if (_IsRound)
{
fCapWidth = _BarSize;
fCapHalfWidth = _BarSize / 2.0f;
}
float fRatio = Convert.ToSingle(_Value-_Minimum) / (_Maximum - _Minimum);
if (_Orientation == Orientation.Horizontal_LR)
{
float fPointValue = fRatio * (Width - fCapWidth) + fCapHalfWidth;
mousePoint = new PointF(fPointValue, fCapHalfWidth);
}
else if (_Orientation == Orientation.Horizontal_RL)
{
float fPointValue = Width - fCapHalfWidth - fRatio * (Width - fCapWidth);
mousePoint = new PointF(fPointValue, fCapHalfWidth);
}
else if (_Orientation == Orientation.Vertical_TB)
{
float fPointValue = fRatio * (Height - fCapWidth) + fCapHalfWidth;
mousePoint = new PointF(fCapHalfWidth, fPointValue);
}
else
{
float fPointValue = Height - fCapHalfWidth - fRatio * (Height - fCapWidth);
mousePoint = new PointF(fCapHalfWidth, fPointValue);
}
}
pValueToPoint

之所以没有注释,实在是太过浅显无可注释,单纯的看代码很难理解,下面我将通过示意图的方法讲解,其实只要看了示意图,就会恍然大悟,会发现其实很简单。


7,示意图解

对于LTrackBar而言,有两种样式:直角和圆角。这两种的实现并没有太大不同,主要是Pen的LineCap属性不同,LineCap说明见前文。

(以下将以横向、从左到右的样式(_Orientation = Orientation.Horizontal_LR)进行讲解,其他类同,不多赘述。)

示意图1:


我在图中标注了一些点,主要用来详解。

上图中的B点(Rect.B、Round.B)即是当前鼠标点击的点,也是代表当前值的点,也是蓝色条的宽度。

示意图2:


在LineCap=Round时,其在绘制的线条两端会各绘制一个半圆,如上图中紫色所示。其半圆直径等于线条宽度。

下面我会讲解一下上面那些代码中的那些算式是怎么来的。

(1)直角

1)计算

已知:

起始点:Rect.A;

结束点:Rect.C;

点Rect.A 对应的值为: L_Minimum;

点Rect.C 对应的值为: L_Maximum;

鼠标可点击范围=控件宽度 = Bar.Width;

实际取值范围 = (L_Maximum-L_Minimum);

鼠标点击处的X值=点Rect.B = Slider.Width;

鼠标点击处的X值与鼠标可点击范围的比值=该点击处对应的实际值与取值范围的比值,即:

对应值/取值范围=Slider.Width/Bar.Width;

所以:

对应值(_Value)=Slider.Width/Bar.Width*(L_Maximum-L_Minimum);

由于最左侧的点Rect.A并不是0,而是对应着L_Minimum,所以,最后得到的真实值(L_Value)=_Value+L_Minimum;

2)绘制设置Pen的宽度=Bar.Height

所以要从控件高度的中间开始绘制,其起终坐标如下:

起点:(Rect.A)=(0,Bar.Height/2);

终点:(Rect.C)=(Bar.Width,Bar.Height/2);

(2)圆角

1)计算

已知:

因为设置了圆角(LineCap=Round),所以线条两端会各绘制一个半圆(示意图中紫色半圆所示),其半圆直径等于线条宽度。

那么其开始点便不再是点Round.A,而是点Round.D,同理,其结束点也不是点Round.C,而是点Round.E。

点Round.D 对应的值为: L_Minimum;

点Round.E 对应的值为: L_Maximum;

鼠标可点击范围=控件宽度减去两个半圆的宽度 = (Bar.Width-Bar.Height);

实际取值范围 = (L_Maximum-L_Minimum);

鼠标点击处的X值 (点Round.B) = (Slider.Width-Bar.Height/2);(注意:此时鼠标点击处所产生的视觉效果范围是(Round.A~Round.F),但其真正移动的范围是(Round.D~Round.B)。)

鼠标点击处的X值与鼠标可点击范围的比值=该点击处对应的实际值与取值范围的比值,即:

对应值/取值范围= (Slider.Width-Bar.Height/2)/ (Bar.Width-Bar.Height);

所以:

对应值(_Value)= (Slider.Width-Bar.Height/2)/ (Bar.Width-Bar.Height)*(L_Maximum-L_Minimum);

由于可点击的最左侧的点Round.D对应着L_Minimum,所以,最后得到的真实值(L_Value)=_Value+L_Minimum;

2)绘制

设置Pen的宽度=Bar.Height,所以要从控件高度的中间开始绘制。

又因为设置LineCap=Round,导致两端各绘制了一个半圆,所以其起点和终点的坐标也应减去相应的值:

起点:(Round.D)=(Bar.Height/2,Bar.Height/2);

终点:(Round.E)=(Bar.Width-Bar.Height/2,Bar.Height/2);

 


四,效果演示及调整优化


1,演示

我们在项目上右键,选择生成,之后在同一解决方案下新建一WinForm项目,此时在工具箱的最上层会有我们的自定义控件——LTrackBar。

如图:


我们选中并添加到主界面上,并设置相应的属性。

同时添加一个label,用来显示当前的值。 

其实效果如下:

在实际运行时,我们会发现在点击和拖动时,控件会有闪烁(由于GIF录制帧率,所以上面的动图不看不闪烁)。

为了解决闪烁的问题,我们在LTrackBar的构造函数上添加对双缓冲的支持。


个人经验之谈

关于双缓冲

一般而言,只要涉及到了GDI+,都会使用双缓冲技术去减少闪烁,而且使用也很简单,就两行代码而已:

SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

当然,ControlStyles还有很多属性,其作用也各有作用,在以后的文章中如果有用到我会再说明的。


2,默认事件

默认事件,顾名思义,就是双击控件时自动生成的事件,像双击Button时的Click事件,双击TextBox时的TextChanged事件等。

要实现这种效果,需要在代码的最上面加上DefaultEvent事件,如下:


 

 其中“LValueChanged”就是我们要设置的默认事件。这样在我们双击LTrackBar时,便会自动生成该事件。

 


五、结束语

通篇下来,其实可以发现并没有用到多深的知识,更多的是想像力,解放你的思想,不要被常规所束缚。

 


六、源代码及工程下载

https://files.cnblogs.com/files/lesliexin/LTrackBar.7z

 



推荐阅读
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • ASP.NET2.0数据教程之十四:使用FormView的模板
    本文介绍了在ASP.NET 2.0中使用FormView控件来实现自定义的显示外观,与GridView和DetailsView不同,FormView使用模板来呈现,可以实现不规则的外观呈现。同时还介绍了TemplateField的用法和FormView与DetailsView的区别。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 给定一个二维平面上的一些点,通过计算曼哈顿距离,求连接所有点的最小总费用。只有任意两点之间有且仅有一条简单路径时,才认为所有点都已连接。给出了几个示例并给出了对应的输出。 ... [详细]
author-avatar
有些事想不到
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有