Lesson 3: 创建窗口

原文地址:
http://www.directxtutorial.com/Lesson.aspx?lessonid=11-1-3

与Lesson2一样,我们使用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);
}

运行效果如下:
这里写图片描述

窗口创建四部曲:
设计->注册->创建->显示与更新

其实这些看似复杂的步骤也就归结为下列代码:

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

窗口类的注册:
这里的窗口类并不是C++中定义的类,而是window下的一种特定的模板,如图所示:
这里写图片描述
窗口类1用于定义窗口1和窗口2的基本属性,同样的,窗口类2用于定义窗口3和窗口4的基本属性。每个窗口都有自己独特的属性,例如窗口的大小、位置、内容等等,但基本属性仍是同一个窗口类中的。
在这一步,我们会注册一个窗口类,意味着我们告诉了Windows我们提供给这个窗口类的基本属性是什么。所以我们通过以下代码实现注册:

// 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;
这是一个结构体,包含了窗口类的信息。我们不需要设定它的全部内容,因为有一些在游戏编程里不需要用到。想要了解它的所完整结构,可以在MSDN中进行查阅。
另外,EX表明这是WNDCLASS的升级版。这可以同样推广到CreateWindowEx() and RegisterClassEx()这两个函数。

ZeroMemory(&wc, sizeof(WNDCLASSEX));
ZeroMemory这个函数用于初始化一个完整的内存块为NULL,第一个参数设置了内存块的起始位置,第二个参数表明了内存块的大小。很明显,这里的大小应该是WNDCLASSEX的结构体大小。

wc.cbSize = sizeof(WNDCLASSEX);
UINT类型的cbSize,表示该结构体的字节数大小。

wc.style = CS_HREDRAW | CS_VREDRAW;
UINT类型的style,指定这一类型窗口的风格样式。通常在游戏编程里不会用到。但我们现在使用CS_HREDRAW and logically OR it with CS_VREDRAW告诉Windows当窗口水平方向上的宽度以及垂直方向上的高度发生变化时需要重新绘制窗口。

wc.lpfnWndProc = WindowProc;
WNDPROC类型的lpfnWndProc,它是一个函数指针,这里指向窗口过程函数,而窗口过程函数是一个回调函数。这个值告诉窗口类当它得到一个消息时需要使用哪一个函数。

wc.hInstance = hInstance;
指定包含窗口过程的程序的实例句柄,只要把Windows在WinMain函数里传给我们的值赋给它就可以了。

wc.hCursor = LoadCursor(NULL, IDC_ARROW);
它表示窗口类的光标句柄,我们用LoadCursor函数来为其加载一个光标资源,返回系统分配给该光标的句柄。这个LoadCursor()函数有两个参数,第一个参数是pointer graphic的句柄,我们没有得到这个,因此设为NULL;第二个参数是默认的鼠标指针,我们一般把它设为默认的箭头光标IDC_ARROW

wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
HBRUSH类型的hbrBackground,指定窗口类的背景画刷句柄(handle to brush)。当窗口发生重绘时,系统使用这里指定的画刷来擦除窗口的背景。也可以把它取为一个标准的系统颜色(HBRUSH)COLOR_WINDOW,即白色。

wc.lpszClassName = L”WindowClass1”;
LPCTSTR类型的lpszClassName,这是我们创建的窗口类的名字,用来标识我们正在定义的这个窗口类。

在字符串前面出现的这个“L”,只是告诉编译器这个字符串是用16-bit Unicode 字符编码的,而不是常用的8-bit ANSI字符编码。

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

其中,使用到的CreateWindowEx函数它的参数有很多,在MSDN中的原型如下:

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,
DWORD类型的dwExStyle,用于指定创建的窗口样式,仅作为CreateWindow的升级版CreateWindowEx时出现在第一个参数中。这是第四个参数dwStyle的扩充,为窗口提供更多的样式。我们不再贴出它可选用的参数值了,这些都能在MSDN中找到。这个例子中我们设置为NULL。

LPCTSTR lpClassName,
LPCTSTR类型的lpClassName,指定对应窗口类的名称。我们刚刚注册了窗口类WindowClass1,那么我们现在想把它对应在WindowClass1则填写的是L”WindowClass1”。

LPCTSTR lpWindowName,
LPCTSTR类型的lpWindowName,用于指定创建的窗口名字,也就是显示在标题栏上的程序名字。同样使用Unicode编码。

DWORD dwStyle,
DWORD类型的dwStyle,用于指定创建的窗口样式。我们可以在MSDN中找到它可用的参数值。
在这里我们使用的是WS_OVERLAPPEDWINDOW,它是一个多种窗口类型的组合体。
int x,
用于指定窗口的水平位置。

int y,
用于指定窗口的竖直位置。

int nWidth,
用于指定窗口的宽度

int nHeight,
用于指定窗口的高度

HWND hWndParent,
HWND类型的hWndParent,用于指定被创建窗口的父窗口句柄。举个例子,Microsoft Word,可以在同一个窗口中打开多个文档,那么这就是由一个父窗口和多个子窗口组成的程序。这个例子中我们没有父窗口,设为NULL。

HMENU hMenu,
HMENU类型的hMenu,用于指定窗口菜单的资源句柄。我们没有使用任何菜单栏,设为NULL。

HINSTANCE hInstance,
HINSTANCE类型的hInstance,用于指定窗口所属的应用程序实例的句柄。

LPVOID lpParam
当我们创建了多个窗口时,我们需要用到这个参数作为WM_CREATE消息的附加参数lParam传入的数据指针。但我们这里没有使用到,设为NULL。

Return Value
返回值,如果窗口创建成功,CreatWindows函数将返回系统为该窗口分配的句柄;如果调用失败,即窗口创建失败,则会返回NULL。

