【DirectX教程】Lesson 3:创建一个窗口

Lesson 3:创建一个窗口

课程概览

很不幸运,不像消息框,没有单独的函数供给我们调用去创建窗口。这有两个原因。第一,Windows用单个函数调用需要太多的数据。第二,Windows是基于事件的,这需要额外的代码做处理。为了以防你忘记事件是一些发生在窗口上的行为,例如按钮点击,调整窗口大小,按键等等。当这些发生的时候,Windows给我们的程序发送一条消息,该消息会被对应的WinProc()函数处理。

本节课程分为3块。首先,看一个创建窗口的代码,然后仔细分析两个重要的部分去发现他们的工作机制并在必要的时候操纵它们。

第一个窗口化程序

正像之前的课程,我们将使用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);
}

哇奥!运行结果就像这样:


构建一个窗口

那么好吧,我承认刚写的程序不是可以直接就容易记住的。幸运的是,你不需要记住。你可能觉得看到的代码差不多都长一个样。在进入实际的游戏编程后你能够理清一些东西,但是现在让我们分析它的每个部分。

事实上上面的代码创建一个窗口只有3个步骤,剩下的是让它保持运行。步骤如下:

1.注册窗口类

2.创建窗口

3.显示窗口

这些步骤相当简单,除去变量和参数初始化,它真的可以归结如下:

RegisterClassEx();
CreateWindowEx();
ShowWindow();

那么好滴,让我们把细节也放进去来观察每一个步骤。

1.Registering the Window Class(注册窗口类)

简言之,一个窗口类是一个基本的用来处理不同窗口属性和动作的结构。我么不需要担心这个细节,但应该知道它不是C++类定义。基本上窗口类是一种针对窗口属性的模板。


上图中,“window class1”用来定义"window1"和"window2"的基础属性,"window class2"对"window3"和"window4"也是如此。每个窗口都有自己独立的属性,例如窗口大小,内容等等,但是基础的属性仍然是取决于"窗口类"。

这一步我们注册一个基于我们提供数据的窗口类。为了这个目的有了程序中的如下代码:

// 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);

让我们快速拆解代码细节。

WNDCLASSEX wc;

我们将会调用我们的结构"wc"。另外这个'EX'用来指明WNDCLASS结构是一个扩展版本。比老版本有更好的性能。除了一对额外的东东之外基本是相同的,这里我们不涉及。

ZeroMemory(&wc,sizeof(WINDCLASSEX));

ZeroMemory是一个把一整块内存初始化为NULL的函数。第一个参数提供的地址设置块开始的地方。第二个参数指定块的长度。

wc.cbSize=sizeof(WINDCLASSEX);

这很明显,我们需要估计这个结构的大小,我们通过sizeof()操作符达到这个目的。

wc.style=CS_HREDRAW | CS_VREDRAW;

在这个成员里我们存储窗口的风格。我们可以插入大量的值,但是在游戏编程中我们几乎用不到。你可以在MSDN中WNDCLASSEX下找到这些值。现在我们将用CS_HREDRAW和CS_VERDRAW逻辑与运算。他们告诉Windows如果垂直或平移窗口的时候重绘窗口。但是对于游戏不是很有用。我们稍后会在全屏游戏中重置这个值。

wc.lpfnWndProc = WindowProc;

这个值告诉窗口类在获得来自Windows的消息时调用哪个函数。在我们的程序中,这个函数是WindowProc(),但也可以是WndProc()甚至是ASDF()。

wc.hInstance = hInstance;

我们在上节课介绍了这个,他是指向我们应用副本的一个句柄。把Windows给我们的值放到WinMain()函数里。

wc.hCursor = LoadCursor(NULL, IDC_ARROW);

这个成员存储窗口类的默认指针图片。这点通过使用函数LoadCursor()来完成,这个函数有两个参数。第一个是应用的hInstance,他存储了图形指针。在这里我们把它设置为NULL。第二个值包含了默认的鼠标指针。

wc.hbrBackground = (HBRUSH)COLOR_WINDOW;

