与消息框不同,没有可以调用的单个函数用来创建窗口。这里有两个原因:一、创建窗口创建需要太多的数据;二、窗口是基于事件的,而事件需要其他代码来处理。当事件发生时,Windows便发送一个消息到我们的程序,然后由WindProc()
函数进行处理。
这一节将分三个部分进行介绍。首先,我们将展示用于创建窗口的代码,然后将详细解析程序中两个重要的部分,以了解它们的工作方式以及在必要时如何进行操作。
我们的第一个窗口程序
同前面一样,我们将用一个WinMain()
函数来开始我们的程序。此外,我们还将使用一个称为WinProc()
的函数,这个函数用于处理Windows在程序运行期间发送给我们的任何事件消息。
下面是一个包含构建和运行窗口的程序代码:
// include the basic windows header file
#include <windows.h>
#include <windowsx.h>
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// the handle for the window, filled by a function
HWND hWnd;
// this struct holds information for the window class
WNDCLASSEX wc;
// clear out the window class for use
ZeroMemory(&wc, sizeof(WNDCLASSEX));
// fill in the struct with the needed information
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpszClassName = L"WindowClass1";
// register the window class
RegisterClassEx(&wc);
// create the window and use the result as the handle
hWnd = CreateWindowEx(NULL,
L"WindowClass1", // name of the window class
L"Our First Windowed Program", // title of the window
WS_OVERLAPPEDWINDOW, // window style
300, // x-position of the window
300, // y-position of the window
500, // width of the window
400, // height of the window
NULL, // we have no parent window, NULL
NULL, // we aren't using menus, NULL
hInstance, // application handle
NULL); // used with multiple windows, NULL
// display the window on the screen
ShowWindow(hWnd, nCmdShow);
// enter the main loop:
// this struct holds Windows event messages
MSG msg;
// wait for the next message in the queue, store the result in 'msg'
while(GetMessage(&msg, NULL, 0, 0))
{
// translate keystroke messages into the right format
TranslateMessage(&msg);
// send the message to the WindowProc function
DispatchMessage(&msg);
}
// return this part of the WM_QUIT message to Windows
return msg.wParam;
}
// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// sort through and find what code to run for the message given
switch(message)
{
// this message is read when the window is closed
case WM_DESTROY:
{
// close the application entirely
PostQuitMessage(0);
return 0;
} break;
}
// Handle any messages the switch statement didn't
return DefWindowProc (hWnd, message, wParam, lParam);
}
如果我们运行这个程序,将得到如下的窗口:
建立窗口
创建窗口的代码几乎是差不多的,当你开始实际的游戏编程时,你可以稍微清理其中的一些内容,但现在,我们将学习它的每个部分的功能。
在上面的程序代码中,只有三个步骤是用于创建窗口的,其余的则是用于保持窗口的运行,这三个步骤是:1、 注册窗口类;2、 创建窗口;3、显示窗口。实际上这三个步骤主要体现在以下的三个函数中:
RegisterClassEx();
CreateWindowEx();
ShowWindow();
注册窗口类
窗口类是Windows用于处理各种窗口的属性和行为的基本结构,我们不必去深究它的细节,但是需要知道,窗口类不同于C++中“类”的概念。实际上,窗口类是窗口的某些属性的一种模板。如下图所示,对窗口类进行了说明:
如上图所示,“window class 1”用于定义“windows 1”和“window 2”的基本属性,“window class 2”用于定义“windows 3”和“window 4”的基本属性。每个窗口都有其自己的属性,例如窗口的大小、位置、内容等,但它们都具有其所属的窗口类的基本属性。
在这一步中,我们将注册一个窗口类。这意味着,Windows将根据我们提供数据创建一个窗口类。为此,我们的程序中会包含以下代码:
// 这个结构体用于保存窗口类相关的信息
WNDCLASSEX wc;
// 清空窗口类以供使用
ZeroMemory(&wc, sizeof(WNDCLASSEX));
// 在结构体中填写所需的窗口类信息
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpszClassName = L"WindowClass1";
// 注册这个窗口类
RegisterClassEx(&wc);
下面将详细介绍每行代码的作用:
代码 | 作用 |
---|---|
WNDCLASSEX wc; | 这是一个包含窗口类信息的结构体,我们不会介绍它的所有内容,只介绍游戏编程相关的一些内容。为了方便起见,我们将这种将这种结构体称为“wc”。顺便提一句,这里的“EX”表示它是结构体WNDCLASS的扩展版本,它们两者基本相同,但是缺少一些额外的内容。 |
ZeroMemory(&wc, sizeof(WNDCLASSEX)); | ZeroMemory()函数的作用是将整个内存块初始化为NULL,第一个参数&wc提供的地址表示这个块开始的位置,第二个参数sizeof(WNDCLASSEX)表示这个块的长度。 |
wc.cbSize = sizeof(WNDCLASSEX); | 这个成员表示需要调整的结构体的大小,这里采用sizeof()运算符来进行计算。 |
wc.style = CS_HREDRAW or CS_VREDRAW; | 这个成员表示窗口的样式。我们可以传入很多值,但是在游戏编程中我们基本不会用到。这里我们采用的值是CS_HREDRAW和CS_VREDRAW 的或运算,它们告诉WIndows在垂直或水平方向移动窗口时重绘该窗口,这对窗口有用,但是对游戏没什么用。当我们进行全屏游戏时,这个值会被重置。 |
wc.lpfnWndProc = WindowProc; | 这个成员告诉窗口类,当从WIndows收到一条消息时,应该调用哪个函数。在我们的程序中,调用的函数的WindowProc(),另外如果适合的话,还可能是WndProc()或者WinProc()甚至是ASDF() 。我们只需要将这个值告诉窗口类,至于如何调用我们并不关心。 |
wc.hInstance = hInstance; | 这个成员在上一节介绍过,它是一个应用程序副本。在这里我们只需要将Windows传递给WinMain()函数的hinstance值赋给它就行。 |
wc.hCursor = LoadCursor(NULL, IDC_ARROW); | 这个成员存储窗口类的默认鼠标图像。这可以通过使用LoadCursor()函数的返回值来完成,该函数具有两个参数:第一个参数是存储指针图像的应用程序的hInstance,这里没有涉及,因此为NULL;第二个参数是包含默认鼠标指针的值。 |
wc.lpszClassName = L"WindowClass1"; | 这是我们正在创建的窗口类的名称,将其命名为"WindowsClass 1"。字符串前面的“L”告诉编译器该字符串由16位Unicode字符组成,而不是由通常的8位ANSI字符组成。 |
RegisterClassEx(&wc); | 这个函数最后注册窗口类,我们传入一个填满窗口类信息的结构体的地址,剩下的工作由Windows来完成。 |
创建窗口
下一步是创建一个窗口。现在我们已经创建了窗口类,我们可以基于该类来创建窗口了。我们只需要一个窗口,因此不会很复杂。要创建窗口,我们需要以下的代码:
// 创建窗口,并将返回的结果作为句柄
hWnd = CreateWindowEx(NULL,
L"WindowClass1", // 窗口类的名字
L"Our First Windowed Program", // 窗口的标题
WS_OVERLAPPEDWINDOW, // 窗口的样式
300, // 窗口的x坐标
300, // 窗口的y坐标
500, // 窗口的宽度
400, // 窗口的高度
NULL, // 我们没有父窗口,设置为NULL
NULL, // 我们不使用菜单,设置为NULL
hInstance, // 应用程序句柄
NULL); // 与多个窗口一起使用,设置为NULL
这个函数可以创建一个窗口,它有很多参数,但是都很简单,下面我们来介绍一下它的原型:
HWND CreateWindowEx(DWORD dwExStyle,
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam);
参数 | 作用 |
---|---|
DWORD dwExStyle | 这个参数是第四个参数dwStyle的扩展,只是为窗口样式添加了更多的选项。在这里我们设置为NULL。 |
LPCTSTR lpClassName | 这个参数表示我们的窗口将使用的窗口类的名称,这里我们使用刚刚设置的窗口类名:L"WindowClass1" |
LPCTSTR lpWindowName | 这是窗口的名称,它将显示在窗口的标题栏中,这里也使用Unicode。 |
DWORD dwStyle | 这个参数可以为窗口定义各种选项,例如你可以执行以下操作:去掉最小化和最大化按钮、使其不可调整大小、具有滚动条以及各种很酷的功能。在这里我们使用WS_OVERLAPPEDWINDOW这单个值,这个值包含了其他值,这些值共同构成具有标准功能的基本窗口。 |
HWND hWndParent | 这个参数告诉Windows是哪个父窗口创建了我们正在创建的窗口,父窗口是指包含了其他窗口的窗口,我们的程序中没有任何父窗口,所以设置为NULL。 |
HMENU hMenu | 这是菜单栏的句柄,我们没有任何菜单栏,所以设置为NULL。 |
HINSTANCE hInstance | 这是实例的句柄,所以将其设置为hInstance。 |
LPVOID lpParam | 如果要创建多个窗口,将使用此参数,我们没有多个窗口,所以设置为NULL。 |
Return Value | 函数的返回值是Windows为新窗口分配的句柄,我们将这个句柄存储在hWnd 变量中。 |
显示窗口
显示窗口比创建消息框更加容易,该函数原型如下:
BOOL ShowWindow(HWND hWnd,
int nCmdShow);
参数 | 作用 |
---|---|
HWND hWnd | 这个参数是我们刚刚创建的窗口的句柄,因此我们将Windows分配给窗口的值赋给它。 |
int nCmdShow | 还记得WinMain()函数的最后一个参数吗?这里将用到它,让Windows告诉我们该怎么显示。在游戏中,这个值并不重要,因为你的窗口将全屏显示。 |
至此,我们创建了一个窗口,但是运行一个窗口所需的还远不止这些,下面我们将进入程序的下一部分:WinProc()函数
和消息循环。
处理Windows事件和消息
创建窗口之后,我们需要保持窗口运行,这样才能与它进行交互。如果我们什么都不做,只是在WinMain()函数
的结尾处创建和销毁窗口,那么我们就只能看到一闪而过的窗口。我们并不是直接退出,而是在创建完窗口之后,进入到主循环中。正如我们前面所讲,Windows编程是基于事件的,这就意味着,只有在Windows允许我们执行操作时,我们的窗口才需要执行某些操作,其他时间我们只需要等待即可。
当Windows向我们传递消息时,会马上发生一系列事。消息是放在事件队列中的,我们用GetMessage()
来从队列中检索该消息,使用TranslateMessage()
来处理某些消息格式,通过DispatchMessage()
将消息分派到WindowsProc()
函数中进行处理,然后运行相应的代码作为消息的结果。下图整个事件消息的处理流程:
程序剩下的另一半是事件的处理,它主要分为两个部分:1、主循环;2、WindowProc()函数
主循环由getMessage(), TranslateMessage()和 DispatchMessage()
这三个函数组成。WindowProc()
函数主要是在处理这些消息时需要运行的代码。
主循环
主循环包括三个函数,每个函数实际上都非常简单,我们将简要介绍它们。下面是主循环部分的代码:
// 这个结构体包含Windows事件消息
MSG msg;
// 等待队列中的下一条消息,并将结果存在“msg”中
while(GetMessage(&msg, NULL, 0, 0))
{
// 将敲击键盘的消息转换成正确的格式
TranslateMessage(&msg);
// 将这个消息发送到WindowProc()函数
DispatchMessage(&msg);
}
MSG msg;
:MSG是一种结构体,其中包含了单个事件消息的所有数据。通常你不需要直接访问这个结构体的内容,但我们还是会介绍一下其中包含的内容:
成员 | 描述 |
---|---|
HWND hWnd | 包含接收消息的窗口的句柄 |
UINT message | 包含已发送消息的标识符 |
WPARAM wParam | 包含有关消息的其他信息,具体内容取决与发送的消息 |
LPARAM lParam | 与WPARAM相同,只是包含更多的信息 |
DWORD time | 包含发布在事件队列中的消息的确切时间 |
POINT pt | 包含发布消息时鼠标的确切位置,用屏幕坐标表示 |
while(GetMessage(&msg, NULL, 0, 0))
:GetMessage()函数的作用是从消息队列中获取所有信息并送入msg结构体。它总是返回TRUE,除非程序将要退出,它将返回FALSE。这样,只有当程序完全完成之后,while()循环才会中断。GetMessage()的函数原型如下:
BOOL GetMessage(LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax);
参数 | 意义 |
---|---|
LPMSG lpMsg | 这个参数是一个指向消息结构体的指针 |
HWND hWnd | 这个参数是一个句柄,表示消息来自哪个窗口。NULL表示获取的下一条消息可以来自任意窗口。我们也可以将值hWnd放在此处,在本程序中不会有任何区别,但如果我们有多个窗口,那么就有区别了。 |
UINT wMsgFilterMin, UINT wMsgFilterMax | 这两个参数用于限制要从消息队列中检索的消息类型,这里设置为0表示我们希望收集任何消息,无论其值是什么。 |
TranslateMessage(&msg);
:TranslateMessage()函数的作用是将某些按键转换为正确的格式。这个转换时自动执行的,因此我们不必要太关心细节。
DispatchMessage(&msg);
:DispatchMessage()函数的作用就是分派消息,它将消息分派到WindowProc()函数,这个函数只有一个参数,即msg结构体的地址。
WindowProc()函数
主循环执行的操作是获取一条消息、然后对其进行翻译,最后DispatchMessage()
将消息分派到适当的WindowProc()
函数进行处理。本程序中只有一个WindowProc()
函数,因此相对比较简单。当调用这个函数时,将传入来自MSG
结构体的4个成员,其函数原型如下:
LRESULT CALLBACK WindowProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
当消息进入WindowProc()
函数时,我们可以通过这4个参数来确定它是什么类型的消息。很多程序员在这里使用switch
语句来确定消息类型,通常采用如下示例的代码:
// sort through and find what code to run for the message given
switch(message)
{
// this message is read when the window is closed
case WM_DESTROY:
{
// ...
// ...
} break;
}
通过swich
语句来查找将要运行的代码。在本程序中,只提供了WM_DESTORY
消息,这意味着,如果发送其他任何消息,我们都会忽略。只有当窗口关闭时才会发送WM_DESTOR
,因此,当窗口关闭时,我们可以执行清理应用程序所需的任何操作,最后返回0,表示已清除所有内容。具体代码如下:
case WM_DESTROY:
{
// close the application entirely
PostQuitMessage(0);
return 0;
} break;
PostQuitMessage()
函数会发送一个WM_QUIT
消息,该消息的整数值为“0”。回顾前面介绍的主循环中的GetMessage()
函数,它仅在程序退出时才会返回False,PostQuitMessage()
函数发送一个WM_QUIT消息,结束整个主循环。最后我们向Windows返回一个“0”,表示我们已经处理了该消息。如果返回别的内容,Windows将无法进行处理。
WindowProc()
函数的末尾还调用了一个DefWindowProc()
函数,它的主要作用是处理我们未处理的其他信息,简言之,就是处理我们没有返回“0”的消息。将它放在最后就可以捕获我们错过的任何消息。