最后一步!
窗口的显示:
窗口的显示要在消息盒的创建之前,它只通过一个带两个参数的函数完成。函数的原型如下:

BOOL ShowWindow(HWND hWnd,
                int nCmdShow);

HWND hWnd,
很明显这里就是我们刚刚创建的窗口的句柄。

int nCmdShow
用于指定窗口的显示状态,这里只要设置成WinMain函数传进来的nCmdShow参数就可以了。

补充:
更新窗口
在调用完ShowWindow函数之后,紧接着调用UpdateWindow来刷新窗口。
UpdateWindow函数的原型声明很简单,只有一个HWND类型的hWnd参数,指的是创建成功后的窗口句柄。
UpdateWindow将WM_PAINT消息直接发送给了窗口过程函数进行处理,而没有放到我们前面所说的消息队列里。
调用实例UpdateWindow(hWnd);

Handling Windows Events and Messages
处理Windows事件和消息:
当我们创建窗口之后,我们需要一直保持窗口的运行状态以实现交互。当然了,如果就只有上述的代码,程序不会编译成功。即使成功了,也只是创建的窗口闪烁了一下之后就消失了。
所以我们引入了消息循环体系,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中获取消息,有两个函数可供我们选择:GetMessage()和PeekMessage()
消息循环体系可以用下图来表明:
这里写图片描述

引用《Windows游戏编程之从零开始》p70-p71页里的一段话来说明整个体系:

过程<1>:操作系统接受到应用程序的窗口消息,并将消息投递到该应用程序的消息队列中。
过程<2>:应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,例如放弃对某些消息的响应,或者调用TranslateMessage产生新的消息。
过程<3>:应用程序调用DispatchMessage,将消息回传给操作系统。消息是由MSG结构体对象来表示的,其中就包含了接受消息的窗口的句柄。
过程<4>:系统利用WNDCLASSEX结构体的lpfnWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行了处理(即“系统给应用程序发送了消息”)

事件的处理由两部分来组成:主循环和窗口过程函数。
主循环由 GetMessage(), TranslateMessage() and DispatchMessage()三个函数组成。
窗口过程函数用来处理发送给窗口的消息。

主循环的代码如下:

// 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结构体,在这里要给它附上初值。这个结构体存储了Windows的事件消息。我们不需要了解结构里的完整内容。

while(GetMessage(&msg, NULL, 0, 0))
一个while循环的开始,判断条件是GetMessage的返回值。而GetMessage函数只有在接受到WM_QUIT(需要退出)消息时,才返回0;而没有接受到这个消息时,应用程序就通过这个while循环来保证程序始终处于运行状态。另外,GetMessage的作用是从消息队列中取出消息,如果队列里一条消息也没有的话,它就会一直等待,直到出现一条消息。

这个函数的原型如下:

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

LPMSG lpMsg,
LPMSG类型的lpMsg,它指向一个消息(MSG)结构体,GetMessage函数从线程的消息队列中取出的消息信息将保存在该结构体当中。

HWND hWnd,
指定接收属于哪一个窗口的消息,通常设置为NULL,表示用于接收属于调用线程的所有窗口的窗口消息。

UINT wMsgFilterMin, UINT wMsgFilterMax
指定要获取消息的最小值和最大值,如果两者都设置为0,就表示接收所有信息。

TranslateMessage(&msg);
这个函数把虚拟键转化为字符消息,仅仅传入消息结构体的地址就可以了,不需要了解太多的细节。

DispatchMessage(&msg);
把窗口消息分发给窗口过程函数,仅仅传入消息结构体的地址就可以了。

窗口过程函数:
DispatchMessage()函数之后会调用这个窗口过程函数,函数被调用时,MSG结构体内的4个信息将被传进来。

窗口过程函数的原型如下:

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

HWND hWnd,
需要处理消息的那个窗口的句柄。

UINT message,
待处理消息的ID,即消息的类型。

WPARAM wParam, LPARAM lParam
用于表示消息的附加信息,这个附加信息会随信息类型的不同而不同。

我们常常在窗口过程函数中使用switch/case语句来确定窗口过程接受的是什么消息,以及如何对这个消息进行处理:

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

这里的WM_DESTROY消息是当窗口正在被关闭时发送的,我们需要做一些事情来清空我们的程序。清空完毕后,需要返回一个0表示正常退出。

这里的PostQuitMessage()函数传递了一个WM_QUIT消息,当你回去主循环里调用GetMessage()时就会得到这个消息,返回一个FALSE,程序因此能得到退出。

接下里我们需要返回一个’0’,表示我们已经处理了这个消息。
在最后,DefWindowProc()用于处理我们没有返回’0’的消息。

补充:
PeekMessage()函数
在游戏编程中,相对于GetMessage函数,用的更多的是PeekMessage函数。关键点是,无论应用程序消息队列是否有消息,PeekMessage函数都立即返回,程序得以继续执行后面的语句。因为在游戏程序中,我们时时刻刻都要调用用于绘制的函数进行画面的绘制,也就是说没有收到消息的时候,我们的程序还得继续往下运行。
另外,PeekMessage函数的返回值与GetMessage不一样。如果PeekMessage能在消息队列中取得消息,那么返回值为非0;如果不能再消息队列中取得消息,返回值为0。

这个函数的原型如下:

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

前四个参数和GetMessage函数的前四个参数完全一致。只是多了第五个参数,用于指定消息的获取方式。一般可以在PM_NOREMOVE和PM_REMOVE中取值。
如果取值为PM_NOREMOVE的话,那么PeekMessage函数取出某条消息后,这条消息将不会从消息队列中被移除;而如果取PM_REMOVE得话,这条消息被取出后将从消息队列中被移除。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值