WinForm二三事(一)补遗

时间:2021-11-18 04:26:43

在WinForm二三事(一)里,我们谈了WinForm上的事件(比如点击啊,双击啊)是借助消息循环,消息分发的机制实现的。但那篇里只是一笔带过。后来有人问我这中间的具体关系是什么呢?那今天我们就来详细谈谈从Win32的Message到WinForm上的Event。

Win32中的Hello world

要具体了解这个问题,我们先来看看在Win32的时候,使用原生的API(或者叫Native API)如何做个简单的Hello World的小窗体:

   1: #include <windows.h>
   2:  
   3: //这就是就受消息,然后处理的地方了
   4: LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,WPARAM wparam,LPARAM lparam)
   5: {
   6:     switch(msg)
   7:     {
   8:         case WM_DESTROY:PostQuitMessage(0);return 0;
   9:         default:return DefWindowProc(hwnd,msg,wparam,lparam);
  10:     }
  11: }
  12:  
  13: int WINAPI WinMain(HINSTANCE instance,HINSTANCE prev,LPSTR cmdLine,int cmdShow)
  14: {
  15:     //一个窗体类,用来设置窗体的各种属性,比如大小啊、背景颜色啊等等
  16:     WNDCLASSEX windowClass;
  17:     MSG msg;
  18:     HWND window;
  19:     memset(&windowClass,0,sizeof(WNDCLASSEX));
  20:     windowClass.cbSize = sizeof(WNDCLASSEX);
  21:     windowClass.hInstance = instance;
  22:     windowClass.lpszClassName = "HelloWorld";
  23:     //注意这里,当消息分发到这个窗体上来的时候,就是由WndProc这个函数进行处理
  24:     windowClass.lpfnWndProc = WndProc;
  25:     windowClass.hbrBackground = (HBRUSH)COLOR_WINDOW+1;
  26:     
  27:     if(!RegisterClassEx(&windowClass))
  28:     {
  29:         MessageBox(NULL,"不能注册窗体类","",MB_OK|MB_ICONERROR);
  30:         return 1;
  31:     }
  32:     window = CreateWindowEx(0,"MainWnd","Hello World",WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX,
  33:                 CW_USEDEFAULT, CW_USEDEFAULT, 200, 150, NULL, NULL, instance, NULL);
  34:     ShowWindow(window,cmbShow);
  35:     
  36:     //消息循环,从消息队列里获取消息,然后分发消息
  37:     //实际上GetMessage方法会返回-1(意思是出错了),所以下面这样的写法并不总是正确的
  38:     while(GetMessage(&msg,NULL,0,0)) 
  39:     {
  40:         TranslateMessage(&msg);
  41:         DispatchMessage(&msg);
  42:     }
  43:     
  44:     return 0;
  45: }

啊,在Win32的时候,要弹出一个空白窗口就要干这么多事儿啊。仔细看看上面的代码,不去深究Win32底层的细节,我们可以这么理解:GetMessage从当前线程的消息队列上获取一条消息(什么是消息?鼠标在窗体上点击一下,键盘的案件按下,窗体创建了,窗体开始绘制,窗体销毁这都是消息,这些消息会一个个的发送到一个消息队列中),然后TranslateMessage负责转换这个消息(意思就是将原生的消息,转换成“好懂”的消息,比如你按个F1,它将其转换为WM_HELP消息),然后DispatchMessage就是分发这个消息,发到哪里呢?就是上面的WndProc函数,WndProc函数接收到消息后,里面有个switch,switch根据消息的类型来决定干什么,比如接收到一个按钮点击然后调用按钮点击的调用函数。整个过程就像一个流水线一样,按部就班的执行。

ok,对Win32的讨论到此为止,我们再回到.Net。我们来看看在.Net里如何编写一个窗体呢?

WinForm中的Hello world

   1: public class HelloWorld:Form
   2: {
   3:     public HelloWorld()
   4:     {
   5:         InitializeComponent();
   6:     }
   7:     
   8:     private void InitializeComponent()
   9:     {
  10:         //这里是不是很像Win32中对WNDCLASSEX的设置?
  11:         this.Size = new Size(300,200);
  12:        this.Text = "Hello World";
  13:        this.Name = "HelloWorld";
  14:  
  15:        this.Click += new System.EventHandler(helloworld_Click);
  16:     }
  17:  
  18:     private void helloworld_Click(object sender,EventArgs e)
  19:    {
  20:        MessageBox.Show("窗体点击了");     
  21:    }
  22: }

