如何实现仅在目标游戏窗口上方渲染的透明点击穿透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忽略所有鼠标事件:
- 首先修改
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 );
- 然后在
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); } }
- 最后移除主循环中手动设置Overlay位置的代码,因为子窗口会自动跟随父窗口的移动和尺寸变化。
这个方案的优势是Overlay完全和游戏窗口同步状态,不需要手动维护位置和显示状态,同时通过WM_NCHITTEST的处理保留了点击穿透的功能。
总结
- 如果你只是快速解决文字显示问题,选方案1;
- 如果你想要更干净的体验,选方案2;
- 如果你希望Overlay完全和游戏窗口绑定(比如支持游戏窗口移动、缩放),选方案3。
备注:内容来源于stack exchange,提问作者carloslorcas




