You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

如何实现仅在目标游戏窗口上方渲染的透明点击穿透Overlay

如何实现仅在目标游戏窗口上方渲染的透明点击穿透Overlay

看起来你遇到的是一个很典型的Overlay窗口适配问题——既要保持透明点击穿透的特性,又要让它只在目标游戏窗口激活时才显示内容。我来给你拆解几个可行的解决方案,一步步帮你解决问题:

问题回顾

你当前的代码实现了一个和游戏同尺寸的透明Overlay,游戏最小化时它会停止渲染(这很好),但切换到其他窗口(游戏未最小化)时,Overlay的文字依然会显示。之前尝试将Overlay设为游戏的子窗口解决了显示问题,但丢失了WS_EX_TRANSPARENT带来的点击穿透功能。

你的两种场景:

  • ✅ 正常情况:Overlay在游戏窗口上方正确显示文字
  • ❌ 问题场景:切换到非游戏窗口时,Overlay文字仍然悬浮在屏幕上

你提供的原始代码如下:

#include<iostream>
#include<windows.h>
#include<winuser.h>
using namespace std;

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){
    switch(msg)
    {
        case WM_PAINT:
        {   
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

            // pintamos todo de negro (osea transparente)
            HBRUSH brush = CreateSolidBrush(RGB(0,0,0));
            FillRect(hdc, &ps.rcPaint, brush);
            DeleteObject(brush);

            // todo el renderizado va entre estas dos lineas //

            HWND cs2 = FindWindowA(NULL, "Counter-Strike 2");
            HWND overlay = FindWindowA("carloslorcas", NULL);

            LPCSTR output = "carlos lorcas presente";

            if(GetForegroundWindow() == overlay){
                TextOutA(hdc, 30, 50, output, strlen(output));
            }
            
            // todo el renderizado va entre estas dos lineas //
            EndPaint(hwnd, &ps);
            return 0;
        }

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

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

int main(){
    HWND hwnd = FindWindowA(NULL, "Counter-Strike 2");

    if(hwnd == NULL){
        printf("no se pudo encontrar la ventana correctamente");
    }
    else{
        printf("se encontro la ventana correctamente");
    }

    DWORD pid; GetWindowThreadProcessId(hwnd, &pid);

    if(pid == 0){
        printf("el pid no se pudo encontrar correctamente");
    }
    else{
        printf("\nse encontro el pid: %i", pid);
    }

    RECT rect;
    if(GetWindowRect(hwnd, &rect) == 0){
        printf("\nhubo un error encontrando las dimensiones de la ventana");
    }
    else{
        printf("\nse encontraron las dimensiones de la ventana del juego correctamente");
    }

    int width = rect.right - rect.left;
    int height = rect.bottom - rect.top;;

    printf("\nwidth: %i, height: %i", width, height);

    //////////////////////////////////////////////////////////////////////////////////
    //////////////    creating window here! /////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    // aca definimos la clase
    HINSTANCE hInstance = GetModuleHandle(NULL);
    WNDCLASSEXW wc = {0};
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"carloslorcas";

    if (!RegisterClassExW(&wc)) {
        printf("\nerror registrando clase: %lu\n", GetLastError());
        return 0;
    }

    HWND window = CreateWindowExW(
        // hacemos que la ventana sea layered ya que esto permite que tenga transparencia
        WS_EX_TRANSPARENT |WS_EX_LAYERED,  /*para que la ventana sea transparente y este encima de todas, para usar WX_EX_TOPMOST usar SetWindowPos. para lograr transparencia sin restricciones usar SetWindowRgn.*/
        L"carloslorcas",
        L"OVERLAY",
        WS_POPUP , /*para ser transparente tiene que ser popup, asi no tendra bordes*/ // puede ser que si es popup y no dibujas nada, no se muestre absolutamente ninguna ventana
        rect.left,
        rect.top,
        width,
        height,
        NULL,
        NULL,
        hInstance,
        NULL
    );

    // aclaracion:
    //  por alguna razon aunque la documentacion de windows dice que las cosas son opcionales, bueno
    //  no lo son
    //  TENES que crear una clase, si no no se va a llamar a "WndProc", que es la funcion que maneja los eventos (no, la documentacion tampoco te avisa que tenes que usar esto)

    if(!window){
        printf("\nerror creando ventana");
        //return 0;
    }

    ShowWindow(window, SW_SHOW);
    UpdateWindow(window);
    SetLayeredWindowAttributes(window, RGB(0,0,0), 0, LWA_COLORKEY); // todo lo que sea negro sera invisible // nos sirve para hacer que la ventana sea transparente pero no lo que dibujemos en ella
    InvalidateRect(window, NULL, TRUE); // actualiza el texto constantemente
    
    while(true){
        MSG Msg;
        while (PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }

        GetWindowRect(hwnd, &rect);
        
        SetWindowPos(
            window,
            HWND_TOPMOST,
            rect.left,
            rect.top,
            width,
            height,
            SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS
        );
    }
    return 0;
}

解决方案1:修改渲染判断逻辑(最简单快速)

你当前在WM_PAINT里判断的是GetForegroundWindow() == overlay,这逻辑反了——应该判断目标游戏窗口是否是当前激活的前台窗口,只有满足这个条件时才绘制文字:

修改WM_PAINT中的判断代码:

case WM_PAINT:
{   
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    // pintamos todo de negro (osea transparente)
    HBRUSH brush = CreateSolidBrush(RGB(0,0,0));
    FillRect(hdc, &ps.rcPaint, brush);
    DeleteObject(brush);

    // todo el renderizado va entre estas dos lineas //
    LPCSTR output = "carlos lorcas presente";
    HWND gameHwnd = FindWindowA(NULL, "Counter-Strike 2");

    // 只有当游戏窗口是前台且可见时,才绘制文字
    if (gameHwnd != NULL && GetForegroundWindow() == gameHwnd && IsWindowVisible(gameHwnd)) {
        TextOutA(hdc, 30, 50, output, strlen(output));
    }
    // todo el renderizado va entre estas dos lineas //

    EndPaint(hwnd, &ps);
    return 0;
}

这个修改非常轻量,不需要改变窗口的父子关系,只是在渲染时做了更精准的判断——游戏没激活时,Overlay只会绘制透明背景,不会显示文字。


解决方案2:完全隐藏Overlay(体验更流畅)

如果希望游戏未激活时Overlay完全消失(而不是只隐藏文字),可以在主消息循环中检查游戏窗口的激活状态,动态显示/隐藏Overlay:

修改主循环的代码:

while(true){
    MSG Msg;
    while (PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE))
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }

    GetWindowRect(hwnd, &rect);
    HWND foregroundWnd = GetForegroundWindow();
    bool isGameActive = (foregroundWnd == hwnd) || (GetParent(foregroundWnd) == hwnd);
    
    // 根据游戏是否激活,动态显示/隐藏Overlay
    if (isGameActive && IsWindowVisible(hwnd)) {
        ShowWindow(window, SW_SHOW);
        SetWindowPos(
            window,
            HWND_TOPMOST,
            rect.left,
            rect.top,
            width,
            height,
            SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS
        );
    } else {
        ShowWindow(window, SW_HIDE);
    }

    // 加入小延迟,降低CPU占用
    Sleep(10);
}

