深入Windows窗体原理及控件重绘技巧

来源:互联网 发布:joo yeon-seo中文名字 编辑:程序博客网 时间:2024/06/06 09:43

之前有学MFC的同学告诉我觉得Windows的控件重绘难以理解,就算重绘成功了还是有些地方不明白,我觉得可能很多人都有这样的问题,在这里我从Windows窗体的最基本原理来讲解,如果你有类似的疑惑希望这篇文章可以帮你解惑。

1.Windows窗体原理

首先,如果看过Win32 SDK编程的都知道Windows的三大核心系统:负责窗口对象产生和消息分发的USER模块,负责图像显示绘制的GDI模块,负责内存、进程、IO管理的KERNEL模块。试想象一下如何在一个像素阵列上产生窗口对象,其实就是使用GDI绘制窗口,不停的以一定的频率刷新显示在屏幕上,这就是图形界面,如果由在DOS或Windows DOS模拟器下编写图形界面的经验这个比较好理解。所以说其实USER模块中的窗口产生是依靠GDI模块的(包括菜单、滚动条等都是使用GDI来绘制的)。

那么,下面我们就从USER模块和GDI模块来说说Windows 的窗体原理。

如果接触过Win32 SDK编程的知道一个标准Windows窗体的产生过程:设计窗口类、注册窗口类、创建窗口、显示窗口、启动消息循环泵循环获取消息分发到窗体过程函数处理。为了保证博客的连贯性,在这里我贴上一个标准Windows窗体的产生代码。

#include <windows.h>LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){    static TCHAR szAppName[] = TEXT ("窗口类名称");    HWND         hwnd;    MSG          msg;    WNDCLASSEX   wndclassex = {0};//设计窗口类    wndclassex.cbSize        = sizeof(WNDCLASSEX);    wndclassex.style         = CS_HREDRAW | CS_VREDRAW;    wndclassex.lpfnWndProc   = WndProc;    wndclassex.cbClsExtra    = 0;    wndclassex.cbWndExtra    = 0;    wndclassex.hInstance     = hInstance;    wndclassex.hIcon         = LoadIcon (NULL, IDI_APPLICATION);    wndclassex.hCursor       = LoadCursor (NULL, IDC_ARROW);    wndclassex.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);    wndclassex.lpszMenuName  = NULL;    wndclassex.lpszClassName = szAppName;    wndclassex.hIconSm       = wndclassex.hIcon;//注册窗口类    if (!RegisterClassEx (&wndclassex))    {        MessageBox (NULL, TEXT ("RegisterClassEx failed!"), szAppName, MB_ICONERROR);        return 0;    }//产生窗口    hwnd = CreateWindowEx (WS_EX_OVERLAPPEDWINDOW,                   szAppName,                   TEXT ("窗口名称"),                  WS_OVERLAPPEDWINDOW,                  CW_USEDEFAULT,                   CW_USEDEFAULT,                   CW_USEDEFAULT,                   CW_USEDEFAULT,                   NULL,                   NULL,                   hInstance,                  NULL); //显示窗口    ShowWindow (hwnd, iCmdShow);    UpdateWindow (hwnd);//启动消息循环泵循环获取消息分配到窗体过程函数处理    while (GetMessage (&msg, NULL, 0, 0))    {        TranslateMessage (&msg);        DispatchMessage (&msg);    }    return msg.wParam;}//窗体过程函数LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){    HDC hdc;    PAINTSTRUCT ps;    switch (message)    {    case WM_CREATE:        return (0);    case WM_PAINT:        hdc = BeginPaint (hwnd, &ps);        EndPaint (hwnd, &ps);        return (0);    case WM_DESTROY:        PostQuitMessage (0);        return (0);    }    return DefWindowProc (hwnd, message, wParam, lParam);}

需要明白的是,所有Windows的窗体及控件归根结底都是使用CreateWindow或CreateWindowEx来创建的,他们都需要标准Windows窗体的产生过程

普通的窗体好理解,主要需要弄清楚是对话框及控件的产生和消息分派处理流程。

对话框及其子控件的管理依靠Windows内建的对话框管理器,对话框管理器的工作包括:

1.根据我们在资源设计器中设计的对话框及子控件产生的.rc文件来自动生成对话框和子控件(如果有手动编写.rc文件的经历的话,知道编写RC文件其实就是指定窗口和子控件大小、类型、样式等参数,对话框管理器将这些参数传入CreateWindow函数产生窗体)

2.模态对话框直接显示窗体,非模态对话框消息指明WS_VISIBLE属性的话,需要调用ShowWindow来显示窗体。

