概述
之前有学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)
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函数中加载相关资源和设置按钮大小
3.控件重绘(控件子类化)
子类化是借鉴C++的面向对象中的继承和重载的思想,基本意思就是如果子类对消息处理了的话对应C++的重载,这时候父类就没办法再处理这个消息,除非人为的将消息传递给父类,所有的消息先流经子类再到父类, 当然这一过程需要子类的配合,具体意思我们用代码来说明。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_CREATE
if (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函数中根据不同状态绘制不同样式的按钮,在其他消息中处理按钮的按下或弹起、获得焦点、或失去焦点等状态转变。
最后
以上就是无辜樱桃为你收集整理的深入Windows窗体原理及控件重绘技巧的全部内容,希望文章能够帮你解决深入Windows窗体原理及控件重绘技巧所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复