ImGUI 1.87 繪製D3D外部選單

2022-09-30 12:01:57

ImGUI 它是與平臺無關的C++輕量級跨平臺圖形介面庫,沒有任何第三方依賴,可以將ImGUI的原始碼直接加到專案中使用,該框架通常會配合特定的D3Dx9等圖形開發工具包一起使用,ImGUI常用來實現程序內的選單功能,而有些輔助開發作者也會使用該框架開發選單頁面,總體來說這是一個很不錯的繪相簿,如下將公開新版ImGUI如何實現繪製外部選單的功能。

ImGUI官方下載地址:https://github.com/ocornut/imgui/releases

在使用ImGUI頁面之前需要先來實現一個簡單的附著功能,即如何將一個表單附著到另一個表單之上,其實程式碼很簡單,如下所示當用戶輸入程序PID時,會自動跟隨表單並附著在表單頂部。

#include <Windows.h>
#include <iostream>

struct handle_data
{
	unsigned long process_id;
	HWND best_handle;
};

// By: LyShark
BOOL IsMainWindow(HWND handle)
{
	return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle);
}

BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam)
{
	// By: LyShark
	handle_data& data = *(handle_data*)lParam;
	unsigned long process_id = 0;
	GetWindowThreadProcessId(handle, &process_id);
	if (data.process_id != process_id || !IsMainWindow(handle)) {
		return TRUE;
	}
	data.best_handle = handle;
	return FALSE;
}

// By: LyShark
HWND FindMainWindow(unsigned long process_id)
{
	handle_data data;
	data.process_id = process_id;
	data.best_handle = 0;
	EnumWindows(EnumWindowsCallback, (LPARAM)&data);
	return data.best_handle;
}

int main(int argc, char* argv[])
{
	DWORD pid = 28396;

	std::cout << "輸入程序PID: " << std::endl;
	std::cin >> pid;

	// 獲取螢幕寬和高
	int iWidth = ::GetSystemMetrics(SM_CXSCREEN);
	int iHeight = ::GetSystemMetrics(SM_CYSCREEN);

	// 根據PID尋找遊戲視窗
	HWND hwnd = FindMainWindow(pid);

	while (1)
	{
		SetTimer(hwnd, 1, 150, NULL);

		// 實現透明必須設定WS_EX_LAYERED標誌
		LONG lWinStyleEx = GetWindowLong(hwnd, GWL_EXSTYLE);
		lWinStyleEx = lWinStyleEx | WS_EX_LAYERED;

		SetWindowLong(hwnd, GWL_EXSTYLE, lWinStyleEx);
		SetLayeredWindowAttributes(hwnd, 0, RGB(40, 40, 40), LWA_ALPHA);

		// 去掉標題列及邊框
		LONG_PTR Style = GetWindowLongPtr(hwnd, GWL_STYLE);
		Style = Style & ~WS_CAPTION & ~WS_SYSMENU & ~WS_SIZEBOX;
		SetWindowLongPtr(hwnd, GWL_STYLE, Style);

		// 至頂層視窗 最大化
		SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, iWidth, iHeight, SWP_SHOWWINDOW);

		// 設定表單可穿透滑鼠
		SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_TRANSPARENT | WS_EX_LAYERED);

		// 繪圖
		HDC hdc = ::GetDC(hwnd);
		HDC mdc = ::CreateCompatibleDC(hdc);

		// 建立畫筆
		HPEN hpen = CreatePen(PS_SOLID, 10, RGB(0, 255, 0));
		// DC 選擇畫筆
		SelectObject(hdc, hpen);
		// (畫筆)從初始點移動到 50,50
		MoveToEx(hdc, 100, 100, NULL);
		// (畫筆)從初始點畫線到 100,100
		LineTo(hdc, 1000, 1000);

		RECT rect = {0};

		rect.bottom = 10;
		rect.left = 20;
		rect.right = 20;
		rect.top = 15;

		DrawText(hdc, L"hello lyshark.com", strlen("hello lyshark.com"), &rect, DT_CALCRECT | DT_CENTER | DT_SINGLELINE);
	}

	return 0;
}

