首页 > 开发 > .Net > 正文

揭开.NET消息循环的神秘面纱

2020-02-03 16:01:07
字体:
来源:转载
供稿:网友
曾经在win32平台下奋战的程序员们想必记得,为了弄清楚“消息循环”的概念,度过多少不眠之夜。尽管如今在应用程序代码的编写过程中,我们已经不再需要它,但是深刻理解windows平台内部的消息流转机制依然必要..

  在早年直接用win32/win16 api写程序的时代,消息循环是我们必须搞懂的第一个观念。现在,不管你用是windows上面的哪一套application framework(mfc、vcl、vb、.net framework),甚至unix、linux、macosx上面的application framework,都不太容易看到消息循环。事实上,消息循环依然存在,只是被这些applicationframework包装起来,深深地埋藏在某个角落。

  本文章试图唤起大家对于消息循环的回忆,也试图解释消息循环如何被封装进.net framework的windows forms中。虽然windows forms将这一切都藏起来,但是也留下许多空间,让我们可以自行处理win32的消息。

  传统的windows 程序

  传统的windows程序,只利用win32 api撰写,下面是一个程序范例,为了节省篇幅,我将其中许多程序代码省略:

// 程序进入点

int apientry _twinmain(hinstance hinstance, hinstance hprevinstance,

lptstr lpcmdline, int ncmdshow){
 msg msg;
 if (!initinstance (hinstance, ncmdshow)){
  return false;
 }

 // 主消息循环:

 while (getmessage(&msg, null, 0, 0)){
  translatemessage(&msg); dispatchmessage(&msg);
 }
 return (int) msg.wparam;
}

// 函数: wndproc(hwnd, unsigned, word, long)
// 用途: 处理主窗口的消息。

lresult callback wndproc(
 hwnd hwnd, uint message, wparam wparam, lparam lparam) {
  int wmid, wmevent; paintstruct ps;
  hdc hdc;
  switch (message){
   case wm_command:
    wmid = loword(wparam);
    wmevent = hiword(wparam);
    // 剖析菜单选取项目:
    switch (wmid){
     case idm_about:
      dialogbox(hinst, (lpctstr)idd_aboutbox,hwnd, (dlgproc)about);
      break;
     case idm_exit:
      destroywindow(hwnd);
      break;
     default:
      return defwindowproc(hwnd, message,wparam,lparam);
    }
    break;
   case wm_paint:
    hdc = beginpaint(hwnd, &ps);

    // todo: 在此加入任何绘图程序代码...
    endpaint(hwnd, &ps);
    break;

   case wm_destroy:
    postquitmessage(0);
    break;
   default:
    return defwindowproc(hwnd, message,wparam, lparam);
  }
  return 0;
 }

 // [关于] 方块的消息处理例程。

 lresult callback about(hwnd hdlg, uint message,

 wparam wparam, lparam lparam){
  switch (message){
   case wm_initdialog:
    return true;
   case wm_command:
    if (loword(wparam) == idok || loword(wparam) == idcancel){
     enddialog(hdlg, loword(wparam));
     return true;
    }
    break;
   }
   return false;
  }


  1、从_twinmain内,程序进入主消息循环;

  2、消息循环从消息队列(message queue)中取得一个消息(透过调用getmessage())。每个执行中的程序都有一个属于自己的消息队列;

  3、消息循环根据消息内容来决定消息应该送给哪个windows procedure(wndproc),.. 这就称为消息分发(message dispatch)。通常“每一种”窗口或控件(control)都有一个windows procedure,来处理该种窗口/控件的行为;

  4、windows procedure根据消息内容来决定应该调用哪个函数(利用switch/case语法);..

  5、windows procedure处理完,控制权回到消息循环。继续进行2、3、4、5的动作;

  6、当消息队列为空的时候,getmessage()无法取得任何消息,就会进入idle(空闲)状态,进入睡眠状态(而不是busy waiting)。当消息队列不再为空的时候,程序会自动醒过来,继续进行2、3、4、5的动作;

  7、当取得的消息是wm_quit,getmessage()就会得到0的返回值,因而离开消息循环,程序结束。程序会利用调用postquitmessage()来将wm_quit放置进消息队列中,来造成稍后结束,而不会直接贸然跳离开循环来结束。

  虽名为队列(queue),.. 但是消息队列中的消息并非总是先进先出(first in first out,fifo),有一些特例:

  . 只要消息队列中有wm_quit ,就会先取出wm_quit,导致程序结束。

  . 只有在没有其它消息的时候,wm_paint 和wm_timer才会被取出。且多个wm_paint可能会被合并成一个,wm_timer也是如此。

  . 利用translatemessage()来处理消息,可能会造成新消息的产生。例如:translatemessage()可以辨识出wm_keydown(按键按下)加上wm_keyup(按键放开)就产生wm_char(字符输入)。

  何谓消息

  鼠标移动、按键被按下、窗口被关闭.,这些都会产生消息。在windows操作系统中,消息是以下面的数据结构存在的(定义在winuser.h档案中):..

