|
|
1 vuosi sitten | |
|---|---|---|
| .. | ||
| Image | 1 vuosi sitten | |
| src | 1 vuosi sitten | |
| README.md | 1 vuosi sitten | |
直接使用 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 表示没有任何问题
至于 WINAPI 和 CALLBACK 宏
#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 调用约定,参数都是从右到左入栈的。然而,在一些其他语言或特定的场景中,可能会看到从左到右的参数推入顺序。这些语言或平台可能设计了不同的调用约定来满足特定的需求或优化
使用 WINAPI 和 CALLBACK 宏的主要目的是确保函数与操作系统的互操作性,保持调用约定的一致性,从而使编译生成的代码能够正确地与操作系统交互。不正确的调用约定可能导致运行时错误,比如堆栈损坏,这会是难以调试的错误。通过标准化调用约定,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
);
wMsgFilterMin 和 wMsgFilterMax 用与过滤信息
如果 hWnd 为 NULL,GetMessage 将检索属于当前线程的任何窗口的消息
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) 没有任何含义,单纯就是为了测试输出结果是否生效
消息的类型有很多
大概四百种类型,每种消息的触发条件可能需要自行测试
当然官网上也有一些消息类型的解释
除了官网和谷歌之外,还可以通过运行代码测试,何种情况触发何种宏来确定宏的触发条件
项目中使用 WIndowsMessageMap 来测试宏的触发,代码地址
以键盘按键为例
一次键盘的按下和松开会触发三个消息:WM_KEYDOWN、WM_CHAR 和 WM_KEYUP。当按下 D 键时,WM_KEYDOWN 的 wParam 输出为 0x0000044 ;当按下 F 键时,wParam 输出为 0x0000046,所以 wParam 可能存储了按下按钮相关信息
WM_CHAR 是用于文本输入的信息,所以一些按键按下之后不会触发 WM_CHAR,比如 F1、F2,并且 WM_CHAR 是大小写敏感的,WM_KEYDOWN 则大小写不敏感
同样按下 F 键,WM_KEYDOWN 的 wParam 是 0x0000046,表示大写的 F;而 WM_CHAR 的 wParam 是 0x0000066 表示小写的 f
case WM_CHAR:
{
static std::string title;
title.push_back((char)wParam);
SetWindowText(hWnd, to_wide_string(title).c_str());
break;
}
以鼠标点击为例
主要的消息触发就是:WM_LBUTTONDOWN 和 WM_LBUTOTNUP 来表示鼠标左键的点击和松开,对应的鼠标右键点击就是 WM_RBUTTONDOWN 和 WM_RBUTTONUP,鼠标移动有 WM_MOUSEMOVE
LBUTTONDOWN 更多参数信息官方文档有说明
POINT pt;
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
pt = MAKEPOINTS(lParam);
通过上面俩种方法可以获得鼠标相对工作区左上角的坐标
case WM_LBUTTONDOWN:
{
POINTS pt = MAKEPOINTS(lParam);
std::ostringstream oss;
oss << "(" << pt.x << ", " << pt.y << ")";
SetWindowText(hWnd, to_wide_string(oss.str()).c_str());
break;
}
由于该项目只有一个窗口,所以直接做成一个单例类
class Window
{
private:
// singleton manages registration/cleanup of window class
class WindowClass;
private:
static LRESULT CALLBACK HandleMsgSetup(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept;
static LRESULT CALLBACK HandleMsgThunk(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept;
LRESULT HandleMsg(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept;
}
WindowClass 是窗口的实例类,由 Window 来管理
WindowClass 是单例类,所以会在获取 GetInstance 时构建和注册一个窗口
Window::WindowClass::WindowClass() noexcept : hInst(GetModuleHandle(nullptr))
{
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(wc);
wc.style = CS_OWNDC;
wc.lpfnWndProc = HandleMsgSetup;
// ... 其他注册内容
RegisterClassEx(&wc);
}
在 Window 类创建的时候会通过 WindowClass 构建和注册一个窗口,再由 Window 来创建出窗口
Window::Window(int InWidth, int InHeight, const wchar_t* InName) noexcept
{
RECT Wr;
Wr.left = 100;
Wr.right = InWidth + Wr.left;
Wr.top = 100;
Wr.bottom = InHeight + 100;
// AdjustWindowRect 会根据样式重新计算 RECT 中各个参数的值
AdjustWindowRect(&Wr, WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU, FALSE);
// 重新设置过 RECT 参数,所以这里不能直接使用 InWidth 和 InHeight
hWnd = CreateWindow(
WindowClass::GetName(), InName,
WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
CW_USEDEFAULT, CW_USEDEFAULT, Wr.right - Wr.left, Wr.bottom - Wr.top,
nullptr, nullptr, WindowClass::GetInstance(), this
);
ShowWindow(hWnd, SW_SHOWDEFAULT);
}
这里需要注意的是 wc.lpfnWndProc = HandleMsgSetup 绑定的窗口信息函数
LRESULT Window::HandleMsgSetup(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept
{
if (msg == WM_NCCREATE)
{
const CREATESTRUCTW* const pCreate = reinterpret_cast<CREATESTRUCTW*>(lParam);
Window* const pWnd = static_cast<Window*>(pCreate->lpCreateParams);
SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pWnd));
SetWindowLongPtr(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&Window::HandleMsgThunk));
return pWnd->HandleMsg(hWnd, msg, wParam, lParam);
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
当 WM_NCCREATE 被触发的时候,重新设置 GWLP_USERDATA 和 GWLP_WNDPROC,也就将消息的回调函数设置成了 Window::HandleMsgThunk
为什么要切换信息回调函数呢?
HandleMsgSetup 专注于窗口创建时的设置工作,而 HandleMsgThunk 用于处理窗口的常规消息。这种分离使得代码更加清晰和易于管理HandleMsgSetup 确保只有在关联建立之后,消息才会被转发到窗口类的实例HandleMsgThunk 将直接转发消息到窗口类的实例,无需每次都检查 WM_NCCREATE 消息。这提高了消息处理的效率HandleMsgSetup 函数即可,而不会影响到常规消息处理的代码这里不得不提到 WM_NCCREATE ,这个宏的 NCCREATE 可以拆分成 NC 和 CREATE,后者 CREATE 就是创建的意思;前者 NC 表示的是 No-Client
以上图为例,图中标题栏的边框,最大化最小化按钮,以及其他UI元素,这个边框称为 Window的 no-client 区域。
WM_NCCREATE 在 WM_CREATE 之前发送
在这之前的消息都会被 HandleMsgSetup 吃掉,因为客户端一般不用处理这之前的消息
通过 SetWindowLongPtr 就设定了 GWLP_USERDATA 用户自定义数据中存储的 Window 对象指针
LRESULT Window::HandleMsgThunk(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept
{
Window* const pWnd = reinterpret_cast<Window*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
return pWnd->HandleMsg(hWnd, msg, wParam, lParam);
}
在 HandleMsgThunk 函数中,通过 GetWindowLongPtr 的方式将 Window 对象的指针从 GWLP_USERDATA 中取出,并将消息转发到 HandleMsg 中
由于 WINAPI 回调函数需要符合特定的签名,并且必须能够通过全局访问,因此它们不能直接绑定到类的非静态成员函数,这个函数必须是一个全局函数或静态成员函数
所以通过上面的方法,将全局事件分发到成员函数 Window::HandleMsg 中
// 异常基类
class ChiliException: public std::exception
{
public:
ChiliException(int line, const char* file) noexcept;
const char* what() const noexcept override;
virtual const char* GetType() const noexcept;
int GetLine() const noexcept;
const std::string& GetFile() const noexcept;
std::string GetOriginString() const noexcept;
private:
int line; // 错误出现的行
std::string file; // 错误出现的文件
protected:
mutable std::string whatBuffer;
};
// Window 异常类
class Exception : public ChiliException {
public:
Exception(int line, const char* file, HRESULT InHr);
const char* what() const noexcept override;
virtual const char* GetType() const noexcept;
static std::string TranslateErrorCode(HRESULT InHr);
HRESULT GetErrorCode() const noexcept;
std::string GetErrorString() const noexcept;
private:
HRESULT hr;
};
#define CHWND_EXCEPT(hr) Window::Exception(__LINE__, __FILE__, hr)
所以的异常都基于 ChiliException 扩展即可,重写 GetType 和 what 函数,即可将错误分类
那么,在日常使用的时候,如果 Window 需要抛出异常,直接使用 CHWND_EXCEPT 即可
常见的 system error code 在 官方文档 中有比较详细的介绍
throw CHWND_EXCEPT(ERROR_ARENA_TRASHED)
D3D: DirectX 3D
D3D 是面向对象对象的架构,建立在 COM(Component Object Moduel, 组件对象模型) 对象上,这些对象表示着 D3D 中的实体(例如:着色器、纹理、状态等),这些物体的父类都是 Device
COM 是 D3D 中使用的软件架构,旨在突进软件组件之间的交互。COM 是一个面向接口的系统,它定义了对象如何通过一组明确的接口与外部世界通信。所有 COM 对象都继承自 IUnknow 接口,这是 COM 规范中的基础接口,提供了对象生命周期管理(引用计数)和接口查询功能
在 Direct3D 中,几乎所有的对象(如设备、纹理、缓冲区等)都是以 COM 接口的形式实现的。这些对象实现 COM 接口,确保了它们可以在多种编程环境中以一致的方式使用,并且能够在不同版本的 Direct3D 间提供一定程度的向后兼容性
Query Interface):每个 COM 对象都可以通过 QueryInterface 来查询是否支持特定的接口,这运行开发者根据运行时可以同的功能动态访问不同的接口AddRef 和 Release 来增减引用计数Device 在 Direct3D 中扮演中心角色,是进行所有渲染操作和资源管理的核心接口。它是由 Direct3D 创建并返回给应用程序的一个 COM 接口,允许应用程序通过调用此接口的方法来执行图形和计算任务
ID3D11Device::CreateTexture2D 等方法创建ID3D11Device::CreateVertexShader、ID3D11Device::CreatePixelShader 等方法创建ID3D11Device::CreateBlendState、ID3D11Device::CreateRasterizerState 等方法创建在 Direct3D 中,Device 和其他资源如着色器、纹理、状态等之间的关系可以总结如下:
Device 是创建和管理所有其他图形资源的中心。所有这些资源(着色器、纹理、状态等)都由 Device 接口创建Device 以及它创建的所有其他资源都是 COM 对象,遵守 COM 接口和引用计数的规则COM 对象,Device 和其他资源的实现被抽象和封装,开发者主要通过接口与这些资源交互。这允许 Direct3D 在不影响已有应用的情况下进行更新和改进如果显存中只存在一块区域用与显示,每次显卡都会将这块区域的数据发送到显示器中绘制。当我们更新数据时,可能只绘制了一部分就被显卡发送到显示进行绘制了,这样会出现屏幕的撕裂
所以一般使用俩块缓冲区,一块专门用于提供给显示器进行渲染,称为前缓冲区;一块专门用于实际绘制,称为后缓冲区
当后缓冲区绘制完毕之后,会使用 Present 的方法,将后缓冲区非常快的复制到前缓冲上;或者使用 Flip 的方法,直接将后缓冲重命名为前缓冲,前缓冲重命名为后缓冲
这里必须提到 DXGI(DirectX Graphics Infrastructure) 是一个低级的 API,用于抽象和管理图形硬件资源。它是 DirectX 家族的一部分,主要负责处理图形设备的枚举、显示监控的管理、交换链(用于图像呈现的缓冲区管理)以及帧的呈现
DXGI 为 DirectX 提供核心功能,特别是与显示设备和屏幕缓冲区的交互
DXGI 的目的是与内核模式驱动程序和系统硬件通信
应用程序可以直接访问 DXGI,也可以调用 Direct3D API,以处理与 DXGI 的通信。 如果应用程序需要枚举设备或控制如何将数据呈现给输出,则可能需要直接使用 DXGI
D3D 除了创建交换链、设备外还会创建上下文(Context),用于发出渲染你命令平配置渲染管道
一般来说 设备(Device) 用于创建物体,上下文(Context) 用于绘制
上下文分为即时和延迟俩种,当调用即时上下文时,会让硬件马上执行渲染工作;延迟上下文会建立指令集,然后将指令发布到即时上下文中,所以延迟上下文在多线程中表现良好