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

WinRT屏幕共享应用:拆分捕获代码及解决边框、失真问题

Windows窗口捕获(WinRT)问题解决方案

1. 拆分单体代码为初始化、捕获、清理函数

将原有一次性执行的代码拆分为类成员函数,复用D3D设备、捕获会话等资源,避免每次捕获重复初始化:

类成员变量定义

class WindowCapturer {
private:
    winrt::apartment_context m_apartment;
    winrt::com_ptr<ID3D11Device> m_d3dDevice;
    winrt::com_ptr<ID3D11DeviceContext> m_d3dContext;
    winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice m_device;
    winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool m_framePool;
    winrt::Windows::Graphics::Capture::GraphicsCaptureSession m_session;
    winrt::Windows::Graphics::Capture::GraphicsCaptureItem m_captureItem;
    HWND m_hwnd = nullptr;
    bool m_isCapturing = false;
    std::atomic<bool> m_frameArrived = false;
    winrt::com_ptr<ID3D11Texture2D> m_capturedTexture;

    winrt::event_token m_frameArrivedToken;
};

初始化函数

HRESULT WindowCapturer::Init(HWND hwnd) {
    m_hwnd = hwnd;

    // 初始化COM(仅执行一次)
    winrt::init_apartment(winrt::apartment_type::multi_threaded);
    m_apartment = winrt::apartment_context();

    // 创建D3D设备
    HRESULT hr = D3D11CreateDevice(
        nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
        D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0,
        D3D11_SDK_VERSION, m_d3dDevice.put(), nullptr, nullptr
    );
    if (FAILED(hr)) return hr;

    m_d3dDevice->GetImmediateContext(m_d3dContext.put());

    // 转换为WinRT兼容的Direct3D设备
    auto dxgiDevice = m_d3dDevice.as<IDXGIDevice>();
    winrt::com_ptr<::IInspectable> inspectable;
    hr = CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), inspectable.put());
    if (FAILED(hr)) return hr;
    m_device = inspectable.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();

    // 创建捕获项
    const auto activationFactory = winrt::get_activation_factory<winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
    auto interopFactory = activationFactory.as<IGraphicsCaptureItemInterop>();
    hr = interopFactory->CreateForWindow(
        hwnd, winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
        reinterpret_cast<void**>(winrt::put_abi(m_captureItem))
    );
    if (FAILED(hr)) return hr;

    // 获取窗口大小并创建帧池
    RECT rect{};
    DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(RECT));
    const auto size = winrt::Windows::Graphics::SizeInt32{ rect.right - rect.left, rect.bottom - rect.top };

    m_framePool = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::Create(
        m_device, winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
        2, size
    );

    // 注册帧到达回调
    m_frameArrivedToken = m_framePool.FrameArrived([this](auto& pool, auto&) {
        if (m_frameArrived) return;
        auto frame = pool.TryGetNextFrame();
        auto access = frame.Surface().as<IDirect3DDxgiInterfaceAccess>();
        access->GetInterface(winrt::guid_of<ID3D11Texture2D>(), m_capturedTexture.put_void());
        m_frameArrived = true;
    });

    // 创建捕获会话
    m_session = m_framePool.CreateCaptureSession(m_captureItem);

    return S_OK;
}

捕获帧函数

