You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SetWindowDisplayAffinity导致Electron窗口录屏黑屏的问题求助

问题描述

我通过DLL注入在其他进程中调用SetWindowDisplayAffinity,传入WDA_EXCLUDEFROMCAPTURE参数让窗口在录屏软件中完全隐藏。初始状态下功能正常,但当目标Electron应用最小化到托盘后再显示窗口时,录屏软件会看到黑屏(效果类似WDA_MONITOR)。

系统环境

Windows 11 Pro 23H2

我的代码

#include "library.h"

#include <vector>
#include <windows.h>

auto createdWindows = std::vector<HWND>();

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
    DWORD windowPID = 0;
    GetWindowThreadProcessId(hwnd, &windowPID);

    DWORD currentPID = GetCurrentProcessId();

    if (windowPID == currentPID) {
        SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
    }

    return TRUE;
}

VOID CALLBACK TimerProc(HWND hwnd, UINT message, UINT idTimer, DWORD dwTime) {
    for (auto hWnd : createdWindows) {
        SetWindowDisplayAffinity(hWnd, WDA_EXCLUDEFROMCAPTURE);
    }

    createdWindows.clear();
}

void StartTimer() {
    SetTimer(nullptr, 0, 1000, reinterpret_cast<TIMERPROC>(TimerProc));
}

LRESULT CALLBACK WindowProcHook(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HCBT_CREATEWND) {
        HWND hwnd = (HWND)wParam;
        SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
    }

    return CallNextHookEx(nullptr, nCode, wParam, lParam);
}

void SetWindowHook() {
    SetWindowsHookExA(WH_CBT, WindowProcHook, nullptr, GetCurrentThreadId());
}

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpvReserved )  // reserved
{
    EnumWindows(EnumWindowsProc, 0);
    SetWindowHook();
    StartTimer();

    return TRUE;
}

解决方案

问题根源

Electron应用从托盘恢复时,窗口的显示亲和性(Display Affinity)会被系统或框架重置;现有逻辑仅覆盖了窗口创建和初始枚举场景,未处理窗口状态变化(如从托盘恢复)的情况。另外,WH_CBT钩子仅挂在当前线程,Electron多线程创建窗口时可能遗漏,1秒间隔的定时器也会导致窗口恢复后出现短暂黑屏。

修复步骤

  1. 监听窗口状态变化:通过WH_CALLWNDPROC钩子捕获WM_SHOWWINDOW、WM_ACTIVATE等消息,在窗口恢复显示时重新设置WDA_EXCLUDEFROMCAPTURE。
  2. 覆盖所有线程的钩子:枚举目标进程的所有线程并逐个挂钩子,确保捕获Electron多线程创建的窗口消息。
  3. 优化定时器逻辑:定时器触发时重新枚举当前进程所有窗口并设置亲和性,避免遗漏状态变化的窗口。
  4. 重置窗口状态:窗口恢复时先设WDA_NONE清除异常状态,再重新设置目标亲和性,确保状态正确。

修改后的代码示例

#include "library.h"
#include <vector>
#include <windows.h>
#include <tlhelp32.h>

// 枚举当前进程的所有线程ID
std::vector<DWORD> GetProcessThreads(DWORD pid) {
    std::vector<DWORD> threads;
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE) return threads;

    THREADENTRY32 te;
    te.dwSize = sizeof(THREADENTRY32);
    if (Thread32First(hSnapshot, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                threads.push_back(te.th32ThreadID);
            }
        } while (Thread32Next(hSnapshot, &te));
    }
    CloseHandle(hSnapshot);
    return threads;
}

// 窗口消息钩子,捕获显示/激活相关消息
LRESULT CALLBACK WindowMessageHook(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode >= 0) {
        CWPSTRUCT* msg = reinterpret_cast<CWPSTRUCT*>(lParam);
        // 窗口显示、激活时重新设置亲和性
        if (msg->message == WM_SHOWWINDOW || msg->message == WM_ACTIVATE) {
            if (msg->wParam != 0) {
                SetWindowDisplayAffinity(msg->hwnd, WDA_NONE);
                SetWindowDisplayAffinity(msg->hwnd, WDA_EXCLUDEFROMCAPTURE);
            }
        }
    }
    return CallNextHookEx(nullptr, nCode, wParam, lParam);
}

// 枚举当前进程所有窗口并设置亲和性
void SetAllWindowsAffinity() {
    DWORD currentPID = GetCurrentProcessId();
    EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL {
        DWORD windowPID = 0;
        GetWindowThreadProcessId(hwnd, &windowPID);
        if (windowPID == reinterpret_cast<DWORD>(lParam)) {
            SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
        }
        return TRUE;
    }, reinterpret_cast<LPARAM>(currentPID));
}

VOID CALLBACK TimerProc(HWND hwnd, UINT message, UINT idTimer, DWORD dwTime) {
    SetAllWindowsAffinity();
}

void StartTimer() {
    // 缩短间隔,减少黑屏持续时间
    SetTimer(nullptr, 0, 500, reinterpret_cast<TIMERPROC>(TimerProc));
}

// 给当前进程所有线程挂消息钩子
void SetGlobalHooks() {
    DWORD currentPID = GetCurrentProcessId();
    auto threads = GetProcessThreads(currentPID);
    HINSTANCE hModule = GetModuleHandleA(nullptr);
    for (DWORD tid : threads) {
        SetWindowsHookExA(WH_CALLWNDPROC, WindowMessageHook, hModule, tid);
    }
}

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,
    DWORD fdwReason,
    LPVOID lpvReserved
) {
    // 仅在DLL注入进程时执行初始化
    if (fdwReason == DLL_PROCESS_ATTACH) {
        SetAllWindowsAffinity();
        SetGlobalHooks();
        StartTimer();
    }
    return TRUE;
}

关键说明

  • GetProcessThreads函数枚举当前进程所有线程,确保钩子覆盖Electron的多线程窗口创建场景。
  • 钩子监听窗口显示/激活消息,通过先重置再设置的方式避免状态残留。
  • 定时器逻辑改为枚举所有窗口,覆盖所有可能遗漏的窗口状态变化。
  • DllMain中仅在DLL_PROCESS_ATTACH时执行初始化,避免重复触发导致的异常。

内容的提问来源于stack exchange,提问作者Rabbit0w0

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 09:43:14