总体要求
1、了解事件源、侦听器、事件处理程序、事件接收器等基本概念。
2、掌握委托的声明、实例化和使用方法,了解多路广播及其应用。
3、掌握事件的声明、预订和引用,熟悉事件数据类的使用方法。
4、了解Windows应用程序的工作机制,了解Windows窗体和控件的常用事件,理解事件和事件方法之间的关系。
相关知识点
1、基于事件的编程思想
2、委托和事件
学习重点
1、委托的声明、实例化与使用
2、事件的声明、预定和引用
学习难点
1、多路广播与委托
2、自定义事件
基于事件驱动的程序设计是目前主流的程序设计方法,它是Windows应用程序设计和Web应用程序设计的基础。但长期以来,基于事件驱动模型都被广大初学者视为难以理解的内容。为此,本章将形象、直观、系统地阐述基于事件驱动的程序设计方法及其应用。
8.1 基于事件的编程思想
在现实生活中,事件是一个日常用语,人们每天都会耳闻目睹各种各样的事件,有的让人快乐,有的让人痛苦,有的让人震惊。例如,火灾、洪灾、泥石流等事件是人人都不愿意看到的,但又是时常发生而不得不面对的。为了将危害降到最低,成立了专门的机构来处理这些突发事件。在事件没有发生时,这些机构会预先指定各种防灾救灾的应对方案;相反,在事件发生时,这些机构就会迅速展开救灾工作。
为了有效地防范灾害,相关机构总是从三个方面入手:一是制定完善的事件处置程序,二是构建有效的报警系统,三是迅速分析查找事件源。其中,事件处置程序体现了突发事件的防范措施,包括人员组织、物质准备、设备配置等。报警系统保证发生突发事件时相关信息能迅速传递给相关机构。事件源是引发灾害的根源,只有找到了灾害的源头才能进行有效地处理。
可见,突发事件的处理机制应该是:首先事件源触发某个灾害事件(如天然气泄漏引发火灾),然后是报警系统发送消息给相关部门(无论是人工电话报警,还是自动报警,只是一种形式),之后相关机构在收到消息后启动事件处置程序,迅速展开行动。这种事件处理机制称为事件驱动模型。
由于事件驱动模型在社会生活中无处不在,因此现在的计算机高级程序设计语言很自然地把它引入到程序设计之中,从而形成了事件驱动地程序设计方法。
早期的程序设计语言(如C语言)采用过程驱动模型,把一个程序划分为若干个更小的子程序或函数,通过子程序调用或函数调用最终形成一个有机的整体。各语句之间的关系可归结为顺序、分支和循环,不过一个程序无论包含多少个分支或循环,总体上仍是一个顺序结构,从上到下地执行。因此,对于初学者来说,一旦习惯了结构化的程序设计方法,学习事件驱动模型时都会感到不适应。
为了提高程序设计的效率,目前几乎所有的软件开发工具都支持可视化设计。很多人对可视化编程的认识非常表面,认为能够使用拖放方式完成一个界面设计就是可视化设计的全部,而没有去认真理解可视化编程背后的实质是事件驱动模型。
其实,事件驱动的程序设计并不难理解,其过程与防灾救灾是相通的。完整的事件处理系统必须包含以下三大组成要素。
(1)事件源:指能触发事件的对象,有时又称为事件的发送者或事件的发布者。
(2)侦听器:指能收到事件消息的对象,Windows提供了基础的事件侦听服务。
(3)事件处理程序:在事件发生时能对事件进行有效处理,又称事件方法或事件函数。包含事件处理程序的对象称为事件的接收者,有时又称事件的订阅者。
基于.NET的Windows应用程序和Web应用程序都是基于事件驱动的,当且仅当事件的发布者触发了事件时,事件的接受者才执行事件处理程序。因此,事件处理程序不是顺序执行的。
Windows应用程序或Web应用程序的每一个窗体及控件都有一个预定义的事件集。其中,每一个事件都会同某个事件处理程序对应。在程序运行时,Windows系统内置的侦听器会自动监听事件源的每一个事件,事件一旦触发就会通过事件接收者执行事件处理程序,完成事件的处理,如图8-1所示。
图 8-1 单个事件的处理流程
为了确保事件处理程序被执行,在程序设计时必须预先将一个事件处理与事件源对象联系起来,这个操作称为事件的绑定。C#通过委托来绑定事件,本章将详细介绍有关委托与事件绑定的内容。
8.2 委托
一个C#程序至少由一个自定义类组成,类的主要代码由方法组成。遵照“方法名(实参列表)”的基本格式,一个方法可以调用另外一个方法,从而使整个程序形成一个有机整体。显然,这种方法调用是根据程序逻辑预先设计的,一经设计不再更改。但有时希望根据当前程序运行状态,动态地改变要调用的方法,特别是在事件系统中,必须根据当前突发事件,动态地调用事件处理程序(即方法)。为此,C#提供委托调用机制。本节将详细介绍委托的概念及其应用。
8.2.1 委托概述
委托(delegate)是一种动态调用方法的类型,它与类、接口和数组相同,属于引用型。
委托是对方法的抽象和封装,这一点与类具有一定的相似性,类是对相同对象的抽象和封装,通过定义类,实例化对象,引用对象的顺序就可以使用对象了。在C#程序中,可以声明委托类型、创建委托的实例(即委托对象)、把方法封装于委托对象之中,这样通过该对象就可以调用方法了。一个完整的方法具有名字、返回值和参数列表,用来引用方法的委托也要求必须具有同样的参数和返回值。
因为C#允许把任何具有相同签名(相同的返回值类型和参数)的方法分配给委托变量,所以可通过编程的方式来动态更改方法调用,因此委托是实现动态调用方法的最佳方法,也是C#实现事件驱动的编程模型的主要途径。
委托对象本质上代表了方法的引用(即内存地址)。在.NET Framework中,委托具有以下特点。
(1)委托类似于C++函数指针,但与指针不同的是,委托是完全面向对象的,是安全的数据类型。
(2)委托允许将方法作为参数进行传递。
(3)委托可用于定义回调方法。
(4)委托可以把多个方法链接在一起。这样,在事件触发时可同时启动多个事件处理程序。
(5)委托签名不需要与方法精确匹配
8.2.2 委托的声明、实例化与使用
1、委托的声明
委托是一种引用的数据类型,在C#中使用关键字delegate声明委托,一般形式如下:
[访问修饰符] delegate 返回值类型 委托名 ([参数列表]);
其中,访问修饰符与声明类、接口和结构的访问修饰符相同,返回值类型是指要将动态调用的方法的返回值类型,参数列表是将要调用的方法的形参列表,当方法无参数时,省略参数列表。例如:
public delegate int Calculate(int x, int y);
就表示声明了一个名为Calculate的委托,可以用来引用任何具有两个int型的参数且返回值也是int型的方法。
在.NET Framework中,自定义的委托自动从Delegate类派生,因此不能再从Delegate中派生委托。由于委托是密封的,因此也不能从自定义的委托派生。委托类型一般使用默认的构造函数。
2、委托的实例化
因为委托是一种特殊的数据类型,因此必须实例化之后才能用来调用方法。实例化委托的一般形式如下:
委托类型 委托变量名 = new 委托型构造函数(委托要引用的方法名)
其中,委托类型必须事先使用delegate声明。
例如,假设有如下两个方法:
int Mul(int x ,int y) { return x*y; } int Div(int x ,int y) { return x/y; }
则使用上一例的Calculate委托来引用它们的语句可写成:
Calculate a = new Calculate(Mul); Calculate b = new Calculate(Div);
其中,a和b为委托型的对象。
由于实例化委托实际上是创建了一个对象,所以委托对象可以参与赋值运算,甚至作为方法参数进行传递。
例如,委托对象a和b分别引用的方法是Mul和Div,如果要交换二者所引用的方法,则可执行以下语句:
Calculate temp = a; a =b; b=temp;
3、使用委托
在实例化委托之后,就可以通过委托对象调用它所引用的方法。在使用委托对象调用所引用的方法时,必须保证参数的类型、个数、顺序和方法声明匹配。
例如:
Calculate calc = new Calculate(Muil); int result =calc(3,7);
就表示通过Calculate型的委托对象calc来调用方法Mul,实参为3和7,因此最终返回并赋给变量result的值为21。
4、使用匿名方法
从C#2.0开始,C#就引入了匿名方法的概念,它允许将代码块作为参数传递,以避免单独定义方法。使用匿名方法创建委托对象的一般形式如下:
委托类型 委托变量名 = delegeate([参数列表]){代码块};
例如:
Calculate calc = delegate(int x,int y){return (int)Math.Pow(x,y);};
就表示用匿名方法定义了一个Calculate型的委托对象calc,用来计算x的y次方值。
【实例 8-1】 创建一个Windows程序,利用委托求两个数的加、减、乘、除的结果,效果如图8-2所示:
图 8-2 运行效果
在本例中,首先声明了委托类型Calculate,然后定义了委托型的字段变量handler,之后创建委托对象同时封装Add方法,再通过handler来调用该方法,得到两数之和。接下来以同样的方式调用Sub、Mul和Div方法,以计算两数相减、相乘、相除的值。
在实例8_1中,每次委托调用都只是调用一个指定的方法,这种只引用一个方法的委托称为单路广播委托。实际上,C#允许使用一个委托对象来同时调用多个方法,当委托添加更多的指向其他方法的引用时,这些引用将被存储在委托的调用列表中,这种委托就是多路广播委托。
C#的所有委托都是隐式的多路广播委托。向一个委托的调用列表添加多个方法引用,可通过该委托一次性调用所有的方法,这一过程称为多路广播。
实现多路广播的方法有以下两种。
(1)通过“+”运算符直接将两个同类型的委托对象组合起来。
例如,
Calculate a = new Calculate(Mul); Calculate b = new Calculate(Div); a = a+ b;
这样,通过委托对象a就可以同时调用Mul和Div了。
(2)通过“+=”运算符将新创建的委托对象添加到委托调用列表中。另外,还可以使用“-=”运算符来移除调用列表中的委托对象。
例如,
Calculate a = new Calculate(Mul); a + = new Calculate(Div);
这样,Mul和Div方法都列入了委托对象a的调用列表。
注意,由于一个委托对象只能返回一个值且只返回调用列表中最后一个方法的返回值,因此为避免混淆,建议在使用多路广播时每个方法均用void定义。
【实例 8-2】 利用多路广播机制,修改实例8-1的代码。
(1)将实例8-1的委托封装改为如下代码:
private void button1_Click(object sender, EventArgs e) { int a = Convert.ToInt32(textBox1.Text); int b = Convert.ToInt32(textBox2.Text); handler = new Calculate(Add); //创建委托对象同时封装方法 handler += new Calculate(Sub); handler += new Calculate(Mul); handler += new Calculate(Div); lblShow.Text = handler(a, b).ToString(); //通过委托对象调用方法 }
在该程序中,连续使用“+=”运算符将每次创建的委托对象添加到委托调用列表中,handler字段最终保存的是Add、Sub、Mul和Div的引用。当最后执行“handler(a,b);”语句时,将按先后顺序执行这4个方法,但委托对象只能返回最后一个委托方法的返回值,所以当输入8和4时,只能返回方法Div的运行结果2。
(2)改进Add、Sub、Mul和Div方法,使其返回值为void。主要代码如下:
public delegate void Calculate(int x, int y); //声明委托 public Calculate handler; //定义委托型的字段 public void Add(int x,int y) { lblShow.Text = (x+y).ToString(); } //定义相关方法 public void Mul(int x,int y) { lblShow.Text += "\n\n"+(x * y).ToString(); } public void Sub(int x,int y) { lblShow.Text += "\n\n" + (x - y).ToString(); } public void Div(int x,int y) { lblShow.Text += "\n\n" + (x / y).ToString(); } private void button1_Click(object sender, EventArgs e) { int a = Convert.ToInt32(textBox1.Text); int b = Convert.ToInt32(textBox2.Text); handler = new Calculate(Add); //创建委托对象同时封装方法 handler += new Calculate(Sub); handler += new Calculate(Mul); handler += new Calculate(Div); handler(a, b); //通过委托对象调用方法 }
在该程序中,修改Add、Sub、Mul和Div的方法的实现并使其返回值为void,同时相应地修改委托地声明,handler使用多路广播引用4个方法,执行“handler(a,b);”语句时,将按先后顺序执行这4个方法,通过程序地运行效果与实例8-1的运行效果相同。
8.3 事件
触发事件的对象称为发布者,提供事件处理程序的对象称为订阅者,基于事件驱动模型的程序使用委托来绑定事件和事件方法。C#允许使用标准的EvenHandler委托来声明标准事件,也允许先自定义委托,再声明自定义事件。本节将详细介绍相关内容。
8.3.1 事件的声明
EventHandler是一个预定义的委托,它定义了一个无返回值的方法。在.NETFramework中,它的定义格式如下:
public delegate void EventHandler(object sender,EventArgs e)
其中,第一个参数sender,类型为Object,表示事件发布者本身。第二个参数e,用来传递事件的相关数据信息,数据类型为EventArgs及其派生类。
实际上,标准的EventArgs并不包含任何事件数据,因此EventHandler专用于表示不生成数据的事件方法。如果事件要生成数据,则必须提供自定义事件数据类型,该类型从EventArgs派生,提供保存事件数据所需的全部字段或属性,这样发布者可以将特定的数据发送给接收者。
用标准的EventHandler委托可声明不包含数据的标准事件,一般形式如下:
public event EventHandler 事件名;
其中,事件名通常使用on作为前缀符。
例如,
public event EventHandler onClick;
就表示定义了一个名为onClick的事件。
要想生成包含数据的事件,必须先自定义事件数据类型,然后再声明事件。具体实现方法有以下两种。
(1)先自定义委托,再定义事件,一般形式如下:
public class 事件数据类型:EventArgs{//封装数据信息}
public delegate 返回值类型 委托类型名(Object sender,事件数据类型 e);
public event 委托类型名 事件名;
例如,在Windows窗口中有一张图片,如果希望把鼠标指针单击其中某个位置的数据信息传递给单击事件方法,则可使用以下代码声明该事件:
public class ImageEventArgs:EventArgs { public int x; public int y; } public delegate void ImageEventHandler(Object sender,ImageEventArgs); public event ImageEventHandler onClick;
(2)使用泛型EventHandler定义事件,一般形式如下:
public class 事件数据类型:EventArgs{//封装数据信息}
public event EventHandler<事件数据类型>事件名
例如,在高温预警系统中,一般是根据温度值确定预警等级,这可采用事件驱动模型进行程序设计,其基本思想如下:当温度变化时,触发温度预警事件,系统接收到事件消息后启动事件处理程序,根据温度的高低,确定预警等级。为此,需要设计一个TemperatureEventArgs类,它在温度预警事件触发时封装并传递温度信息,代码如下。
//定义事件相关信息类 class TemperatureEventArgs : EventArgs { int temperature; public TemperatureEventArgs(int temperature) //声明构造函数 { this.temperature = temperature; } public int Temperature //定义只读属性 { get { return temperature; } } }
另外,需要定义一个TemperatureWarning类,在该类中先声明了一个温度预警的委托类型TemperatureHandler,再用该委托类型声明一个温度预警事件OnWarning,代码如下:
class TemperatureWarning { //声明温度预警的委托类型 public delegate void TemperatureHandler(object sender, TemperatureEventArgs e); //声明温度预警事件 public event TemperatureHandler OnWarning; //... }
也可以使用泛型Event Handler定义温度预警事件OnWarning,注意,使用泛型EventHandler必须指出事件数据类型。代码如下:
class TemperatureWarning { //声明温度预警的委托类型 public delegate void TemperatureHandler(object sender, TemperatureEventArgs e); //声明温度预警事件 //public event TemperatureHandler OnWarning; public event EventHandler<TemperatureEventArgs> OnWarning; //... }
8.3.2 订阅事件
声明事件的实质只是定义了一个委托型的变量,并不意味着就能成功触发事件,还需要完成如下工作:1、在事件的接收者中定义一个方法来影响这个事件;2、通过创建委托对象把事件与事件方法联系起来(又称绑定事件,或订阅事件)。负责绑定事件与事件方法的类就称为事件的订阅者。
订阅事件的一般形式如下:
事件名+= new 事件委托类名(事件方法);
例如,要想对温度的变化情况进行预警,可先创建一个tw_OnWarning方法,该方法根据温度高低,进行预警,然后把该方法和事件OnWarning绑定起来即可。这样,当温度预警事件触发时,该方法将被自动调用。绑定OnWarning事件代码如下:
TemperatureWarning tw = new TemperatureWarning(); tw.OnWarning + = new TemperatureWarning.TemperatureHandler(tw_OnWarning);//订阅事件
如果使用泛型EventHandler定义的事件,则使用如下代码:
tw.OnWarning+=new EventHandler<TemperatureEventArgs>(tw_OnWarning); //订阅事件
其中,“+=”运算符把新创建的引用tw_OnWarning方法的委托对象与OnWarning事件绑定起来,也就完成了TemperatureWarning类的OnWarning事件的订阅操作。
事件触发时,调用的tw_OnWarning方法签名如下:
private void tw_OnWarning(object sender,TemperatureEventArgs e);
【注意】
在订阅事件时要注意以下几点。
(1)订阅事件的操作由事件接收者类实现。
(2)每个事件可有多个处理程序,多个处理程序按顺序调用。如果一个处理程序引发异常,还未调用的处理程序则没有机会接收事件。为此,建议事件处理程序迅速处理事件并避免引发异常。
(3)订阅事件时,必须创建一个与事件具有相同类型的委托对象,把事件方法作为委托目标,使用+=运算符把事件方法添加到源对象的事件之中。
(4)若要取消订阅事件,可以使用-=运算符从源对象的事件中移除事件方法的委托。
8.3.3 触发事件
在完成事件的声明与订阅之后,就可以引用事件了。引用事件又称触发事件或点火,而负责触发事件的类就称为事件的发布者。C#程序中,触发事件与委托调用相同,但要注意使用匹配的事件参数。事件一旦触发,即将调用相应的事件方法。如果该事件没有任何处理程序,则该事件为空。
因此在触发事件之前,事件源应确保该事件不为空,以避免NullReferenceException异常,每个事件都可以分配多个事件方法。在这种情况下,每个事件方法将被自动调用,且只能被调用一次。
例如,每当温度变化时,就会触发温度预警事件OnWarning,从而调用tw_OnWarning方法进行温度预警。为此,可在TemperatureWarning类中声明一个开始监气温的方法Monitor来触发温度预警事件。当然,在触发事件之前,必须提前把温度信息封装为TemperatureEventArgs事件参数,其源代码如下:
class TemperatureWarning { //声明温度预警事件 public event EventHandler<TemperatureEventArgs>OnWarning; //开始监控气温,同时发布事件 public void Monitor(int tp) { TemperatureEventArgs e = new TemperatureEventArgs(tp); if(OnWarning !=null) { OnWarning(this,e); } } }
【实例 8-3】创建一个Windows程序,利用事件驱动模型来解决温度预警问题,运行效果如图8-3所示。
图 8-3 运行效果

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第八章 { public partial class Test8_3 : Form { Random r = new Random(); //产生一个随机数生成器 TemperatureWarning tw = new TemperatureWarning(); public Test8_3() { InitializeComponent(); //第四步:订阅事件 tw.OnWarning += new TemperatureWarning.TemperatureHandler(tw_OnWarning); } private void btnMonitor_Click(object sender, EventArgs e) { timer1.Enabled = true; //开始每1秒钟改变一次温度 } //第三步:声明事件产生时调用的方法 private void tw_OnWarning(object sender, TemperatureEventArgs e) { if (e.Temperature < 35) { lblShow.Text = "正常"; lblColor.BackColor = Color.Blue; } else if (e.Temperature < 37) { lblShow.Text = "高温黄色预警!"; lblColor.BackColor = Color.Yellow; } else if (e.Temperature < 40) { lblShow.Text = "高温橙色预警!"; lblColor.BackColor = Color.Orange; } else { lblShow.Text = "高温红色预警!"; lblColor.BackColor = Color.Red; } } //每隔1秒钟激发一次该方法,用来模拟温度值的改变 private void timer1_Tick_1(object sender, EventArgs e) { int nowTemp; //原来的温度值 if (txtTemp.Text == "") nowTemp = 35; else nowTemp = Convert.ToInt32(txtTemp.Text); int change = r.Next(-2, 3); //产生一个在-2~2之间的随机数 txtTemp.Text = (change + nowTemp).ToString(); //新的温度值 //第五步:触发事件 tw.Monitor(change + nowTemp); } } //第一步:定义事件相关信息类 class TemperatureEventArgs : EventArgs { int temperature; public TemperatureEventArgs(int temperature) //声明构造函数 { this.temperature = temperature; } public int Temperature //定义只读属性 { get { return temperature; } } } //第二步:定义事件 class TemperatureWarning { //2.1声明温度预警的委托类型 public delegate void TemperatureHandler(object sender, TemperatureEventArgs e); //2.2声明温度预警事件 public event TemperatureHandler OnWarning; //public event EventHandler<TemperatureEventArgs> OnWarning; //2.3开始监控气温,同时发布事件 public void Monitor(int tp) { TemperatureEventArgs e = new TemperatureEventArgs(tp); if(OnWarning !=null) { OnWarning(this, e); } } } }
其中,Random类是伪随机数生成类,该类的Next(minValue,maxValue)方法可以产生一个大于等于minValue并小于maxValue的随机整数。
Timer控件是一个计时器控件,可以周期性地产生一个Tick事件,可以用该控件周期性地执行某些操作。当Timer控件的Enable属性设置为true时可以启用该控件,设置为false时关闭计时。Interval属性是Timer控件的激发间隔,单位是毫秒。另外,控件事件所关联的方法只有订阅后才能生效,方法之一是双击该控件,在产生的方法中输入代码,才能够执行相应的方法。所以,在这里双击Timer控件后,在产生的方法private void timer1_Tick(object sender, EventArgs e)中,写入代码,可以模拟温度的变化。
从上例中可以看到,采用基于事件驱动模型进行程序设计,其实现步骤包括以下5个。
(1)定义事件相关信息类。
(2)在事件发布者类(事件源)中声明事件,并声明一个负责触发事件的方法。
(3)在事件接收者类中声明事件产生时调用的方法。
(4)在事件接收者中订阅事件。
(5)在事件接收者类中触发事件。
8.4 基于事件的Windows编程
目前,Windows操作系统是计算机主流操作系统,而Windows操作系统的灵魂是基于事件的消息运行机制。因此,无论哪一种语言开发工具都必须接受Windows的运行机制。本节将详细介绍C#语言的基于事件的Windows编程方法。
8.4.1 Windows应用程序概述
1、Windows应用程序的工作机制
Windows操作系统提供了两种事件模型,即“拉”模型和“推”模型。在“推”模型中,Windows应用程序首先指示对哪些条件感兴趣,然后等待事件发生,一旦接收到事件消息就执行事件处理程序,在“拉”模型中,系统必须不停地轮询或监测资源或条件,以决定是否触发事件并执行事件处理程序。因此,“推”模型是被动地等待事件地发生,而“拉”模型是主动地问询事件是否发生。
事实上,Windows操作系统本身就使用“拉”模型运行机制。它为每一个正在运行地应用程序建立消息队列。在事件发生时,它并不是将这个触发事件直接传送给应用程序,而是先将其翻译成一个Windows消息,再把这个消息加入消息队列中。应用程序通过消息循环从消息队列中接收消息,执行相应地事件处理程序。
在整个Windows应用系统中,生成事件地应用程序被称为事件源,接收通知或检查条件地应用程序被称为事件接收器。事件源和事件接收器也可以位于同一个应用程序。在Windows系统中,事件接收器采用以下几种事件处理机制。
1)轮询机制
在这种机制下,事件接收器定期询问事件源是否有它感兴趣地事件发生。这样,虽然可以获得事件,能解决问题,但是有以下两个弊端。
(1)事件接收器不知道它所感兴趣地事件什么时候发生,所以必须频繁地访问事件源,以便第一时间内获得事件。通常事件的发生频率要比轮询的频率小得多,所以大部分资源都做了无用功,并且事件源每次也要响应询问,大大浪费了资源,降低了效率。
(2)针对第一种情况,如果开发人员降低轮询频率,以增加效率和减少系统的负荷,那么新的问题就来了,随着访问频率的降低,事件发生的时间和事件接收器得知的时间将会越来越长。显然,这是很难让人接受的。
2)回调函数机制
回调函数是最原始但很有效的机制。在这个机制里,事件源定义回调函数的模板(又称原型),事件接收器实现该函数的实际功能,并让事件源中的回调函数指针指向自己的实际函数。当事件源中的事件发生时,就调用回调函数的指针,这样事件接收器就最先得到了通知并进行处理。
3)Microsoft.NET Framework事件机制
.NET Framework基于委托事件模型是以回调函数机制为基础的。只是用委托代替了函数指针,这样就降低了编程的难度,而且委托是类型安全的。在运行期间,事件接收器实例化一个委托对象并把它传递给事件源。
2、Windows应用程序项目的组织结构
在VS 2019中,一旦创建一个Windows应用程序项目,即可在解决方案资源管理器中看到如图8-4所示的组织结构。
图 8-4 Windows应用程序项目的组织结构
事实上,无论采用哪一种事件处理机制,Windows应用程序和控制台应用程序一样,必须从Main方法开始执行。在创建Windows应用程序时,VS 2019会自动生成Program.cs文件,并在该文件中会自动生成Main方法,也会根据程序设计员的操作自动更新Main方法中的语句。因此,程序设计员通常不需要在Main方法中添加任何代码。
以下是Program.cs文件的典型结构。
using System; using System.Collections.Generic; using System.Windows.Forms; namespace test8_3 { static class Program { static void Main() { Application.EnableVisualStyles(); //启用程序的可视样式 Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); /创建Windows 窗体对象并显示 //之后开始消息循环 } } }
在上述代码中,Main函数有三条语句,前两个语句主要与程序的外观显示相关,不影响程序的执行流程,只有Application.Run函数起到关键作用,它将创建一个Windows窗体对象并显示,之后开始一个标准的消息循环,以便整个程序保持在运行状态而不结束。如果将第三句改为如下的句子:
MainForm frm = new MainForm(); frm.Show();
运行时就会发现,窗体显示一下后立即消失,程序也运行结束。
由此可见,整个程序能够保持运行而不结束,主要是由于Application.Run的作用,Application.Run在当前线程上开始一个标准的消息循环,从而使得窗体能够保持运行。
8.4.2 Windows窗体概述
1、Windows窗体概述
Windows窗体体现了.NET Framework的智能客户端技术。智能客户端是指易于部署和更新的图像丰富的应用程序,无论是否连接到Internet,智能客户端都可以工作,并且比传统的基于Windows的应用程序更安全的方式访问本地计算机上的资源。在使用类似VS 2019的开发环境时,可以创建Windows窗体智能客户端应用程序,以显示信息、请求用户输入以及通过网络与远程计算机通信。
一个Windows应用程序是由若干个Windows窗体组成的,从用户的角度来讲,窗体是显示信息的图形界面,从程序的角度来讲,窗体是System.Windows.Forms命名空间中Form类的派生类。通常,一个窗体包含各种控件,如标签、文本框、按钮、下拉框、单选按钮等。控件是相对独立的用户界面元素,它既能显示数据或接收数据输入,又能响应用户操作(如单击鼠标或按下按键)。
例如,在实例8-3中,该Windows应用程序由一个窗体组成,该窗体的类名是Test8_3,是基类Form的派生类。在该窗体中,一共有6个控件,包括3个标签、1个文本框、1个按钮和1个定时器。其中,文本框接收用户所输入的数据,按钮负责响应用户单击鼠标操作,定时器负责在规定的间隔时间中激发事件。当用户单击按钮时,系统将触发一个事件消息,并调用相应的事件方法(如btnAdd_Click)。
在设计时,Windows窗体有两种视图模式:包括设计器视图(如图8-5所示)和源代码编辑视图(如图8-6所示)。设计器视图支持以拖拽方式从工具箱往Windows窗体添加控件,源代码编辑视图支持智能感知技术,快速输入源代码。
图 8-5 窗体设计器窗口 图 8-6 窗体代码编辑窗口
在Windows窗体的源代码中,窗体类名之前带partial关键字。VS 2019使用该关键字将同一个窗体的代码分离存放在两个文件中,一个文件存放由它自动生成的代码,文件的后缀名一般为xxx.Designer.cs;另一个存放程序员自己编写的代码,后缀名一般为xxx.cs。
其中,xxx.Desinger.cs的代码结构如下所示。

namespace 第八章 { partial class Test8_3 { /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// Clean up any resources being used. /// </summary> /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.label1 = new System.Windows.Forms.Label(); this.txtTemp = new System.Windows.Forms.TextBox(); this.btnMonitor = new System.Windows.Forms.Button(); this.lblColor = new System.Windows.Forms.Label(); this.lblShow = new System.Windows.Forms.Label(); this.timer1 = new System.Windows.Forms.Timer(this.components); this.SuspendLayout(); // // label1 // this.label1.AutoSize = true; this.label1.Location = new System.Drawing.Point(70, 89); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(52, 15); this.label1.TabIndex = 0; this.label1.Text = "温度:"; // // txtTemp // this.txtTemp.Location = new System.Drawing.Point(129, 78); this.txtTemp.Name = "txtTemp"; this.txtTemp.Size = new System.Drawing.Size(286, 25); this.txtTemp.TabIndex = 1; // // btnMonitor // this.btnMonitor.Location = new System.Drawing.Point(422, 78); this.btnMonitor.Name = "btnMonitor"; this.btnMonitor.Size = new System.Drawing.Size(87, 30); this.btnMonitor.TabIndex = 2; this.btnMonitor.Text = "监控"; this.btnMonitor.UseVisualStyleBackColor = true; this.btnMonitor.Click += new System.EventHandler(this.btnMonitor_Click); // // lblColor // this.lblColor.AutoSize = true; this.lblColor.Location = new System.Drawing.Point(168, 246); this.lblColor.Name = "lblColor"; this.lblColor.Size = new System.Drawing.Size(175, 15); this.lblColor.TabIndex = 3; this.lblColor.Text = " "; // // lblShow // this.lblShow.AutoSize = true; this.lblShow.Location = new System.Drawing.Point(168, 194); this.lblShow.Name = "lblShow"; this.lblShow.Size = new System.Drawing.Size(0, 15); this.lblShow.TabIndex = 4; // // timer1 // this.timer1.Interval = 1000; this.timer1.Tick += new System.EventHandler(this.timer1_Tick_1); // // Test8_3 // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(800, 450); this.Controls.Add(this.lblShow); this.Controls.Add(this.lblColor); this.Controls.Add(this.btnMonitor); this.Controls.Add(this.txtTemp); this.Controls.Add(this.label1); this.Name = "Test8_3"; this.Text = "Test8_3"; this.ResumeLayout(false); this.PerformLayout(); } #endregion private System.Windows.Forms.Label label1; private System.Windows.Forms.TextBox txtTemp; private System.Windows.Forms.Button btnMonitor; private System.Windows.Forms.Label lblColor; private System.Windows.Forms.Label lblShow; private System.Windows.Forms.Timer timer1; } }
Windows窗体的两个代码文件在编译时将自动合并。代码分离的好处是程序员不必关心 VS 2019自动生成的那些代码,操作更加简洁方便。
2、Windows窗体中的事件
Windows应用程序在运行时,用户针对窗体或某个控件进行的任何键盘或鼠标操作,都会触发Windows系统的预定义事件,这些事件是多种多样的,往往因控件类型而异。
例如,按钮提供Click事件,文本框提供TextChanged事件,单选按钮或复选框提供CheckedChanged事件,组合框提供SelectedIndexChanged事件等。
当然,大多数的控件可能也拥有相同的事件。表8-3列出了Windows应用程序常用的事件。
事件 | 描述 | 事件 | 描述 |
Activated | 使用代码激活或用户激活窗体时发生 | TextChanged | Text属性值更改时发生 |
Deactivated | 窗体失去焦点并不再是活动窗体时发生 | Enter | 当控件成为活动控件时发生 |
Load | 用户加载窗体时发生 | Leave | 当控件不再是活动控件时发生 |
FormClosing | 关闭窗体时发生 | CheckedChanged | Checked属性值更改时发生 |
FormClosed | 关闭窗体后发生 | SelectedIndexChanged | SelectedIndex属性值更改时发生 |
Click | 单击控件时发生 | Paint | 控件需要重新绘制时发生 |
DoubleClick | 双击控件时发生 | KeyPress | 按下并释放某键后发生 |
MouseDown | 按下鼠标时发生 | KeyDown | 首次按下某个键时发生 |
MouseEnter | 鼠标进入控件的可见部分时发生 | KeyUp | 释放某个键时发生 |
MouseOver | 鼠标指针移过控件时发生 | SizeChanged | 控件的大小改变时发生 |
MouseUp | 释放鼠标按键时发生 | BackColorChanged | 背景色更改时发生 |
3、事件方法
从表8-3可知,Windows窗体及其控件事件非常多,设计程序时是不是需要为每一个事件编写相应的事件方法呢?当然,是完全没必要的,通常根据需要只编写其中几个事件方法。事件方法的基本格式一般为:
private void 事件方法名(object sender,EventArgs e) { //事件处理语句 }
其中,事件方法名一般按行业规范命名,C#建议使用“控件名_事件名”的命名格式。形参sender代表事件的发布者,常常是控制自身。形参e为事件参数对象,它包含事件发布者要传递给事件接收者的详细数据。
4、事件方法与窗体或控件的绑定
Windows窗体中的事件从代码的角度来看实质上Form类或控件类的一个属性,其数据类型通常是EventHandler。由于触发事件的实质是调用该委托所引用的事件方法,因此为了保证事件能够成功触发、完成事件处理,就必须将事件方法与表示Form类或控件类的事件属性联系起来。把事件方法与事件属性联系的操作称为事件绑定。
在设计Windows窗体时,因为已经确定了一个窗体所包含的所有构成元素(即控件),因此可以直接把一个事件方法与窗体或控件的事件属性绑定。此时,可利用VS 2019自动生成事件和自动进行事件绑定的功能来实现,具体操作方法如下。
(1)首先切换到VS 2019窗体设计视图;
(2)把控件从工具箱拖放到窗体设计视图;
(3)右击目标控件(如一个按钮控件和一个文本框)并选择“属性”命令,以打开控件的“属性”窗口;
(4)在“属性”窗口中单击按钮,以打开事件属性列表;
(5)在事件属性列表中双击事件名(如双击Click事件);
(6)之后,VS 2019自动生成相应的事件方法,并自动把该事件方法与控件的相应事件绑定起来。
注意,刚生成的事件方法是不包含任何语句的空方法,需要自行完成代码的编写。
【实例 8-4】设计一个简单的Windows应用程序,实现以下功能:文本框默认显示提示文字“在此,请输入任意文字!”;进入该文本框时自动清除提示文字;之后由用户输入字符,每输入一个字符就在标签控件中显示一个字符;离开该文本框时显示“输入结束,您输入的文字是:”并显示所输入的文字,同时,文本框再次显示“在此,请输入任意文字!”。运行效果如图8-7所示。
图 8-7 运行效果
【分析】控件事件的绑定的实质是利用事件方法构造一个EventHandler事件委托的对象,并将这个对象赋值给控件的事件属性。
该赋值语句的基本格式为:
控件名.事件+= new EventHandler(事件方法);
例如,本实例中绑定文本框控件txtSource的TextChanged事件的语句如下:
txtSource.TextChanged += new EventHandler(txtSource_TextChanged);
其实,通过事件属性列表绑定的事件,VS 2019也会为其自动生成同样的代码,在完成上述操作后,在VS 2019的解决方案资源管理器中打开窗体的设计文件(如果窗体的源代码文件为Form1.cs,则其设计文件为Form1.Designer.cs),就可以发现Enter和Leave事件的绑定语句如下:
this.txtSource.Enter += new System.EventHandler(this.txtSource_Enter); this.txtSource.Leave += new System.EventHandler(this.txtSource_Leave);
【课后实例】
(1)设计一个Windows应用程序,随机生成0~100之间的10个数字,并通过委托实现升序或降序排列,效果如图8-9所示。
图8-9 运行效果

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第八章 { public partial class 课后实例1 : Form { public 课后实例1() { InitializeComponent(); } int[] a = new int[10]; delegate bool Compare(int x, int y); //声明委托类型 void SortArry(Compare compare) //委托形参 { for (int i = 0; i < a.Length; i++) for (int j = 0; j <= i; j++) if (compare(a[i], a[j])) //使用委托调用方法,以比较大小 { int t = a[i]; a[i] = a[j]; a[j] = t; } } bool Ascending(int x,int y) //比较x是否小于y { return x < y; } bool Desecding(int x,int y) //比较x是否大于y { return x > y; } void display() //输出数组 { txtTarget.Text = ""; foreach(int i in a) { txtTarget.Text += i + "\r\n"; } } //降序排列 private void btnDescSort_Click(object sender, EventArgs e) { SortArry(new Compare(Desecding)); //实参是新创建的委托对象 display(); } //升序排列 private void btnAscSort_Click(object sender, EventArgs e) { SortArry(new Compare(Ascending)); //实参是新创建的委托对象 display(); } //生成数组 private void btnCreateArray_Click(object sender, EventArgs e) { txtSource.Text = ""; txtTarget.Text = ""; Random r = new Random(); for(int i = 0; i < a.Length; i++) { a[i] = r.Next(100); //取0~100间的随机数 txtSource.Text += a[i] + "\r\n"; } } } }
(2)设计一个Windows应用程序,模拟高温高压炉降压处理,运行效果如图8-10所示。
图 8-10 运行效果

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第八章 { public partial class Test8_5 : Form { public class BoilerArgs : EventArgs //锅炉数据参数类 { private int pressure; //锅炉压强 public BoilerArgs(int n) { pressure = n; } public int Press { get { return pressure; } } } public class Boiler //锅炉类 { public int k; //锅炉压强 public Boiler() { k = 0; } public EventHandler<BoilerArgs> onAlarm; //1.定义锅炉警报事件 public void ProcessAlarm() //2.触发警报事件 { this.onAlarm(this, new BoilerArgs(k)); } } private Boiler boiler; public Test8_5() { InitializeComponent(); boiler = new Boiler(); if (boiler.onAlarm == null) //3.订阅事件 boiler.onAlarm += new EventHandler<BoilerArgs>(boiler_Alarm); } private void boiler_Alarm(object sender,BoilerArgs e) //4.声明警报事件方法 { if (e.Press > 50 && e.Press < 80) { lblShow.Text = "黄色警告!"; lblShow.BackColor = Color.Yellow; } else if (e.Press >= 80 && e.Press < 90) { lblShow.Text = "橙色警告!"; lblShow.BackColor = Color.Orange; } else if (e.Press >= 90 && e.Press < 100) { lblShow.Text = "红色警告!"; lblShow.BackColor = Color.Red; } else if (e.Press == 100) { lblShow.Text = "已经降压!... ..."; lblShow.BackColor = SystemColors.Control; txtPressure.Text = "30"; boiler.k = 30; } } private void btnPressure_Click(object sender, EventArgs e) { autoTimer.Start(); } private void btnManual_Click(object sender, EventArgs e) { if (Convert.ToInt32(txtPressure.Text) > 30) { lblShow.Text = "已经降压!... ..."; lblShow.BackColor = SystemColors.Control; txtPressure.Text = "30"; boiler.k = 30; } else { lblShow.Text = "无需降压!... ..."; } } private void autoTimer_Tick(object sender, EventArgs e) { boiler.k++; txtPressure.Text = boiler.k.ToString(); boiler.ProcessAlarm(); //5.发布新事件 } } }