《Windows32 SDK教程》03章 窗口消息处理


Windows窗口就是通过消息来与操作系统互动,窗口不能直接与使用者交流,使用者移动鼠标、敲键盘都是通过操作系统与窗口通信的,因此WinMain()函数中的消息循环很重要,没有它窗口便不能工作,而窗口消息处理函数更为重要,也是我们编程的主要对象。

一、消息循环

消息循环共用到3个函数,GetMessage()的返回值一直不为0,直到接受WM_QUIT时才为0(也就是false)。DispatchMessage()是将消息传给窗口消息处理函数。对初学者来说,这3个函数写法比较固定,小雅不作详细说明。因为内容多了反而把人的思想搞乱,但初学者不要认为只能这样写。

对消息的正确理解是比较关键的,不要编了很多程序,仍不知消息究竟是什么。我们一起看一下消息的结构体。

typedef struct {
    HWND hwnd;        //接受消息的窗口的句柄
    UINT message;     //消息(应用程序只能用低字节,高字节留给系统用)
    WPARAM wParam;    //消息的第一附加信息
    LPARAM lParam;    //消息的第二附加信息
    DWORD time;       //Specifies the time at which the message was posted. 
    POINT pt;         //Specifies the cursor position, in screen coordinates, when the message was posted. 
} MSG, *PMSG;

注意:上面消息循环中的三个函数用的消息是这个结构体的类型,而下面要阐述的消息处理函数的参数,用的是结构体中的第2-4元素即message、wParam、lParam。

二、消息处理函数

Windows中消息的种类大致有2种,即系统消息和应用程序消息,消息的前缀也多种多样。不过日常用到的消息大都以“WM_”开头,而且许多消息的wParam和lParam都为0(也就是不使用),一般只有“WM_COMMAND”菜单命令或键盘鼠标的消息才使用wParam和lParam。

Windows中消息虽然很多,但并不是每一消息都要编程处理,我们一般把不需要特别处理的消息交给缺省消息处理函数DefWindowProc(),例子中的default部分便是。另外,有一函数必须处理,也就是关闭窗口消息WM_DESTROY,这时必须调用PostQuitMessage(0)来向操作系统发送一条终止程序的消息。

#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL InitApp(HINSTANCE, LPCSTR);
BOOL InitInstance(HINSTANCE, LPCSTR, int);

//主入口函数
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst,
                   LPSTR lpsCmdLine, int nCmdShow)
{
    MSG msg;
    char szClassName[] ="DrawRect";  //窗口名

    //注册窗口类
    if (!InitApp(hCurInst, szClassName)) 
        return FALSE;

    //初始化窗口
    if (!InitInstance(hCurInst, szClassName, nCmdShow)) { 
        return FALSE;
    }

    //消息循环
    while (GetMessage(&msg, NULL, NULL, NULL)) { 
        TranslateMessage(&msg);  //消息解释
        DispatchMessage(&msg);   //消息发送
    }

    return (int)msg.wParam;
}

用的最多的消息可能是WM_PAINT,当窗口改变大小、或有别的窗口从这个窗口面前经过,这个窗口中的文字或图案被冲掉,这就必须重新描画。因此,窗口中要显示的文字或图像一般都是在WM_PAINT中实现的。下面我们以在窗口上显示一行字作例子,不过,这时要稍微知道一点GDI(Graphic Device Interface)的知识。

三、GDI的基础知识

要在窗口描画文字或图像,首先要先取得设备文本(Device Contexts)。取设备文本一般有2种方法,BeginPaint()函数和GetDC()函数。描画结束后必须调用EndPaint()函数和ReleaseDC来释放设备文本。后面2章将继续介绍GDI的基础知识。

要在窗口的某个位置描画文字,必须取窗口的有效区域(ClientRect),因为窗口的边框、菜单、工具条、状态条等部分是不能利用的。例如,要在窗口的正中间写一个字,用窗口宽度和高度的一半来取正中位置是不对的,除非这个窗口无任何边框、菜单、工具条、状态条等。正确的方法是先定义一个区域变量(类型为RECT),然后用GetClientRect()函数取出窗口有效区域放入这个变量中。最后,调用DrawText()函数在矩形区输出文字。DrawText()将在下一章细说,所谓区域类型RECT其实就是一个存放矩形块座标的结构体,下面是RECT的原型。

int DrawText(                                   //|   typedef struct _RECT {            
  HDC hDC,          //设备文本的句柄             //|        LONG left;      //左边位置  
  LPCTSTR lpString, //要输出的字符串             //|        LONG top;       //上边位置  
  int nCount,       //字符串长度                 //|        LONG right;     //右边位置  
  LPRECT lpRect,    //要输出的区域               //|        LONG bottom;    //下边位置  
  UINT uFormat      //输出的格式                 //|    } RECT, *PRECT;                         
);                                               //|

使用BeginPaint()函数、EndPaint()函数时会同时得到一个结构体,里面有画图时的一些状态,一般为系统保留使用,小雅在初学VC时就想,不修改仅仅使用应该可以的吧?例如,窗口有效区域,不调用GetClientRect()函数,而直接从这个结构体中就可取出使用。下面例子中11行的“ps.rcPaint”这样也能得到正确结果。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hdc;
    TCHAR szHello[50]="Wecome you to Http://www.quanxue.cn/";

    switch (message)
    {
        case WM_PAINT:       //重画窗口消息
            hdc = BeginPaint(hWnd, &ps);  //开始描画
            DrawText(hdc, szHello, (int)strlen(szHello), &ps.rcPaint, 
                     DT_CENTER|DT_SINGLELINE|DT_VCENTER); //显示文字(横向对中|不自动回行|纵向对中)
            EndPaint(hWnd, &ps);          //结束描画
            break;
        case WM_DESTROY:     //关闭窗口消息
            PostQuitMessage(0);           //发送关闭消息
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);    //缺省窗口消息处理函数
   }
   return 0;
}

事实上,这段程序是错误的!当改变窗口位置大小时程序没错,一旦窗口的一部分被其它窗口遮盖过(如下图红线部分),显示就不对了。原因是什么呢?其实是未真正了解这个“ps”的作用,ps中的rcPaint是窗口需要重新刷新的部分,并不是在任何情况都等于窗口的有效区域。下面部分是更正后的程序,另外,将以前的程序修改了窗口名、大小、背景等。


//登记窗口类
BOOL InitApp(HINSTANCE hInst, LPCSTR szClassName)
{
    WNDCLASS wc;

    ZeroMemory(&wc, sizeof(wc));
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = (LPCSTR)szClassName;
    return (RegisterClass(&wc));
}

//初始化窗口类
BOOL InitInstance(HINSTANCE hInst, LPCSTR szClassName, int nCmdShow)
{
    HWND hWnd;

    hWnd = CreateWindow(szClassName,
                        "劝学网SDK教程",
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        400,   120,
                        NULL,  NULL,
                        hInst, NULL);
    if (!hWnd)  return FALSE;
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hdc;
    TCHAR szHello[50]="Wecome you to Http://www.quanxue.cn/";

    switch (message)
    {
        case WM_PAINT:       //重画窗口消息
            hdc = BeginPaint(hWnd, &ps);  //开始描画
            RECT rc;
            GetClientRect(hWnd, &rc);      //取窗口有效区域
            //显示文字(横向对中|不自动回行|纵向对中)
            DrawText(hdc, szHello, (int)strlen(szHello), &rc, 
                     DT_CENTER|DT_SINGLELINE|DT_VCENTER); 
            EndPaint(hWnd, &ps);          //结束描画
            break;
        case WM_DESTROY:     //关闭窗口消息
            PostQuitMessage(0);           //发送关闭消息
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);    //缺省窗口消息处理函数
   }
   return 0;
}