繪製效果圖:

接著我們使用Imgui繪製一個動態選單,首先下載imgui並開啟專案中的examples目錄,找到example_win32_directx9開啟後自己設定好dx9SDK開發工具包。

程式碼直接呼叫,並附加到Counter-Strike Source遊戲表單之上即可,這段程式碼也很簡單。

#include "imgui.h"
#include "imgui_impl_dx9.h"
#include "imgui_impl_win32.h"

#include <d3d9.h>
#include <tchar.h>
#include <iostream>

#pragma execution_character_set("utf-8")

// 全域性變數
// lyshark.com
static HWND hwnd;
static HWND GameHwnd;
static RECT WindowRectangle;
static int WindowWide, WindowHeight;

static LPDIRECT3D9 g_pD3D = NULL;
static LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;
static D3DPRESENT_PARAMETERS g_d3dpp = {};

// 單選框設定狀態
bool show_another_window = false;

// imgui 回撥函數
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

// By: LyShark
bool CreateDeviceD3D(HWND hWnd)
{
	if ((g_pD3D = Direct3DCreate9(D3D_SDK_VERSION)) == NULL)
	{
		return false;
	}

	ZeroMemory(&g_d3dpp, sizeof(g_d3dpp));
	g_d3dpp.Windowed = TRUE;
	g_d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	g_d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
	g_d3dpp.EnableAutoDepthStencil = TRUE;
	g_d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
	g_d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;

	if (g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &g_d3dpp, &g_pd3dDevice) < 0)
	{
		return false;
	}
	return true;
}

void CleanupDeviceD3D()
{
	if (g_pd3dDevice)
	{
		g_pd3dDevice->Release();
		g_pd3dDevice = NULL;
	}

	if (g_pD3D)
	{
		g_pD3D->Release();
		g_pD3D = NULL;
	}
}

void ResetDevice()
{
	ImGui_ImplDX9_InvalidateDeviceObjects();
	HRESULT hr = g_pd3dDevice->Reset(&g_d3dpp);
	if (hr == D3DERR_INVALIDCALL)
	{
		IM_ASSERT(0);
	}
	ImGui_ImplDX9_CreateDeviceObjects();
}

LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
	{
		return true;
	}

	switch (msg)
	{
	case WM_SIZE:
		if (g_pd3dDevice != NULL && wParam != SIZE_MINIMIZED)
		{
			g_d3dpp.BackBufferWidth = LOWORD(lParam);
			g_d3dpp.BackBufferHeight = HIWORD(lParam);
			ResetDevice();
		}
		return 0;

	case WM_SYSCOMMAND:
		if ((wParam & 0xfff0) == SC_KEYMENU)
		{
			return 0;
		}
		break;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}

	return DefWindowProc(hWnd, msg, wParam, lParam);
}