4.维护一个消息循环泵,对于模态对话框来说这个消息泵的消息不经过父窗口,所以表现为模态;对于非模态对话框这个消息泵消息经过主窗口,必须由主窗口传给非模态对话框,表现为非模态。

3.维护一个内建的窗体过程函数,对于对话框来说会处理对话框的关闭打开及子窗口的焦点、tab等,对于子控件也是一样,每个子控件会有自己类型的窗体过程函数,窗体过程函数处理子控件的获得或失去焦点、按下或弹起、创建等表现样式和行为。对于对话框来说,他会开放一个对话框过程函数,让部分消息先通过对话框管理函数处理,如果对话框过程函数不处理才交给默认的内建过程函数处理,对于子控件来说,他们并没有开放过程函数,而是由内建窗体函数将要处理的消息发给父窗口处理。

那么对话框管理器完成了标准Windows窗体的产生中后半部分工作,至于设计窗口类和注册窗口类这是由Windows自己预先做好了的,如常见的“button”、“listbox”、“edit”类等等。

一个简要的示意图如下


那么既然所有的窗体(包括对话框和控件)产生过程一样,那么我们就可以将对话框管理器的部分工作替换掉:

1.不使用对话框读取.rc模板的方式,直接将参数传递给CreateWindow函数来创建对话框和控件,这就是常见的动态创建控件原理。

2.设置控件自绘制如BS_OWNDRAW属性,开放控件的WM_DRAWITEM消息给父窗口,由父窗口来绘制按钮样式,这就是常见的控件重绘原理。

3.替换掉内建的窗体函数,将消息传到自定义的窗体过程函数处理,这就是常见的控件子类化原理。


下面,为了做演示,先用通用模板创建的方式创建一个模态对话框和其子控件,然后模板创建一个非模态对话框,在非模态对话框中使用动态创建的方式创建和模态对话框中模板创建一样的按钮(当然位置和大小等可能不一样,这里只是为了说明原理故笔者并没有去管这些细节,如果你愿意完全可以把它们做的一模一样)。

代码太长,这里只贴出部分代码,详细代码请下载演示文件

主窗口消息泵

    while (GetMessage (&msg, NULL, 0, 0))    {//注意非模态对话框消息由主窗口分发if (hDlgModeless == NULL || !IsDialogMessage(hDlgModeless, &msg)){TranslateMessage (&msg);DispatchMessage (&msg);}    }
主窗口菜单响应

case IDM_TEMPLATE:DialogBox(GetWindowLong(hwnd, GWL_HINSTANCE),  IDD_TEMPLATE,  hwnd,  TemplateProc);break;case IDM_CREATE:hDlgModeless = CreateDialog(GetWindowLong(hwnd, GWL_HINSTANCE), MAKEINTRESOURCE(IDD_CREATE), hwnd, CreateProc);ShowWindow(hDlgModeless, SW_NORMAL);//注意非模态对话框不指明WS_VISIBLE属性必须显示调用ShowWindow来显示break;
模板创建的模态对话框对话框过程函数

BOOL CALLBACK  TemplateProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){switch(message){case WM_CLOSE:        {            EndDialog(hDlg,0);        }return (TRUE);case WM_COMMAND:switch (LOWORD(wParam)){case IDCANCEL:            {                SendMessage(hDlg, WM_CLOSE, 0, 0);            }            return (TRUE);case IDOK:            {                            }return (TRUE);}return (FALSE);}return (FALSE);}

模板创建的非模态对话框的对话框过程函数

