从WinForm程序中看委托和事件

作为一个自学C#的小白,无论我们的学习起点是各种书籍还是视频,最开始总是从控制台程序和窗体应用程序,一行简单的Console.WriteLine("Hello World");或者是一个窗体几个控件就能实现一个小程序。作者本意或许是想告诉初学者们,编程并不难,并且很有趣。消除学生的畏难情绪,培养学习兴趣。但是,这会不会在学生心中留下一个印象,编程不过如此?从此很长一段时间内,编程水平只是停留在拖控件。我们所不知道的是,这种简单背后,是功能强大的visual studio和C#语言规范在支撑。在我学习委托和事件时,在博客园,编程书,视频网站各种渠道去找资料,却总是一知半解。并不是这些资料讲解的不到位,相反,每篇文章都会在某一点上对我有新的启发,如果没有这些资料,今天我也写不出这篇文章,只是在看资料之后,之前的我没有真正的去思考,更准确一点说,是不知道如何去思考。

这篇文章不敢说写的多好,存在错误也绝非我所愿,这并不是一句谦辞,实在是我目前编程水平有限,也欢迎看到这篇文章的朋友能够指出其中错误。我愿意把它当成一个起点,从这里开始,一步步去积累提高自己。

 

就从窗体应用程序说起吧

在创建项目时,vs自动生成的Form1类,继承了Form类。在主程序program.cs中,创建了Form1类的实例,并调用Application.Run(new Form1())来运行。这里有两点需要注意:

1、 Form1类就是一个普通类,和我们自己后面添加的类文件没有什么不同。

2、  Form1类的修饰符partial,这个类有两部分组成:第一部分是开发人员自己编写的程序代码,存放在Form1.cs文件,我们在vs中选中窗体按F7显示的那些代码。第二部分是Form1.Designer.cs文件,这里的代码是vs自动生成的,都是和Form1窗体中的控件有关的,可以认为该文件负责管理窗体中的所有控件。

 

先来看Form1.Designer.cs(后面用Form1类代替,知道这些代码是写在该文件中即可)

其中包含两个方法,Dispose()和InitializeComponent(),还有一些私有字段,如panel1,button1。Dispose()是在Form1类中实现父类Form类中的虚方法,释放Form1类所占用的资源,不用多说。从InitializeComponent()中,可以知道当我们在vs中向一个窗体拖动一个控件时,究竟发生了什么?

例如,向一个空窗体中添加一个button按钮,这是每个学习winform编程的人第一节课必备操作,此时vs的操作是:

1、  在Form1类中声明一个私有字段 button1

private System.Windows.Forms.Button button1;

button1是System.Windows.Forms.Button类型的变量,转到Button类的定义,可以看到该类继承了System.Windows.Forms.ButtonBase和System.Windows.Forms.IButtonControl,而System.Windows.Forms.ButtonBase继承了System.Windows.Forms.Control类。到此为止,我们等下再去仔细研究Control类的内容。

【拓展:和Button类似,Winform中其他控件类也都是这种方式实现的。如果想自己设计一款控件,就可以按照这种方式来实现。先设计一个MyComponent类,让它继承自Control类,然后在MyComponent类中实现该控件的特有功能】

2、  vs做的第二件事,是在InitializeComponent()方法中对button1变量进行了实例化:

this.button1 = new System.Windows.Forms.Button();

我们转到Button类、ButtonBase类、Control类的定义中可以看到,其中有许多属性

3、 vs做的第三件事就是对button1对象的常用属性进行了初始化(这里我截取一部分)

//

            // button1

            //

            this.button1.BackColor = System.Drawing.Color.ForestGreen;

            this.button1.Font = new System.Drawing.Font("Microsoft YaHei", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));

            this.button1.Location = new System.Drawing.Point(20, 273);

如果我们后面在设计器中对控件的其他属性做了修改,vs也会将代码添加到这里。

到此为止,在我们没有对button1控件做任何操作之前,vs为我们做了以上三件事,来简化编程流程,让我们更专注于Form1.cs文件中代码的开发。

 

