《Windows游戏编程大师技巧》学习笔记(三)

《Windows游戏编程大师技巧》学习笔记(三)

实用的Windows应用程序概要

Windows程序的关键是打开窗口,并对窗口进行文本、图像的显示工作,以及对来自窗口的交互消息做出响应,步骤如下:

  1. 创建一个Windows类;
  2. 创建一个事件句柄或WinProc回调函数;
  3. 注册先前创建的Windows类到Windows系统中;
  4. 用注册过的Windows类创建一个窗口;
  5. 创建一个能从事件句柄获取事件或向事件句柄传递Windows信息的事件循环。

Windows类

每一个应用程序至少需要创建一个Windows类,用于描述窗口信息;
描述Windows类信息的数据结构有两个,WNDCLASSWNDCLASSEX,最好选用较新的扩展版本WNDCLASSEX
在Unicode环境下,WNDCLASSEXW被定义成了WNDCLASSEX,相关定义如下:

typedef struct tagWNDCLASSEXW {
    UINT        cbSize;         // 结构体大小
    /* Win 3.x */
    UINT        style;          // 样式
    WNDPROC     lpfnWndProc;    // 窗口事件回调函数指针
    int         cbClsExtra;     // 额外的类信息
    int         cbWndExtra;     // 额外的窗口信息
    HINSTANCE   hInstance;      // 应用程序实例句柄
    HICON       hIcon;          // 主图标句柄
    HCURSOR     hCursor;        // 鼠标句柄
    HBRUSH      hbrBackground;  // 窗口背景填充笔刷句柄
    LPCWSTR     lpszMenuName;   // 需要附加的菜单名
    LPCWSTR     lpszClassName;  // 类本身的名字
    /* Win 4.0 */
    HICON       hIconSm;        // 小图标句柄
} WNDCLASSEXW, *PWNDCLASSEXW, NEAR *NPWNDCLASSEXW, FAR *LPWNDCLASSEXW;

下面分别对每个字段进行介绍:

  • UINT cbSize:记录WNDCLASSEX结构体本身大小,用于帮助结构体作为指针传递时仅根据第一个字段获取整个结构体大小,一般情况只需要赋值为sizeof(WNDCLASSEX)

  • UINT style:描述窗口属性样式,支持逻辑或运算,常用标志如下:
    窗口属性样式表

  • WNDPROC lpfnWndProc:窗口事件回调函数,使用方法和回调过程如下,更多内容见后文详述:

    	LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
    	winclass.lpfnWndProc = WindowProc;
    

    Windows事件句柄回调执行过程

  • int cbClsExtraint cbWndExtra:指示Windows将附加的运行时间信息保存到Windows类某些单元中,大多数情况下可以直接设置为0;

  • HINSTANCE hInstance:应用程序实例句柄,即WinMain函数中的HINSTANCE hinstance参数;

  • HICON hIconHCURSOR hCursor:应用程序的图标句柄和光标句柄,可以分别通过LoadIconLoadCursor进行加载,这两个函数在Unicode环境下的原型如下:

    	HICON WINAPI LoadIconW(HINSTANCE hInstance, LPCWSTR lpIconName);
    	HCURSOR WINAPI LoadCursorW(HINSTANCE hInstance, LPCWSTR lpCursorName);
    

    第一个参数表示应用程序实例,函数会从这个应用程序的资源数据中加载指定名称的自定义图标/光标,设置为NULL表示从系统资源中加载光标,支持的系统资源名称分别如下:

    • LoadIcon()的取值:
      LoadIcon的取值表
    • LoadCursor()的取值:
      LoadCursor的取值表一
      LoadCursor的取值表二
  • HBRUSH hbrBackground:需要绘制和刷新窗口时背景填充颜色和样式,即“画刷”,与前述图标和光标资源加载类似,函数原型和画刷标识符如下:

    	WINGDIAPI HGDIOBJ WINAPI GetStockObject(int i);
    

    画刷标识符列表

  • LPCWSTR lpszMenuName:描述菜单资源名称,用于加载和选用窗口,后续深入讨论,可以设置为NULL

  • LPCWSTR lpszClassName:Windows类名称,相当于ID,用于Windows系统标识和识别此Windows类,后续在完成对此类的注册后,可以使用该名称索引此类;

  • HICON hIconSmWNDCLASSEX中新增功能,用于指向窗口标题栏和桌面任务栏的图标句柄,同样可以使用LoadIcon()进行相关资源的加载。

