这一系列文章中,我主要讨论如何由浅入深的创建自定义控件。首先,我们需要认识什么是控件,以及自定义控件的应用如何节省时间和金钱。
第一篇中,我们将对.NET自定义控件有一个快速的认识,并在文章结束时创建一个具有一定功能的自定义控件。在开始创建你自己的控件之前,理解他们的架构是一个重要的先决条件。
- • 了解什么是自定义控件,以及他们为何如此有用。
- • 理解自定义控件的组成。
- • 使用其他控件和组件创建你的第一个自定义控件。
什么是控件
控件是可重用的用户界面功能组件。在Windows的世界里,控件代表了用户与应用程序交互的方法。控件允许用户输入数据并且可以操作数据,从而使用户在应用程序上执行某些操作,如输入数据、友好的显示数据。应用程序的界面是由控件以及控件的功能组成的。控件的功能是基于控件本身与后台代码之间的互相调用而实现的。
让我们看看接下来的几张照片,你就会了解当今最流行的控件。你必须了解,使用控件进行程序设计比手动从头开始创建控件要简单的多。
每个应用程序都有不可见的另一面,但是这些不可见的部分却为可见的用户界面提供着实际的方法操作。控件完整的软件设计元素,它既用图形化的方式表现在屏幕上,而且它还包含着代码来支持界面的功能。
控件具有两个主要的功能:
- • 监听用户的操作指令,并将质量发送给应用程序
- • 以便于用户理解的方式,展示应用程序处理结果
除此之外,你可以使用颜色选取器来修改控件的背景色,可以通过按下一个按钮来执行具体的操作,或者你可以通过进度条来查看执行的进度状态变化。另一类控件只是作为显示用,即他们使用图形的方式显示数据内容,但是不允许客户修改显示的内容。标签控件就是最好的例子。
有趣的是,整个控件构思的发展的根源是个人计算机应用的开发。开发人员必须为应用程序提供一些良好用户体验的控件。这里让我们用一个常见的按钮为例。现实中的按钮具有不同的形状和尺寸,以及多种状态。它可以被按下或释放,或者可以当按下时发出声音。为了真实再现控件的特点,我们可以使用很多技巧。
自定义控件介绍
自定义这个词非常的神奇。在你的很多开发任务中,你只可以使用开发环境中默认提供的控件,但是有些情况下你可能需要定义你自己的用户控件。
自己定义的控件,顾名思义,叫做自定义控件。自定义控件是由你自己进行设计和编程的控件,而且在设计和开发过程中,你可以利用已有的控件。有时自定义控件也可以叫做第三方控件。
自定义控件说明,.NET框架提供的默认控件基类,无法满足需要时,或者你从第三方购买的控件要么过于昂贵要么无法满足需要时,创建自定义控件就成了必然的要求。
你还可以通过实现自定义控件的方法来提高代码的效率。这样做的意义可以用一个很简单的例子说明。如果在你的应用程序中必须使用一个包含多个元素的饼状图,从而达到以友好的方式显示多个结果的目的。有两种方法可以实现。
• 你可以直接在窗体中编写代码。首先,你必须在某个特定的位置绘制一个包含不同元素的饼状图。然后,你需要重写鼠标处理事件,来获得图形事件。最后假设这个图形具有一些功能,你就必须将图形所需代码直接附加到窗体的代码上。
你要想在应用程序中操作这个饼状图,就必须按照上面三个步骤完成才可。接下来,即使进行一些简单功能的更改,如将某个操作从左键触发,转移到右键触发,都需要执行上面的三个步骤。你的代码将会包含大量重复的功能,使其可读性极差,理解、调试、扩展都非常困难。更何况你每次更改这个图表,你都需要重新生成你的整个应用程序。
• 你可以创建一个自定义控件。你可以定义一个饼状图控件,使其可以自己绘制饼状图,而且可以具有自己的事件和事件处理机制。它将想窗体抛出不同的属性和必要的方法。这个自定义控件的位置可以通过设置它的坐标,轻易的在窗体中变化。另外,一旦创建了这样的自定义控件,你就会节省宝贵的时间,因为你用在修改自定义控件、添加扩展功能以及调试自定义控件所化的时间都很短暂,而且代码的变更也只出现在一个地方——控件的代码中。
封装方法到窗体的用户控件中,带来了几个好处:
- • 建立自定义控件方便代码重用。相同的自定义控件可以用在任意多个窗体或选项卡中(甚至其他自定义控件中),而不需要再编写重复的代码。这会节省应用程序开发中的大量时间,以及代码量。
- • 它鼓励基于面向对象的黑盒原则的功能重用。你不需要知道控件内部的工作原理;你所需要知道的一切就是它抛出的公共接口。例如,可以以一个最简单的控件——标签控件,为例。当在Windows 窗体项目中使用标签时,你需要设置标签的Text属性为你想要显示的文本。你从不需要关心标签内部的工作机制,以及它是如何绘制文本到显示器上的。从这个简单的案例推断,你可以感觉到黑盒的概念也适用于更多复杂的控件。
- • 它使应用程序代码简单。比如,你需要你的应用程序,或者其他东西,知道如何播放声音。使用一个自定义控件来实现播放声音功能,可以最少化应用程序窗体中的代码。与其创建按钮和组件,然后在应用程代码中添加并处理他们的时间,还不如创建一个简单的自定义控件(如TinyNoiseMaker)来实现播放的功能,然后将它通过公共接口暴露给应用程序使用。使用自定义控件后,由于功能的实现包含在控件内部,而不是在应用程序的窗体中,这样就保持应用程序代码的简单。理想状态下,窗体应该只用来与控件进行互操作,而不应该包含任何方法实现。
- • 自定义控件可以单独开发、编译、封装以及出售,就像通常的应用程序那样。这就给开发和使用控件提供了极大的灵活性。
- • 创建自定义控件,通过实现用户友好的代码和设计,可以更容易的改善你的应用程序的外观和可用性。如果你想让你的应用程序以某种样式展现,而你需要的样式无法通过设置.NET框架的控件外观属性来满足。你可以创建你想要的外观样式的自定义控件,你可以大大提高你的应用程序的用户体验和功能。这可能是赢得更多用户对你的好感的简便方法。因为在用户界面上创建自定义控件可以比只使用.NET框架内置的控件更友好。
用户控件的类别
根据绘制方式的不同,自定义控件分为三类:
- • 非自定义绘制:这些控件使用其他控件的用户界面来覆盖自己的用户界面。例如一个工具栏控件,就是使用工具栏上的按钮来覆盖它的界面。
- • 自定义绘制:这些控件根据输入的数据来绘制自己的界面,鼠标和按键事件、焦点以及其他变量。例子之一就是饼状图控件就是由控件自身绘制的。这样的控件在绘制时需要了解GDI+的相关知识。
- • 混合模式:混合模式控件使用上面的两种方式创建用户界面。例如,一个带滚动条的图表控件。在这个系列中你会看到大量的混合模式控件的例子。尤其重要的是饼状图控件这样的复杂控件。
自定义控件的组成
为了实现自定义控件,我们需要理解自定义控件及其组成部分的工作原理。我们现在需要了解控件的可见和不可见部分。自定义控件由两个主要部分组成。第一部分是黑盒。这部分保存了控件及其私有成员数据和内部的功能。第二部分是控件的公共接口。这部分接口由公共属性、事件和方法组成。这些抛出的控件的功能允许在使用控件的代码中,以编程方式操纵控件。
从技术上讲,控件就是一个继承自System.Windows.Forms.Control类的自定义类。它包含任何控件的基本方法,如响应鼠标事件、键盘事件、焦点以及绘图事件、预设样式、属性。定义一个最基本自定义控件的方法如下所示:
1: public class MyControl:Control
2: {
3: }
接下来,我们会学习一个控件类的基本组成。重要的是知道并理解这些组成元素是什么,以及他们如何实现控件功能。这些组成部分组成了控件类,并通过你继承的基础控件类,将你的更改落实到你的自定义控件上。换句话说,我们从Control类继承了一些基本的功能,这些功能是所有控件都共有的,而且我们可以通过添加这些组成部分来为自己的控件构建自定义功能。我们还可以修改现有的控件来添加一些额外的功能。
私有变量
私有变量,顾名思义,是一个不能被外部访问到的变量。当构建一个自定义控件时,“外部”就是指使用控件的应用程序(也可能是使用这个控件的其他自定义控件)。通常情况下,对于每个公共属性来说,都至少会有一个私有变量类存储抛出的数据。
一个良好的编程习惯是声明私有类变量,然后通过公共属性将私有变量抛出。这里我们使用的命名规则定义为,类命名使用Pascal命名(每个单词的首字母都大写,如ProgesssBar),而变量使用驼峰命名(首字母小写,其他单词首字母均大写,如myProgessBar)。
下面是一个代码段,它定义了一个名为MyControl的,具有私有变量的控件:
1: public class MyControl : Control
2: {
3: private Color backgroundColor;
4: private Color foregroundColor;
5: private int intemCount;
6: private Brush backBrush;
7: }
属性
当你在Visual Studio的窗体设计器中选中一个控件后,你可以就可以在属性窗口中查看控件的属性。属性是一个类或对象相关联的特点。例如,一个常见的按钮就有很多属性:名称、文本、字体、大小等等。按钮所抛出的所有属性都可以在属性窗口中查看。
属性是任何控件向外抛出控件的设置和数据的关键特征。通过公共属性用户可以对控件的配置进行一些列的交互,而且通过这种方法,用户可以获取和设置保存配置和数据的私有变量。
属性在Get和Set访问器中定义了数据可读或可写的过滤代码。这些访问器通常用来读取或设置那些真正保存数据的私有成员的值。通过为属性只定义Get访问器,你可以控制它的只读;通过只定义Set访问器,你可以控制它只写。这里以背景色为例,正如解释私有变量时所说,数据和配置隐藏在控件内的私有变量中,这些数据和配置通过属性抛出到控件的外部。
属性的默认结构是:
1: public <type> <PropertyName>
2: {
3: get
4: {
5: return <fieldName>;
6: }
7: set
8: {
9: <fieldName> = value;
10: }
11: }
这里,<type>表示属性的数据类型(如string),<PropertyName>是属性的名称(如BackgroundColor),而<fieldName>是存储属性值的私有变量的名称。注意,属性本身不保存任何数据,而且它可以使用Get和Set访问器,自由返回或设置私有变量的值。
属性基本上包含两种方法来读取或设置成员的值。他们还可以帮助实例化空字段,或者当成员改变时,执行某些活动。下面代码就说明了这些:
1: private Brush backBrush = null;
2:
3: public Brush BackBrush
4: {
5: get
6: {
7: if(backBrush == null)
8: {
9: backBrush = new SolidBrush(Color.Black);
10: }
11: return backBrush;
12: }
13: set
14: {
15: if(backBrush != value)
16: {
17: backBrush = value;
18: Invalidate();
19: }
20: }
21: }
上面的代码中,get访问器内,首先需要获得backBrush私有变量的实例引用,属性使用默认值将其初始化,并返回它的值。如果代码尝试读取BackBrush属性的值,而BackBrush属性并未放入try/catch块儿中,那么一个空引用异常将会抛出。
查看Set访问器中的代码:当设置backBrush变量时,如果它的值与旧值相同,则什么事情也不发生。这有助于优化应用程序代码,使得不必要时,某些方法可以不调用。
索引器
假如你有一个控件,或数据类中包含一系列项的集合。与其使用共有属性抛出这个集合,还不如为控件添加索引器来抛出这个集合。索引器是一种特殊类型的属性,它可以使类以数组下标的方式访问,这样就可以通过索引值获得其内部的对象。
例如,如果一个名为list的对象,包含一个索引器,你就可以通过读取list[1],list[2]等等来获取集合中的项。如果没有索引器,你只能通过list.Items[1],list.Items[2]等来访问。索引器与属性的差别是,索引器需要传递参数。
默认声明索引器的格式如下:
1: public <type> this[int index]
2: {
3: get
4: {
5: //return the object at the index;
6: }
7: set
8: {
9: //set the object at the index;
10: }
11: }
通常情况下,索引器返回的是一个数组中给定索引位置的成员。
让我们用一个例子来更好的理解使用索引器的好处。颜色选择器是一个控件,它可以让你选择一种颜色。作为一个额外的功能,提供通过颜色的索引器来在默认颜色中或最近使用的颜色中进行选择。颜色数组中的对象通过调用ColorPicker的索引器就可以获得。
1: public class ColorPicker : Control
2: {
3: private Color[] colors;
4: public Color this[int index]
5: {
6: get
7: {
8: if(index >= 0 && index < colors.Length)
9: {
10: return colors[index];
11: }
12: else
13: {
14: return null;
15: }
16: }
17: set
18: {
19: if(index >= 0 && index < colors.Length)
20: {
21: colors[index] = value;
22: }
23: }
24: }
25: }
如何使用索引器呢:
1: ColorPicker colorPicker1;
2: colorPicker1[0] = Color.Red;
get/set语句实际上鼓励你验证你要读取或设置的值。你可以在上面的代码段中看到简单的验证结构。
一般来说,当你可以将数据以数组的结构保存时,你可以使用索引器;这就意味着数据的个数应该是可数的。例如,你可以使用属性来存储气温的温度,而你可以使用属性来存储一个星期的每一天。
事件和委托
事件和委托是Windows平台程序设计的核心,因为他们是应用程序与用户进行互操作的主要机制(但是它们也可以作为其他用途)。事件和委托允许一个控件(或其它类型的类),当它自身发生一些事件时,发送出信号,而且你可以编写C#方法来依据控件发送的信号,自动执行某些功能。而这个信号,就是所激发的事件本身。委托是一种对象类型,它允许你注册本地C#方法(那些我们在事件处理时需要调用的),当事件触发时执行。
若要用一个例子说明,那么假设我们有一个窗体,这个窗体中包含一个按钮。我们也许想知道当单击按钮时,我们可以执行一些C#代码作为响应。按钮与窗体之间的链接是通过事件实现的。当单击按钮时,按钮的Click事件被触发,用来表示按钮已经被单击。为了响应这个Click事件,你可以创建一个事件处理程序,它可以是一个C#方法,而且它可以在单击时激发执行。
事件定义了控件产生的活动。这些活动可以由用户对界面的互操作产生,或者通过其它实现逻辑。鼠标单击是一个事件,键盘按下也是一个事件,也可以说明当前控件下产生了一事件触发。事件由发送者激发,由接收者捕捉。委托作为一种特殊的调用形式,成为发送者和接收者之间的链接。
前面我们说到,为了响应触发的事件,我们可以注册一个本地方法让它自动执行。虽然这些事件可以帮你规划你的系统功能如何执行,但是,如果在真正的C#代码实现中,你什么代码也不写,那么没有任何功能会自动执行。当某个控件发生事件后,这个控件就知道如何执行使用这个控件的类中的方法;为了实现这种效果,对于某个方法的引用必须发送到该控件。而委托就是这样的一种方法引用。委托是一个数据类型,它定义了方法的模板,而且委托的实例是指向具体方法的引用。控件所能触发的每个事件,都有其具体执行的委托类型,而正是这些委托类型将事件需要的参数发送到控件本身执行(这些参数包含事件的细节,而这些细节都依据事件的不同而不同)。
下面我们看一下委托如何声明:
1: public delegate <return type> <delegate name> ( <parameter list> );
1: public delegate int myDelegate( int intValue );
这个委托表示一个具有一个整型参数的方法模板,而且这个方法会返回一个整数类型的结果。其他类中的很多方法,只要满足这个委托模板的规定,都可以绑定到这个事件委托上来。这些方法会在事件触发时被调用。通常事件委托是如下格式:
1: public delegate void <myEventHandler>(object sender, <EventArgs> e);
这里,<myEventHandler>代表当事件触发时,执行的本地方法。方法的命名约定是使用事件名“EventHandler”作为方法名的后缀,如ClickEventHandler,KeyDownEventHandler和MouseOverEventHandler。如果使用Visual Studio生成的事件处理程序的命名规范是<触发事件的控件名称>_<事件的名称>,如myButton_Click。
当使用.NET内置控件时,<EventArgs>是一个继承自.NET框架的EventArgs类的类型。你可以定义你自己的事件和委托,但是使用上面介绍的结构创建这些事件与委托是最佳的做法。
现在我们看看如何定义事件。事件必须声明在触发它的控件内,并使用下面这种默认方式声明:
1: public event <myEventHandler> <Event_name>;
例如:
1: public event ClickEventHandler Click;
当事件触发时调用的接收器方法叫做事件处理程序。换句话说,当创建一个控件时,要想让某个动作触发时,通知窗体或其他控件,你需要添加一个公共的事件到控件中,并在活动发生时,触发它。然后,任何引用了你的控件的类,可以通过将事件处理程序关联到事件上,从而获得事件的通知。现在让我们看看如何触发事件,以及在另外类中处理这个事件。
触发一个事件,通常是由控件中的一个受保护的虚方法来完成的。它的声明如下:
1: protected virtual void On<event name>(EventArgs e)
2: {
3: if(<event name> != null)
4: {
5: <event name>(this,e);
6: }
7: }
事实上,方法名以“On”开头,并不是必须,但确实推荐的做法。例如,MouseOver事件就是通过OnMouseOver激活事件方法来触发的。这样的代码更容易阅读和理解。
激活方法是虚方法,这就意味着当你需要改变这些方法默认的行为时,可以通过继承这个控件类来重写这些方法。
下面的例子中,如果你设置控件的BackgroundColor属性,就会使控件的backgroundColor成员改变,从而触发BackgroundColorChanged事件。
1: public class MyControl : Control
2: {
3: ...
4: private Color backgroundColor;
5: public event EventHandler BackgroundColorChanged;
6: public Color BackgroundColor
7: {
8: get
9: {
10: return backgroundColor;
11: }
12: set
13: {
14: if(backgroundColor != value)
15: {
16: backgroundColor = value;
17: OnBackgroundColorChanged(EventArgs.Empty);
18: }
19: }
20: }
21: protected virtual void OnBackgroundColorChanged(EventArgs e)
22: {
23: if(BackgroundColorChanged != null)
24: {
25: BackgroundColorChanged(this,e);
26: }
27: }
28: ...
29: }
事件变量为空时,说明没有事件处理程序连接到它。如果没有异常处理程序附加到这个事件上,则必须通过方法验证或异常处理进行评比。
处理事件:
为了处理我们上面定义的MyControl类的事件,你需要在使用这个控件的类上添加事件处理程序。事件处理程序是一个方法,它可以位于任何想要处理这个事件的类中。事件以EventHandler的形式声明,这只是一个标记而已,而且不需要为事件传递任何数据,因为EventArgs类并没有要求传递参数。在.NET 框架中的声明如下:
1: public delegate void EventHandler(object sender,EventArgs e);
事件处理程序必须满足事件委托定义的方法标识规定。
1: MyControl myControl;
2: ...
3: // Attaches the myControl_BackgroundColorChanged method to the event
4: myControl.BackgroundColorChanged += new EventHandler(myControl_BackgroundColorChanged);
5: ...
6: private void myControl_BackgroundColorChanged(object sender, EventArgs e)
7: {
8: // Code that is executed when the BackgroundColorChanged event is fired.
9: }
每次MyControl类中的BackgroundColorChanged事件触发时,调用类都会调用myControl_BackgroundColorChanged方法,并执行其中的代码。
要实现传递数据到事件处理程序中这种更复杂的事件,例如控件的状态或其他参数(比如,单击事件就需要传递鼠标的坐标位置),你既可以使用.NET内置的事件(如ClickEventHandler),也可以创建自己的事件。
集合:
你将要编写的一些控件可能需要存储集合项。例如,一个ListView控件就有一个ListViewItems集合,用来保存控件中每一行的数据。
有很多类型的结构可以用来在你的控件中存储这样的数据。我们快速看看这里都有哪些可能,然后我们会在下一篇文章中详细分析他们。
基类对象内使用数组来管理许多元素。但是使用数组来存储有它明显的缺点就是数组大小固定,这就限制了它使用的途径。你不应该使用数组来存储控件中索引的数据,除非数组中的元素个数是固定的(例如数组中的5种颜色)。
1: Color[] colors = new Color[3];
ArrayList是.NET框架中基于索引方式存储任意类型集合项的集合类型。它最主要的优势是具有动态可变的大小,以及动态在某个特定的索引位置添加、插入和移除的能力。但是,在构建控件时,将要索引的数据存储在ArrayList中并不是好的做法,因为对象存入ArrayList时的数据类型,需要在从ArrayList中读取该对象前获得。注意,使用using System.Collections代码可以在代码中引入ArrayList集合对象。
1: ArrayList aList = new ArrayList();
2: aList.Add(Color.Red);
3: aList.Add(Color.Green);
4: aList.Add(Color.Blue);
5: aList.RemoveAt(1);
6: // Conversion is needed because the arrayList indexer return type is object
7: Color col = (Color) aList[0];
ArrayList并不是System.Collections命名空间中的唯一类别。在后面的文章中我们详细介绍。
你甚至可以通过继承CollectionBase基类来创建自己的集合类。这样你可以创建自定义集合来统一保存你规定的对象类型。ArrayList类可以保存任意类型的对象,通过它的索引可以返回object类型的对象。而你自己定义的集合类型可以返回你集合中所特有的统一对象类型。通过使用自定义集合类,你可以重写基类中的方法,以实现附加的功能。例如,CollectionChanged事件可以追加到集合的父类上,当集合改变时可以触发。此外,集合是大小可变的,不需要像数组那样,在创建时就规定元素的个数。
集合类都是派绳子CollectionBase类。这个类包含了一个内部数组,它可以用来存储元素。最好的做法是将集合命名为以对象的类型为前缀,以“Collection”作为后缀,如ColorCollection。
让我们看看如何构建一个集合类。下面的代码是一个控制台应用程序,其中包含了我们在上面阐述的一些理论。如果你想在Visual Studio中创建一下代码,你需要引用System.Drawing命名空间。
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Collections;
6: using System.Drawing;
7:
8: namespace ConsoleApplicationColorCollection
9: {
10: public class ColorCollection : CollectionBase
11: {
12: //this event is fired when the collection's items have changed
13: public event EventHandler Changed;
14: //this is the constructor of the collection.
15: public ColorCollection()
16: {
17: }
18: //the indexer of the collection
19: public Color this[int index]
20: {
21: get
22: {
23: return (Color)this.List[index];
24: }
25: }
26: //this method fires the Changed event.
27: protected virtual void OnChanged(EventArgs e)
28: {
29: if (Changed != null)
30: {
31: Changed(this, e);
32: }
33: }
34: //returns the index of an item in the collection
35: public int IndexOf(Color item)
36: {
37: return InnerList.IndexOf(item);
38: }
39: //adds an item to the collection
40: public void Add(Color item)
41: {
42: this.List.Add(item);
43: OnChanged(EventArgs.Empty);
44: }
45: //inserts an item in the collection at a specified index
46: public void Insert(int index, Color item)
47: {
48: this.List.Insert(index, item);
49: OnChanged(EventArgs.Empty);
50: }
51: //removes an item from the collection.
52: public void Remove(Color item)
53: {
54: this.List.Remove(item);
55: OnChanged(EventArgs.Empty);
56: }
57: }
58: class Program
59: {
60: static void Main(string[] args)
61: {
62: // create a color collection
63: ColorCollection colorCollection = new ColorCollection();
64: // add two colors to the collection
65: colorCollection.Add(Color.Red);
66: colorCollection.Add(Color.Blue);
67: // you can reference a color from the collection without making a cast
68: Color color = colorCollection[0];
69: // you can refer to collection items like any Color object
70: Console.WriteLine(colorCollection[1].Name);
71: }
72: }
73: }
上述代码中描述了一个简单的颜色对象的集合。下一篇文章中我们会看到更多关于集合的课题,其中包含.NET 2.0版本中的泛型。
枚举
枚举是用户自定义数据类型,用来存储可命名的相同数据类型的一系列内容。枚举可以用来接受一组固定类型的值。例如,你可以设置一组类型来定义控件边框的风格:none,2D,3D:
1: borderControl.Border = BorderType.Border3D;
默认的枚举声明方式:
1: public enum <Name>
2: {
3: <value1>,
4: <value2>,
5: ...
6: <valuen>
7: }
边框样式的枚举案例如下:
1: public enum BorderType
2: {
3: None, Border2D, Border3D
4: }
你可以将枚举作为控件的属性来使用:
1: public class BorderControl : Control
2: {
3: ...
4: private void BorderType border;
5: ...
6: public BorderType Border
7: {
8: get
9: {
10: return border;
11: }
12: set
13: {
14: border = value;
15: }
16: }
17: }
现在,要在另一个调用类中设置枚举的值,只需要进行选择即可:
1: BorderControl borderControl;
2: borderControl.Border = BorderType.Border3D;
动手实验室:创建微型声音播放器
前面我们看了一些理论的内容,现在我们花一些时间创建一个简单的,但是功能健全的自定义控件。
这里我们需要创建一个声音播放控件,叫做TinyNoiseMaker。创建它的过程中,可以告诉你基本的控件架构:怎样构建黑盒,然后使用事件和属性公开接口。这里还要用到两个控件,分别是:OpenFileDialog和SoundPlayer控件。
该控件播放一个本地硬盘的音频文件。它的界面由三个按钮组成。而这三个按钮代表了三个主要的功能:加载,播放和停止。
下面的步骤中将会引导你创建TinyNoiseMaker。首先你会创建一个Windows应用程序,叫做SoundPlayerTest,然后在这个项目内部才会创建TinyNoiseMaker控件。
开始创建TinyNoiseMaker:
- 启动Visual Studio 2010并创建一个Visual C#的Windows应用程序,将其命名为SoundPlayerTest,如下图所示:
- 添加一个新的控件项目。在解决方案浏览器中,右键单击项目名称(不是解决方案名称),然后单击添加|用户控件。将控件名改为TinyNoiseMaker,然后单击添加,如下图所示:
- 接下来需要为控件添加内容。此时TinyNoiseMaker的设计界面已经打开。打开工具箱,然后从常用控件选项卡中,添加三个按钮控件到TinyNoiseMaker控件的界面上。再从工具箱中的对话框选项卡中添加一个OpenFileDialog控件到TinyNoiseMaker控件的界面上。如下图所示:
- 按照下表所示,在属性窗口中,设置新控件的属性值:
控件类别 控件名 文本 按钮 openButton Open 按钮 playButton Play 按钮 stopButton Stop 打开文件对话框 openFileDialog
- 通过在空白处单击,选择控件的设计界面,然后可以设置整个自定义控件的属性:
属性名 属性值 边框类型 FixedSingle 背景色 ControlLight 高度 32 - 设置的结果如下图所示:
- 接下来需要编写一些代码。切换到TinyNoiseMaker.cs到代码视图,然后在using指令区域添加如下代码:
1: using System.IO;
2: using System.Media;
- 为TinyNoiseMaker类添加一个新的成员变量:
1: namespace SoundPlayerTest
2: {
3: public partial class TinyNoiseMaker : UserControl
4: {
5: private SoundPlayer soundPlayer;
- 在构造函数中初始化soundPlayer
1: public TinyNoiseMaker()
2: {
3: InitializeComponent();
4: soundPlayer = new SoundPlayer();
- 切换回设计视图,然后双击Open按钮,让Visual Studio自动生成按钮单击事件的一个事件处理方法。然后将下面代码加入到生成的事件处理方法中:
1: private void openButton_Click(object sender, EventArgs e)
2: {
3: if (openFileDialog.ShowDialog() == DialogResult.OK)
4: soundPlayer.Stream = new FileStream(openFileDialog.FileName, FileMode.Open, FileAccess.Read);
5: }
- 再次切换到设计视图,双击Play按钮,然后将下面的代码加入到生成的事件处理程序中
1: private void playButton_Click(object sender, EventArgs e)
2: {
3: soundPlayer.Play();
4: }
- 针对Stop按钮再次执行步骤11,将下面代码,加入到自动生成的事件处理程序中
1: private void stopButton_Click(object sender, EventArgs e)
2: {
3: soundPlayer.Stop();
4: }
- TinyNoiseMaker已经准备好了。生成解决方案,确保你没有输入或语法错误。然后双击打开Form1.cs,并从工具箱中拖拽TinyNoiseMaker到Form1中。
- 运行应用程序(按下F5),单击Open按钮,然后选择一个可用的*.wav文件(你可以试试C:\\Windows\Media\tada.wav文件)。然后选择播放来欣赏声音或音乐。
效果如何?
你成功建立了一个Windows窗体应用程序,并且添加了一个新的自定义控件。然而,TinyNoiseMaker控件是独立于应用程序之外的。这个练习的最后一步时,你需要添加这个控件到应用程序的主界面。这样功能就完成了。
TinyNoiseMaker功能依赖于SoundPlayer组件,这个组件是你以私有成员的方式添加的。SoundPlayer控件位于.NET 2.0框架中的System.Media命名空间下,这就是为什么我们需要首先添加针对System.Media的引用。分析openButton_Click,playButton_Click和stopButton_Click,就揭示了你如何从硬盘打开一个文件,以及如何播放。
如果你视图加载一个非支持的类型,应用程序将会抛出异常(如下图所示),这就需要你在生产代码中考虑错误处理相关的技术。
现在,虽然你的应用程序可以运行了,那么接下来值得考虑的是Visual Studio都建立了那些文件:
TinyNoiseMaker.cs文件包含了控件的逻辑,TinyNoiseMaker.Designer.cs包含了Visual Studio为界面生成的代码。
扩展TinyNoiseMaker
这一节中,你将通过公共的方法、属性和事件为控件添加功能,以便使其功能可以编程访问。与在界面的控件上按下按钮效果相同的是,通过共有方法调用来执行操作。控件的公共接口将会定义为两个方法(Play(),Stop()),以及一个属性(FileName),以及两个事件(PlayStart,PlayStop).
动手实验室:添加公共功能
- 打开TinyNoiseMaker控件的代码视图,然后添加如下两个方法到TinyNoiseMaker类:
1: public void Play()
2: {
3: soundPlayer.Play();
4: }
5: public void Stop()
6: {
7: soundPlayer.Stop();
8: }
- 修改播放和停止时间处理程序,使他们调用者两个公有方法。当两个按钮——播放和停止——被按下,则他们会调用Play()和Stop()方法来激活期望的事件处理程序。
1: private void playButton_Click(object sender, EventArgs e)
2: {
3: Play();
4: }
5:
6: private void stopButton_Click(object sender, EventArgs e)
7: {
8: Stop();
9: }
- 此时可以编译项目了,而且你可以再次执行这个应用程序以确保功能仍能正常执行。接下来我们需要继续添加名为FileName的属性,该属性使用一个名为fileName的私有变量用来存储文件的名称。这样的目的是使客户端可以以编程的方式访问控件,他们不希望仅仅依赖播放、打开和停止按钮里进行控制。那么添加如下代码到TinyNoiseMaker类:
1: private string fileName;
2: public string FileName
3: {
4: get
5: {
6: return fileName;
7: }
8: set
9: {
10: if (fileName != value)
11: {
12: fileName = value;
13: soundPlayer.Stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
14: }
15: }
16: }
- 更新openButton_Click()方法来应用我们这个新属性:
1: private void openButton_Click(object sender, EventArgs e)
2: {
3: if (openFileDialog.ShowDialog() == DialogResult.OK)
4: FileName = openFileDialog.FileName;
5: //soundPlayer.Stream = new FileStream(openFileDialog.FileName, FileMode.Open, FileAccess.Read);
6: }
- 下一步,我们为TinyNoiseMaker控件添加PlayStart和PlayStop事件。当控件开始播放或停止播放时,这些事件会触发,这样就给使用TinyNoiseMaker的控件提供了响应播放动作的机会。添加如下事件到TinyNoiseMaker类:
1: public event EventHandler PlayStart;
2: public event EventHandler PlayStop;
- 下一步,我们添加方法来触发PlayStart和PlayStop事件。新的方法叫做OnPlayStart()和OnPlayStop(),并且他们都是虚方法,这就意味着他们可以被将来潜在的继承TinyNoiseMaker的控件所重写。类似于OnPlayStart()和OnPlayStop()这样的方法需要将某些方法(事件处理方法)绑定到激活的事件上。添加如下方法到TinyNoiseMaker类中,且紧挨上一步中声明的两个事件委托:
1: // fire the PlayStart event
2: protected virtual void OnPlayStart(EventArgs e)
3: {
4: if (PlayStart != null)
5: {
6: PlayStart(this, e);
7: }
8: }
9: // fire the PlayStop event
10: protected virtual void OnPlayStop(EventArgs e)
11: {
12: if (PlayStop != null)
13: {
14: PlayStop(this, e);
15: }
16: }
- 现在,你已经将事件的激活与具体的方法绑定了,接下来就可以实际使用他们。修改Play()和Stop()方法,使控件的播放和停止可以激活事件。
1: public void Play()
2: {
3: soundPlayer.Play();
4: OnPlayStart(EventArgs.Empty);
5: }
6: public void Stop()
7: {
8: soundPlayer.Stop();
9: OnPlayStop(EventArgs.Empty);
10: }
- 现在你的控件已经准备好了,并且具有了一些新的功能。这些新功能只能通过编程的方式访问,所以如果你现在执行项目,你不会看到任何变化。现在,执行项目,并确保它可以正常执行。下面是完整的TinyNoiseMaker.cs代码,以及一些补充的注视,供大家参考:
1: using System;
2: using System.Collections.Generic;
3: using System.ComponentModel;
4: using System.Drawing;
5: using System.Data;
6: using System.Linq;
7: using System.Text;
8: using System.Windows.Forms;
9: using System.IO;
10: using System.Media;
11:
12: namespace SoundPlayerTest
13: {
14: // the TinyNoiseMaker user control
15: public partial class TinyNoiseMaker : UserControl
16: {
17: // private members
18: private SoundPlayer soundPlayer;
19: private string fileName;
20: // public events
21: public event EventHandler PlayStart;
22: public event EventHandler PlayStop;
23: // fire the PlayStart event
24: protected virtual void OnPlayStart(EventArgs e)
25: {
26: if (PlayStart != null)
27: {
28: PlayStart(this, e);
29: }
30: }
31: // fire the PlayStop event
32: protected virtual void OnPlayStop(EventArgs e)
33: {
34: if (PlayStop != null)
35: {
36: PlayStop(this, e);
37: }
38: }
39: // public property stores the name of the file to be played
40: public string FileName
41: {
42: get
43: {
44: return fileName;
45: }
46: set
47: {
48: if (fileName != value)
49: {
50: fileName = value;
51: soundPlayer.Stream = new FileStream(fileName, FileMode.Open,
52: FileAccess.Read);
53: }
54: }
55: }
56: // constructor initializes the SoundPlayer control
57: public TinyNoiseMaker()
58: {
59: InitializeComponent();
60: soundPlayer = new SoundPlayer();
61: }
62: // open the file to be played
63: private void openButton_Click(object sender, EventArgs e)
64: {
65: if (openFileDialog.ShowDialog() == DialogResult.OK)
66: {
67: FileName = openFileDialog.FileName;
68: }
69: }
70: // play the file when the Play button is clicked
71: private void playButton_Click(object sender, EventArgs e)
72: {
73: Play();
74: }
75: // stop playing when the Stop button is clicked
76: private void stopButton_Click(object sender, EventArgs e)
77: {
78: Stop();
79: }
80: // start playing the sound file
81: public void Play()
82: {
83: // play the sound
84: soundPlayer.Play();
85: // fire the event
86: OnPlayStart(EventArgs.Empty);
87: }
88: // stop playing the sound file
89: public void Stop()
90: {
91: // stop playing the sound
92: soundPlayer.Stop();
93: // fire the event
94: OnPlayStop(EventArgs.Empty);
95: }
96: }
97: }
效果如何?
现在,你的控件中包含了可以被其他类通过编程方式操作的属性和事件。我们之所以添加这些功能,是因为通常情况下,可编程访问通常是必要的需求。这样你的客户端可以不仅仅使用可视化的用户界面,就可以使用控件提供的功能了。
接下来,我们看看如何使用这些新的公共方法。当扩展了控件的方法后,你就会看到如何在示例应用程序中使用TinyNoiseMaker控件的公共接口。这些接口包含两个方法(Play(),Stop()),一个属性(FileName),以及两个事件(PlayStart,PlayStop)。
动手实验室:使用控件的公共接口
- 在解决方案浏览器中打开SoundPlayerTest项目的Form1窗体。
- 在窗体中选择TinyNoiseMaker控件,然后按下F4,打开属性窗口。在属性窗口中,通过点击事件按钮(属性窗口顶端,黄色闪电按钮),可以看到两个新的事件。
- 在属性窗口中双击PlayStart和PlayStop,就可以为两个事件添加事件处理程序。接下来,我们需要通过修改窗体的标题栏,来反映当前的播放状态。
1: private void tinyNoiseMaker1_PlayStart(object sender, EventArgs e)
2: {
3: Text = "Play: " + tinyNoiseMaker1.FileName;
4: }
5: private void tinyNoiseMaker1_PlayStop(object sender, EventArgs e)
6: {
7: Text = "Stop: " + tinyNoiseMaker1.FileName;
8: }
- 现在,再次执行项目,打开文件,然后单击Play按钮。你可以通过窗体标题栏的变化,得到控件的状态反馈:
接下来,我们看看如何不使用控件的可视化界面,也可以执行控件的功能。添加一个按钮到你的应用程序的窗体中,将它的名称设置为playButton,它的显示文本修改为Play My File。然后添加这个按钮的单击事件,假如如下代码。将文件名修改为你本地的一个音频文件的位置。1: private void playButton_Click(object sender, EventArgs e)
2: {
3: tinyNoiseMaker1.FileName = "C:\\Windows\\Media\\tada.wav";;
4: tinyNoiseMaker1.Play();
5: }
- 执行项目,单击新按钮,效果应该如下所示:
- 最后,我们保存解决方案。
效果如何?
这个练习中,你的客户端程序使用了控件的公有功能来播放声音文件。另外,你处理了控件抛出的两个事件,来告诉用户控件正在做的事情。这些任务通常都是构建自定义控件时常用的。
这里你已经学会了如何构建一个简单的,非自己绘制的控件。它的接口是基于按钮控件的,而且它的功能是基于SoundPlayer和OpenFileDialog组件的。通常的做法是使用控件的可视化界面元素来操作控件,但是你也看到了,你可以自由的允许其它程序,以编程的方式使用你控件抛出的功能。而控件可以通过公有属性、方法、事件来抛出自己的功能。