// 繪製主方法
// www.cnblogs.com/lyshark
void DrawImGUI()
{
	// 啟動IMGUI自繪
	ImGui_ImplDX9_NewFrame();
	ImGui_ImplWin32_NewFrame();
	ImGui::NewFrame();

	static float f = 0.0f;
	static int counter = 0;
	static char sz[256] = { 0 };

	ImGui::Begin("LyShark 輔助GUI主選單");
	ImGui::Text("這是一段測試字串");
	ImGui::Checkbox("彈出子視窗", &show_another_window);
	ImGui::SliderFloat("浮點條", &f, 0.0f, 1.0f);

	ImGui::InputText("輸入內容", sz, 256, 0, 0, 0);

	if (ImGui::Button("點我觸發"))
		counter++;
	ImGui::SameLine();
	ImGui::Text("觸發次數 = %d", counter);

	ImGui::Text("當前FPS: %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
	ImGui::End();

	if (show_another_window)
	{
		ImGui::Begin("我是子表單", &show_another_window);
		ImGui::Text(" 您好,LyShark !");
		if (ImGui::Button("關閉表單"))
			show_another_window = false;
		ImGui::End();
	}
	ImGui::EndFrame();
}

// 自身視窗迴圈事件
void WindowMessageLoop()
{
	bool done = false;
	while (!done)
	{
		// 每次都將表單置頂並跟隨遊戲表單移動
		GetWindowRect(GameHwnd, &WindowRectangle);
		WindowWide = (WindowRectangle.right) - (WindowRectangle.left);
		WindowHeight = (WindowRectangle.bottom) - (WindowRectangle.top);
		DWORD dwStyle = GetWindowLong(GameHwnd, GWL_STYLE);
		if (dwStyle & WS_BORDER)
		{
			WindowRectangle.top += 23;
			WindowHeight -= 23;
		}

		// 跟隨視窗移動
		MoveWindow(hwnd, WindowRectangle.left + 8, WindowRectangle.top + 8, WindowWide - 11, WindowHeight - 11, true);

		// 開始訊息迴圈
		MSG msg;
		while (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
			if (msg.message == WM_QUIT)
			{
				done = true;
			}
		}

		if (done)
		{
			break;
		}

		// 開始繪製
		DrawImGUI();

		g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, FALSE);
		g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
		g_pd3dDevice->SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE);
		g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0, 1.0f, 0);

		if (g_pd3dDevice->BeginScene() >= 0)
		{
			ImGui::Render();
			ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
			g_pd3dDevice->EndScene();
		}

		HRESULT result = g_pd3dDevice->Present(NULL, NULL, NULL, NULL);
		if (result == D3DERR_DEVICELOST && g_pd3dDevice->TestCooperativeLevel() == D3DERR_DEVICENOTRESET)
		{
			ResetDevice();
		}
	}
}

int main(int argc, char *argv[])
{
	// 登錄檔單類
	WNDCLASSEX wc;

	// 附加到指定表單上
	wc.cbClsExtra = NULL;
	wc.cbSize = sizeof(WNDCLASSEX);
	wc.cbWndExtra = NULL;
	wc.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(0, 0, 0));
	wc.hCursor = LoadCursor(0, IDC_ARROW);
	wc.hIcon = LoadIcon(0, IDI_APPLICATION);
	wc.hIconSm = LoadIcon(0, IDI_APPLICATION);
	wc.hInstance = GetModuleHandle(NULL);
	wc.lpfnWndProc = (WNDPROC)WndProc;
	wc.lpszClassName = L" ";
	wc.lpszMenuName = L" ";
	wc.style = CS_VREDRAW | CS_HREDRAW;

	RegisterClassEx(&wc);

	// 得到視窗控制程式碼
	GameHwnd = FindWindowA(NULL, "Counter-Strike Source");
	GetWindowRect(GameHwnd, &WindowRectangle);
	WindowWide = WindowRectangle.right - WindowRectangle.left;
	WindowHeight = WindowRectangle.bottom - WindowRectangle.top;

	// 建立表單
	hwnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TOOLWINDOW, L" ", L" ", WS_POPUP, 1, 1, WindowWide, WindowHeight, 0, 0, wc.hInstance, 0);

	// 顯示視窗
	SetLayeredWindowAttributes(hwnd, 0, RGB(0, 0, 0), LWA_ALPHA);
	SetLayeredWindowAttributes(hwnd, 0, RGB(0, 0, 0), LWA_COLORKEY);
	ShowWindow(hwnd, SW_SHOW);

	// 初始化D3D
	if (!CreateDeviceD3D(hwnd))
	{
		CleanupDeviceD3D();
		UnregisterClass(wc.lpszClassName, wc.hInstance);
		return 0;
	}

	// 更新表單
	UpdateWindow(hwnd);

	// 初始化ImGUI
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); (void)io;
	io.Fonts->AddFontFromFileTTF("c:/windows/fonts/simhei.ttf", 13.0f, NULL, io.Fonts->GetGlyphRangesChineseSimplifiedCommon());

	ImGui::StyleColorsDark();
	ImGui_ImplWin32_Init(hwnd);
	ImGui_ImplDX9_Init(g_pd3dDevice);

	// 開始執行繪製迴圈事件
	WindowMessageLoop();

	ImGui_ImplDX9_Shutdown();
	ImGui_ImplWin32_Shutdown();
	ImGui::DestroyContext();

	CleanupDeviceD3D();
	DestroyWindow(hwnd);
	UnregisterClass(wc.lpszClassName, wc.hInstance);
	return 0;
}

