nicetry12138 38d9f3bf9b feat: 添加键盘输入事件的监听 1 rok temu
..
Image 38d9f3bf9b feat: 添加键盘输入事件的监听 1 rok temu
src 38d9f3bf9b feat: 添加键盘输入事件的监听 1 rok temu
README.md 38d9f3bf9b feat: 添加键盘输入事件的监听 1 rok temu

README.md

DirectX学习

教程地址

创建窗口项目

直接使用 visual studio 创建空白项目,并创建 WinMain.cpp 作为程序入口

由于设置的是窗口系统,所以不能使用 main 作为入口函数,而是 WinMain

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hInstance, 
					HINSTANCE hPrevInstance, 
					LPSTR lpCmdLine, 
					int nCmdSHow) {
	while (true)
	{
        // 防止程序结束 用 while 阻塞函数 
	}

	return 0;
}

微软官方文档

根据文档解释,WinMain 函数的参数分别表示如下几个

  • hInstance 是 实例的句柄 或模块的句柄。 当可执行文件加载到内存中时,操作系统使用此值来标识可执行文件或 EXE。 某些 Windows 函数需要实例句柄,例如加载图标或位图
  • hPrevInstance 没有任何意义。 它在 16 位 Windows 中使用,但现在始终为零
  • pCmdLine 以 Unicode 字符串的形式包含命令行参数
  • nCmdShow 是一个标志,指示主应用程序窗口是最小化、最大化还是正常显示

nCmdShow 的使用,文档中也有解释

至于 WinMain 函数返回值,一般来说操作系统不使用返回值,但是可以使用该值将状态代码传递给另一个程序

一般来说返回 0 表示没有任何问题

至于 WINAPICALLBACK

#define CALLBACK    __stdcall
#define WINAPI      __stdcall
#define WINAPIV     __cdecl

一般来说

  • WINAPI 指的是 Windows API 使用的一种调用约定。通常用于定义 API 函数,很多 Windows API 都这样
  • CALLBACK 常用于回调函数,例如事件处理或窗口处理函数(如 WindowProc)。这确保这些函数能与发出回调的 Windows 操作系统兼容

使用 __stdcall 调用约定意味着参数从右至左被推送到堆栈上,且函数自己清理堆栈。这对于减少应用程序中的错误非常有用,因为堆栈管理是自动的

除了 __stdcall 之外,还有 __cdecl__fastcall

  • __cdecl: 参数同样是从右至左推入堆栈,但是调用者清理堆栈。这使得 __cdecl 支持可变数量的参数
  • __fastcall: 一种尽可能通过寄存器而非堆栈传递参数的调用约定,可以提高函数的调用效率,特别是在参数数量较少时

因为 __stdcall 是由函数自己清理堆栈,所以需要明确知道堆栈上有多少字节需要被清理。所以 __stdcall 不支持可变数量参数。而 __cdecl 是调用者清理堆栈,所以根据传递给函数实际参数数量来调整堆栈指针

参数从左到右入栈的顺序不是在常见的 C/C++ 调用约定中看到的模式,因为在 C/C++ 中,无论是 __cdecl 还是 __stdcall 调用约定,参数都是从右到左入栈的。然而,在一些其他语言或特定的场景中,可能会看到从左到右的参数推入顺序。这些语言或平台可能设计了不同的调用约定来满足特定的需求或优化

使用 WINAPICALLBACK 宏的主要目的是确保函数与操作系统的互操作性,保持调用约定的一致性,从而使编译生成的代码能够正确地与操作系统交互。不正确的调用约定可能导致运行时错误,比如堆栈损坏,这会是难以调试的错误。通过标准化调用约定,Windows 确保了不同编译器和代码库之间的兼容性和稳定性

注册窗口

对于一个窗口程序来说,要做的事情有:窗口显示(窗口样式、行为等),在 Win32 程序中一般是先注册窗口类,再根据注册类创建窗口实例,实例就是真正控制的窗口