这个成员包含的“画笔”会给窗口背景上色。我的设置是给COLOR_WINDOW指定一个绘制白色的画笔。

wc.lpszClassName = L"WindowClass1";

这是构建的窗口类的名称。我们把它命名为"WindowClass1",你怎么命名它不重要,只要你在制作窗口的时候指定它就可以了。

出现在字符串之前的‘L’仅仅是告诉编译器这个字符串是以16位Unicode字符编码的而不是通常的8位ANSI字符。

RegisterClassEx(&wc);

这个函数最终注册窗口类。我们用上面设计的窗口类结构的地址作为它的唯一参数。其他的事由Windows来关心。简单,真实。

2.创建窗口

下一步是创建一个窗口。现在我们有自己制作的窗口类,我们可以制作基于该类的窗口。为了创建窗口我们只需要这么做:

// 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

干的漂亮。我说了谎。仅用一个函数创建窗口。这个窗口有很多参数,但是它们都很简单。在我们仔细检查他们之前让我们先瞅一下函数原型。

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,


第一个参数在RegisterClass()更新到RegisterClassEx()的时候被加入。只是添加了更多的窗口风格选项。这里我们设置该值为NULL。

LPCTSTR lpClassName,

这是我们的窗口将用的类的名称。因为我们只有一个类,就用L"WindowClass1"。这个字符串也用16位Unicode字符,因此我们在它前面放了‘L’。

LPCTSTR lpWindowName,

这是一个窗口的名字,它会显示在窗口的标题栏上。

DWORD dwStyle,

这里我们可以定义各种各样的窗口选项。你可以去掉最小化和最大化按钮,让它不可变大小。使它具有滚动条和其他很酷的东东。

我们将用单个值WS_OVERLAPPEDWINDOW,它是包含构建基本窗口标准特征的快捷方式。

HWND hWndParent,

这是告诉Windows哪个父窗口创建了现在的窗口的参数。父窗口就是包含其他窗口的窗口。这里我们设置参数值为NULL。

HMENU hMenu,

这是一个菜单栏句柄,和上面一样我们没有使用,所以值为NULL就好。

LPVOID lpParam

如果我们创建多窗口可能会使用这个参数。为了简单这里设置为NULL。

Return Value(返回值)

函数的返回值是一个句柄,指示Windows分配的新窗口。我们将直接把它存储到我们的hWnd变量

3.显示窗口

显示窗口比创建一个消息框更简单。他需要含有两个参数的函数。原型如下:

BOOL ShowWindow(HWND hWnd,
                int nCmdShow);

HWND hWnd,

这只是指向我么刚创建的窗口的句柄。

int nCmdShow

还记得WinMain()的最后一个参数吗?是滴,这是我们使用它的地方。当然我们不必如此,……

如果此时此刻你的程序还不能运行,那实际上是因为我们还没有完成。我的确已经创建了一个窗口。但是一个窗口需要的绝不止如此。我们继续前进到程序的下个部分,WinProc()函数和消息循环。

处理Windows事件和消息

一旦我们创建了自己的窗口,我们需要保持窗口运行以至于互动。目前为止假设它能够编译我们将会只看到一个一闪而过的窗口在达到WinMain()时就销毁了。

这不是退出,我们的程序完成了创建创建窗口。正如我们前面讨论的Windows编程是基于事件的。当Windows给我们传递一条消息时,立刻发生了一些事情。消息被放入消息队列。我们用GetMessage()函数去接受来自队列的消息,我们用TranslateMessage()去处理某个消息格式,我们用DispatchMessage()函数联系WindowsProc()函数并决定作为消息结果的运行代码。

跟着下面的图君让我们复习一下脑子里的脉络。

图片中的事情可以分为两个部分:

1.主循环

2.WindowProc()函数

主循环只是由GetMessgae(),TranslateMessage()和DispatchMessage()组成。WindowProc()函数仅包含某消息被发送后的运行代码。现在让我们挖掘了解它。

1.主循环

就如上图所示的那样,这部分仅由3个函数组成。每个函数其实都相当简单,我们虽然不讨论细节但是我们会扼要地介绍。

