13.ImGui-搭建内部绘制的ImGui项目框架(无消息循环的简单ImGui实例)
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
本次游戏没法给
内容参考于:微尘网络安全
上一个内容:12.ImGui-外部绘制独立的界面窗体
下面开始写代码了,代码看不懂没关系,要知道怎样去复制(写的很细全程傻瓜模式),代码的说明会放在最后(不放在最后,截图的时候不好截,看起来会很乱,用来说代码的来源会很不友好),说明用小白也能看懂的语言写的,看多了慢慢就知道了,不知道也没关系会复制会用就行
到这ImGui的基础就可以了,然后接下来是如何在某程序内部使用ImGui绘制窗口
首先打卡VisualStudio2022创建一个新项目
内部绘制就要把我们程序放到目标程序里,也就是要用动态链接库,所以就创建一个动态链接库的项目,如下图红框
随便输入一个项目名和保存位置,然后点击创建
创建完成
然后导入ImGui用到的库(文件) ,首先把下图红框的文件复制一下
把复制的上图红框里的文件放到下图红框的目录,也就是上方创建项目的时候设置的那个目录
然后创建一个文件夹
然后再复制下图红框的两个文件
把它们复制到刚刚创建的ImGui文件夹里,如下图红框
然后在把下图红框的两个文件复制到ImGui文件夹中
复制完后,文件相关的就完事了
然后来到vs2022里,首先新加一个筛选器
然后把下图红框里的4个文件拖到新加的筛选器里
然后把下图红框的文件拖到源文件里
然后右击头文件选择新建项
创建一个main.h文件
main.h里添加内容
#pragma once
#include <d3d11.h>
#include "ImGui/imgui_impl_dx11.h"
#include "ImGui/imgui_impl_win32.h"
然后设置包含目录,如下图鼠标右击选择属性
如下图设置包含目录
添加 $(ProjectDir),然后点击确定
$(ProjectDir)是vs中内置的目录,它表示.vcxproj文件所在的目录,也就是下图红框文件的所在目录,这样写换个电脑就不用重新设置目录了,让vs自己找目录
然后在给附加依赖项添加d3d11.lib; 如下图红框
然后点击运行,会出现错误
在预编译头里选择不使用预编译头,然后点击确定
然后点击下图红框运行代码,会出现下图蓝框的提示,这是因为我们的项目是一个dll,不是exe这是正常的
可以点击重新生成
然后编译成功
导致ImGui需要的东西就都搞完了,也都是之前的内容,然后接下来就开始写主要代码了,然后来到DllMain里,这是dll的入口,就是当dll放到目标程序里,就会执行DllMain里的代码
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
/*** DllMain:Windows动态链接库(DLL)的入口点函数* 当DLL被加载到进程、线程创建/销毁、DLL被卸载时,系统会自动调用该函数* 用于执行DLL的初始化、资源分配、清理等操作** 参数说明:* - hModule:DLL的模块句柄(HMODULE),可理解为DLL在内存中的唯一标识* 类似于EXE的实例句柄,可用于加载资源、获取路径等* - ul_reason_for_call:调用该函数的原因(触发事件),为DWORD类型* - lpReserved:保留参数,用于传递额外信息,不同事件含义不同** 返回值:BOOL类型* - 对于DLL_PROCESS_ATTACH事件:返回TRUE表示DLL加载成功;返回FALSE会导致加载失败* - 其他事件:返回值通常被忽略(系统不处理)*/
BOOL APIENTRY DllMain(HMODULE hModule, // DLL模块句柄(当前DLL的标识)DWORD ul_reason_for_call, // 调用原因(触发的事件类型)LPVOID lpReserved // 保留参数(不同事件含义不同)
)
{// 根据调用原因处理不同事件switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:// 事件含义:DLL被成功加载到某个进程的地址空间时触发// 触发时机:进程首次加载DLL时(如通过LoadLibrary函数)// 典型用途:初始化DLL的全局资源(如分配内存、创建对象、注册回调)// 注意:此时进程的主线程可能还未开始执行,避免在此创建UI或复杂操作// lpReserved:若为NULL,表示DLL是被显式加载(如LoadLibrary);非NULL表示隐式加载(如依赖项加载)break;case DLL_THREAD_ATTACH:// 事件含义:进程中创建新线程时,系统会通知所有已加载的DLL// 触发时机:新线程启动时(线程函数执行前)// 典型用途:为新线程分配专属资源(如线程局部存储TLS)// 注意:每个新线程创建都会触发,需避免资源泄露// lpReserved:始终为NULLbreak;case DLL_THREAD_DETACH:// 事件含义:进程中的线程结束时,系统会通知所有已加载的DLL// 触发时机:线程正常退出时(线程函数返回后)// 典型用途:释放该线程在DLL中分配的专属资源(如TLS中的数据)// 注意:若线程异常终止(如TerminateThread),此事件不会触发// lpReserved:始终为NULLbreak;case DLL_PROCESS_DETACH:// 事件含义:DLL从进程的地址空间中卸载时触发// 触发时机:进程调用FreeLibrary释放DLL,或进程退出时// 典型用途:释放DLL_PROCESS_ATTACH中分配的全局资源(如内存、文件句柄)// 注意:若进程退出(如ExitProcess),部分系统资源可能已失效,避免复杂操作// lpReserved:若为NULL,表示DLL被显式卸载(如FreeLibrary);非NULL表示进程正在退出break;}// 返回TRUE表示所有事件处理成功// 对于DLL_PROCESS_ATTACH,返回FALSE会导致DLL加载失败return TRUE;
}
然后创建一个MyDx.h和MyDx.cpp文件
然后在main.h文件中添加下图红框的内容 #include <windows.h>
MyDx.h的内容
#pragma once
// 声明一个函数叫做Go
DWORD Go(LPVOID lpThreadParameter
);
MyDx.pp的内容
#include "main.h"
// 实现Go函数
DWORD Go(LPVOID lpThreadParameter
) {return 0;
}
然后dllmain.cpp里的内容
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "main.h"
/*** DllMain:Windows动态链接库(DLL)的入口点函数* 当DLL被加载到进程、线程创建/销毁、DLL被卸载时,系统会自动调用该函数* 用于执行DLL的初始化、资源分配、清理等操作** 参数说明:* - hModule:DLL的模块句柄(HMODULE),可理解为DLL在内存中的唯一标识* 类似于EXE的实例句柄,可用于加载资源、获取路径等* - ul_reason_for_call:调用该函数的原因(触发事件),为DWORD类型* - lpReserved:保留参数,用于传递额外信息,不同事件含义不同** 返回值:BOOL类型* - 对于DLL_PROCESS_ATTACH事件:返回TRUE表示DLL加载成功;返回FALSE会导致加载失败* - 其他事件:返回值通常被忽略(系统不处理)*/
BOOL APIENTRY DllMain(HMODULE hModule, // DLL模块句柄(当前DLL的标识)DWORD ul_reason_for_call, // 调用原因(触发的事件类型)LPVOID lpReserved // 保留参数(不同事件含义不同)
)
{// 根据调用原因处理不同事件switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:// 事件含义:DLL被成功加载到某个进程的地址空间时触发// 触发时机:进程首次加载DLL时(如通过LoadLibrary函数)// 典型用途:初始化DLL的全局资源(如分配内存、创建对象、注册回调)// 注意:此时进程的主线程可能还未开始执行,避免在此创建UI或复杂操作// lpReserved:若为NULL,表示DLL是被显式加载(如LoadLibrary);非NULL表示隐式加载(如依赖项加载)// 启动一个线程调用Go函数::CreateThread(0,0,Go,0,0,0);break;case DLL_THREAD_ATTACH:// 事件含义:进程中创建新线程时,系统会通知所有已加载的DLL// 触发时机:新线程启动时(线程函数执行前)// 典型用途:为新线程分配专属资源(如线程局部存储TLS)// 注意:每个新线程创建都会触发,需避免资源泄露// lpReserved:始终为NULLbreak;case DLL_THREAD_DETACH:// 事件含义:进程中的线程结束时,系统会通知所有已加载的DLL// 触发时机:线程正常退出时(线程函数返回后)// 典型用途:释放该线程在DLL中分配的专属资源(如TLS中的数据)// 注意:若线程异常终止(如TerminateThread),此事件不会触发// lpReserved:始终为NULLbreak;case DLL_PROCESS_DETACH:// 事件含义:DLL从进程的地址空间中卸载时触发// 触发时机:进程调用FreeLibrary释放DLL,或进程退出时// 典型用途:释放DLL_PROCESS_ATTACH中分配的全局资源(如内存、文件句柄)// 注意:若进程退出(如ExitProcess),部分系统资源可能已失效,避免复杂操作// lpReserved:若为NULL,表示DLL被显式卸载(如FreeLibrary);非NULL表示进程正在退出break;}// 返回TRUE表示所有事件处理成功// 对于DLL_PROCESS_ATTACH,返回FALSE会导致DLL加载失败return TRUE;
}
然后上方的Go函数实现还没完,我们的ImGui的代码要写在Go函数里,所以接下来开始复制ImGui的代码,下图蓝框的代码是创建一个窗口,现在我们是内部窗口目标程序已经有窗口了,所以现在不需要它们,直接从CreateDeviceD3D开始
把下图红框的代码全部复制,
复制过来后补齐代码,都是从ImGui源码里复制的
复制方式,来到ImGui源码里,然后找到缺少的代码,按着CTRL然后鼠标左键单击,就会给跳到它的声明位置(声明可以理解为创建)
就会跳到下图红框位置,把它复制过去就可以了
然后再补全CreateRenderTarget函数,按着CTRL鼠标左键单击下图红框位置
跳到CreateRenderTarget函数后,把下图红框的代码复制过去
复制过来后,可以看到它缺少 g_mainRenderTargetView,再找g_mainRenderTargetView
按着CTRL使用鼠标左键单击下图红框位置
然后它就跳转到下图红框位置,把下图红框代码复制过去
然后就不报错了
下方就不贴复制后的图了,只贴需要复制的代码的图,默认把它们依次复制过去就行
然后是创建ImGui的上下文,也就是让ImGui初始化它需要的数据(上下文可以理解为需要的数据)
然后是ImGui的样式
然后是初始化WIn32(连接ImGui与Windows窗口系统)和DirectX 11(连接ImGui与DirectX 11渲染系统)
然后是Begin和End
然后数渲染视图
然后绑定视图
连接DirectX 11
复制完的代码
重新生成,可以正常生成
以上就是一个从官方的实例中摘出来的最简单最基本的ImGui实例,想要运行还需要把渲染的代码放到虚表里,下一节hook虚表,然后把我们的代码放进去
带说明的代码,由Ai生成
// 包含程序所需的头文件(通常包含DirectX、ImGui等库的声明)
#include "main.h"// 获取目标窗口的句柄(HWND)
// FindWindowA参数说明:
// - 第一个参数:窗口类名(为空字符串表示不限制类名)
// - 第二个参数:窗口标题(NULL表示查找任何窗口)
// 作用:获取一个已存在窗口的句柄,后续DirectX渲染会输出到该窗口
HWND hWnd = FindWindowA("", NULL);// DirectX 11核心全局资源(整个程序共享)
static ID3D11Device* g_pd3dDevice = nullptr; // D3D11设备对象,负责创建渲染资源
static ID3D11DeviceContext* g_pd3dDeviceContext = nullptr; // D3D11设备上下文,负责执行渲染命令
static IDXGISwapChain* g_pSwapChain = nullptr; // 交换链对象,管理前后缓冲区实现双缓冲
static ID3D11RenderTargetView* g_mainRenderTargetView = nullptr; // 主渲染目标视图,绑定交换链的后台缓冲区/*** 线程函数:负责初始化DirectX 11和ImGui,并执行首次UI渲染* 函数签名符合Windows线程函数要求(LPVOID参数,返回DWORD)* 参数:* - lpThreadParameter:线程启动时传递的参数(此处未使用)* 返回值:DWORD类型,0通常表示执行成功*/
DWORD Go(LPVOID lpThreadParameter // 线程参数(未使用,保留为兼容线程函数签名)
) {// 初始化交换链描述结构体(DXGI_SWAP_CHAIN_DESC),定义交换链的属性DXGI_SWAP_CHAIN_DESC sd;ZeroMemory(&sd, sizeof(sd)); // 清空结构体,避免未初始化内存导致的错误// 配置交换链核心参数sd.BufferCount = 2; // 缓冲区数量(双缓冲:1个前台显示,1个后台渲染)sd.BufferDesc.Width = 0; // 缓冲区宽度(0表示自动匹配窗口宽度)sd.BufferDesc.Height = 0; // 缓冲区高度(0表示自动匹配窗口高度)sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 像素格式(32位RGBA,8位/通道)sd.BufferDesc.RefreshRate.Numerator = 60; // 刷新率分子(60Hz)sd.BufferDesc.RefreshRate.Denominator = 1; // 刷新率分母(60/1=60Hz)sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // 允许切换显示模式(窗口/全屏)sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 缓冲区用途(作为渲染目标)sd.OutputWindow = hWnd; // 绑定的窗口句柄(渲染结果输出到该窗口)sd.SampleDesc.Count = 1; // 多重采样数量(1表示无抗锯齿)sd.SampleDesc.Quality = 0; // 采样质量(0表示默认)sd.Windowed = TRUE; // 窗口模式(TRUE为窗口化,FALSE为全屏)sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; // 交换效果(交换后丢弃后台缓冲区数据)// 创建设备的标志(可添加调试标志)UINT createDeviceFlags = 0;// createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; // 启用调试模式(需要安装DirectX SDK)D3D_FEATURE_LEVEL featureLevel; // 存储实际支持的Direct3D特性级别(版本)// 支持的D3D版本列表(优先使用11.0,不支持则降级到10.0)const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0 };// 创建设备、设备上下文和交换链HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, // 显卡适配器(nullptr使用默认显卡)D3D_DRIVER_TYPE_HARDWARE, // 驱动类型(硬件加速,使用GPU)nullptr, // 软件渲染模块(未使用)createDeviceFlags, // 创建设备的标志featureLevelArray, // 支持的D3D版本列表2, // 版本列表数量D3D11_SDK_VERSION, // SDK版本(使用当前版本)&sd, // 交换链描述结构体&g_pSwapChain, // 输出:创建的交换链&g_pd3dDevice, // 输出:创建的D3D设备&featureLevel, // 输出:实际支持的D3D版本&g_pd3dDeviceContext // 输出:创建的设备上下文);// 若硬件加速失败(如显卡不支持D3D11),尝试使用WARP软件渲染驱动if (res == DXGI_ERROR_UNSUPPORTED)res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, // 软件渲染驱动(CPU模拟GPU)nullptr, createDeviceFlags,featureLevelArray, 2,D3D11_SDK_VERSION, &sd,&g_pSwapChain, &g_pd3dDevice,&featureLevel, &g_pd3dDeviceContext);// 若设备/交换链创建失败,返回错误(0表示成功,此处返回false可能是笔误,应为非0)if (res != S_OK)return false;// 获取交换链的后台缓冲区(用于创建渲染目标视图)ID3D11Texture2D* pBackBuffer; // 临时指针,存储后台缓冲区g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); // 0表示第一个缓冲区// 使用后台缓冲区创建渲染目标视图(告诉GPU渲染结果输出到这里)g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_mainRenderTargetView);pBackBuffer->Release(); // 释放临时缓冲区指针(渲染目标视图已引用该资源)// 初始化ImGui上下文(管理UI状态和资源的核心对象)ImGui::CreateContext();// 设置ImGui的UI风格为暗色主题(内置风格之一,还有亮色、经典风格等)ImGui::StyleColorsDark();// 初始化ImGui的Win32后端(连接ImGui与Windows窗口系统,处理输入事件)ImGui_ImplWin32_Init(hWnd);// 初始化ImGui的DX11后端(连接ImGui与DirectX 11,负责UI渲染)ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);// 定义一个ImGui窗口("Hello, world!"为窗口标题)// 注意:正常流程中,窗口定义应放在NewFrame()之后、Render()之前的主循环内// 此处直接调用Begin/End缺少帧初始化,可能导致UI无法正确显示ImGui::Begin("Hello, world!"); // 开始定义窗口内容(此处无实际内容)ImGui::End(); // 结束窗口定义// 生成UI绘制数据(将窗口/控件转换为GPU可识别的绘制命令)ImGui::Render();// 设置渲染目标(告诉GPU接下来的渲染输出到主渲染目标)g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr);// 通过DX11后端执行绘制命令,将ImGui UI渲染到屏幕上ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());// 线程执行完成,返回0表示成功return 0;
}
AI生成的总结
上方代码的核心目的:在一个已有的 Windows 窗口上,用 DirectX11(负责画图的工具)和 ImGui(快速做 UI 的库),画一个简单的 “Hello, world!” 窗口。
一、先搞懂 3 个关键工具是干嘛的
在看代码前,先记 3 个 “小角色”:
- DirectX11:相当于 “画笔 + 画布”,负责把图像(比如按钮、文本)画到屏幕上;
- ImGui:相当于 “UI 模板库”,不用自己写按钮、窗口的底层代码,直接调用现成的功能;
- Windows 窗口句柄(hWnd):相当于 “贴画的地方”,UI 最终要画在这个窗口上。
二、代码分 5 步走:从 “准备工具” 到 “画出 UI”
第 1 步:找到要画 UI 的窗口(找 “贴画的地方”)
HWND hWnd = FindWindowA("", NULL);
- 作用:找一个已经存在的 Windows 窗口(比如记事本、浏览器窗口),
hWnd
就是这个窗口的 “身份证号”;- 小白注意:这里参数是空的,可能会找到 “不对的窗口”(比如本来想画在游戏窗口,结果画在了桌面窗口),实际用的时候要填窗口标题或类名。
第 2 步:初始化 DirectX11(准备 “画笔 + 画布”)
这部分是最复杂的,但核心就干 3 件事:
- 创建 “设备(g_pd3dDevice)”:相当于 “画笔工厂”,负责造画图需要的各种工具(比如画 UI 的颜色、形状);
- 创建 “交换链(g_pSwapChain)”:相当于 “双画布”,一个画布显示当前画面,另一个画布偷偷画下一针,画完再切换,避免画面闪烁;
- 创建 “渲染目标(g_mainRenderTargetView)”:相当于 “指定画布的作画区域”,告诉 DirectX “UI 要画在交换链的这个区域里”。
- 小白注意:如果显卡不支持 DirectX11,代码会自动用 “软件渲染”(用 CPU 模拟显卡,速度慢但能跑)。
第 3 步:初始化 ImGui(准备 “UI 模板”)
ImGui::CreateContext(); // 开一个ImGui的“工作环境” ImGui::StyleColorsDark(); // 给UI选个风格(这里是深色主题,还有亮色、经典款) ImGui_ImplWin32_Init(hWnd); // 让ImGui能“认”这个Windows窗口,处理鼠标/键盘输入 ImGui_ImplDX11_Init(...) // 让ImGui能“用”DirectX11的画笔,把UI画出来
- 作用:把 ImGui 和 Windows 窗口、DirectX11 连起来,不然 ImGui 不知道该画在哪、用什么工具画。
第 4 步:定义要画的 UI(选 “UI 模板”)
ImGui::Begin("Hello, world!"); // 新建一个窗口,标题是“Hello, world!” ImGui::End(); // 窗口定义结束(这里窗口里没内容,只有个空标题栏)
- 小白注意:这就像搭积木,
Begin
是 “开始搭一个窗口”,中间可以加按钮(ImGui::Button
)、文本(ImGui::Text
),End
是 “搭完这个窗口”。第 5 步:把 UI 画到屏幕上(动手 “贴画”)
ImGui::Render(); // 让ImGui把定义好的UI转换成“画图指令” g_pd3dDeviceContext->OMSetRenderTargets(...); // 告诉DirectX“接下来画到这个区域” ImGui_ImplDX11_RenderDrawData(...); // 让DirectX执行画图指令,把UI画出来
- 作用:从 “纸上谈兵”(定义 UI)到 “实际动手”(画到屏幕),这一步才是真正让 UI 显示出来。
三、小白要注意的 3 个 “坑”
- 没有 “循环”,UI 只显示一次:这段代码画完一次 UI 就结束了,没法互动(比如点按钮没反应)。实际用的时候要加个
while
循环,让代码反复画 UI、处理输入;- 没 “清理垃圾”:代码创建的 DirectX 和 ImGui 资源(比如设备、交换链)没释放,程序关了可能还占内存。要在最后加
Release()
(释放 DirectX 资源)和Shutdown()
(关闭 ImGui);- 找窗口可能找错:
FindWindowA("", NULL)
会找 “任意窗口”,如果开了多个程序,可能画错地方。要改成FindWindowA("窗口类名", "窗口标题")
(比如找记事本窗口,标题是 “无标题 - 记事本”)。四、整体逻辑一句话总结
找一个窗口 → 准备好画图工具(DirectX11) → 准备好 UI 模板(ImGui) → 选一个 UI 模板(空窗口) → 用工具把 UI 画到窗口上。