typedef struct tagmsg {
 hwnd hwnd;
 uint message;
 wparam wparam;
 lparam lparam;
 dword time;
 point pt;
} msg;
  消息内有六个信息,分别是:

  . hwnd:窗口/控件的唯一hwnd的编号。消息循环会根据此信息,将消息送到正确目标。

  . message:windows预先定义的消息种类的id。

  . wparam 与lparam:有些message本身需要携带更多的信息,这些信息就放在wparam与lparam中。

  . time与pt:消息发生当时的时间与鼠标位置。

  .net framework如何封装消息循环

  .net framework的windows forms将消息循环封装起来,以方便我们使用。本节中所提到的类(class),都是属于system.windows.forms名字空间(namespace)。

  简单归纳如下:消息循环被封装进了application类的run()静态方法中;windows procedure被封装进了nativewindow 与control 类中;个别的消息处理动作被封装进control 类的onxyz()(例如onpaint())。我们可以覆盖(override)onxyz(),来提供我们自己的程序。也可以利用.net的事件(event)机制,在xyz事件上,加入我们的事件处理函数(event handler)。control类的onxyz()会主动调用xyz 事件的处理函数。

  请注意,因为xyz 的事件处理函数是由control类的onxyz()方法所调用的,所以当你覆写onxyz()方法时,不要忘了调用control类的onxyz()(除非你有特殊需求),否则xyz事件处理函数将会没有作用。只要调用base.onxyz(),就可以调用到control类的onxyz()方法,如下所示:

protected override void onpaint(painteventargs e){
 base.onpaint (e);// todo: 加入 form1.onpaint 实作
}
  我们可以利用覆写control类的onxyz(),来决定该消息发生时要做些什么。同理,我们甚至可以覆写control与nativewindow类的wndproc(),来定义windows procedure。

  再次提醒你,因为onxyz()系列方法是由control类的wndproc()所调用的,所以当你覆写wndproc()时,不要忘了调用control类的wndproc()(除非你有特殊需求),否则onxyz()系列方法(以及xyz事件处理函数)将会没有作用。只要调用base.wndproc(),就可以调用到control类的wndproc(),如下所示:

protected override void wndproc(ref message m){
 base.wndproc (ref m);//todo: 加入form1.wndproc 实作
}
  你可能也注意到了,wndproc()需要一个message类的参数,这正是msg被封装成.net版本的结果。

  一个windows forms的范例

  为了让读者更加了解实际的状况,我用下面的实例范例作说明:

namespace windowsapplication1{
 /// form1 的摘要描述。
 public class form1 : form{
  /// 设计工具所需的变数。
  private container components = null;
  public form1(){
   autoscalebasesize = new size(5, 15);
   clientsize = new size(292, 266);
   name = "form1";
   text = "form1";
   paint += new painteventhandler(this.form1_paint);
   paint += new painteventhandler(this.form1_paint2);
  }
  /// 应用程序的主进入点。
  [stathread]
  static void main(){
   application.run(new form1());
  }
  protected override void onpaint(painteventargs e){
   base.onpaint (e); // 2
  }
  private void form1_paint(object sender, painteventargs e){
   // 3
  }
  private void form1_paint2(object sender, painteventargs e){
   // 4
  }
  protected override void wndproc(ref message m){
   base.wndproc (ref m); // 1
  }
 }
}
  1、在main()中,利用application.run()来将form1窗口显示出来,并进入消息循环。程序的执行过程中,application.run()一直未结束。

  2、os在此process的消息队列内放进一个wm_paint消息,好让窗口被显示出来。

  3、wm_paint被application.run()内的消息循环取出来,分发到wndproc()。由于多态(polymorphism)的因素,此次调用(invoke)到的wndproc()是属于form1的wndproc(),也就是上述程序中批注(comment)1的地方,而不是调用到 control.wndproc()。

  4、在form1.wndproc()的最后,有调用base.wndproc(),这实际上调用到control.wndproc()。

  5、control.wndproc()从message参数中得知此消息是wm_paint,于是调用onpaint()。由于多态的因素,此次调用到的onpaint()是属于form1的onpaint(),也就是上述程序中批注2的地方,而不是调用到 control.onpaint()。

  6、在form1.onpaint()的最后,有调用base.onpaint(),这实际上调用到control.onpaint()。

  7、我们曾经在form1的构造函数(constructor)中将form1_paint()与form1_paint2()登记成为paint事件处理函数
(event handler)。control.onpaint()会去依序去调用这两个函数,也就是上述程序中批注3与4的地方。

  干嘛知道这么多?拜工具之赐,现在的程序员很幸福,可以在糊里胡涂的情况下写出程序来。不过这样的程序员恐怕竞争力不强,毕竟将组件(component)拖放(drag and drop)到画面上,再设定组件属性的工作,称不上有太大的难度。只有深入了解内部原理,才能让自己对技术融会贯通,也才能让程序员之路走得更稳健、更长久。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表