按钮控件最常用的功能就是点击Click,下面来看下这个过程是怎么实现的。

在设计器中双击该控件(button控件的默认事件就是click),vs在Form1.cs中会自动生成并跳转到button1_Click方法,我们在这个方法体内部实现button1按钮被点击时程序要执行的操作。

private void button1_Click(object sender, EventArgs e)

{

//

}

可以看到,该方法有两个参数,第一个是object类型,sender;第二个是System.EventArgs类型,e。这两个参数是什么意思,会留到很后面再说,在这里简单提一下。

 

同时,vs在Form1.Designer.cs中,会生成如下代码:

this.button1.Click += new System.EventHandler(this.button1_Click);     (1)

这行代码什么意思?它跟我们点击按钮要实现的功能有什么关系?

举个例子,你用美团app定了一份水饺,+=符号就是“订”这个动作。+=左边就表示水饺,而右边明确指出这是一份牛肉水饺。(简单理解)

+=:称为委托操作符,字面意思可理解为“订阅”。

在它的左边是this.button1.Click是一个“事件Event”,查看其定义可以发现,这是一个声明在Control类中的东西(在不知道它到底是啥之前,暂且称之为东西):

public event EventHandler Click;                                           (2)

这东西既不是属性,也不是字段,更不是方法,看起来更像变量声明。从声明中可以看出,有一个event关键字。这个东西是EventHandler类型的。

我们从EventHandler类型入手,在vs中F12转到EventHandler类型的定义,可以看到以下内容:

public delegate void EventHandler(object sender, EventArgs e);         (3)

很明显,这又是一个声明,看起来和方法声明差不多,多了一个delegate关键字,这个单词有“授权,委托”的意思,当我们再想看看delegate是什么东西的时候,vs出现了“无法导航到插入符号下的符号”弹窗。

 

到这里,我们既没有搞明白+=左边的this.button1.Click到底是什么?它跟Control类中的Click有什么关系?而且还多了一个delegate关键字和event关键字不知道是干嘛的。

 

这个时候把程序编译下,看源码。需要弄明白

public delegate void EventHandler(object sender, EventArgs e);

这句代码到底做了什么。这句代码的字面意思是声明一个名为EventHandler的委托,显然,delegate是声明委托的关键字。

从下面这张图可以看出,当声明GreetingDelegate委托时,编译器自动声明一个密封类,类名就是GreetingDelegate。所以声明委托就是声明类,委托本质上就是一个类。

类中有一个构造函数和BeginInvoke、EndInvoke、Invoke方法。

 

                 ——图片引用自https://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html,作者是《.NET之美》一书的作者,有兴趣的朋友可以看下。

 

我们知道,String类表示字符串,Int32类表示32位有符号整数。既然委托就是类,那EventHandler类表示什么?

“为了增强灵活性和减少重复代码,可以将方法作为参数传递给另一方法。为了能将方法作为参数传递,必须要有一个能表示方法的数据类型。这个数据类型就是委托”

                                                                                                                                       ——《C#本质论第六版》

现在我们知道,EventHandler类表示方法。C#中方法成千上万,都用一个类肯定不能表示。所以一个委托只能表示一类方法。哪一类方法呢?

看一下最开始vs自动生成的button1_Click方法:

private void button1_Click(object sender, EventArgs e)

委托声明:           

public delegate void EventHandler(object sender, EventArgs e)

二者相同点有返回值,参数类型和参数顺序。所以,委托就表示它声明中返回值,参数类型和顺序相同的一类方法。

 

现在回到(2),现在我们知道这个Click就是一个EventHandler类型的“事件”,表示一个参数是(object sender, EventArgs e)的方法。这里要注意,Click是一个事件,而不是一个委托。

event关键字是干嘛用的呢?这就涉及到publish-subscribe模式和委托的缺点。具体内容见《C#本质论第六版》P378-384。简单来讲,就是委托的封装不充分,而事件的封装更充分,更不容易出错,事件是一种特殊的委托。所以在publish-subscribe模式中,我们使用事件。

