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

1、前言

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

WinForm有着很是多的优势,在使用WinForm久了以后,不免会以为WinForm自带的某些控件外观上有些许朴素、或者功能上有些不如意,天然而然便想去美化这些控件,或者给控件添加一些额外功能,而这即是自定义控件的意义所在。框架

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

1,通常的教材不会讲——由于仍是有难度的,并且通常用不上;函数

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

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

我也是这样一步步走来的,因此不想让你们再花费这么多的时间去掌握一项并不太难的知识,便有了这篇文章。优化

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

本篇的自定义控件是:TrackBar设计

本文地址:http://www.javashuo.com/article/p-qqasooqt-md.html3d


 

2、前期分析

(一)为何须要去自定义控件?

咱们来分析一下为何要去自定义控件。

以本文要实现的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 对象以该线帽结束一段直线。

 


 

3、开始实现

(一)前期准备

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 < fCapHalfWidth) fPointValue = fCapHalfWidth;
        if (fPointValue > Width - fCapHalfWidth) fPointValue = Width - fCapHalfWidth;
    }
    else
    {
        e.Graphics.DrawLine(penBarBack, Width / 2f, fCapHalfWidth, Width / 2f, Height - fCapHalfWidth);

        fPointValue = mousePoint.Y;
        if (fPointValue < fCapHalfWidth) fPointValue = fCapHalfWidth;
        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时,便会自动生成该事件。


 

5、结束语

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


 

6、源代码及工程下载

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

相关文章
相关标签/搜索