如何避免窗口被游戏反作弊误杀以及替代方案
同步透明覆盖窗口与任务栏状态
核心问题
任务栏可能: - 被全屏应用遮挡/隐藏 - 自动隐藏 - 位置/大小变化 - 虚拟桌面切换
方案:监听任务栏状态同步
class TaskbarOverlay {
HWND m_hOverlay = NULL;
HWND m_hTray = NULL;
// 检测任务栏是否可见
bool IsTaskbarVisible() {
if (!m_hTray || !IsWindowVisible(m_hTray))
return false;
// 检查是否被全屏窗口遮挡
APPBARDATA abd = { sizeof(abd) };
UINT state = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd);
// ABS_AUTOHIDE 时可能隐藏了
if (state & ABS_AUTOHIDE) {
RECT rc;
GetWindowRect(m_hTray, &rc);
// 任务栏缩到1像素说明已隐藏
if ((rc.bottom - rc.top) <= 2 || (rc.right - rc.left) <= 2)
return false;
}
// 检查是否有全屏窗口遮挡
return !IsFullscreenAppRunning();
}
bool IsFullscreenAppRunning() {
// 方法1: 用 Shell API
APPBARDATA abd = { sizeof(abd) };
// ABM_GETSTATE 不能直接检测全屏
// 用前台窗口判断
HWND hFg = GetForegroundWindow();
if (!hFg || hFg == m_hTray) return false;
RECT rcWnd, rcMonitor;
GetWindowRect(hFg, &rcWnd);
HMONITOR hMon = MonitorFromWindow(hFg, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
GetMonitorInfoW(hMon, &mi);
rcMonitor = mi.rcMonitor;
// 窗口覆盖整个屏幕 = 全屏
return (rcWnd.left <= rcMonitor.left &&
rcWnd.top <= rcMonitor.top &&
rcWnd.right >= rcMonitor.right &&
rcWnd.bottom >= rcMonitor.bottom);
}
void SyncOverlay() {
if (IsTaskbarVisible()) {
RECT rc;
GetWindowRect(m_hTray, &rc);
SetWindowPos(m_hOverlay, HWND_TOPMOST,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
SWP_SHOWWINDOW | SWP_NOACTIVATE);
} else {
ShowWindow(m_hOverlay, SW_HIDE);
}
}
};
更好的方案:Hook Shell 事件
注册 Shell Hook 消息
// 注册 Shell Hook —— 收到前台窗口变化等通知
UINT WM_SHELLHOOK = RegisterWindowMessageW(L"SHELLHOOK");
RegisterShellHookWindow(m_hOverlay);
// WndProc 中处理
case WM_SHELLHOOK: // 即上面注册的消息
switch (wParam) {
case HSHELL_RUDEAPPACTIVATED: // 全屏应用激活
ShowWindow(m_hOverlay, SW_HIDE);
break;
case HSHELL_WINDOWACTIVATED: // 普通窗口激活
SyncOverlay();
break;
case HSHELL_REDRAW:
SyncOverlay();
break;
}
break;
监听 ABN_FULLSCREENAPP
// 注册 AppBar 接收全屏通知
APPBARDATA abd = { sizeof(abd) };
abd.hWnd = m_hOverlay;
abd.uCallbackMessage = WM_USER + 100;
SHAppBarMessage(ABM_NEW, &abd);
// WndProc
case WM_USER + 100:
if (wParam == ABN_FULLSCREENAPP) {
if (lParam) {
// 全屏应用进入 —— 隐藏
ShowWindow(m_hOverlay, SW_HIDE);
} else {
// 全屏应用退出 —— 恢复
SyncOverlay();
}
}
break;
最简方案:组合使用
static HWND g_hOverlay;
static HWND g_hTray;
LRESULT CALLBACK OverlayProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
static UINT WM_SHELLHOOK;
switch (msg) {
case WM_CREATE:
WM_SHELLHOOK = RegisterWindowMessageW(L"SHELLHOOK");
RegisterShellHookWindow(hwnd);
// 同时注册 AppBar 以获取全屏通知
{
APPBARDATA abd = { sizeof(abd), hwnd, WM_USER + 1 };
SHAppBarMessage(ABM_NEW, &abd);
}
// 定时器兜底(处理边界情况)
SetTimer(hwnd, 1, 1000, NULL);
break;
case WM_TIMER:
SyncPosition();
break;
case WM_USER + 1: // AppBar callback
if (wp == ABN_FULLSCREENAPP) {
ShowWindow(hwnd, lp ? SW_HIDE : SW_SHOWNOACTIVATE);
if (!lp) SyncPosition();
} else if (wp == ABN_POSCHANGED) {
SyncPosition(); // 任务栏位置变了
}
break;
case WM_DESTROY:
DeregisterShellHookWindow(hwnd);
{
APPBARDATA abd = { sizeof(abd), hwnd };
SHAppBarMessage(ABM_REMOVE, &abd);
}
break;
}
// Shell Hook 消息
if (msg == WM_SHELLHOOK) {
switch (wp) {
case HSHELL_RUDEAPPACTIVATED:
ShowWindow(hwnd, SW_HIDE);
break;
case HSHELL_WINDOWACTIVATED:
SyncPosition();
ShowWindow(hwnd, SW_SHOWNOACTIVATE);
break;
}
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
void SyncPosition() {
RECT rc;
GetWindowRect(g_hTray, &rc);
SetWindowPos(g_hOverlay, HWND_TOPMOST,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
总结
| 事件 | 检测方式 | 响应 |
|---|---|---|
| 全屏游戏进入 | ABN_FULLSCREENAPP / HSHELL_RUDEAPPACTIVATED |
隐藏 overlay |
| 全屏游戏退出 | ABN_FULLSCREENAPP(lp=0) / HSHELL_WINDOWACTIVATED |
显示 + 同步位置 |
| 任务栏移动/调整大小 | ABN_POSCHANGED / Timer 兜底 |
重新定位 |
| 任务栏自动隐藏 | Timer 检查 GetWindowRect |
跟随隐藏 |
ABN_FULLSCREENAPP 是最可靠的全屏检测,Windows Shell 自己就是用这个判断是否隐藏任务栏。
避免被游戏反作弊误杀的替代方案
为什么 SetWindowDisplayAffinity 会被误杀
反作弊引擎(EAC、BattlEye、Vanguard)会:
- 枚举所有窗口,检测 WDA_EXCLUDEFROMCAPTURE
- 认为这是作弊覆盖层(隐藏外挂 UI)
- 直接封号或踢出
替代方案
方案 1:不隐藏,而是让窗口"本来就不会被捕获"
利用 WS_EX_NOREDIRECTIONBITMAP:
HWND hOverlay = CreateWindowExW(
WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOPMOST |
WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW,
L"Overlay", NULL,
WS_POPUP | WS_VISIBLE,
x, y, w, h, NULL, NULL, hInst, NULL);
这个标志让窗口没有重定向表面(Redirection Surface):
- BitBlt/PrintWindow 捕获到的是空的
- 部分截图工具抓不到内容
- 但需要自己通过 DirectComposition 渲染
// 配合 DirectComposition 渲染
IDCompositionDevice* pDCompDevice = nullptr;
DCompositionCreateDevice(nullptr, IID_PPV_ARGS(&pDCompDevice));
IDCompositionTarget* pTarget = nullptr;
pDCompDevice->CreateTargetForHwnd(hOverlay, TRUE, &pTarget);
IDCompositionVisual* pVisual = nullptr;
pDCompDevice->CreateVisual(&pVisual);
pTarget->SetRoot(pVisual);
// 用 DXGI surface 或 D2D 绘制内容到 visual 上
// ...
pDCompDevice->Commit();
缺点: 不是所有截图方式都能规避,Windows.Graphics.Capture 仍可能抓到。
方案 2:根本不用窗口 —— 直接画到任务栏进程内
注入 explorer.exe,在内部创建 DirectComposition visual:
// 注入到 explorer.exe 后
// 获取任务栏的 IDCompositionTarget
// 在现有 visual tree 上添加自己的 visual
// 这样内容属于任务栏本身,不存在独立窗口
优点: 没有独立窗口,反作弊无从检测 缺点: 注入 explorer 本身风险高
方案 3:监听截图行为,临时销毁/重建窗口
不是隐藏,而是真正销毁,截图结束后重建:
// 全局监听
HWINEVENTHOOK hEventHook = SetWinEventHook(
EVENT_SYSTEM_CAPTURESTART, EVENT_SYSTEM_CAPTUREEND,
NULL, WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT);
// 更实用:监听已知截图进程
void MonitorThread() {
while (running) {
bool screenshotActive = false;
// 检测常见截图工具进程
// SnippingTool, ScreenClippingHost, ShareX 等
HWND hSnip = FindWindowW(L"Microsoft-Windows-SnipperToolbar", NULL);
if (hSnip && IsWindowVisible(hSnip))
screenshotActive = true;
// 检测 PrintScreen 状态(剪贴板监听)
static DWORD lastSeq = GetClipboardSequenceNumber();
DWORD curSeq = GetClipboardSequenceNumber();
if (curSeq != lastSeq) {
lastSeq = curSeq;
// 剪贴板变化,可能是截图
}
if (screenshotActive && g_hOverlay) {
DestroyWindow(g_hOverlay);
g_hOverlay = NULL;
} else if (!screenshotActive && !g_hOverlay) {
g_hOverlay = CreateOverlay();
}
Sleep(50);
}
}
缺点: 无法覆盖所有截图方式,有延迟
方案 4:用普通窗口 + 全透明,截图中自然"看不到"
如果 overlay 只是少量文字/图标,让它截图时无害可见:
// 不做任何隐藏处理
// 让 overlay 在截图中也出现,但伪装成任务栏的一部分
// 视觉上与任务栏融为一体,截图里看到也无所谓
最安全的策略:如果不需要隐藏,就别隐藏。
最佳实践总结
| 场景 | 建议 |
|---|---|
| 不跑游戏/无反作弊 | WDA_EXCLUDEFROMCAPTURE |
| 有反作弊运行 | 不隐藏,让 overlay 与任务栏融为一体 |
| 需要绝对隐藏 | WS_EX_NOREDIRECTIONBITMAP + DComp 渲染 |
| 最安全 | 注入 explorer,不创建独立窗口 |
现实建议: 如果 overlay 只是显示信息(时钟、监控等),让它截图中可见反而最安全。反作弊针对的是刻意隐藏的覆盖层,一个正常可见的窗口不会触发检测。