消息机制

上一章主要是介绍了怎样用VC来创建工程,并且还编出了一个令各位很有成就感的对话框。此时你一定会说,这和DOS下的编程有什么不同,只不过是把main换成了WinMain而已。呵呵,你以为Windows的编程都是那么简单?如果让你编个显示窗口的程序呢,也是调用一个显示窗口的函数就可以了吗?如果你真的那样做,你将会惊奇的发现,窗口只是显示了一瞬间,然后就消失了。为什么会这样?很简单呀,你的程序在调用完显示窗口的函数后就结束了,窗口还留在那里干什么?那么有没有让窗口不消失的办法?有,那就是做个循环,一直循环到窗口被用户关闭为止。在整个循环过程中,你的这个窗口可以不停的处理用户发出的各种操作信息,但如果收不到任何信息怎么办?这个不用担心,反正你的程序是在循环中,既然程序没有结束,窗口自然也就不会消失。现在明白了吧,Windows下编程可没你想象的那么简单,你必须适应一种新的程序结构,那就是消息机制,也就是刚才说的那个循环,在对消息循环有了大体的认识之后,我们就可以学习编写显示窗口的程序了。
  首先呢,还是得include那个windows.h,接下来,又是那个WinMain,这次要重点说一说这个函数了,这个函数可以把它看作是Windows下程序的入口点,它前面的int把函数定义为整形,WinAPI指示了函数参数在调用时放在栈中的方式,在这里就不再深入探讨编译原理了,我们以后所有的程序都将采用WinAPI。这个函数有四个参数,其中第一个HINSTANCE是个局柄类型的标示符,标示程序实例,在创建窗口时要用到。第二个参数是16位Windows中的重要角色,在32位的系统中已用不到了,在调用中包含它只是为了和老系统兼容,第三个参数是一个字符串指针,可以提供程序执行时的命令行参数,以前在DOS下到是很有用。第四个参数确定了窗口最初的显示方式,Windows在执行运用程序时将会为它分配一个值。
下面就到初始化窗口了,这里将会用到一个WNDCLASSEX结构,大家必须掌握。此结构定义如下:

typedef struct tagWNDCLASSEX { 
UINT cbSize;该结构大小,即sizeof(WNDCLASSEX)代表结构版本
UINT style;窗口类的风格,一般用CW_HREDRAW|CW_VREDRAW:长宽改变则发重绘消息
WNDPROC lpfcWndProc;处理消息的函数指针,所有的发给此窗口的消息,都将通知给这个函数
int cbWndExtra;初始化为0就可以了
int cbClsExtra;初始化为0就可以了
HINSTANCE hInstance;窗口过程的实例句柄,初始化为刚才WinMain的第一个参数
HICON hIcon;程序最小化时的图标句柄
HCURSOR hCursor光标形状
HBRUSH hbrBackground;背景刷
LPCSTR lpszMenuName;菜单的资源名,没有菜单的话,就设为NULL
LPCSTR lpszClassName;窗口类名称(你得为你创建的类起个名字)
HICON hIconSm;程序在资源管理器中显示的图标
} WNDCLASSEX; 


  初始化完winclassex后需要注册窗口。这时调用函数RegisterClassEx(&winclassex),它的参数是一个窗口结构的地址。如果注册成功,返回一个非零值,就可以用CreateWindowEx来创建窗口了。此函数有多达12个参数。在后面的程序中你将可以看到对它们的解释,创建成功后,返回一个窗口句柄。但是,窗口还不会被显示出来,它只是被存在内存中,此时要用到ShowWindow和UpdateWindow,ShowWindow设置窗口的显示状态,它有两个参数,一个是CreateWindowEx返回的窗口句柄,另一个是WinMain的最后一个参数nCmdShow。UpdateWindow通过给窗口发送一个WM_PAINT消息来通知窗口绘制自身。如果一切顺利的话,此刻窗口就已经被显示出来了,现在就可以开始消息循环。下面将给出创建窗口的源代码,之后再详细讨论消息循环。
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
    WNDCLASSEX    wndclass;        // 用于申明窗口类的结构
    HWND          hWnd;            // 程序的窗口句柄
    MSG           msg;             // 接收窗口消息的结构

    // 为创建窗口类填写类结构
    wndclass.cbSize = sizeof (wndclass);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = "TestWindowClass";
    wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    // 创建窗口类
    if (!RegisterClassEx(&wndclass)) return 0;

    // 用上面创建的类创建新窗口
    hWnd = CreateWindowEx(0,            //扩展窗口风格,0为默认
                "TestWindowClass",      //创建窗口所用的类
                "First Program",        //窗口名称看运行结果就知道了
                WS_OVERLAPPEDWINDOW,    //窗口风格OVERLAPPED风格(和记事本一样的),
                CW_USEDEFAULT,          //这四个参数定义了窗口的位置和大小,现为默认
                CW_USEDEFAULT,
                CW_USEDEFAULT,
                CW_USEDEFAULT,
                NULL,                   //父窗口句柄,程序没有,所以设为NULL
                NULL,                   //菜单句柄没有为NULL
                hInstance,              //WinMain传递的实例句柄
                NULL);                  //创建参数,用于给程序传递数据,程序没用到
    if (!hWnd) return FALSE;
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd); 
    
    // 下面就是消息循环 
    while (GetMessage(&msg,NULL,0,0))   // 在消息队列中取消息直到收到退出消息QUIT_MESSAGE 
    { 
        TranslateMessage(&msg);         // 翻译虚拟击键消息
        DispatchMessage(&msg);          // 将消息投递到对应的窗口
    }
    return msg.wParam;                  // 如果没有特殊要求,这里也可以是0代表成功执行
} 

LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)
{
    switch (message)    // 接受消息循环的函数,采用switch处理各种消息 
    {
    case WM_CLOSE:      // 窗口关闭前的消息(比如用户点了关闭按钮)
            break;      // 此时Windows还根本没有对窗口做任何事,可以用MessageBox来测试
    case WM_DESTROY:    // 窗口关闭以后的消息,此时Windows已经毁坏了窗口
            PostQuitMessage(0); 
            return 0;
    } 
    return DefWindowProc(hwnd, message, wParam, lParam); // 默认消息处理
}



  这次弄了半天,总算是编出了一个真正的窗口了,好好看一下代码,大多数都是讲过的,只是多出了那个消息循环以及MSG结构变量,MSG是用来配合消息循环的,它保存着所有的消息。函数GetMessage一直在检索消息,直到收到WM_QUIT返回0,结束程序。TranslateMessage把击健化为字符,然后传给消息队列。DispatchMessage把消息发给窗口过程,也就是那个WinProc。WinProc前的CALLBACK符号表示它是回调函数,等价于WINAPI。它后面的参数可以把它看成是各种消息,通过这些参数可以获得所有需要消息。本例中只给出了对退出消息的处理,以后在游戏编写过程中,将要对各种消息进行处理,那时的WndProc就不会这么简单了。

  现在大家再回忆一下消息机制,其实就是一个循环,不停的接受消息,再把消息传给一个专门的函数去处理。如何,明白了就变得很简单了吧?。
  估计现在各位也该Level Up!了,那么下一章我们将正式学习DirectX的编程方法。

=============================

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

曾经在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

 

==========================

在创建窗口、显示窗口、更新窗口后,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,我们需要调用GetMessage()函数,该函数的原型声明如下:

BOOL GetMessage(

        LPMSG lpMsg,            // address of structure with message

        HWND hWnd,               // handle of window

        UINT wMsgFilterMin,     // first message

        UINT wMsgFilterMax      // last message

);

参数lpMsg指向一个消息(MSG)结构体,GetMessage从线程的消息队列中取出的消息信息将保存在该结构体对象中。

参数hWnd指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。

参数wMsgFilterMin指定要获取的消息的最小值,通常设置为0。

参数wMsgFilterMax指定要获取的消息的最大值。如果wMsgFilterMin和wMsgFilter Max都设置为0,则接收所有消息。

GetMessage函数接收到除WM_QUIT外的消息均返回非零值。对于WM_QUIT消息,该函数返回零。如果出现了错误,该函数返回-1,例如,当参数hWnd是无效的窗口句柄或lpMsg是无效的指针时。

通常我们编写的消息循环代码如下:

MSG msg;

while(GetMessage(&msg,NULL,0,0))

{

    TranslateMessage(&msg);

    DispatchMessage(&msg);

}

前面已经介绍了,GetMessage函数只有在接收到WM_QUIT消息时,才返回0。此时while语句判断的条件为假,循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时,Windows应用程序就通过这个while循环来保证程序始终处于运行状态。

TranslateMessage函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用GetMessage函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN和WM_KEYUP消息。这两个消息的附加参数(wParam和lParam)包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的ASCII码,TranslateMessage这个函数就可以将WM_KEYDOWN和WM_ KEYUP消息的组合转换为一条WM_CHAR消息(该消息的wParam附加参数包含了字符的ASCII码),并将转换后的新消息投递到调用线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。

DispatchMessage函数分派一个消息到窗口过程,由窗口过程函数对消息进行处理。DispachMessage实际上是将消息回传给操作系统,由操作系统调用窗口过程函数对消息进行处理(响应)。

Windows应用程序的消息处理机制如图1.2所示。

(1)操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。

(2)应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,例如,放弃对某些消息的响应,或者调用TranslateMessage产生新的消息。

(3)应用程序调用DispatchMessage,将消息回传给操作系统。消息是由MSG结构体对象来表示的,其中就包含了接收消息的窗口的句柄。因此,DispatchMessage函数总能进行正确的传递。

(4)系统利用WNDCLASS结构体的lpfnWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理(即“系统给应用程序发送了消息”)。

以上就是Windows应用程序的消息处理过程。

提示:

(1)从消息队列中获取消息还可以调用PeekMessage函数,该函数的原型声明如下所示:

BOOL PeekMessage(

  LPMSG lpMsg,           // message information

  HWND hWnd,              // handle to window

  UINT wMsgFilterMin, // first message

  UINT wMsgFilterMax,  // last message

  UINT wRemoveMsg    // removal options

);

前4个参数和GetMessage函数的4个参数的作用相同。最后1个参数指定消息获取的方式,如果设为PM_NOREMOVE,那么消息将不会从消息队列中被移除;如果设为PM_REMOVE,那么消息将从消息队列中被移除(与GetMessage函数的行为一致)。关于PeekMessage函数的更多信息,请参见MSDN。

(2)发送消息可以使用SendMessage和PostMessage函数。SendMessage将消息直接发送给窗口,并调用该窗口的窗口过程进行处理。在窗口过程对消息处理完毕后,该函数才返回(SendMessage发送的消息为不进队消息)。PostMessage函数将消息放入与创建窗口的线程相关联的消息队列后立即返回。除了这两个函数外,还有一个PostThreadMessage函数,用于向线程发送消息,对于线程消息,MSG结构体中的hwnd成员为NULL。

虽名为队列(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(字符输入)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值