繪製效果如下:

另外,Imgui也支援繪製到整個螢幕上,也可以當作全域性GUI介面來使用。

#include "imgui.h"
#include "imgui_impl_dx9.h"
#include "imgui_impl_win32.h"

#include <d3d9.h>
#include <tchar.h>
#include <iostream>

#pragma execution_character_set("utf-8")

// 全域性變數
static HWND hwnd;
static HWND GameHwnd;
static RECT WindowRectangle;
static int WindowWide, WindowHeight;

static LPDIRECT3D9 g_pD3D = NULL;
static LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;
static D3DPRESENT_PARAMETERS g_d3dpp = {};

// 單選框設定狀態
bool show_another_window = false;

// imgui 回撥函數
// By: LyShark
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

bool CreateDeviceD3D(HWND hWnd)
{
	if ((g_pD3D = Direct3DCreate9(D3D_SDK_VERSION)) == NULL)
	{
		return false;
	}

	ZeroMemory(&g_d3dpp, sizeof(g_d3dpp));
	g_d3dpp.Windowed = TRUE;
	g_d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	g_d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
	g_d3dpp.EnableAutoDepthStencil = TRUE;
	g_d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
	g_d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;

	if (g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &g_d3dpp, &g_pd3dDevice) < 0)
	{
		return false;
	}
	return true;
}

void CleanupDeviceD3D()
{
	if (g_pd3dDevice)
	{
		g_pd3dDevice->Release();
		g_pd3dDevice = NULL;
	}

	if (g_pD3D)
	{
		g_pD3D->Release();
		g_pD3D = NULL;
	}
}
// https://www.cnblogs.com/lyshark
void ResetDevice()
{
	ImGui_ImplDX9_InvalidateDeviceObjects();
	HRESULT hr = g_pd3dDevice->Reset(&g_d3dpp);
	if (hr == D3DERR_INVALIDCALL)
	{
		IM_ASSERT(0);
	}
	ImGui_ImplDX9_CreateDeviceObjects();
}

LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
	{
		return true;
	}

	switch (msg)
	{
	case WM_SIZE:
		if (g_pd3dDevice != NULL && wParam != SIZE_MINIMIZED)
		{
			g_d3dpp.BackBufferWidth = LOWORD(lParam);
			g_d3dpp.BackBufferHeight = HIWORD(lParam);
			ResetDevice();
		}
		return 0;

	case WM_SYSCOMMAND:
		if ((wParam & 0xfff0) == SC_KEYMENU)
		{
			return 0;
		}
		break;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}

	return DefWindowProc(hWnd, msg, wParam, lParam);
}

