13.2 外部DirectX绘制实现

发布时间 2023-10-13 14:02:04作者: lyshark

在前一节中我们简单介绍了D3D绘制窗体所具备的基本要素,本节将继续探索外部绘制技术的实现细节,并以此实现一些简单的图形绘制功能,首先外部绘制的核心原理是通过动态创建一个新的窗口并设置该窗口属性为透明无边框状态,通过消息循环机制实现对父窗口的动态跟随附着功能,当读者需要绘制新的图形时只需要绘制在透明窗体之上即可实现动态显示的效果。

13.2.1 必要参数定义

首先第一步定义所需要的关键变量如下,代码中包含了DirectX 9DWM的必要库,代码初始化了一些Direct3D 9的变量和指针,包括Direct3D 9设备、呈现参数、Direct3D 线条对象和 Direct3D 字体对象。代码还定义了一个窗口类和一个用于渲染矩形的全局函数指针。

#include <d3dx9.h>
#include <dwmapi.h>

#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
#pragma comment(lib, "dwmapi.lib")

static MARGINS Margin;
static LPDIRECT3D9 g_pD3D = NULL;
static LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;
static D3DPRESENT_PARAMETERS g_d3dpp = {};
static ID3DXLine* pLine = 0;
static ID3DXFont* Font;

// 存储全局窗口信息
static HWND AuxiliaryWindowHandle, GameHwnd;
static RECT WindowRectangle;
static int WindowWidth, WindowHeight;

// 注册窗口类
static WNDCLASSEX wClass;

// 绘制矩形全局指针
typedef VOID(*Draw)();
static Draw Render;

13.2.2 初始化绘图引擎

初始化绘图引擎InitD3D函数,函数通过Direct3DCreate9创建Direct3D对象,并用g_pD3D指针指向它,并将绘制结构体g_d3dpp中一些参数初始化,例如启用窗口模式、交换方式等等。通过CreateDevice方法创建Direct3D绘图设备,通过D3DXCreateLine方法创建Direct3D线条对象,以便绘制直线段。最后调用D3DXCreateFontW来创建Direct3D字体对象,使得程序可以在绘图中使用特定的字体呈现文字。

// 初始化绘制引擎
BOOL InitD3D()
{
    // 初始化绘制引擎
    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;

    // 创建D3D绘制设备
    if (g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, AuxiliaryWindowHandle, D3DCREATE_HARDWARE_VERTEXPROCESSING, &g_d3dpp, &g_pd3dDevice) < 0)
    {
        return FALSE;
    }

    // 创建D3D线条
    if (pLine == NULL)
    {
        D3DXCreateLine(g_pd3dDevice, &pLine);
    }

    // 创建D3D字体
    D3DXCreateFontW(g_pd3dDevice, 16, 0, FW_DONTCARE, D3DX_DEFAULT, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, FF_DONTCARE, L"Vernada", &Font);

    return TRUE;
}

13.2.3 初始化消息循环

封装实现CreateTransparentWindow函数,该函数用于创建一个透明窗口来显示Direct3D渲染的图形和文本,函数接受两个参数,游戏窗口句柄和绘制函数,其中游戏窗口句柄表示将要在其上绘制图形和文本的窗口句柄,而绘制函数则是指向绘制矩形的全局指针。函数WindowMessageLoop则用于等待消息循环,在该循环内我们通过不间断调用GetWindowRect获取父窗口大小变化或移动位置变化,并通过MoveWindow动态调整,该流程可实现动态跟随窗体移动。

// 窗口消息处理函数
LRESULT WinProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam)
{
    switch (Message)
    {
    case WM_PAINT:
        if (g_pd3dDevice)Render();
        break;

    case WM_CREATE:
        DwmExtendFrameIntoClientArea(hWnd, &Margin);
        break;

    case WM_DESTROY:
    {
        g_pD3D->Release();
        g_pd3dDevice->Release();
        exit(1);
        return 0;
    }
    default:
        return DefWindowProc(hWnd, Message, wParam, lParam);
        break;
    }
    return 0;
}

// 创建透明窗口过程
VOID CreateTransparentWindow(HWND 游戏窗口句柄, Draw 绘制函数)
{
    if (绘制函数 == NULL || 游戏窗口句柄 == 0)
    {
        return;
    }

    GameHwnd = 游戏窗口句柄;
    Render = 绘制函数;

    // 初始化窗口类
    wClass.cbClsExtra = NULL;
    wClass.cbSize = sizeof(WNDCLASSEX);
    wClass.cbWndExtra = NULL;
    wClass.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(0, 0, 0));
    wClass.hCursor = LoadCursor(0, IDC_ARROW);
    wClass.hIcon = LoadIcon(0, IDI_APPLICATION);
    wClass.hIconSm = LoadIcon(0, IDI_APPLICATION);
    wClass.hInstance = GetModuleHandle(NULL);
    wClass.lpfnWndProc = (WNDPROC)WinProc;
    wClass.lpszClassName = L" ";
    wClass.lpszMenuName = L" ";
    wClass.style = CS_VREDRAW | CS_HREDRAW;

    // 注册窗口
    if (RegisterClassEx(&wClass) == 0)
    {
        exit(1);
    }

    //创建窗口
    GetWindowRect(GameHwnd, &WindowRectangle);
    WindowWidth = WindowRectangle.right - WindowRectangle.left;
    WindowHeight = WindowRectangle.bottom - WindowRectangle.top;
    AuxiliaryWindowHandle = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_LAYERED, L" ", L" ", WS_POPUP, 1, 1, WindowWidth, WindowHeight, 0, 0, 0, 0);

    //显示窗口
    SetLayeredWindowAttributes(AuxiliaryWindowHandle, 0, RGB(0, 0, 0), LWA_COLORKEY);
    ShowWindow(AuxiliaryWindowHandle, SW_SHOW);

    // 初始化D3D引擎
    InitD3D();
}

