DirectX教程(2):创建一个窗口

  与消息框不同,没有可以调用的单个函数用来创建窗口。这里有两个原因:一、创建窗口创建需要太多的数据;二、窗口是基于事件的,而事件需要其他代码来处理。当事件发生时,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);
}

  如果我们运行这个程序,将得到如下的窗口:
windows

建立窗口

  创建窗口的代码几乎是差不多的,当你开始实际的游戏编程时,你可以稍微清理其中的一些内容,但现在,我们将学习它的每个部分的功能。
  在上面的程序代码中,只有三个步骤是用于创建窗口的,其余的则是用于保持窗口的运行,这三个步骤是: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()函数主要是在处理这些消息时需要运行的代码。

扫描二维码关注公众号,回复: 10999387 查看本文章

  主循环

  主循环包括三个函数,每个函数实际上都非常简单,我们将简要介绍它们。下面是主循环部分的代码:

    // 这个结构体包含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”的消息。将它放在最后就可以捕获我们错过的任何消息。

发布了19 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/hjc132/article/details/104781000