一个程序一般不止一个窗口

ATOM
WINAPI
RegisterClassExW(
    _In_ CONST WNDCLASSEXW *);
#ifdef UNICODE
#define RegisterClassEx  RegisterClassExW
#else
#define RegisterClassEx  RegisterClassExA
#endif // !UNICODE

一般使用 RegisterClassEx 来注册类,它是 RegisterClass 函数的扩展

  • RegisterClass 参数为 WNDCLASS 的结构指针
  • RegisterClassEx 参数为 WNDCLASSEX 的结构指针,该结构包括 WNDCLASS 的全部内容,并添加了额外字段:小图标(hIconSm)、任务栏图标等
typedef struct tagWNDCLASSEXA {
  UINT      cbSize;             
  UINT      style;              
  WNDPROC   lpfnWndProc;        
  int       cbClsExtra;         
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCSTR    lpszMenuName;
  LPCSTR    lpszClassName;
  HICON     hIconSm;
} WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;

WNDCLASSEX 官方解释

官方文档对结构体成员属性有比较详细的解释,这里就不再搬运

需要注意的是 style 属性,我们使用的是 CS_OWNDC,也就是为类中的每个窗口分配唯一的设备上下文,也就是 Device Context 简称 DC,然后每个窗口就能被独立渲染。通常情况下,多个窗口可能会共享相同的设备上下文。如果一个窗口类被定义为 CS_OWNDC,则每个该类的窗口将获取一个独占的设备上下文,并保持这个设备上下文,直到窗口被销毁。这意味着窗口不需要在每次绘制时重新获取设备上下文,可以提高绘制效率

关于 style官方解释

另一个需要注意的是 WNDPROC lpfnWndProc,指向窗口过程的指针。这个函数将处理所有有关这个窗口的信息,这些消息可以是用户的操作(如键盘输入、鼠标移动、点击等),或者是系统事件(如绘制消息、窗口大小改变等)。

typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

关于 WNDPROC官方文档有比较详细的解释

创建窗口

创建窗口一般使用 CreateWindowExA 函数

HWND CreateWindowExA(
  [in]           DWORD     dwExStyle,   // 窗口样式
  [in, optional] LPCSTR    lpClassName,
  [in, optional] LPCSTR    lpWindowName,
  [in]           DWORD     dwStyle,     // 窗口样式
  [in]           int       X,           // 窗口位置 X 坐标
  [in]           int       Y,           // 窗口位置 Y 坐标
  [in]           int       nWidth,      // 窗口宽度
  [in]           int       nHeight,     // 窗口高度
  [in, optional] HWND      hWndParent,
  [in, optional] HMENU     hMenu,
  [in, optional] HINSTANCE hInstance,
  [in, optional] LPVOID    lpParam      // 用与传递自定义数据
);

关于创建窗口,官方文档提供了比较详细的解释

创建窗口之后需要展示窗口, 也就是 ShowWindow

BOOL ShowWindow(
  [in] HWND hWnd,
  [in] int  nCmdShow
);

关于显示窗口,官方文档有比较详细的解释

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nCmdSHow) {

	const wchar_t* pClassName = L"hw3dbutts";

	// 注册类
	WNDCLASSEX wc = { 0 };
	wc.cbSize = sizeof(wc);
	wc.style = CS_OWNDC;
	wc.lpfnWndProc = DefWindowProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = hInstance;
	wc.hIcon = nullptr;
	wc.hCursor = nullptr;
	wc.hbrBackground = nullptr;
	wc.lpszMenuName = nullptr;
	wc.lpszClassName = pClassName;
	wc.hIconSm = nullptr;
	RegisterClassEx(&wc);

	// 创建窗口
	HWND hWnd = CreateWindowEx(
		WS_EX_RIGHTSCROLLBAR,
		pClassName,
		L"Hello World",
		WS_SYSMENU | WS_CAPTION | WS_MAXIMIZEBOX,
		200, 200, 640, 480,
		nullptr, nullptr, hInstance, nullptr
	);

	// 展示窗口
	ShowWindow(hWnd, SW_SHOW);


	while (true)
	{

	}

	return 0;
}

