Windows Game Programming study notes --Window the first to create a window

  • WinMain function prototype: 

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: calling convention tells the compiler that there should be windows compatible manner to generate machine instructions.

_In: macro, expressed the need to manually enter a parameter to the function execution.

The reason for the establishment of problem windows handle, from the memory management mechanism fundamentally - the virtual address, the address of the short data needs change, you need someone to change after the records management changes (just like the same household management), so the system with handles to record change data addresses.

 

  • MessageBox function prototype:

int WINAPI MessageBox(
    _In_opt_HWND hWnd,            //显示的消息框所属的窗口句柄,NULL表示消息框从属于桌面
    _In_opt_LPCTSTR lpText,       //表示要显示的消息的内容(注意内容要用“ ”括起来)
    _In_opt_LPCTSTR lpCaption,    //表示要显示的消息框的标题的内容
    _In_UNIT uType                //消息窗口的显示样式
);

MessageBox for displaying a message box may be set by the message box style parameters.

_In_opt: opt = optional, optional input parameters. If you do not want to fill in the parameters, you can also write directly to NULL.

LPCTSTR: This parameter is a string type.

Common message box style:

Return Value Type:

Case

#include <Windows.h>

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

After commissioning the pop-up box appears as follows:

In this article I only judge the case of a function equal to IDCANCEL (click on the Cancel button) in the pop-up window, so click Cancel will pop up:

 

You want to use a plurality of identification, or can use the logical operators, i.e. |, for example: MB_OKCANCEL | MB_ICONSTOP

 

 

  • PlaySound function prototype:

#include "winmm.lib"    //要使用PlaySound函数需要链接该库

BOOL PlaySound(
    LPCTSTR pszSound,       //指定了要播放的声音文件
    HMODULE hmod,           //包含了上个参数中指定的声音文件作为资源的可执行文件的句柄
    DWORD fdwSound          //控制声音播放的标志,多个标志可用 | 来连接
);

This is a realization of music playback function.

Common sound mark
Mark Resolve
SND_APPLICATION Specified by the associated application to play sounds
SND_ALIAS pszSound parameter specifies the system registry or WIN.INI event of an alias
SND_ALIAS_ID pszSound parameter specifies a predefined sound identifier
SND_ASYNC Play sound asynchronous mode, PlaySound function returns immediately after the start of play
SND_FILENAME pszSound parameter specifies the WAVE file name
SND_LOOP Repeat play a sound, it must be used with SND_ASYNC logo
SND_MEMORY Play sound loaded into memory, then pszSound is a pointer to audio data
SND_NODEFAULT 不播放默认声音,若无此标志,则PlaySound在没找到声音时会播放默认声音
SND_NOSTOP PlaySound不打断原来的声音播出并立即返回FALSE
SND_NOWAIT 如果驱动程序正忙则函数就不播放声音并立即返回
SND_PURGE 停止所有与调用任务有关的声音。若参数pszSound为NULL,就停止所有的声音,否则,停止pszSound指定的声音
SND_RESOURCE pszSound参数时WAVE资源的标识符,这时要用到hmod参数
SND_SYNC 同步播放声音,在播放完后PlaySound函数才返回

案例

在进行案例前需要做一些准备工作,此书中使用的示例音效是FirstBlood,首先在工程文件夹下面新建一个Music文件夹并将该音效丢进去:

示例代码如下:

#include <Windows.h>
#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的音效,注意音效的路径。

 

  • MSG结构体(消息队列):

typedef struct tagMSG
{    
    HWND hwnd;                 //指定消息所属窗口。windows中通常用HWND类型的变量来标识窗口
    UNIT message;              //指定了消息的标识符。其格式为WM_XXX    WM = 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 <Windows.h>
#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 <Windows.h>

#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;                                       //正常退出
}

 

发布了10 篇原创文章 · 获赞 4 · 访问量 1500

Guess you like

Origin blog.csdn.net/weixin_43369654/article/details/102823553