最后一个问题,我们知道Click事件声明在Control类中,而Button类通过ButtonBase类,间接地继承于Control类,所以Button类的实例button1自然也可以调用Click事件了。

总结一下,等号左边的this.button1.Click是一个事件,是EventHandler类型的一个变量。

 

+=右边,就很简单了,使用new关键字,实例化一个EventHandler类型的对象。这个对象指向Form1类中button1_Click方法

this.button1.Click += new System.EventHandler(this.button1_Click);

这行代码含义:为该类中button1_Click()方法订阅button1.Click事件。

 

问题在于,为什么是+=,而不是=?

我们习惯了下面这种写法:把右边的对象赋值给左边的变量。

String str = “abc”;

FileInfo f = new FileInfo(@“”);

 

这就是事件的作用,也就是前面提到的,为什么事件比委托封装的更充分,更不易出错。具体内容见C#本质论P383“高级主题:事件的内部机制”

这里只要知道,在调用事件时,赋值操作符是禁用的。只能使用+=或-=来订阅或者取消订阅。

 

继续说,

我们废了这么多劲去订阅这个事件,为了什么呢?答:人机交互。

当用户点击了界面上的按钮(Click事件被触发,实际上是调用了Click事件的Invoke()方法,前面有提到),就会调用button1_Click方法,程序就会继续运行来执行某种我们希望的操作。

在控制台应用程序中,Control类是Click事件的发布者(Publisher),而Form1类中的button1_Click()是Click事件的订阅者(Subscriber)。当Click.Invoke()方法被调用,发布者就会通知所有订阅者。

假设这样一种情况,如果发布者中包含订阅者感兴趣的数据,这些数据对订阅者的执行至关重要,数据应该如何传递给订阅者??

更直接一点,委托声明:

public delegate void EventHandler(object sender, EventArgs e)

sender和e是什么?EventArgs是什么类型?要弄明白这个问题,再看一段新的代码:

 1 using System;
 2 
 3 namespace ConsoleApp1
 4 {
 5     public class Thermostat
 6     {
 7         public class TemperatureArgs:EventArgs
 8         {
 9             public float NewTemperature { get; set; }
10 
11             public TemperatureArgs(float newTemperature)
12             {
13                 this.NewTemperature = newTemperature;
14             }
15 
16         }
17 
18         public event MyEventHandler<TemperatureArgs> OnTemperatureChange;
19 
20         private float currentTemperature;
21         public float CurrentTemperature
22         {
23             get { return currentTemperature; }
24 
25             set
26             {
27                 if (value != currentTemperature)
28                 {
29                     currentTemperature = value;
30 
31                     OnTemperatureChange?.Invoke(this, new TemperatureArgs(value));
32                 }
33             }
34         }
35         public int Number { get; set; }
36 
37     }
38 }
 1 using System;
 2 
 3 
 4 namespace ConsoleApp1
 5 {
 6     class Program
 7     {
 8         static void Main(string[] args)
 9         {
10             Heater heater1 = new Heater(90);
11             
12             Cooler cooler = new Cooler(60);
13             Thermostat thermostat1 = new Thermostat() { CurrentTemperature =0,Number=1};
14             Thermostat thermostat2 = new Thermostat() { CurrentTemperature = 0, Number = 2 };
15 
16             thermostat1.OnTemperatureChange += heater1.OnTemperatureChanged;
17             thermostat2.OnTemperatureChange += heater1.OnTemperatureChanged;
18 
19             thermostat1.OnTemperatureChange += cooler.OnTemperatureChanged;
20 
21             thermostat1.CurrentTemperature = 100;    //温度变化
22             Console.ReadKey();
23 
24             Console.WriteLine("Hello World");
25         }
26     }
27 
28     //声明泛型委托
29     public delegate void MyEventHandler<TEventArgs>(object sender, TEventArgs e)
30         where TEventArgs : EventArgs;
31 
32     public class Heater
33     {
34         public Heater(float temp)
35         {
36             this.Temperature = temp;
37         }
38 
39         public float Temperature;
40 
41         public void OnTemperatureChanged(object sender,Thermostat.TemperatureArgs e)
42         {
43             Thermostat thermostat = (Thermostat)sender;
44             if (thermostat.Number==1)
45             {
46                 Console.WriteLine("Thermostat Number : {0}",1);
47             }
48             else if (thermostat.Number==2)
49             {
50                 Console.WriteLine("Thermostat Number : {0}", 2);
51             }
52 
53             float newTemperature = e.NewTemperature;
54 
55             if (newTemperature>Temperature)
56             {
57                 Console.WriteLine("Cooler:ON");
58             }
59             else
60             {
61                 Console.WriteLine("Cooler:OFF");
62             }
63         }
64     }
65 
66     public class Cooler
67     {
68         public Cooler(float temperature)
69         {
70             this.Temperature = temperature;
71         }
72         public float Temperature;
73 
74         public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs  e)
75         {
76             float newTemperature = e.NewTemperature;
77 
78             if (newTemperature<Temperature)
79             {
80                 Console.WriteLine("Heater:ON");
81             }
82             else
83             {
84                 Console.WriteLine("Heater:OFF");
85             }
86         }
87     }
88 }

 