然后就可以得到一个不能做任何事情的窗口

消息循环

对于窗口来说,除了窗口显示之外,还需要处理信息

比如 Visual Studio 需要处理键盘输入,我们要处理的窗口消息(Window Message) 本质上来说就是事件(Event)

当鼠标点击、鼠标移动、键盘输入等事件触发之后,窗口会首先把消息按顺序放进到消息队列(Message Queue) 中,可以通过 GetMessage 来获取队列中的消息,之后通过 DispatchMessage 把消息从应用传递给对应的窗口的 lpfnWndProc 函数

BOOL GetMessage(
  [out]          LPMSG lpMsg,           // 消息的指针
  [in, optional] HWND  hWnd,            // 处理信息的窗口指针
  [in]           UINT  wMsgFilterMin,
  [in]           UINT  wMsgFilterMax
);

wMsgFilterMinwMsgFilterMax 用与过滤信息

如果 hWndNULLGetMessage 将检索属于当前线程的任何窗口的消息

GetMessage 的值也同样需要注意,如果是退出窗口 WM_QUIT 则返回值为 0,否则是非零值。如果出现错误,则返回 -1

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
	switch (msg)
	{
	case WM_CLOSE:
		PostQuitMessage(69);
		break;
	default:
		break;
	}
	return DefWindowProc(hWnd, msg, wParam, lParam);
}

// WinMain function 
{
    // Do Something 

	// 消息处理
	MSG msg;
	BOOL gResult;

	while ((gResult = GetMessage(&msg, nullptr, 0, 0)) > 0)
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	if (gResult < 0) {
		return -1;	// 表示程序错误
	}
	return msg.wParam;	// 否则输出我们期望的值 也就是 PostQuitMessage 传入的参数值
}

这里使用 WndProc 自定义的函数来接管默认的 DefWindowProc(Def 开头表示 Default),然后特殊处理 WM_CLOSE 时关闭程序,否则 DefWindowProc 只会关闭窗口而不会关闭进程

PostQuitMessage 函数将 WM_QUIT 消息发布到线程的消息队列并立即返回;函数只是向系统指示线程正在请求在将来的某个时间退出

当线程从其消息队列中检索 WM_QUIT 消息时,它应退出其消息循环,并将控制权返回到系统。 返回到系统的退出值必须是 WM_QUIT 消息的 wParam 参数

所以最后 WinMain 的输出是 msg.wParam,这样程序的代码是可以自定义的,未来可以通过这个结束码通知其他进程

输出为 69 结果生效

这里使用 PostQuitMessage(69) 没有任何含义,单纯就是为了测试输出结果是否生效

消息循环的类型有很多

list of windows Message

大概四百种类型,每种消息的触发条件可能需要自行测试

当然官网上也有一些消息类型的解释

除了官网和谷歌之外,还可以通过运行代码测试,何种情况触发何种宏来确定宏的触发条件

项目中使用 WIndowsMessageMap 来测试宏的触发,代码地址

以键盘按键为例

一次键盘的按下和松开会触发三个消息:WM_KEYDOWNWM_CHARWM_KEYUP。当按下 D 键时,WM_KEYDOWNwParam 输出为 0x0000044 ;当按下 F 键时,wParam 输出为 0x0000046,所以 wParam 可能存储了按下按钮相关信息

  • 关于 WM_CHAR 具体内容可以查官方文档
  • 关于 lParam 表示的虚拟按键,可以通过官方文档 获取更多信息

以鼠标点击为例

主要的消息触发就是:WM_LBUTTONDOWNWM_LBUTOTNUP 来表示鼠标左键的点击和松开,对应的鼠标右键点击就是 WM_RBUTTONDOWNWM_RBUTTONUP,鼠标移动有 WM_MOUSEMOVE