最后需要将定义好的类注册到系统中:

// WNDCLASSEX winclass;
RegisterClassEx(&winclass);

创建窗口

使用CreateWindow()CreateWindowEx(),在Unicode环境下,CreateWindowExW()被定义为CreateWindowEx(),函数原型如下:

HWND WINAPI CreateWindowExW(
    DWORD dwExStyle,            // 窗口扩展样式
    LPCWSTR lpClassName,        // 已注册的类名
    LPCWSTR lpWindowName,       // 窗口标题
    DWORD dwStyle,              // 窗口样式
    int X, int Y,               // 窗口在的位置坐标
    int nWidth, int nHeight,    // 窗口尺寸
    HWND hWndParent,            // 父窗口
    HMENU hMenu,                // 菜单句柄,或子窗口标识
    HINSTANCE hInstance,        // 应用程序实例句柄
    LPVOID lpParam              // 高级特性参数
); 

下面分别对每个参数进行介绍:

  • DWORD dwExStyle:窗口扩展样式为高级特效,大多数情况下可以设置为NULL,常用的值如WS_EX_TOPMOST可以将窗口始终置于最顶层,其他值见Win32 SDK 帮助CreateWindowEx 百度百科
  • LPCWSTR lpClassName:前述注册的Windows类名;
  • LPCWSTR lpWindowName:窗口标题;
  • DWORD dwStyle:窗口样式,支持逻辑或运算,可选值如下:
    image.png
  • int Xint Yint nWidthint nHeight:分别描述窗口左上角在屏幕坐标系下的像素坐标和窗口的像素宽高,都可以设置为CW_USEDEFAULT来让系统决定其数值;
  • HWND hWndParent:存在父窗口的情况下填写父窗口句柄,否则可以设置为NULL
  • HMENU hMenu:窗口菜单句柄,后续深入讨论;
  • HINSTANCE hInstance:应用程序实例句柄,即WinMain函数中的HINSTANCE hinstance参数;
  • LPVOID lpParam:高级特性,暂设为NULL

CreateWindowExW()函数执行成功将返回窗口句柄,否则返回NULL
如果在dwStyle中没有设置WS_VISIBLE,那么可以使用如下函数手动显示窗口:

ShowWindow(hwnd, ncmdshow);

其中hwnd即函数返回的窗口句柄,ncmdshowWinMain函数中的int hinstance参数;
通过调用UpdateWindow()函数来产生WM_PAINT消息刷新窗口;

事件句柄

前述的WNDPROC lpfnWndProc;原型如下:

typedef LRESULT (CALLBACK* WNDPROC)(
    HWND hwnd,      // 窗口句柄
    UINT msg,       // 消息类型ID
    WPARAM wparam,  // 进一步定义的消息数据
    LPARAM lparam   // 进一步定义的消息数据
);

下面分别对每个参数进行介绍:

  • HWND hwnd:只有在建立多个窗口时才用到,可以用来标识消息来自于哪一个窗口,如下图所示:
    同一个类多个窗口事件回调示意图
  • UINT msg:可能是众多消息中的一个,可以配合switch进行处理,常见的消息说明符如下:
    消息说明符简表
  • WPARAM wparamLPARAM lparam:用于存储消息类型之外的更多消息数据。

回调函数返回0通知Windows已经处理完成当前事件,无需更多操作,对于未处理的事件,可以通过DefWindowProc()函数使用默认的方法处理,此函数的参数与回调函数的参数相同;
一个可能的事件回调函数如下:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
	PAINTSTRUCT	ps;
	HDC			hdc;

	switch (msg)
	{
	case WM_CREATE:
		// 在此处执行初始化操作
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		// 在此处执行绘图操作
		EndPaint(hwnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		// 调用默认的消息回调处理我们不关心的事件
		return DefWindowProc(hwnd, msg, wparam, lparam);
	}
}

使用BeginPaint()EndPaint()函数来激活窗口的客户区,并使用先前定义的hbrBackground画刷来填充背景;
其中PAINTSTRUCT ps;存储了重画的矩形区域数据,相关定义如下:

typedef struct tagPAINTSTRUCT {
    HDC         hdc;
    BOOL        fErase;
    RECT        rcPaint;
    BOOL        fRestore;
    BOOL        fIncUpdate;
    BYTE        rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;

更多内容后续深入讨论,此时只需关注RECT rcPaint;,它代表的是最小需重画区域的句型结构,相关定义和示意图如下:

typedef struct tagRECT
{
    LONG    left;
    LONG    top;
    LONG    right;
    LONG    bottom;
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;

重画区域示意图
BeginPaint()返回值为指向描述视频系统和正在绘制表面的数据结构句柄;
注意:WM_DESTROY消息产生于用户关闭窗口时,而非应用程序关闭时,多窗口程序在任一窗口关闭时都将产生WM_DESTROY消息,但是只有所有窗口都被关闭时才会产生WM_QUIT消息,这时可以手动调用函数PostQuitMessage()函数来产生此应用程序退出消息

事件循环

使用GetMessage()函数或PeekMessage()函数都可以获取当前事件类型,随后通过调用DispatchMessage()函数调用先前定义好的事件回调函数进行处理,在Unicode环境下相关函数原型如下:

BOOL WINAPI GetMessageW(
    LPMSG lpMsg,        // 消息结构体指针
    HWND hWnd,          // 窗口句柄
    UINT wMsgFilterMin, // 第一条消息
    UINT wMsgFilterMax  // 最后一条消息
);  // 阻塞式等待消息

BOOL WINAPI PeekMessageW(
    LPMSG lpMsg,        // 消息结构体指针
    HWND hWnd,          // 窗口句柄
    UINT wMsgFilterMin, // 第一条消息
    UINT wMsgFilterMax, // 最后一条消息
    UINT wRemoveMsg     // 是否移除当前获取的事件
);  // 非阻塞获取消息

LRESULT WINAPI DispatchMessageW(CONST MSG *lpMsg);

MSG结构体的定义如下:

typedef struct tagMSG {
    HWND        hwnd;       // 窗口句柄
    UINT        message;    // 消息类型ID
    WPARAM      wParam;     // 进一步定义的消息数据
    LPARAM      lParam;     // 进一步定义的消息数据
    DWORD       time;       // 事件发生的时间
    POINT       pt;         // 用户光标位置
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

两种获取事件函数唯一的区别在于GetMessage()只有发生事件时才会返回,而PeekMessage()不会阻塞程序,发生事件时返回1,否则返回0;
对于PeekMessage()最后一个参数,则表示是否将获取到的消息移除事件队列,PM_REMOVE为移除,PM_NOREMOVE为不移除;如果选择不移除,可以稍后配合GetMessage()函数获取并移除消息,以下两种代码是等价的(一般使用前者):

if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
    // 此处处理相关事件逻辑
}

if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
    GetMessage(&msg, NULL, 0, 0);

    // 此处处理相关事件逻辑
}

完整代码

关于一个实用的Windows应用程序框架,完整的代码如下:

#define WIN32_LEAN_AND_MEAN

#include <Windows.h>

#include <tchar.h>

#define WINDOW_CLASS_NAME "WINCLASS1"

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
	PAINTSTRUCT	ps;
	HDC			hdc;

	switch (msg)
	{
	case WM_CREATE:
		// 在此处执行初始化操作
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		// 在此处执行绘图操作
		EndPaint(hwnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		// 调用默认的消息回调处理我们不关心的事件
		return DefWindowProc(hwnd, msg, wparam, lparam);
	}
}

int WINAPI WinMain(
	HINSTANCE hinstance, 
	HINSTANCE hprevinstance,
	LPSTR lpcmdline,
	int ncmdshow)
{
	WNDCLASSEX	winclass;
	HWND		hwnd;
	MSG			msg;

	winclass.cbSize = sizeof(WNDCLASSEX);
	winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
	winclass.lpfnWndProc = WindowProc;
	winclass.cbClsExtra = 0;
	winclass.cbWndExtra = 0;
	winclass.hInstance = hinstance;
	winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	winclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	winclass.lpszMenuName = NULL;
	winclass.lpszClassName = _T(WINDOW_CLASS_NAME);
	winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

	if (!RegisterClassEx(&winclass)) return -1;

	if (!(hwnd = CreateWindowEx(NULL, 
		_T(WINDOW_CLASS_NAME), 
		_T("Title Here"), 
		WS_OVERLAPPEDWINDOW | WS_VISIBLE, 
		0, 0, 
		400, 400, 
		NULL, 
		NULL, 
		hinstance, 
		NULL)))
		return -1;

	while (TRUE)
	{
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			if (msg.message == WM_QUIT) break;

			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		// 在此处执行游戏的主要逻辑
	}

	return msg.wParam;
}