基本功能介绍:Thermostat类代表恒温器,控制水温保持在一定范围内。Heater类和Cooler类分别是加热器和冷却器,负责调节水温。恒温器能够获取当前水温值,加热器和冷却器根据当前水温值和预设水温值水温值比较,做出相应动作。

在该例中,Thermostat是“温度变化”事件的发布者,Heater类和Cooler中OnTemperatureChanged()是该事件订阅者,并需要获取当前温度值。

首先,在主程序中声明泛型委托:

public delegate void MyEventHandler<TEventArgs>(object sender, TEventArgs e)

        where TEventArgs : EventArgs;

这是一种比较规范的写法,理论上任何委托类型都可以使用。

第一个参数sender:是调用委托的那个类的实例。它有什么作用呢?

假设有两个Thermostat的实例,Heater.OnTemperatureChanged()订阅了这两个实例中的OnOnTemperatureChange事件。此时,任何一个实例都可能触发对OnTemperatureChanged()的调用。判断具体是哪个Thermostat实例触发了事件,需要在Heater.OnTemperatureChanged()内部利用sender参数进行判断。

我们为Thermostat类增加Number属性,代表恒温器编号。在主程序中创建两个Thermostat对象thermostat1和thermostat2,并将编号分别设备1,2,初始温度均为0.

在Heater.OnTemperatureChanged()中,将sender转换为Thermostat对象。并根据该对象的Number属性值执行相应的操作。

第二个参数e:它包含了事件的附件数据。数据类型是TEventArgs,从泛型约束中可知,该类继承自EventArgs类,EventArgs类的定义中只有一个Empty属性,用来指出不存在事件数据。

我们在TEventArgs类中添加了一个新属性NewTemperature,用于将温度从恒温器传递给订阅者。所以这个e参数就是我们用来传递订阅者感兴趣的数据的。

刚才的Number属性也可以添加在TEventArgs类中作为感兴趣数据传递出去,作用都是一样的。

一种最简单,也最常用的委托声明,就是前面提到的EventHandler委托:

Public delegate void EventHandler(object sender,EventArgs e)

类比上面的说明,button1_Click()方法订阅了this.button1.Click事件,我们在该方法中就可以利用sender参数来访问button1对象的各种数据,这里我以访问Text属性为例。

Button button1 = (Button)sender;

string text = button1.Text;

PS:这并不是最简单的方法,这里只是说明下可以这么用。直接用this.button1.Text简单。

这么声明委托就是一种简便方法,我把所有数据都声明在调用委托的类中,使用sender就能访问到所有数据,e变量中不放任何数据。

posted @ 2020-07-02 17:14  旋转的地球  阅读(611)  评论(1编辑  收藏  举报