// 设置窗体消息循环
VOID WindowMessageLoop()
{
    while (1)
    {
        // 动态附着
        if (GameHwnd)
        {
            // 得到窗口大小
            GetWindowRect(GameHwnd, &WindowRectangle);
            WindowWidth = 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(AuxiliaryWindowHandle, WindowRectangle.left, WindowRectangle.top, WindowWidth, WindowHeight, true);
        }

        // 处理窗口消息
        MSG Message;
        ZeroMemory(&Message, sizeof(Message));
        if (PeekMessage(&Message, 0, 0, 0, PM_REMOVE))
        {
            DispatchMessage(&Message);
            TranslateMessage(&Message);
        }

        Sleep(1);
    }

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

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

    CloseWindow(AuxiliaryWindowHandle);

    UnregisterClass(wClass.lpszClassName, wClass.hInstance);
}

13.2.4 封装实现绘图函数

DrawLine,用于绘制线条该函数接受四个参数,分别为线段的起始坐标X1Y1,线段的终止坐标X2Y2,以及颜色Color。该函数使用D3DXVECTOR2结构体初始化两个点型变量Vertex,然后调用pLineSetWidth方法设置绘制线段的宽度为 1,最后调用Draw方法在屏幕上绘制出一条线段。

DrawTextString,用于绘制文本该函数接受四个参数,分别为文本字符串的起始坐标XY,需要显示的文本字符串Str,以及文本颜色Color。该函数首先使用Font对象的DrawTextA方法来测量文本字符串的大小,并将其存储在一个RECT结构体变量Rect中,然后再次使用Font对象的DrawTextA方法来将字符串绘制在屏幕上。

DrawBox,用于绘制矩形该函数接受五个参数,分别为矩形的左上角坐标XY,矩形的宽度W和高度H,以及矩形线条的宽度Width,以及颜色C。该函数使用D3DXVECTOR2结构体初始化5个点型变量Vertex,依次为左上角、右上角、右下角、左下角和左上角。然后调用pLine对象的SetWidth方法,设置绘制线段的宽度为Width,最后调用Draw方法在屏幕上绘制出整个矩形。

// 屏幕画线
VOID DrawLine(float X1, float Y1, float X2, float Y2, D3DCOLOR Color)
{
    D3DXVECTOR2 Vertex[2] = { { X1, Y1 }, { X2, Y2 } };
    pLine->SetWidth(1);
    pLine->Draw(Vertex, 2, Color);
}

// 绘制文本字符串
VOID DrawTextString(float X, float Y, const char* Str, D3DCOLOR Color)
{
    RECT Rect = { (LONG)X, (LONG)Y };
    Font->DrawTextA(NULL, Str, -1, &Rect, DT_CALCRECT, Color);
    Font->DrawTextA(NULL, Str, -1, &Rect, DT_LEFT, Color);
}

// 屏幕画方框
VOID DrawBox(float X, float Y, float W, float H, float Width, D3DCOLOR Color)
{
    D3DXVECTOR2 Vertex[5] = { { X, Y }, { X + W, Y }, { X + W, Y + H }, { X, Y + H }, { X, Y } };
    pLine->SetWidth(Width);
    pLine->Draw(Vertex, 5, Color);
}

13.2.5 实现屏幕绘制功能

在最后读者可将上述功能整合在一起,实现动态绘制功能,首先我们需要得到所需绘制进程的窗口句柄,在VS中的工具类自带了一个Spy++读者可使用该工具得到指定窗体的句柄信息,如下图所示;

当得到句柄后则可填充之如下所示GameHandle变量内,当我们需要绘制图形时只需要在GlobalDrawFunction函数内部编写流程即可,该函数内通过BeginScene设置开始绘制,在绘制代码区读者可自行使用上述封装函数实现自定义绘制,当绘制结束后需要通过EndScene结束本次绘制操作,完整绘制代码如下图所示;

// 调用全局绘制
VOID GlobalDrawFunction()
{
    // 开始绘制
    g_pd3dDevice->Clear(0, 0, D3DCLEAR_TARGET, 0, 1.0f, 0);
    g_pd3dDevice->BeginScene();

    // 绘制文本字符串
    DrawTextString(100, 200, "Hello LyShark", D3DCOLOR_ARGB(56, 0, 52, 0));
    DrawTextString(200, 300, "PowerBy LyShark", D3DCOLOR_ARGB(255, 0, 255, 0));

    // 屏幕画线
    DrawLine(110, 300, 20,  30, D3DCOLOR_ARGB(56, 0, 52, 0));

    // 屏幕画方框
    DrawBox(40, 40, 50, 100, 1, D3DCOLOR_ARGB(255, 0, 52, 0));
    DrawBox(140, 38, 50, 90, 1, D3DCOLOR_ARGB(255, 0, 255, 0));


    // 结束绘制
    g_pd3dDevice->EndScene();
    g_pd3dDevice->Present(0, 0, 0, 0);
}

int main(int argc, char *argv[])
{
    // 获取窗口句柄
    HWND GameHandle = (HWND)0x00506E6;

    while (1)
    {
        // 循环绘制
        CreateTransparentWindow(GameHandle, GlobalDrawFunction);
        WindowMessageLoop();
    }

    return 0;
}

运行上述代码片段,此时读者可看到如下图所示的输出信息,其中包括两个矩形,两个文本字符串,以及一条直线;

注释:仅用于技术研究学习,请勿用于其他用途。
本文作者: 王瑞
本文链接: https://www.lyshark.com/post/990795ac.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!