zoukankan      html  css  js  c++  java
  • 从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变量中不放任何数据。

  • 相关阅读:
    如何唤醒一个处于阻塞状态下的线程
    如何终止一个线程
    网络编程基础_5.1聊天室-客户端
    网络编程基础_4.2TCP-客户端
    网络编程基础_4.1TCP_服务端
    网络编程基础_3.APC队列
    网络编程基础_2.等待事件
    网络编程基础_1.等待句柄
    Window提高_3.1练习_双进程守护
    Windows提高_2.3第三部分:内核区同步
  • 原文地址:https://www.cnblogs.com/zps-blog/p/13225894.html
Copyright © 2011-2022 走看看