MyImageContainer WindowCapturer::CaptureFrame() {
    if (!m_hwnd || !m_session) {
        return error "未初始化捕获器";
    }

    m_frameArrived = false;
    m_session.StartCapture();

    // 等待帧到达(可替换为更高效的同步机制,如事件)
    MSG msg;
    clock_t timer = clock();
    while (!m_frameArrived) {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) > 0) {
            DispatchMessage(&msg);
        }
        if (clock() - timer > 20000) {
            m_session.Close();
            return error "捕获超时";
        }
    }

    m_session.Close();

    // 复制纹理到可CPU读取的 staging 纹理
    D3D11_TEXTURE2D_DESC textureDesc{};
    m_capturedTexture->GetDesc(&textureDesc);
    D3D11_TEXTURE2D_DESC stagingDesc = textureDesc;
    stagingDesc.Usage = D3D11_USAGE_STAGING;
    stagingDesc.BindFlags = 0;
    stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    stagingDesc.MiscFlags = 0;

    winrt::com_ptr<ID3D11Texture2D> stagingTexture;
    HRESULT hr = m_d3dDevice->CreateTexture2D(&stagingDesc, NULL, stagingTexture.put());
    if (FAILED(hr)) {
        return error "创建 staging 纹理失败";
    }

    m_d3dContext->CopyResource(stagingTexture.get(), m_capturedTexture.get());

    // 映射纹理并读取数据
    D3D11_MAPPED_SUBRESOURCE resource{};
    hr = m_d3dContext->Map(stagingTexture.get(), 0, D3D11_MAP_READ, 0, &resource);
    if (FAILED(hr) || !resource.pData) {
        return error "映射纹理失败";
    }

    UINT rowPitch = textureDesc.Width * 4;
    MyImageContainer img;
    img.imageBuffer.resize(textureDesc.Height * rowPitch);
    BYTE* dst = img.imageBuffer.data();
    BYTE* src = static_cast<BYTE*>(resource.pData);

    for (size_t h = 0; h < textureDesc.Height; ++h) {
        memcpy_s(dst, rowPitch, src, std::min<UINT>(rowPitch, resource.RowPitch));
        src += resource.RowPitch;
        dst += rowPitch;
    }

    m_d3dContext->Unmap(stagingTexture.get(), 0);
    return img;
}

清理函数

void WindowCapturer::Cleanup() {
    if (m_session) {
        m_session.Close();
    }
    m_framePool.Close();
    m_frameArrivedToken = {};
    m_captureItem = nullptr;
    m_device = nullptr;
    m_d3dContext = nullptr;
    m_d3dDevice = nullptr;
    m_hwnd = nullptr;
    winrt::uninit_apartment();
}

2. Win10下去除捕获闪烁边框的替代方案

Win10不支持IsBorderRequired属性,可通过DWM窗口隐藏(Cloak)机制临时屏蔽系统绘制的捕获边框:

实现步骤

  1. 在启动捕获前,调用DwmSetWindowAttribute将窗口设置为隐藏状态
  2. 启动捕获后立即取消隐藏,避免窗口长时间不可见

代码修改(在CaptureFrame函数中添加)

// 启动捕获前隐藏窗口
DWORD cloakValue = DWM_CLOAKED_INVISIBLE;
DwmSetWindowAttribute(m_hwnd, DWMWA_CLOAK, &cloakValue, sizeof(cloakValue));

m_session.StartCapture();

// 立即取消隐藏
cloakValue = 0;
DwmSetWindowAttribute(m_hwnd, DWMWA_CLOAK, &cloakValue, sizeof(cloakValue));

注意:需包含dwmapi.h并链接dwmapi.lib;此方法仅屏蔽系统捕获边框,窗口本身仍正常显示。

3. 非全屏窗口捕获失真排查思路

步骤1:检查窗口大小获取逻辑

  • 确认DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS)获取的是窗口实际像素尺寸,而非DPI缩放后的逻辑尺寸
  • 对比window.size.x/y与DWMWA_EXTENDED_FRAME_BOUNDS计算出的宽高是否一致,若不一致则以实际捕获的纹理尺寸为准(避免硬编码window.size)

步骤2:修复纹理复制逻辑

原代码中存在无效语句capturedTextureDesc.Height * 4 - lBmpRowPitch;,需删除或修正。确保行间距(RowPitch)处理正确:

  • 直接使用捕获纹理的实际RowPitch进行数据复制,避免强制截断导致的图像错位
  • 检查stagingDesc.Width/Height是否与捕获纹理的尺寸完全一致,禁止手动修改为window.size(除非window.size是经过DPI校正的实际像素值)

步骤3:排查DPI适配问题

  • Win10高DPI环境下,需确保窗口已启用DPI感知(通过manifest或SetProcessDpiAwarenessContext设置)
  • 若未启用DPI感知,DWMWA_EXTENDED_FRAME_BOUNDS返回的尺寸可能是逻辑尺寸,需乘以DPI缩放系数转换为像素尺寸

步骤4:检查帧池尺寸适配

  • 当窗口大小变化时,需调用m_framePool.Recreate(m_device, pixelFormat, bufferCount, newSize)重新调整帧池尺寸,避免帧池尺寸与窗口实际尺寸不匹配导致的拉伸失真

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 05:14:58