// 繪製主方法
// lyshark.com
void DrawImGUI()
{
	// 啟動IMGUI自繪
	ImGui_ImplDX9_NewFrame();
	ImGui_ImplWin32_NewFrame();
	ImGui::NewFrame();

	static float f = 0.0f;
	static int counter = 0;
	static char sz[256] = { 0 };

	ImGui::Begin("LyShark 輔助GUI主選單");
	ImGui::Text("這是一段測試字串");
	ImGui::Checkbox("彈出子視窗", &show_another_window);
	ImGui::SliderFloat("浮點條", &f, 0.0f, 1.0f);

	ImGui::InputText("輸入內容", sz, 256, 0, 0, 0);

	if (ImGui::Button("點我觸發"))
		counter++;
	ImGui::SameLine();
	ImGui::Text("觸發次數 = %d", counter);

	ImGui::Text("當前FPS: %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
	ImGui::End();

	if (show_another_window)
	{
		ImGui::Begin("我是子表單", &show_another_window);
		ImGui::Text(" 您好,LyShark !");
		if (ImGui::Button("關閉表單"))
			show_another_window = false;
		ImGui::End();
	}
	ImGui::EndFrame();
}

// 自身視窗迴圈事件
void WindowMessageLoop()
{
	bool done = false;
	while (!done)
	{
		// 每次都將表單置頂並跟隨遊戲表單移動
		GetWindowRect(GameHwnd, &WindowRectangle);
		WindowWide = (WindowRectangle.right) - (WindowRectangle.left);
		WindowHeight = (WindowRectangle.bottom) - (WindowRectangle.top);
		DWORD dwStyle = GetWindowLong(GameHwnd, GWL_STYLE);
		if (dwStyle & WS_BORDER)
		{
			WindowRectangle.top += 23;
			WindowHeight -= 23;
		}

		// 跟隨視窗移動
		MoveWindow(hwnd, WindowRectangle.left + 8, WindowRectangle.top + 8, WindowWide - 11, WindowHeight - 11, true);

		// 開始訊息迴圈
		MSG msg;
		while (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
			if (msg.message == WM_QUIT)
			{
				done = true;
			}
		}

		if (done)
		{
			break;
		}

		// 開始繪製
		DrawImGUI();

		g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, FALSE);
		g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
		g_pd3dDevice->SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE);
		g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0, 1.0f, 0);

		if (g_pd3dDevice->BeginScene() >= 0)
		{
			ImGui::Render();
			ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
			g_pd3dDevice->EndScene();
		}

		HRESULT result = g_pd3dDevice->Present(NULL, NULL, NULL, NULL);
		if (result == D3DERR_DEVICELOST && g_pd3dDevice->TestCooperativeLevel() == D3DERR_DEVICENOTRESET)
		{
			ResetDevice();
		}
	}
}

int main(int argc, char *argv[])
{
	// 登錄檔單類
	WNDCLASSEX wc;

	// 附加到整個螢幕上
	wc.cbClsExtra = NULL;
	wc.cbSize = sizeof(WNDCLASSEX);
	wc.cbWndExtra = NULL;
	wc.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(0, 0, 0));
	wc.hCursor = LoadCursor(0, IDC_ARROW);
	wc.hIcon = LoadIcon(0, IDI_APPLICATION);
	wc.hIconSm = LoadIcon(0, IDI_APPLICATION);
	wc.hInstance = GetModuleHandle(NULL);
	wc.lpfnWndProc = (WNDPROC)WndProc;
	wc.lpszClassName = L" ";
	wc.lpszMenuName = L" ";
	wc.style = CS_VREDRAW | CS_HREDRAW;
	::RegisterClassEx(&wc);

	// 螢幕寬度和高度
	WindowWide = GetSystemMetrics(SM_CXSCREEN);
	WindowHeight = GetSystemMetrics(SM_CYSCREEN);

	// 建立表單
	HWND hwnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TOOLWINDOW, L" ", L" ", WS_POPUP, 1, 1, WindowWide, WindowHeight, 0, 0, wc.hInstance, 0);

	// 顯示視窗
	SetLayeredWindowAttributes(hwnd, 0, 1.0f, LWA_ALPHA);
	SetLayeredWindowAttributes(hwnd, 0, RGB(0, 0, 0), LWA_COLORKEY);
	ShowWindow(hwnd, SW_SHOW);

	// 初始化D3D
	if (!CreateDeviceD3D(hwnd))
	{
		CleanupDeviceD3D();
		UnregisterClass(wc.lpszClassName, wc.hInstance);
		return 0;
	}

	// 更新表單
	UpdateWindow(hwnd);

	// 初始化ImGUI
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); (void)io;
	io.Fonts->AddFontFromFileTTF("c:/windows/fonts/simhei.ttf", 13.0f, NULL, io.Fonts->GetGlyphRangesChineseSimplifiedCommon());

	ImGui::StyleColorsDark();
	ImGui_ImplWin32_Init(hwnd);
	ImGui_ImplDX9_Init(g_pd3dDevice);

	// 開始執行繪製迴圈事件
	WindowMessageLoop();

	ImGui_ImplDX9_Shutdown();
	ImGui_ImplWin32_Shutdown();
	ImGui::DestroyContext();

	CleanupDeviceD3D();
	DestroyWindow(hwnd);
	UnregisterClass(wc.lpszClassName, wc.hInstance);
	return 0;
}

繪製效果如下: