在 Windows 桌面上开发游戏,有的时候需要使用到 Windows 提供的对话框控件,例如扫雷、纸牌这种游戏。默认情况下,因为 SDL2 主打可移植的概念,所以并不支持桌面特有的一些控件资源,如果想使用这些操作系统自带的控件资源,就需要使用操作系统本身提供的一些平台相关的 API。这篇文章就简单的介绍一下,如何在 SDL2 游戏中使用这些控件资源,我们这里以 Windows 平台为例,其它平台也是类似的原理。

在开始之前,需要再次强调,例如菜单、对话框等资源都是操作系统相关的,不同的操作系统提供默认样式和操作接口都是不相同的,所以使用这些控件在一定程度上可以说是放弃了可移植,除非你在不同的操作系统上使用类似宏开关的方式实现多份功能相同的代码。如果想要获取可移植的特性,那可能只有自己造轮子使用自绘制的方法了,可以去网上搜一搜也许有别人造的轮子也说不定,不过反过来想想,使用类似 QT 之类的 UI 库不香么?搞屁 SDL……

1. 如何增加菜单栏

这里只是说了增加菜单栏,其实你可以使用同样的方法增加几乎所有的界面控件,因为该方法其实就是调用 Windows API 接口而已,这些跟窗口相关的接口最根本的问题就是如何获取窗口句柄,找到句柄后,你就可以调用几乎所有的 Windows 界面相关的 API 了。

那如何获取窗口句柄?因为 SDL2 已经将窗口创建的代码进行了封装,所以想获取窗口句柄,还是需要从 SDL2 提供的 API 接口入手。

仔细查看文档,你会发现一个叫做 SDL_SetWindowsMessageHook 的函数,该函数自称可以获取 Windows 的原始消息,既然这样是否可以在创建窗口的时候,捕获 WM_NCCREATE 消息,如果捕获了该消息就可以操作创建窗口的结构体,该结构体在如何创建一个窗口? 这篇文章中介绍过,结构体中包含了一个菜单句柄项,你可以将创建的菜单句柄填入,在窗口创建之后,自然就会增加我们设置的菜单栏。但是遗憾的是,这个函数没有返回任何窗口创建的消息,所以这个方法是不可行的,但是用同样的方法,可以获取创建窗口之后的消息,也就是说你可以用该方法实现菜单栏的消息响应。

除了这种策略之外,Windows 还提供独立增加菜单栏的方法,其中 SetMenu 函数就是这样一个接口:

BOOL SetMenu(
  HWND  hWnd,
  HMENU hMenu
);

hMenu 就是我们需要添加的菜单栏句柄,可以使用类似 LoadMenu 接口来加载预设的菜单资源。既然这个参数没问题,那么就只剩下第一个参数,窗口句柄如何获取的问题了。

如果获取窗口句柄?再次查看 SDL2 的文档,你会发现一个叫做 SDL_GetWindowWMInfo 的函数,该函数可以获取窗口相关的信息,其中就包含我们需要的窗口句柄:

SDL_bool SDL_GetWindowWMInfo(SDL_Window*    window,
                             SDL_SysWMinfo* info)

该函数的第二个参数是一个结构体,该结构体封装了各个操作系统平台自身特有的参数信息,例如 Windows 平台的窗口句柄、HDC,android 平台的 ANativeWindow 结构,Cocoa 平台的 NSWindow 结构等等,这里不多展开,大家可以去官方文档自行查看。

使用这个接口的方法很简单:

SDL_SysWMinfo sys;
SDL_VERSION(&sys.version);

SDL_GetWindowWMInfo(pModule->pWin, &sys)

注意版本信息那一步不能省略,否则会提示 Application not compiled with SDL 2.0 这样的错误。

如果函数执行成功,窗口句柄就可以使用下面的路径获取:

HWND hWnd = sys.info.win.window;

有了窗口句柄,其实你就可以“肆意妄为”了……

下面是增加菜单栏的代码:

// SDL 的窗口指针。类似:SDL_Window* pWin; 
HMENU attachMenu()
{
	SDL_SysWMinfo sys;
	SDL_VERSION(&sys.version);

	//获取窗口信息
	if (SDL_FALSE == SDL_GetWindowWMInfo(pWin, &sys)) {
		return NULL;
	}

	HWND hWnd = sys.info.win.window;
	HINSTANCE hInstance = sys.info.win.hinstance;
	HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1));
	if (!hMenu) {
		return NULL;
	}

	return SetMenu(hWnd, hMenu) ? hMenu : NULL;
}

上面的函数如果执行成功会返回添加的菜单栏句柄,在程序退出之前不要忘记使用 DestroyMenu 接口销毁该句柄。

代码中 IDR_MENU1 是程序中创建的菜单资源 ID,推荐使用添加资源的方式创建菜单,可以可视化操作,不要手动使用 API 自己逐一添加,会非常麻烦。

菜单资源

如果一切顺利,那么你程序在启动后会自动添加一个菜单栏。

菜单栏效果

高兴过后,你会发现一个蛋疼的事情,窗口绘制界面的高度缩小了。确切的说,在创建窗口时候传入的高度包含了菜单栏的高度,所以你实际上窗口客户区的高度会减去菜单栏。你需要在创建窗口的时候,特意的增加一个菜单栏的高度来解决这个问题。你可以用下面的代码得到菜单的高度:

int menuHeight = GetSystemMetrics(SM_CYMENU);

在创建窗口的时候,加上这个高度即可。

还有一个问题差点忘写,就是如何响应菜单消息。可以使用前面提到的 SDL_SetWindowsMessageHook 函数,该函数可以传入一个消息响应回调和一个自定指针:

static void windowsMessageHook(void* userdata,
	void* hWnd,
	unsigned int message,
	Uint64       wParam,
	Sint64       lParam) 
{

	switch (message)
	{
	case WM_COMMAND:
		switch (LOWORD(wParam)) {
			//新建
		case ID_40001:
			//TODO: 响应菜单消息
			break;

			//关闭
		case ID_40003:
			SendMessage(hWnd, WM_CLOSE, 0, 0);
			break;
		}
		break;
	}
}

//程序创建之前,插入消息 Hook
SDL_SetWindowsMessageHook(windowsMessageHook, 0);

2. 如何创建对话框

这个问题其实本来不想写的,因为这已经和 SDL2 无关了,仅仅是 Win32 如何创建窗口的知识。SDL2 其实提供了一个创建简单对话框的方法:

int SDL_ShowMessageBox(const SDL_MessageBoxData* messageboxdata,
                       int*                      buttonid)

如果程序中仅仅需要提示消息的对话框,使用它替代 MessageBox 是一个不错的方案。这里主要介绍的不是这种对话框,而是更加复杂一点的模态对话框创建方式。

模态窗口创建同样的先创建一个对话框资源:

对话框资源

然后直接调用 DialogBox 接口即可。具体的代码如下:

static BOOL CALLBACK DiglagItemProc(HWND hwndDlg,
	UINT message,
	WPARAM wParam,
	LPARAM lParam)
{
	switch (message)
	{
	case WM_CLOSE:
		EndDialog(hwndDlg, wParam);
		return TRUE;
	}

	return FALSE;
}

//创建对话框
DialogBox(gHinstace, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, (DLGPROC)DiglagItemProc);

还有有种非模态的窗口,可能更加复杂一点,但和 SDL2 游戏开发本身已经没啥联系了,如果感兴趣直接去网上搜索如果创建非模态窗口即可。