Message与Event的连接

哦,.Net的代码,我们不仅仅实现了一个空白的窗体,还附加了一个点击窗体时的事件,可代码简洁的多,也没有很多的细节。看似这个与Win32的有点联系,又没有联系,这个点击窗体的事件到底是怎么触发的呢?我们一直追索Form的继承层次,发现Form是间接的从Control派生的,而Control类里发现了一个WndProc方法:

   1: protected virtual void WndProc(ref Message m)
   2: {
   3:     if ((this.controlStyle & ControlStyles.EnableNotifyMessage) == ControlStyles.EnableNotifyMessage)
   4:     {
   5:         this.OnNotifyMessage(m);
   6:     }
   7:     switch (m.Msg)
   8:     {
   9:         case 1:
  10:             this.WmCreate(ref m);
  11:             return;
  12:         //............
  13:  
  14:         case 8:
  15:             this.WmKillFocus(ref m);
  16:             return;
  17:         case 0x200:
  18:             this.WmMouseMove(ref m);
  19:             return;
  20:     
  21:         case 0x201:
  22:             this.WmMouseDown(ref m, MouseButtons.Left, 1);
  23:             return;
  24:     
  25:         case 0x202:
  26:             this.WmMouseUp(ref m, MouseButtons.Left, 1);
  27:             return;
  28:        //........
  29:     }
  30:     //.....
  31: }
  32:     

 

也是一个长长的swtich(看来.Net Framework把肮脏的活儿都干了,留给我们的是一片洁净的天空),在这里我们看到了这么一些代码:

case 0x202:
        this.WmMouseUp(ref m, MouseButtons.Left, 1);
        return;

而且在Control类里还定义了一堆的事件成员,还有一堆的OnEventName的虚方法,那我想WmMouseUp里肯定会去调用OnClick方法,然后在OnClick方法里触发事件。

基于上一篇和这一篇的讨论,我们是不是应该有这样的一个认识了:

Application.Run启动一个消息循环,然后Form里的WndProc方法(从Control继承而来的)处理接受到的方法,然后根据方法的类型,触发不同的事件。

WinForm继承层次

貌似讨论就要结束了,但是还有一个问题。这里只是讨论了Form如何接受消息,触发事件,那按钮呢,菜单呢,还有一堆的控件呢?其实我们只要看看WinForm的继承树我们很快就会明白这是怎么一会事儿。
  WinForm二三事(一)补遗
从图上我们可以看到,这些空间啊,窗体啊都是从Control派生的,Control里定义了一堆的通用的事件,而有的控件因为要自己处理一些事件,所以override了Control的WndProc方法。这个图也是一个典型的组合模式(部分-整体模式)的应用,以一个统一的方式处理这么一堆的控件。

缺点

这个结构是很优美,而且把肮脏的细节都给封装了,给我们的是一片的绿荫。但是未免也太过于优美而重型了,一个小小的Button继承了庞大的Control,而Control里有一大堆的虚方法(多达数百个),了解方法表的同学都知道,子类的方法表必须包括所有父类的虚方法。可想而知这个架构极其的“累赘”(实际上,为了避免这种虚方法带来的累赘,在Win32时代,像MFC和VCL等框架都竭力的避免这种庞大的继承体系)。没有办法了,到WPF里解决吧~~

本系列其他文章

WinForm二三事(一)消息循环

WinForm二三事(一)补遗

WinForm二三事(二)异步操作

WinForm二三事(三)Control.Invoke&Control.BeginInvoke

WinForm二三事(四)界面布局(上)

WinForm二三事(四)界面布局(下)

WinForm二三事(五)实作

WinForm二三事(六)数据绑定

WinForm二三事(七)GDI+

WinForm二三事(八)开源项目

WinForm二三事(九)常用第三方控件库

WinForm二三事(十)漫谈