在 WINAPI 中创建窗口程序的一般算法:
那么,现在,应该足够难了。因为有各种的函数,这些函数还有一堆设置、类,这些是你应该知道、理解的,以及一些其他的C和C++语言的知识点。
首先,我们先来描述一下 WinMain() 函数的整体结构:
#include
int WINAPI WinMain(HINSTANCE hInst,
HINSTANCE hPreviousInst,
LPSTR lpCommandLine,
int nCommandShow)
{
// 创建一个窗口句柄
// 创建一个窗口类
// 创建一个窗口,在屏幕上显示它
// 返回值,如果失败的话,或者你退出的时候。
}
首先,创建一个窗口句柄:
HWND hMainWnd; // 创建其他将来可能存在的窗口的句柄
要初始化该窗口类字段,你需要创建它的一个实例,然后在它的类字段中填充。
windows.h 中这个类的声明如下:
struct tagWNDCLASSEX
{
UINT cbSize; // 结构体大小(以byte为单位)
UINT style; // 窗口类风格
WNDPROC WndProc; // 指向一个用户定义的函数名的指针
int cbWndExtra; // 在结构体最后被释放的字节数
int cbClsExtra; // 创建程序实例时释放的字节数
HICON hIcon; // 图标描述符
HICON hIconMini; // 托盘中的小图标描述符
HCURSOR hCursor; // 鼠标光标描述符
HBRUSH hbrBack; // 窗口背景色的画笔描述符
HINSTANCE hInst; // 应用程序实例的描述符
LPCTSTR lpszClassName; // 指向包含类名称字符串的常量指针
LPCTSTR lpszMenuName; // 指向包含给类使用的菜单名称字符串的常量指针
}WNDCLASSEX;
因此,我们需要创建一个类型为 WNDCLASSEX 的变量,通常命名为 wc,然后来初始化类字段,像这样:
WNDCLASSEX wc; // 创建一个实例来获取类成员WNDCLASSEX
wc.cbSize = sizeof(wc); // 结构体的大小(以字节为单位)
wc.lpfnWndProc = WndProc; // 指向一个用户定义的函数的指针
// 其他代码
如果我们以后要使用这个类的话,上面这些代码是必要的。这将成为创建一堆窗口的模板。当然,我们一开始并不需要那么多窗口。然而,注册窗口是绝对必要的!我们甚至可以在初始化字段中设置默认设置,而不是设计什么是窗口(在下一章中,我们将讨论在初始化类字段中必要的参数)。
所以,再说一遍:类中的任何一部分未被初始化的话,窗口都不会被创建。要验证这件事,我们可以用一个很有用的函数 RegisterClassEx()。这是我们填充完 WNDCLASSEX 类字段的下一步:强制检查类的注册:
if (!RegisterClassEx(&wc))
{ // 万一注册失败的话
MessageBox(NULL, L"Cant register the class!",
L"Error", MB_OK);
return NULL; // 返回,退出 WinMain
}
接下来,我们要在 WinMain() 函数中做的事情——调用 CreateWindow() 函数,分配一个描述符值给它,这个值我们已经在第一部分中创建了。代码如下:
hMainWnd = CreateWindow(szClassName, // 类名称
L"Full windows procedure", // 窗口名称(位于标题栏)
WS_OVERLAPPEDWINDOW | WS_VSCROLL, // 窗口显示模式
CW_USEDEFAULT, // x轴的窗口位置(默认)
NULL, // y轴的窗口位置(默认)
CW_USEDEFAULT, // 窗口宽度(默认)
NULL, // 窗口高度(默认)
HWND(NULL), // 父窗口句柄(我们没有父窗口)
NULL, // 菜单栏描述符(我们没有)
HINSTANCE(hInst), // 应用程序实例描述符
NULL); // 我们不从WndProc函数中共享任何东西
在windows.h文件中CreateWindow() 函数的描述如下:
HWND CreateWindow(
LPCTSTR lpClassName, // 类名称
LPCTSTR lpWindowName, // 窗口名称(位于上方)
DWORD dwStyle, // 窗口样式
int x, // 窗口位置x值
int y, // 窗口位置y值
int nWidth, // 窗口宽度
int nHeight, // 窗口高度
HWND hWndParent, // 父窗口句柄
HMENU hMenu, // 菜单栏描述符
HINSTANCE hInst, // 应用程序实例
LPVOID lParam); // 指向从用户定义的函数中传输过来的数据的指针
如果成功的话,hMainWnd函数将不返回NULL值。小白都猜得到在后台检查一下这个值是必要的(就像RegisterClassEx()):
if (!hMainWnd)
{
// 如果创建窗口失败的话(例如有错误的参数什么的)
MessageBox(NULL, L"Can't create the window!", L"Error", MB_OK);
return NULL; // 返回,退出 WinMain
}
然后,我们需要调用两个函数:
ShowWindow(hMainWnd, nCommmandShow);
UpdateWindow(hMainWnd);
没必要详细讨论这俩函数了,他们没几个参数。
第一个函数在PC显示器上显示了一个消息框。它的第一个参数——窗口描述符(通过调用CreateWindow()返回)。第二个参数是显示样式。当你第一次运行该窗口时,它会跟WinMain()函数中的最后一个参数相同,下一次运行该窗口时,你可以输入你需要的值。第二个函数,就像它的函数名提示的那样,负责当窗口最小化或者其他动态信息传来时,刷新窗口。那么,是时候写一个循环来返回函数的值了:
while (GetMessage(&msg, NULL, NULL, NULL))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
最后,修改一下上一个(第一章中)输出信息的程序:
#include
int WINAPI WinMain(HINSTANCE hInst, // 应用程序描述符
HINSTANCE hPreviousInst, // 在Win32中不使用,但是需要声明
LPSTR lpCommandLine, // 需要在命令行模式下使用
int nCommandShow) // 窗口显示模式
{
int result = MessageBox(NULL, L"Do you like WINAPI?!", L"Question", MB_ICONQUESTION | MB_YESNO);
switch (result)
{
case IDYES: MessageBox(NULL, L"Keep up the good work!!!", L"Answer", MB_OK | MB_ICONASTERISK);
break;
case IDNO: MessageBox(NULL, L"It's a pitt!!!", L"Answer", MB_OK | MB_ICONSTOP);
break;
}
return NULL;
}
最后一行的 WinMain() 包含两个函数调用,与我们的 WndProc() 函数通信[ interacting with ],返回声明运算符[ statement operator ]。WndProc() 函数的设计应该像下面这样:
LRESULT CALLBACK WndProc(HWND hWnd, // 窗口描述符
UINT uMsg, // 由操作系统发送的消息
WPARAM wParam, // 参数
LPARAM lParam) //
{
// 1) 创建必要的变量
// 2) 写下 发出要求行为的必要条件
// 3) 返回函数的值
}
LRESULT ——它是返回值。CALLBACK 应该被写上,因为这是个回调函数。首先,你需要创建必要的变量。首先,创建一个设备描述表[ device context 的公认译文 ]的实例 HDC,来标识窗口中正确的文本方向。此外,对于窗口进程,我们需要另外两个变量 RECT(矩形窗口区域) 和 PAINTSTRUCT (保存了窗口绘制信息):
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
要改变文本颜色(不仅限于改变文本颜色),你需要创建一个类型为COLORREF的变量,并且利用三个参数以及函数 RGB() 的返回值给它赋值。
COLORREF colorText = RGB(255, 0, 0); // 参数为整型
该函数把整型的色彩亮度转化,并返回三原色(红绿蓝)的混合亮度值。于是乎,通过这三个颜色等级,可以创建出 255×255×155=16’581’375种颜色。
在每个应用程序中,Windows操作系统和我们程序中各种各样的函数每秒钟会发送数以千计的消息。这些消息可以作为键盘或鼠标按键处理的结果被发送。在这些情况下,我们有一个结构体 MSG,在 WinMain() 中描述过,它存储了这些消息的信息。在 WndProc() 函数中,对这些消息有相应的条件处理[ condition ]。
举个栗子,如果操作系统发送一个消息 WM_PAINT ,那么就该有什么东西被绘制在窗口中。这些消息被条件选择结构处理 switch() (多选操作符),它的参数是 uMsg。 uMsg 是我们在声明 WndProc() 函数时就声明过的。声明中的主要变量,没有这个变量,窗口将不会在被折叠或者关闭 WM_DESTROY 时刷新。同样地, WM_PAINT 是在客户区中绘制的必要消息。WM来自单词Window Message的缩写。亦即:
switch (uMsg)
{
case WM_PAINT:
// 绘制行为
case WM_DESTROY:
// 我们明确定义的当窗口关闭时的操作
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
使用以上这些例子,我们该做些什么呢?当发送一个 WM_PAINT 消息时,我们可以调用绘制函数, BeginPaint(), GetClientRect(), SetTextColor(), DrawText(), EndPaint()。这些函数的名称很明确地表示了这些函数的作用。
BeginPaint() 函数,顾名思义,开始绘制。因此,有必要让这个函数获得窗口的句柄以及一个 PAINTSTRUCT对象(我们定义为ps)。它返回一个类型为 HDC的值,所以,我们需要给它一个hDC。
GetClientRect() 选择了一个区域。它的参数与上一个函数相似:窗口句柄,以及一个指向RECT对象类(我们定义为rect)的指针。SetTextColor() 返回文本的颜色。它的参数有:BeginPaint()函数的返回值 hDC 和一个指向 COLORREF 类对象的指针。我们甚至不能单独设置文本的颜色,创建一个变量的 colorText,但是在这里可以。然而,就代码的可读性和可理解性来说,这是个很基础的错误。尽可能地分开声明变量并且写好注释,为什么需要这些变量,这样你就不会在一段时间之后对这些变量有任何疑问了。
同时,注意观察程序中的匈牙利表示法[ Hungarian notation ],其精髓在于:变量名应该与他们的数据类型和意义有关。函数DrawText()的声明如下:
int DrawText(
HDC hDC, // 设备描述表
LPCTSTR lpchText, // 字符串指针
int nCount, // 文本长度(如果为-1,那么就定义为它自己)
LPRECT lpRect, // 指向RECT对象的指针
UINT uFormat // 文本显示格式
);
前四个参数都有一个明确的意义。第四个uFormat,有几种类型。通常使用DT_SINGLELINE, DT_CENTER 和 DT_VCENTER把文本显示在中心位置、显示为一行。当然你也可以用其他选项。EndPaint() 函数需要两个参数:窗口句柄和对象ps。你注意到它和 BeginPaint() 的相似了吗?你知道当你调用 WM_PAINT 时该做什么了吗(不要忘记在后面加上break)? WM_DESTROY 是函数 DestroyWindow() 发送的,当我们关闭窗口时就会发送。在默认状态下,将调用函数 DefWindowProc() ,它的参数与 WndProc() 一样。在选择结构体中, WM_DESTROY 必须包含一个 PostQuitMessage()函数,该函数发送给 WinMain() 消息 WM_QUIT 。它的设置通常为NULL,中断主函数 WinMain()。
VOID WINAPI PostQuitMessage(int nExitCode);
随着 switch() 的结束,我们利用函数 WndProc() 返回一个空值。然后,我们将回到 WinMain() ,回到消息的处理者。
while (GetMessage(&msg, NULL, NULL, NULL))
{
TranslageMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
GetMessage() 函数描述如下:
BOOL WINAPI GetMessage(LPMSG lpMsg, // 指向结构体MSG的指针
HWND hWnd, // 窗口句柄
UINT wMsgFilterMin, // 过滤器
UINT wMsgFilterMax // 消息样本的过滤器
);
它处理来自操作系统的消息。第一个参数是结构体MSG的地址,存储了下一条消息。第二个参数,窗口句柄。第三个和第四个参数提示程序消息的筛选。通常,定义了零值和任何应该从消息队列中移除的值函数。当循环体收到消息 WM_QUIT 时停止。在这时,它将返回 FALSE,我们将退出程序。TranslateMessage() 和 DispatchMessage() 函数需要在循环中翻译消息。通常被用来处理键盘按键按下消息中。在循环体的之后,我们返回操作系统的返回代码 msg.wParam。最后的最后,我们将得到下面这段代码:
#include
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
TCHAR mainMessage[] = L"Some text to display!";
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
TCHAR szClassName[] = L"My class";
HWND hMainWnd;
MSG msg;
WNDCLASSEX wc;
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.lpszMenuName = NULL;
wc.lpszClassName = szClassName;
wc.cbWndExtra = NULL;
wc.cbClsExtra = NULL;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hIconSm = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.hInstance = hInst;
if (!RegisterClassEx(&wc))
{
MessageBox(NULL, L"Can't register the class!", L"Error", MB_OK);
return NULL;
}
hMainWnd = CreateWindow(
szClassName,
L"Window Procedure",
WS_OVERLAPPEDWINDOW | WS_VSCROLL,
CW_USEDEFAULT,
NULL,
CW_USEDEFAULT,
NULL,
(HWND)NULL,
NULL,
HINSTANCE(hInst),
NULL);
if (!hMainWnd)
{
MessageBox(NULL, L"Can't create the window!", L"Error", MB_OK);
return NULL;
}
ShowWindow(hMainWnd, nCmdShow);
UpdateWindow(hMainWnd);
while (GetMessage(&msg, NULL, NULL, NULL))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
COLORREF colorText = RGB(255, 0, 0);
switch (uMsg)
{
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
SetTextColor(hDC, colorText);
DrawText(hDC, mainMessage, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(NULL);
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return NULL;
}