这个方案的体验更好,因为游戏未激活时Overlay完全不在屏幕上,不会有任何残留,同时依然保留了点击穿透的特性。


解决方案3:绑定游戏窗口为父窗口(彻底同步状态)

如果你希望Overlay完全和游戏窗口绑定(比如游戏移动时自动跟随、游戏最小化时自动隐藏),可以解决子窗口丢失点击穿透的问题——关键是在设置父窗口的同时,让Overlay忽略所有鼠标事件:

  1. 首先修改CreateWindowExW的参数,将游戏窗口设为Overlay的父窗口,并添加WS_CHILD样式:
HWND window = CreateWindowExW(
    WS_EX_TRANSPARENT | WS_EX_LAYERED,
    L"carloslorcas",
    L"OVERLAY",
    WS_POPUP | WS_CHILD, // 加入WS_CHILD样式
    0, 0, // 子窗口坐标相对于父窗口,设为0即可
    width,
    height,
    hwnd, // 父窗口设为游戏窗口
    NULL,
    hInstance,
    NULL
);
  1. 然后在WndProc中添加WM_NCHITTEST消息处理,让所有鼠标事件直接穿透到游戏窗口:
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){
    switch(msg)
    {
        // ... 保留原来的WM_PAINT、WM_DESTROY等处理 ...

        case WM_NCHITTEST:
            return HTTRANSPARENT; // 让所有鼠标事件穿透到父窗口(游戏)
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
}
  1. 最后移除主循环中手动设置Overlay位置的代码,因为子窗口会自动跟随父窗口的移动和尺寸变化。

这个方案的优势是Overlay完全和游戏窗口同步状态,不需要手动维护位置和显示状态,同时通过WM_NCHITTEST的处理保留了点击穿透的功能。


总结

  • 如果你只是快速解决文字显示问题,选方案1
  • 如果你想要更干净的体验,选方案2
  • 如果你希望Overlay完全和游戏窗口绑定(比如支持游戏窗口移动、缩放),选方案3

备注:内容来源于stack exchange,提问作者carloslorcas

火山引擎 最新活动