BOOL CALLBACK  CreateProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){switch(message){case WM_INITDIALOG:{//动态创建控件子窗口CreateWindow(TEXT("button"),  TEXT("确定"),  WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 10, 10, 100, 50, hDlg, (HMENU)IDOK, GetWindowLong(hDlg, GWL_HINSTANCE), NULL);CreateWindow(TEXT("button"), TEXT("取消"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,10, 100,100, 50,hDlg,(HMENU)IDCANCEL,GetWindowLong(hDlg, GWL_HINSTANCE),NULL);}return (TRUE);case WM_CLOSE:DestroyWindow(hDlg);hDlgModeless = NULL;//注意及时将指针置0防止窗口销毁后消窗口分发消息return (TRUE);case WM_COMMAND:switch (LOWORD(wParam)){case IDCANCEL:{SendMessage(hDlg, WM_CLOSE, 0, 0);}return (TRUE);case IDOK:{}return (TRUE);}return (FALSE);}
创建效果

模态对话框


非模态对话框

二者起到了相同的作用,动态创建比模板创建要灵活的多,这个深入学习请自行查找相关资料。上例中需要注意的模态对话框和非模态对话框,前者的消息不流经主窗口消息泵,后者的消息要先流经主窗口消息泵。

2.控件重绘(WM_DRAWITEM)

写这篇博文的初衷就是讲解控件重绘原理,自然不能少了这一内容,在刚刚提到了修改对话框管理器的行为的几种方式,后两种(开放WM_DRAWITEM消息和控件子类化)都是常用的控件重绘技巧,在这一节先讲WM_DRAWITEM消息重绘,下一节讲控件子类化重绘,都是以按钮的重绘为例来讲解。
WM_DRAWITEM顾名思义当控件需要重绘的时候发给主窗口的消息,一般在按钮按下或弹起、获得焦点或失去焦点、创建等时候会产生这一消息,默认是不开启重绘消息的,如果使用模板创建按钮必须在按钮属性中设置OwnDraw属性为True,如果动态创建按钮必须加上BS_OWNDRAW这一属性。
下面我要重绘两个个按钮,按钮是模板创建的,是默认的IDOK和IDCANCEL按钮,希望达到的效果是
按钮普通状态分别为

按钮获得焦点分别为

按钮按下状态分别为


下面先贴出绘制部分代码,再讲解
BOOL CALLBACK SelfDrawProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){LPDRAWITEMSTRUCT pdis;HDC hdc;HDC hMemDc;static HINSTANCE hInstance;static HBITMAP hBitmapOK_D;static HBITMAP hBitmapOK_U;static HBITMAP hBitmapOK_F;static HBITMAP hBitmapCANCEL_D;static HBITMAP hBitmapCANCEL_U;static HBITMAP hBitmapCANCEL_F;static HWND   hwndOk;static HWND    hwndCanel;static BITMAP  bm;switch(message){case WM_INITDIALOG:        {hInstance = GetWindowLong(hDlg, GWL_HINSTANCE);hwndOk = GetDlgItem(hDlg, IDOK);hwndCanel = GetDlgItem(hDlg, IDCANCEL);hBitmapOK_D = LoadBitmap(hInstance, TEXT("image1d"));hBitmapOK_U = LoadBitmap(hInstance, TEXT("image1u"));hBitmapOK_F = LoadBitmap(hInstance, TEXT("image1f"));hBitmapCANCEL_D = LoadBitmap(hInstance, TEXT("image2d"));hBitmapCANCEL_U = LoadBitmap(hInstance, TEXT("image2u"));hBitmapCANCEL_F = LoadBitmap(hInstance, TEXT("image2f"));GetObject(hBitmapCANCEL_D, sizeof(BITMAP), (PTSTR)&bm);//调整按钮大小和最大图片一样大SetWindowPos(hwndOk, HWND_TOPMOST, 0, 0, bm.bmWidth, bm.bmHeight, SWP_NOZORDER | SWP_NOMOVE);SetWindowPos(hwndCanel, HWND_TOPMOST, 0, 0,  bm.bmWidth, bm.bmHeight, SWP_NOZORDER | SWP_NOMOVE);        }return (TRUE);case WM_CLOSE:        {            EndDialog(hDlg,0);        }return (TRUE);case WM_COMMAND:switch (LOWORD(wParam)){case IDCANCEL:            {                SendMessage(hDlg, WM_CLOSE, 0, 0);            }            return (TRUE);case IDOK:            {                            }return (TRUE);}return (FALSE);//自绘制按钮case WM_DRAWITEM://获得绘制结构体,包含绘制的按钮DC和当前按钮状态等pdis = (LPDRAWITEMSTRUCT)lParam;if (pdis->CtlType == ODT_BUTTON)//只绘制button类型{hdc = pdis->hDC;SaveDC(hdc);//保存DC,绘制完必须恢复默认//绘制默认状态hMemDc = CreateCompatibleDC(hdc);SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_U : hBitmapCANCEL_U);BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);DeleteDC(hMemDc);//绘制获取焦点时状态if (pdis->itemState & ODS_FOCUS){hMemDc = CreateCompatibleDC(hdc);SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_F : hBitmapCANCEL_F);BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);DeleteDC(hMemDc);}//绘制下压状态if (pdis->itemState & ODS_SELECTED){hMemDc = CreateCompatibleDC(hdc);SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_D : hBitmapCANCEL_D);BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);DeleteDC(hMemDc);}RestoreDC(hdc, -1);}return (TRUE);}return (FALSE);}
在WM_INITDIALOG函数中加载相关资源和设置按钮大小
在WM_DRAWITEM完成主要绘制工作,获得WM_DRAWITEM消息时获得绘制的结构体,这个结构体包括当前要绘制的按钮的ID、状态等,我们主要的工作就是将对应状态的按钮贴上相应的位图即可。
效果如下
WM_DRAWITEM消息控件重绘是最常用的重绘技巧,在网上常见的别人封装好的自定义控件都是这样的原理。

3.控件重绘(控件子类化)

子类化是借鉴C++的面向对象中的继承和重载的思想,基本意思就是如果子类对消息处理了的话对应C++的重载,这时候父类就没办法再处理这个消息,除非人为的将消息传递给父类,所有的消息先流经子类再到父类,当然这一过程需要子类的配合,具体意思我们用代码来说明。
同样是达到上一节WM_DRAWITEM绘制的按钮效果
我们用控件子类化完成这一效果,贴出部分代码,完整代码请下载演示文件
对话框过程函数
WNDPROC btnOkOldProc, btnCancelOldProc;BOOL CALLBACK SubclassProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){static HWND   hwndOk;static HWND    hwndCanel;switch(message){case WM_INITDIALOG:        {hwndOk = GetDlgItem(hDlg, IDOK);hwndCanel = GetDlgItem(hDlg, IDCANCEL);//窗口子类化btnOkOldProc = SetWindowLong(hwndOk, GWL_WNDPROC, (LONG)BtnProc);btnCancelOldProc = SetWindowLong(hwndCanel, GWL_WNDPROC, (LONG)BtnProc);        }return (TRUE);case WM_CLOSE:        {            EndDialog(hDlg,0);        }return (TRUE);case WM_COMMAND:switch (LOWORD(wParam)){case IDCANCEL:            {                SendMessage(hDlg, WM_CLOSE, 0, 0);            }            return (TRUE);case IDOK:            {                            }return (TRUE);}return (FALSE);}return (FALSE);}
按钮过程函数(子类)
typedef enum tagBUTTONSTATE{BTNSTATE_DEFAULT=0,BTNSTATE_FOCUS,BTNSTATE_SELECTED}BUTTONSTATE;LRESULT CALLBACK  BtnProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){    HDC hdc;HDC hMemDc;    PAINTSTRUCT ps;static int      id;static HINSTANCE hInstance;static HBITMAP hBitmapOK_D;static HBITMAP hBitmapOK_U;static HBITMAP hBitmapOK_F;static HBITMAP hBitmapCANCEL_D;static HBITMAP hBitmapCANCEL_U;static HBITMAP hBitmapCANCEL_F;static BITMAP  bm;static BOOL   bOnce = TRUE;static BUTTONSTATE btnOkState=BTNSTATE_FOCUS;static BUTTONSTATE btnCancelState=BTNSTATE_DEFAULT;id = GetWindowLong(hwnd, GWL_ID);//初次进入函数加载资源,模拟WM_CREATEif (TRUE == bOnce){hInstance = GetWindowLong(hwnd, GWL_HINSTANCE);hBitmapOK_D = LoadBitmap(hInstance, TEXT("image1d"));hBitmapOK_U = LoadBitmap(hInstance, TEXT("image1u"));hBitmapOK_F = LoadBitmap(hInstance, TEXT("image1f"));hBitmapCANCEL_D = LoadBitmap(hInstance, TEXT("image2d"));hBitmapCANCEL_U = LoadBitmap(hInstance, TEXT("image2u"));hBitmapCANCEL_F = LoadBitmap(hInstance, TEXT("image2f"));GetObject(hBitmapCANCEL_D, sizeof(BITMAP), (PTSTR)&bm);SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, bm.bmWidth, bm.bmHeight, SWP_NOZORDER | SWP_NOMOVE);bOnce = FALSE;}    switch (message)    {    case WM_CREATE://注意这个消息不会进入        return (0);    case WM_PAINT:        hdc = BeginPaint (hwnd, &ps);hMemDc = CreateCompatibleDC(hdc);//绘制不同状态下的按钮样式if (btnOkState == BTNSTATE_DEFAULT && id == IDOK){SelectObject(hMemDc, hBitmapOK_U);}if(btnCancelState == BTNSTATE_DEFAULT && id==IDCANCEL){SelectObject(hMemDc, hBitmapCANCEL_U);}if (btnOkState == BTNSTATE_FOCUS && id==IDOK){SelectObject(hMemDc, hBitmapOK_F);}if(btnCancelState == BTNSTATE_FOCUS && id==IDCANCEL){SelectObject(hMemDc, hBitmapCANCEL_F);}if (btnOkState == BTNSTATE_SELECTED && id==IDOK){SelectObject(hMemDc, hBitmapOK_D);}if(btnCancelState == BTNSTATE_SELECTED && id==IDCANCEL){SelectObject(hMemDc, hBitmapCANCEL_D);}BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);DeleteDC(hMemDc);        EndPaint (hwnd, &ps);        return (0);case WM_SETFOCUS:if (id==IDOK){btnOkState = BTNSTATE_FOCUS;}else{btnCancelState = BTNSTATE_FOCUS;}return (0);case WM_KILLFOCUS:if (id==IDOK){btnOkState = BTNSTATE_DEFAULT;}else{btnCancelState = BTNSTATE_DEFAULT;}return (0);case WM_KEYDOWN:   if (wParam == VK_SPACE){if (id==IDOK){btnOkState = BTNSTATE_SELECTED;}else{btnCancelState = BTNSTATE_SELECTED;}InvalidateRect(hwnd, NULL, TRUE);} return (0);case WM_KEYUP:if (wParam == VK_SPACE){if (id==IDOK){btnOkState = BTNSTATE_FOCUS;}else{btnCancelState = BTNSTATE_FOCUS;}InvalidateRect(hwnd, NULL, TRUE);}return (0);case WM_LBUTTONDOWN:SetCapture(hwnd);if (id==IDOK){btnOkState = BTNSTATE_SELECTED;}else{btnCancelState = BTNSTATE_SELECTED;}InvalidateRect(hwnd, NULL, TRUE);return (0);case WM_LBUTTONUP:if (id==IDOK){btnOkState = BTNSTATE_FOCUS;}else{btnCancelState = BTNSTATE_FOCUS;}InvalidateRect(hwnd, NULL, TRUE);ReleaseCapture();return (0);    case WM_DESTROY:        DestroyWindow(hwnd);        return (0);    }return CallWindowProc (id == IDOK ? btnOkOldProc : btnCancelOldProc, hwnd, message, wParam, lParam);}
在以上代码,我们在对话框的WM_INITDIALOG消息中强制换掉按钮原有的内建窗体过程函数,使用我们自己的BtnProc过程函数。需要注意的是在我们换掉
按钮原有的内建窗体过程函数的时候按钮已经创建完成,所以如果我们在BtnProc的WM_CREATE设置断点,程序是不会进入的。和WM_DRAWITEM一样,我们需要按钮的不同状态时绘制,因为我们采用自己的BtnProc过程函数,所以我们只能自己来维护按钮的状态,在WM_PAINT函数中根据不同状态绘制不同样式的按钮,在其他消息中处理按钮的按下或弹起、获得焦点、或失去焦点等状态转变。
创建效果如下

我们基本上模拟了WM_DRAWITEM消息重绘效果:按Tab键切换焦点,按Space键按钮按下弹起(当然只是为了演示原理,会有一些Bug,你可以想办法完善他们)。在上诉代码中,我们在最后调用了原来的内建的窗体过程函数,我们处理了WM_PAINT、WM_KEYUP、WM_KEYDOWN等消息,这些消息都return (0)直接返回了,即内建的窗体过程函数没有机会处理这些消息,其他的子类没有处理的消息都传给原来内建的窗体过程函数处理了,如果我们想原来的内建窗体过程函数也处理WM_PAINT,那么将return (0)改成break即可。这就是我上面提到的子类化的实现必须依靠子类化窗体函数的配合,我们也可以将所有的消息都在子类中处理不回传给原来的内建窗口,但是这样的工作量太大,一般是不会这样做的。
另外,可以看到相比于WM_DRAWITEM消息重绘,子类化实现控件重绘工作量要大得多,当然这样的灵活性要更大。实际上,微软提供子类化的作用更多是为了重新定制子控件的行为,比如说要将一组相同按钮按下时发送一个自定义消息,这时候就可以将这些按钮的消息子类化都先流经一个子类化窗体过程函数,然后再调用内建的窗体过程函数。

总结来说,一般重绘控件样式使用WM_DRAWITEM消息,重新定制控件行为使用窗体子类化

博客完整演示代码下载链接
毕竟现在来说如果不是为了实现一个自绘控件库的话,不会使用SDK自绘的方式,下一次我会讲一下MFC中自绘方式的具体流程和实例。

原创,转载请注明来自http://blog.csdn.net/wenzhou1219
8 0
原创粉丝点击