如何避免窗口被游戏反作弊误杀以及替代方案

同步透明覆盖窗口与任务栏状态

核心问题

任务栏可能: - 被全屏应用遮挡/隐藏 - 自动隐藏 - 位置/大小变化 - 虚拟桌面切换

方案:监听任务栏状态同步

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 只是显示信息(时钟、监控等),让它截图中可见反而最安全。反作弊针对的是刻意隐藏的覆盖层,一个正常可见的窗口不会触发检测。