Directx|Windows游戏编程学习第一篇笔记——Window窗口创建

  • WinMain函数原型:
int WINAPI WinMain( _In_HINSTANCE hInstance,// 该程序当前运行的实例句柄 _In_HINSTANCE hPrevInstance,// 在Win32下该参数总是取NULL,只是需要在书写时表示出来 _In_LPSTR lpCmdLine,//当程序开始运行时 获取用户输入的命令指令 _In_int nCmdShow//指定程序窗口应该如何显示(最大化,最小化,隐藏等等) );

WINAPI:#define WINAPI _stdcall(#define CALLBACK _stdcall )
_stdcall :调用约定,告诉编译器这里应该以windows兼容的方式产生机器指令。
_In : 宏,表示需要自行输入一个参数交给函数执行。
windows 之所以要设立句柄,根本上源于内存管理机制的问题—虚拟地址,简而言之数据的地址需要变动,变动以后就需要有人来记录管理变动,(就好像户籍管理一样),因此系统用句柄来记载数据地址的变更。
  • MessageBox函数原型:
int WINAPI MessageBox( _In_opt_HWND hWnd,//显示的消息框所属的窗口句柄,NULL表示消息框从属于桌面 _In_opt_LPCTSTR lpText,//表示要显示的消息的内容(注意内容要用“ ”括起来) _In_opt_LPCTSTR lpCaption,//表示要显示的消息框的标题的内容 _In_UNIT uType//消息窗口的显示样式 );

MessageBox用于显示一个消息框,可以通过一些参数来设置这个消息框的样式。
_In_opt : opt = optional ,可选的输入参数。如果不想填参数,直接写NULL也可以。
LPCTSTR:该类型的参数为字符串。
常见的消息框样式:
Directx|Windows游戏编程学习第一篇笔记——Window窗口创建
文章图片

Directx|Windows游戏编程学习第一篇笔记——Window窗口创建
文章图片

返回值类型:
Directx|Windows游戏编程学习第一篇笔记——Window窗口创建
文章图片

案例
#include int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { int tbn = MessageBox(NULL, "Hello,Visual Studio!", "消息窗口", MB_OKCANCEL | MB_ICONQUESTION); if (tbn == IDCANCEL) { MessageBox(NULL, "取消", "取消操作", 0); } return 0; }

调试后弹出的框显示如下:
Directx|Windows游戏编程学习第一篇笔记——Window窗口创建
文章图片

本文中我只判断了函数等于IDCANCEL(点击取消按钮)下的情况弹出的窗口,所以点击取消后会弹出:
Directx|Windows游戏编程学习第一篇笔记——Window窗口创建
文章图片


想要同时使用多个标识,可以使用逻辑符或,即 |,例如:MB_OKCANCEL | MB_ICONSTOP


  • PlaySound函数原型:
#include "winmm.lib"//要使用PlaySound函数需要链接该库BOOL PlaySound( LPCTSTR pszSound,//指定了要播放的声音文件 HMODULE hmod,//包含了上个参数中指定的声音文件作为资源的可执行文件的句柄 DWORD fdwSound//控制声音播放的标志,多个标志可用 | 来连接 );

这是一个实现音乐播放的函数。
常用的声音标志
标识 解析
SND_APPLICATION 用应用程序指定的关联来播放声音
SND_ALIAS pszSound参数指定了注册表或者WIN.INI中的系统事件的别名
SND_ALIAS_ID pszSound参数指定了预定义的声音标识符
SND_ASYNC 用异步方式播放声音,PlaySound函数在开始播放后立即返回
SND_FILENAME pszSound参数指定了WAVE文件名
SND_LOOP 重复播放声音,必须与SND_ASYNC标志一起使用
SND_MEMORY 播放载入到内存中的声音,此时pszSound是指向声音数据的指针
SND_NODEFAULT 不播放默认声音,若无此标志,则PlaySound在没找到声音时会播放默认声音
SND_NOSTOP PlaySound不打断原来的声音播出并立即返回FALSE
SND_NOWAIT 如果驱动程序正忙则函数就不播放声音并立即返回
SND_PURGE 停止所有与调用任务有关的声音。若参数pszSound为NULL,就停止所有的声音,否则,停止pszSound指定的声音
SND_RESOURCE pszSound参数时WAVE资源的标识符,这时要用到hmod参数
SND_SYNC 同步播放声音,在播放完后PlaySound函数才返回
案例 在进行案例前需要做一些准备工作,此书中使用的示例音效是FirstBlood,首先在工程文件夹下面新建一个Music文件夹并将该音效丢进去:
Directx|Windows游戏编程学习第一篇笔记——Window窗口创建
文章图片

示例代码如下:
#include #pragma comment(lib,"winmm.lib")int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { PlaySound("Music/FirstBlood.wav", NULL, SND_FILENAME | SND_ASYNC); int tbn = MessageBox(NULL, "FirstBlood!", "一血!", MB_OKCANCEL | MB_ICONQUESTION); if (tbn == IDCANCEL) { MessageBox(NULL, "取消", "取消操作", 0); } return 0; }

运行后会出现FirstBlood的音效,注意音效的路径。
【Directx|Windows游戏编程学习第一篇笔记——Window窗口创建】
  • MSG结构体(消息队列):
typedef struct tagMSG { HWND hwnd; //指定消息所属窗口。windows中通常用HWND类型的变量来标识窗口 UNIT message; //指定了消息的标识符。其格式为WM_XXXWM = windows message WPARAM wParam; //指定此msg的附加信息。 LPARAM lParam; //同上。 DWORD time; //指定投递到消息队列的时间。 POINT pt; //指定投递到消息队列中时鼠标的当前位置。 } MSG;

每一个windows应用程序开启时,系统都会为其创建一个消息队列。比如按下鼠标左键的时候会产生一个WM_LBUTTONDOWN的消息,系统会将这个消息放到前面该程序开启时创建的消息队列中,Windows将产生的消息依次放到消息队列中,二应用程序则通过一个消息循环不断地从消息队列中取出消息并进行响应。
  • 窗口创建四部曲
1.窗口类的设计
2.窗口类的注册
3.窗口的正式创建
4.窗口的显示与更新

1.窗口类的设计
在windows中进行窗口的设计通常使用WNDCLASSEX结构体:
typedef struct tagWNDCLASSEX { UINT cbSize; //表示该结构体的字节数大小。一般取sizeof(WNDCLASSEX) UINT style; //指定这一类型窗口的风格样式。要取多个用 | 连接 WNDPROC lpfnWndProc; //函数指针类型,指向窗口过程函数。窗口过程函数是一个 回调函数 int cbClsExtra; //表示窗口类附加内存,一般设置为0。 int cbWndExtra; //表示窗口的附加内存,一般设置为0。注意这里和上面的窗口类不一样 HINSTANCE hInstance; //窗口过程的程序的实例句柄,即将程序当前运行的实例句柄传给它 HICON hIcon; //指定窗口类的图标句柄,这个成员变量必须是一个图标资源,如果为NULL,系统会提供一个默认的图标 HCURSOR hCursor; //指定窗口类的光标句柄 HBRUSH hbrBackground; //制定窗口类的画刷句柄。可以为其指定一个画刷句柄,或者把它取为一个标准的系统颜色 LPCTSTR lpszMenuName; //指定菜单资源的名字。不需要下拉菜单时直接指定为NULL即可 LPCTSTR lpszClassName; //指定窗口类的名字 HICON hIconSm; //指定窗口类的小图标句柄。电脑桌面任务栏中显示的小图标。这个是WNDCLASS和WNDCLASSEX的区别,游戏程序可以不需要这一项。 } WNDCLASSEX, *PWNDCLASSEX;

UINT style可以在MSDN查到详细的样式,链接:
https://msdn.microsoft.com/zh-cn/vstudio/ff729176(v=vs.90)
回调函数:贴出简书的一个例子
https://www.jianshu.com/p/88f933be2651
HICON:可以使用LoadIcon和LoadImage来加载,不过通常使用的后者。
窗口类的设计代码实现:
#include #pragma comment(lib,"winmm.lib")int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { //PlaySound("Music/FirstBlood.wav", NULL, SND_FILENAME | SND_ASYNC); //int tbn = MessageBox(NULL, "FirstBlood!", "一血!", MB_OKCANCEL | MB_ICONQUESTION); //if (tbn == IDCANCEL) //{ // MessageBox(NULL, "取消", "取消操作", 0); //} //开始设计一个完整的窗口类 //创建一个窗口类对象并命名为wndClass WNDCLASSEX wndClass = { 0 }; wndClass.cbSize = sizeof(WNDCLASSEX); wndClass.style = CS_HREDRAW | CS_VREDRAW; //定义指向窗口过程函数的指针,声明WndProc这一步很重要,不然会报错 WNDPROC WndProc; wndClass.lpfnWndProc = WndProc; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //加载图标icon资源 wndClass.hIcon = (HICON)::LoadImage(hInstance, "Icon/icon_main.jpg", 2, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE); //加载光标资源 wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); //为程序背景指定一个灰色的画刷 wndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH); //指定菜单资源的名字 wndClass.lpszMenuName = NULL; //指定窗口类的名字 wndClass.lpszClassName = "致敬拥有游戏梦想的人"; return 0; }

2.窗口类的注册
设计完窗口类(WNDCLASSEX)后需要调用RegisterClassEx函数对其进行注册,注册成功后才可以创建该类型的窗口。
注册函数原型:
ATOM WINAPI RegisterClassEx(_In_ const WNDCLASSEX *lpwcx);

因为这是一个带*的指针参数,所以要在写的时候换成取地址符& ,如下:
RegisterClassEx(&wndClass);

需要注意的是,一开始创建窗口类使用的是WNDCLASSEX结构体,那么对应的这一步就应该是带Ex尾缀的注册函数。如果一开始使用的是WNDCLASS,那么对应的这一步就应该使用RegisterClass函数注册。

3.窗口的正式创建
调用 AjustWindowRect() 函数来根据我们设定的尺寸和风格来集算窗口的尺寸。它利用一个矩阵定义窗口的左上,左下,右上,右下的窗口区域坐标,左上角的属性代表了窗口的起始位置,结合右下角则可以反应窗口的宽度和高度。该函数中也专门有一个布尔类型的值标明窗口的变量,指示菜单是否拥有菜单栏,有无菜单栏影响着非客户区。
当我们设计好窗口类并将其成功注册后,就可以使用 CreateWindow 函数来创建设计好的窗口了。
CreateWindow函数原型:
HWND WINAPI CreateWindow( _In_opt_ LPCTSTR lpClassName,// 对应窗口类的名称,在窗口类设计的步骤中写的名称是“致敬拥有游戏梦想的人”,这里也应该写上这个名称 _In_opt_ LPCTSTR lpWindowName,// 显示在标题栏上的程序名字,比如“植物大战僵尸” _In_ DWORD dwStyle,// 某个具体的窗口的样式 _In_ int x,// 指定窗口的水平位置,一般取CW_USEDEFAULT表示默认的位置 _In_ int y,// 指定窗口的垂直位置,取值同上。 _In_ int nWidth,// 指定窗口的宽度 _In_ int nHeight,// 指定窗口的高度 _In_opt_ HWND hWndParent,// 指定被创建窗口的父窗口句柄,一般设置为NULL _In_opt_ HMENU hMenu,// 指定窗口菜单的资源句柄,一般设置为NULL _In_opt_ HINSTANCE hInstance,// 指定窗口所属的应用程序实例的句柄,和WinMain的第一个参数一致,对于之前写的WinMain函数,这里取hInstance _In_opt_ LPVOID lpParam); // lpParam作为WM_CREATE消息的附加参数lParam传入的数据指针,一般设置为NULL

dwstyle 指定某个具体的窗口的样式,而WNDCLASSEX的 style,只要基于该style创建出的窗口都具有style的样式。
创建窗口类代码实现:
//创建窗口类 HWND hWnd = CreateWindow( "致敬拥有游戏梦想的人", "GameProject_1", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL );


4.窗口的显示与更新
这一步骤用到了三个函数:设定窗口显示位置的MoveWindow函数,显示窗口的ShowWindow函数,更新窗口的UpdateWindow函数。
MoveWindow函数原型:
BOOL WINAPI MoveWindow( _In_ HWND hWnd,// CreateWindow函数创建的对象,窗口句柄hWnd _In_ int x,// 指定窗口左方相对于屏幕左上角的新位置 _In_ int y,// 指定窗口上方相对于屏幕左上角的新位置 _In_ int nWidth,// 指定窗口的新宽度 _In_ int nHeight,// 指定窗口的新高度 _In_ BOOL bRepaint); // 是否要重新绘制窗口

ShowWindow函数原型:
BOOL WINAPI ShowWindow( _In_ HWND hWnd,// 同上,CreateWindow函数的对象名称 hWnd _In_ int nCmdShow// 指定窗口的显示状态,这里可以直接写nCmdShow,因为这个函数是WinMain内部调用的,直接取WinMain函数即可 );

UpdateWindow函数原型:
// 需要注意的是这里的hWnd与之前两个的含义是不一样的,这里指的是 创建成功后 的窗口的句柄。 BOOL UpdateWindow( _In HWND hWnd);

  • 两套消息循环系统(游戏编程常用第二种)
1.以GetMessage为核心的消息循环系统 GetMessage函数原型:
BOOL WINAPI GetMessage( _Out_ LPMSG lpMsg,// 指向一个消息结构体(MSG),GetMessage从线程的消息队列中取出的消息将保存在该结构体中 _In_opt_ HWND hWnd,// 指定接收属于哪个窗口的消息,一般设置为NULL表示用于接收属于调用线程的所有窗口的窗口消息 _In_ UINT wMsgFilterMin,// 指定要获取的消息的最小值,通常为0 _In_ UINT wMsgFilterMax// 指定要获取的消息的最大值,如果Min和Max都为0表示接收所有消息 );

GetMessage的作用是从消息队列中获取消息,如果队列里一条消息也没有它就会一直等待直到消息出现。
需要注意的是,GetMessage收到除了WM_QUIT以外的消息都会返回非0,对于WM_QUIT消息会返回0,如果出现了错误则会返回-1,比如当hWnd是无效的窗口句柄时GetMessage就会返回-1。
2.以PeekMessage为核心的消息循环系统 PeekMessage函数原型:
BOOL WINAPI PeekMessage( _Out_ LPMSG lpMsg,// 指向一个消息结构体(MSG),GetMessage从线程的消息队列中取出的消息将保存在该结构体中 _In_opt_ HWND hWnd,// 指定接收属于哪个窗口的消息,一般设置为NULL表示用于接收属于调用线程的所有窗口的窗口消息 _In_ UINT wMsgFilterMin,// 指定要获取的消息的最小值,通常为0 _In_ UINT wMsgFilterMax,// 指定要获取的消息的最大值,如果Min和Max都为0表示接收所有消息 _In_ UINT wRemoveMsg// 用于指定消息的获取方式,一般这个参数可以在PM_NOREMOVE和PM_REMOVE中取值 );

可以看出前四个参数和GetMessage函数是一模一样的,对于最后一个参数,如果取PM_NOREMOVE的话,那么PeekMessage函数取出某条消息后,这条消息将不会从消息队列中被移除;而如果取PM_REMOVE的话,那么某条消息被取出来后将从消息队列中被移除。一般我们都是把这个参数取为PM_REMOVE,这样就是和GetMessage一样的取消息操作。
如果PeekMessage能在消息队列中取到消息,那么返回值为非0;如果不能在消息队列中取到消息,则返回值为0。
PeekMessage与GetMessage的异同
相同点:都用于查看应用程序的消息队列,有消息时将队列中的消息派发出去。
不同点:无论应用程序消息队列是否有消息,PeekMessage函数都立即返回,程序得以继续执行后面的语句(无消息则执行其他命令,有消息时一般要将消息派发出去,再执行其他指令)。而GetMessage函数只有再消息队列中有消息时才会返回,队列中无消息就会一直等待,直到下一个消息出现时才返回。
GetMessage和PeekMessage第二个参数通常不要填窗口句柄,最好填0。因为有可能某一时间这个窗口句柄失效了,而消息循环仍在进行,这样就会导致错误。
  • Window程序的“中枢神经” ——窗口过程函数
窗口过程函数(回调函数)主要用于处理发送给窗口的消息。
函数原型:
LRESULT CALLBACK WindowProc(//LRESULT是窗口过程函数的返回值,一般情况下是非0值。CALLBACK告诉Windows这个函数是个回调函数,每当Windows参数遇到了需要处理的事件时,就调用这个函数 _In_ HWND hWnd,//需要处理消息的窗口句柄 _In_ UINT uMsg,//表示待处理消息的ID,即消息的类型 _In_ WPARAM wParam,//表示消息的附加信息,附加信息会随着消息类型的不同而不同 _In_ LPARAM lParam//同上 );

系统通过窗口过程函数的地址( 指针 )来调用窗口过程函数,而不是通过函数的名字来调用。
因为一个程序可以有多个窗口,而窗口过程函数的第一个参数就用于指定了接收消息的那个特定窗口。我们可以同时打开几个窗口,各自窗口具有不同的句柄和分开定义的窗口过程函数来处理各自的消息。
窗口过程函数的名字在实际编写的时候可以随便取,不一定非要叫WindowProc,也可以叫WndProc。
  • 做好善后——窗口类的注销
为了保证稳定性,在WinMain结束之前,最好把在窗口创建四部曲中创建的那个窗口类注销掉。我们进行窗口的注销用到的时UnregisterClass这个函数,与之前的RegisterClassEx函数对应。原型如下:
BOOL WINAPI UnregisterClass( _In_ LPCTSTR lpClassName,// 填我们需要注销的类名称 _In_opt_ HINSTANCE hInstance// 填创建这个类的应用程序的实例句柄,也就是填WinMain函数的hInstance,或者时类的实例句柄wndClass.hInstance,这二者是等价的。 );

下面给出一个实例:
UnregisterClass("致敬拥有游戏梦现的人",wndClass.hInstance);

  • 一个完整的窗口程序的诞生
#include #define WINDOW_WIDTH 800//为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define WINDOW_HEIGHT 600//为窗口高度定义的宏 #define WINDOW_TITLE "致敬拥有游戏开发梦想的人们"//为窗口标题定义宏LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); //应用程序的入口函数,程序从这里开始 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { //【1】窗口创建四部曲之一:设计一个完整的窗口类 WNDCLASSEX wndClass = { 0 }; //用WINDCLASSEX定义了一个窗口类 wndClass.cbSize = sizeof(WNDCLASSEX); //设置结构体的字节数大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //设置窗口样式 wndClass.lpfnWndProc = WndProc; //设置指向窗口过程函数的指针 wndClass.cbClsExtra = 0; //窗口类的附加内存,取0即可 wndClass.cbWndExtra = 0; //同上 wndClass.hInstance = hInstance; //指定包含窗口过程的程序的实例句柄 wndClass.hIcon = (HICON)::LoadImage(NULL, "icon.ico", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE); //本地加载自定义icon wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); //指定窗口类的光标句柄 wndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrbackground指定一个灰色画刷句柄 wndClass.lpszMenuName = NULL; //用一个以空终止的字符串,指定菜单资源的名字 wndClass.lpszClassName = "ForTheDreamOfGameDevelop"; //用一个以空终止的字符串,指定窗口类的名字 //【2】窗口创建四部曲之二:注册窗口类 if (!RegisterClassEx(&wndClass)) return -1; //【3】窗口创建四部曲之三:正式创建窗口 HWND hWnd = CreateWindow( "ForTheDreamOfGameDevelop", WINDOW_TITLE, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, NULL, NULL, hInstance, NULL ); //【4】窗口创建四部曲之四:窗口的移动,显示,更新 MoveWindow(hWnd, 250, 80, WINDOW_WIDTH, WINDOW_HEIGHT, true); //窗口显示时的位置,窗口左上角位于(250,80)处 ShowWindow(hWnd, nShowCmd); //显示窗口 UpdateWindow(hWnd); //对窗口进行更新 //【5】消息循环过程 MSG msg = { 0 }; //定义并初始化msg while (msg.message != WM_QUIT)//如果消息不是WM_QUIT,就继续循环 { if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))//查看应用程序消息队列,有消息时将队列中的消息派发出去 { TranslateMessage(&msg); //将虚拟键消息转换成字符消息 DispatchMessage(&msg); //分发一个消息给窗口程序 } } //【6】窗口类的注销 UnregisterClass ("ForTheDreamOfGameDevelop", wndClass.hInstance); return 0; }//------------------------------------WinProc()函数------------------------------------------------- // 窗口过程函数,对窗口消息进行处理 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)//窗口过程函数要写在WinMain外面 { switch (message) { case WM_PAINT://若是 客户区重绘 消息 ValidateRect(hWnd, NULL); //更新客户区的显示 break; //跳出该switch循环 case WM_KEYDOWN://若是 键盘按下 消息 if (wParam == VK_ESCAPE)//如果被按下的键时ESC DestroyWindow(hWnd); //销毁窗口,并发送一条WM_DESTROY消息 break; //跳出该Switch循环 case WM_DESTROY://若时 窗口销毁 消息 PostQuitMessage(0); //向系统表明有个线程有终止请求,用来响应WM_DESTROY消息 break; //跳出该Switch循环 default://若上述case条件都不符合,则执行该default语句 return DefWindowProc(hWnd, message, wParam, lParam); //调用默认的窗口过程函数 } return 0; //正常退出 }


    推荐阅读