下面是主循环所需的代码:

// 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);
    }

拆解代码:

MSG msg;

MSG是一个包含所有关于事件消息数据的结构。你通常不需要直接访问这个结构的内容,但是我会让你一窥它的芳泽:

while(GetMessage(&msg, NULL, 0, 0))

GetMessage()是从消息队列中取出任意消息到msg结构的函数。他总是返回TRUE,除了程序即将退出的时候(此时返回FALSE)。无论如何当我们的程序完全完成的时候while()循环都会终断。

GetMessage()函数有4个参数去介绍,但是我们会首先介绍它的原型:

BOOL GetMessage(LPMSG lpMsg,
                HWND hWnd,
                UINT wMsgFilterMin,
                UINT wMsgFilterMax);

LPMSG lpMsg,

这个参数是指向消息结构的指针,我们会把我们自己的结构地址放到这里。

HWND hWnd,

这是消息来源窗口的句柄。但是你应该注意到我们在这里设置它为NULL,这个参数值意味着下条消息会是任意一个窗口。

我们可以真的把hWnd放在这里,这没有什么分别,但是如果是多窗口我会把参数值设为NULL。

UINT wMsgFilterMin, UINT wMsgFilterMax

这些参数可以用于限制检索消息队列的消息类型。例如:wMsgFilterMin中的WM_KEYFIRST和WM_KEYLAST用来限制键盘消息类型。WM_KEYFIRST 和 WM_KEYLAST是与第一个和最后一个键盘消息同义的整数值。同样的,WM_MOUSEFIRST 和 WM_MOUSELAST把消息类型限制为鼠标消息。

这些参数有一个特殊的情况。如果你把每个值都设为‘0’,那么GetMessage()认为你想要搜集任何信息,在我们的程序中就是这么做的。

TranslateMessage(&msg);

TranslateMessage(&msg)函数把某个按键翻译为合适的格式。他所有的东东都是自动做的,我们不追究的太深。他唯一的参数就是我们的msg结构的地址。

DispatchMessage(&msg);

DispatchMessage(&msg)名如其意。它是分配消息的。他分派给我们的WindowProc()函数。

2.WindowProc()函数

现在我们已经介绍了主循环,让我们挖掘这所谓的WindowProc()函数。

WindowProc()被调用时,从MSG结构传入4条信息。让我们看一下WindowProc()函数原型。

LRESULT CALLBACK WindowProc(HWND hWnd,
                            UINT message,
                            WPARAM wParam,
                            LPARAM lParam);


当一个消息进入WindowProc,我们能够用uMsg参数决定它是什么消息。很多程序员用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;
    }


这儿switch语句寻找要运行的代码。当然,我么仅提供了WM_DESTROY消息i,这意味着其他任何消息都会被忽略。WM_DESTROY消息在窗口被关闭的时候被发送。现在所有的事情都做完了我们返回一个‘0’,表示一切清理干净。代码如下:

case WM_DESTROY:
            {
                // close the application entirely
                PostQuitMessage(0);
                return 0;
            } break;

PostQuitMessage()函数发送WM_QUIT消息,它是以0为值的整数。如果你回调主循环,记住GetMessage()函数在程序即将退出(例如WM_QUIT)的时候只返回FALSE,所以这个函数主要做的就是告诉程序恰当地结束WinMain()函数。

接下来返回‘0’.这很重要,因为他告诉Windows我们处理过消息了。如果返回其它东东,Windows会困惑。

如果研究我们的程序你会发现有最后一个被描述的函数DefWindowProc()。这个函数处理我们没有自己处理的消息。简言之,它处理我们没有返回‘0’的消息。因此我们把它放在WindowProc()函数的最后,这样我们可以抓住我们错过的任何东东。

总结

如果你读到了这里,那么恭喜你你现在是一个Windows编程大师,游戏编程之路水到渠成。

不要期望能够完美地就记住程序,你可以根据需要回头咀嚼这些代码直到你完全熟悉了为止。

闲话短说,让我们继续前进!在正式搞DirectX之前有两个小课程,没有时间浪费了!





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值