找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3104

积分

0

好友

402

主题
发表于 18 小时前 | 查看: 0| 回复: 0

在 Windows 平台上实现屏幕截图,主要有两种主流的 API 方案:传统的 GDI API 和现代高性能的 Desktop Duplication API。此前我们在另一篇文章中介绍过基于 GDI 的实现,它虽然经典但面对游戏或视频等硬件加速内容时力不从心。本文将重点探讨后者——Desktop Duplication API,并提供一个可直接运行的 C++ 实现。

为什么选择 Desktop Duplication API?

如其名所示,Desktop Duplication API 的核心功能是直接“复制”整个桌面图像缓冲区。它自 Windows 8/8.1 起引入,隶属于 DirectX 家族,是目前微软推荐的高性能屏幕捕获方案。其优势与局限性非常鲜明:

优点

  • 极致性能与低延迟:通过 GPU 加速获取图像,适合游戏录制、实时流媒体等高要求场景。
  • 完整内容捕获:能够抓取包括 GDI、DirectX 以及 OpenGL 在内的所有图形输出,不留“黑窗”。
  • 系统友好:对前台应用程序的干扰最小。

缺点

  • 编程复杂度高:需要一定的 DirectX 和 COM 编程知识。
  • 系统版本限制:仅支持 Windows 8 及以上版本的操作系统。
  • 需要管理员权限:在某些安全配置下,需要以管理员身份运行程序。

总而言之,如果你需要构建一个专业的录屏、远程桌面或高性能截图工具,Desktop Duplication API 是当前 Windows 平台上的不二之选;若仅是偶尔截取静态画面,传统的 GDI 方式则更为简单直接。

核心实现:DesktopDuplicator 类

接下来,我们直接切入正题,通过一个封装好的 DesktopDuplicator 类来展示具体实现。这个类的职责非常清晰:初始化 DirectX 环境,通过 Desktop Duplication 技术抓取桌面帧,并最终将其保存为 BMP 图像文件。

class DesktopDuplicator
{
private:
  ComPtr<ID3D11Device> m_device;
  ComPtr<ID3D11DeviceContext> m_context;
  ComPtr<IDXGIOutputDuplication> m_duplication;
  ComPtr<ID3D11Texture2D> m_stagingTexture;

  DXGI_OUTPUT_DESC m_outputDesc{};
  DXGI_OUTDUPL_DESC m_duplDesc{};

  bool m_initialized = false;

public:
  DesktopDuplicator() = default;

  bool Initialize()
  {
    HRESULT hr = S_OK;

    // 1. 创建 D3D11 设备
    D3D_FEATURE_LEVEL featureLevels[] = {
      D3D_FEATURE_LEVEL_11_1,
      D3D_FEATURE_LEVEL_11_0
    };

    hr = D3D11CreateDevice(
      nullptr,
      D3D_DRIVER_TYPE_HARDWARE,
      nullptr,
      D3D11_CREATE_DEVICE_BGRA_SUPPORT,
      featureLevels,
      ARRAYSIZE(featureLevels),
      D3D11_SDK_VERSION,
      &m_device,
      nullptr,
      &m_context
    );
    if (FAILED(hr)) {
      std::cerr << "Failed to create D3D11 device: 0x" << std::hex << hr << std::endl;
      return false;
    }

    // 2. 获取 DXGI 适配器
    ComPtr<IDXGIDevice> dxgiDevice;
    hr = m_device.As(&dxgiDevice);
    if (FAILED(hr)) {
      std::cerr << "Failed to get DXGI device: 0x" << std::hex << hr << std::endl;
      return false;
    }

    ComPtr<IDXGIAdapter> dxgiAdapter;
    hr = dxgiDevice->GetAdapter(&dxgiAdapter);
    if (FAILED(hr)) {
      std::cerr << "Failed to get DXGI adapter: 0x" << std::hex << hr << std::endl;
      return false;
    }

    // 3. 获取主输出
    ComPtr<IDXGIOutput> dxgiOutput;
    hr = dxgiAdapter->EnumOutputs(0, &dxgiOutput);
    if (FAILED(hr)) {
      std::cerr << "Failed to get DXGI output: 0x" << std::hex << hr << std::endl;
      return false;
    }

    hr = dxgiOutput->GetDesc(&m_outputDesc);
    if (FAILED(hr)) {
      std::cerr << "Failed to get output description: 0x" << std::hex << hr << std::endl;
      return false;
    }

    std::wcout << L“Output device: “ << m_outputDesc.DeviceName << std::endl;
    std::cout << “Desktop coordinates: (“
      << m_outputDesc.DesktopCoordinates.left << “, “
      << m_outputDesc.DesktopCoordinates.top << “) - (“
      << m_outputDesc.DesktopCoordinates.right << “, “
      << m_outputDesc.DesktopCoordinates.bottom << “)” << std::endl;

    // 4. 获取输出1接口
    ComPtr<IDXGIOutput1> dxgiOutput1;
    hr = dxgiOutput.As(&dxgiOutput1);
    if (FAILED(hr)) {
      std::cerr << “Failed to get DXGIOutput1: 0x” << std::hex << hr << std::endl;
      return false;
    }

    // 5. 创建桌面复制
    hr = dxgiOutput1->DuplicateOutput(m_device.Get(), &m_duplication);
    if (FAILED(hr)) {
      std::cerr << “Failed to duplicate output: 0x” << std::hex << hr << std::endl;
      if (hr == E_ACCESSDENIED) {
        std::cerr << “Access denied. Please run as administrator!” << std::endl;
      }
      return false;
    }

    // 6. 获取复制描述
    m_duplication->GetDesc(&m_duplDesc);
    std::cout << “Desktop duplication initialized successfully!” << std::endl;
    std::cout << “ModeDesc format: “ << m_duplDesc.ModeDesc.Format << std::endl;
    std::cout << “ModeDesc width: “ << m_duplDesc.ModeDesc.Width << std::endl;
    std::cout << “ModeDesc height: “ << m_duplDesc.ModeDesc.Height << std::endl;

    // 7. 创建暂存纹理
    CreateStagingTexture();

    m_initialized = true;
    return true;
  }

  void CreateStagingTexture()
  {
    D3D11_TEXTURE2D_DESC textureDesc = {};
    textureDesc.Width = m_duplDesc.ModeDesc.Width;
    textureDesc.Height = m_duplDesc.ModeDesc.Height;
    textureDesc.MipLevels = 1;
    textureDesc.ArraySize = 1;
    textureDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    textureDesc.SampleDesc.Count = 1;
    textureDesc.SampleDesc.Quality = 0;
    textureDesc.Usage = D3D11_USAGE_STAGING;
    textureDesc.BindFlags = 0;
    textureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    textureDesc.MiscFlags = 0;

    HRESULT hr = m_device->CreateTexture2D(&textureDesc, nullptr, &m_stagingTexture);
    if (FAILED(hr)) {
      std::cerr << “Failed to create staging texture: 0x” << std::hex << hr << std::endl;
    }
  }

  bool CaptureFrame(std::vector<BYTE>& outBuffer, int& width, int& height)
  {
    if (!m_initialized) {
      std::cerr << “Not initialized!” << std::endl;
      return false;
    }

    ComPtr<IDXGIResource> desktopResource;
    DXGI_OUTDUPL_FRAME_INFO frameInfo;

    // 尝试获取桌面帧
    HRESULT hr = m_duplication->AcquireNextFrame(100, &frameInfo, &desktopResource);
    if (FAILED(hr)) {
      std::cerr << “Failed to acquire next frame: 0x” << std::hex << hr << std::endl;
      return false;
    }

    // 获取纹理
    ComPtr<ID3D11Texture2D> desktopTexture;
    hr = desktopResource.As(&desktopTexture);
    if (FAILED(hr)) {
      m_duplication->ReleaseFrame();
      std::cerr << “Failed to get desktop texture: 0x” << std::hex << hr << std::endl;
      return false;
    }

    // 复制到暂存纹理
    m_context->CopyResource(m_stagingTexture.Get(), desktopTexture.Get());

    // 释放帧
    m_duplication->ReleaseFrame();

    // 映射到内存
    D3D11_MAPPED_SUBRESOURCE mapped;
    hr = m_context->Map(m_stagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mapped);
    if (FAILED(hr)) {
      std::cerr << “Failed to map texture: 0x” << std::hex << hr << std::endl;
      return false;
    }

    D3D11_TEXTURE2D_DESC desc;
    m_stagingTexture->GetDesc(&desc);

    width = desc.Width;
    height = desc.Height;

    // 分配缓冲区
    outBuffer.resize(width * height * 4);

    // 复制数据
    BYTE* src = static_cast<BYTE*>(mapped.pData);
    BYTE* dst = outBuffer.data();
    for (int i = 0; i < height; ++i) {
      memcpy(dst + i * width * 4, src + i * mapped.RowPitch, width * 4);
    }

    // 取消映射
    m_context->Unmap(m_stagingTexture.Get(), 0);

    return true;
  }

  bool SaveAsBMP(const std::vector<BYTE>& imageData, int width, int height, const std::wstring& filename)
  {
    std::ofstream file(filename, std::ios::binary);
    if (!file) {
      std::cerr << “Failed to open file for writing” << std::endl;
      return false;
    }

    // BMP 文件头
    BITMAPFILEHEADER bmpHeader = {};
    bmpHeader.bfType = 0x4D42; // “BM”
    bmpHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + width * height * 3;
    bmpHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

    // BMP 信息头
    BITMAPINFOHEADER bmpInfo = {};
    bmpInfo.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo.biWidth = width;
    bmpInfo.biHeight = -height; // 负高度表示从上到下
    bmpInfo.biPlanes = 1;
    bmpInfo.biBitCount = 24;  // 24位BMP
    bmpInfo.biCompression = BI_RGB;
    bmpInfo.biSizeImage = width * height * 3;

    file.write(reinterpret_cast<const char*>(&bmpHeader), sizeof(bmpHeader));
    file.write(reinterpret_cast<const char*>(&bmpInfo), sizeof(bmpInfo));

    // BGRA 转 BGR
    std::vector<BYTE> bgrData(width * height * 3);
    const BYTE* src = imageData.data();
    BYTE* dst = bgrData.data();
    for (int i = 0; i < width * height; ++i) {
      dst[0] = src[0];  // B
      dst[1] = src[1];  // G
      dst[2] = src[2];  // R
      src += 4;
      dst += 3;
    }

    file.write(reinterpret_cast<const char*>(bgrData.data()), bgrData.size());
    file.close();

    return true;
  }

  void Cleanup()
  {
    m_stagingTexture.Reset();
    m_duplication.Reset();
    m_context.Reset();
    m_device.Reset();
    m_initialized = false;
  }

  ~DesktopDuplicator()
  {
    Cleanup();
  }
};

类设计与流程解析

上述 DesktopDuplicator 类的设计可以清晰地分为三个阶段:

1. 初始化流程 (Initialize())
这是最关键的步骤,负责搭建整个 DirectX 捕获管道:

  • 创建 D3D11 设备和上下文,这是所有后续操作的基础。
  • 通过 DXGI 接口链(设备 -> 适配器 -> 输出)定位到主显示器。
  • 调用核心的 DuplicateOutput() 方法创建 IDXGIOutputDuplication 对象,获得桌面复制的能力。
  • 创建一个 STAGING 类型的纹理,用于后续将 GPU 数据拷贝到 CPU 可读的内存中。
  • 记录并输出显示器信息,如分辨率、桌面坐标等。

2. 帧捕获流程 (CaptureFrame())
这是执行截图的核心操作,遵循“获取-复制-读取”的模式:

  • AcquireNextFrame():从复制接口获取最新的桌面帧及其元信息。
  • CopyResource():将获取到的 GPU 纹理数据复制到之前创建的 STAGING 纹理中。
  • Map():将暂存纹理映射到 CPU 地址空间,以便直接读取像素数据。
  • 数据拷贝:将映射出来的 BGRA 格式数据按行复制到用户提供的缓冲区中,并返回图像的宽高。
  • Unmap()ReleaseFrame():释放资源,确保管道可持续运行。

3. 保存图像 (SaveAsBMP())
一个实用的辅助函数,负责将内存中的 BGRA 数据转换为标准的 24 位 BGR BMP 文件格式并保存到磁盘。注意这里通过设置 biHeight 为负值,来指定图像数据是“从上到下”存储的。

实战调用与注意事项

有了封装好的类,在主函数中调用就非常直观了。但这里有几个实战中容易遇到的坑需要特别注意:

int main(int argc, char *argv[])
{
  std::cout << “Desktop Capture Test Program” << std::endl;
  std::cout << “=============================” << std::endl;

  // 检查管理员权限
  BOOL isAdmin = FALSE;
  HANDLE hToken = NULL;

  if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
    TOKEN_ELEVATION elevation;
    DWORD size = sizeof(TOKEN_ELEVATION);
    if (GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &size)) {
      isAdmin = elevation.TokenIsElevated;
    }
    CloseHandle(hToken);
  }

  if (!isAdmin) {
    std::cout << “WARNING: Not running as administrator. Desktop duplication may fail!” << std::endl;
  } else {
    std::cout << “Running as administrator” << std::endl;
  }

  DesktopDuplicator duplicator;
  if (!duplicator.Initialize()) {
    std::cerr << “初始化失败!” << std::endl;
    return 1;
  }

  // 添加一点延迟,确保有桌面更新
  std::this_thread::sleep_for(std::chrono::milliseconds(100));

  std::vector<BYTE> imageData;
  int width, height;

  bool success = duplicator.CaptureFrame(imageData, width, height);
  if (success && !imageData.empty()) {
    // 生成文件名
    SYSTEMTIME st;
    GetLocalTime(&st);
    wchar_t filename[MAX_PATH];
    swprintf_s(filename, L“screenshot_%04d%02d%02d_%02d%02d%02d.bmp”,
      st.wYear, st.wMonth, st.wDay,
      st.wHour, st.wMinute, st.wSecond);

    if (duplicator.SaveAsBMP(imageData, width, height, filename)) {
      std::wcout << L“截图已保存为: “ << filename
        << L“ (“ << width << “x” << height << L“)” << std::endl;
    } else {
      std::cerr << “保存截图失败!” << std::endl;
    }
  } else {
    std::cerr << “截图失败!” << std::endl;
  }

  return 0;
}

关键点提醒

  1. 管理员权限检查:由于该 API 涉及系统底层图形资源,在某些配置下(特别是捕获安全桌面时)必须使用管理员权限运行。代码开头对此进行了检查并给出明确提示。
  2. 初始化后延迟:在 Initialize() 和第一次 CaptureFrame() 之间,我们故意添加了一个短暂的延迟(如100毫秒)。
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    这是为了解决一个常见问题:初始化后立即抓取,可能会得到一张未更新的黑色或旧图像。这个延迟是为了“等待”一次桌面更新发生。这虽然是个有效的 workaround,但并非最优雅的方案。一个更优的实践可能是循环调用 AcquireNextFrame 直到它返回一个非超时的成功状态,表示真正获取到了新帧。欢迎大家在 云栈社区 分享更好的解决方案。

运行效果展示

编译并运行上述程序后,控制台会输出初始化信息,如下图所示:

DesktopDuplicator程序运行输出

程序成功运行后会生成一个以时间戳命名的 BMP 文件。下图是一次常规桌面捕获的示例:

使用DesktopDuplication API捕获的桌面画面

为了测试其捕获高性能图形内容的能力,我们可以在后台定时运行该截图程序。下图展示了用它连续捕获的一款 DirectX 游戏的画面,效果非常完整,没有出现黑屏或卡顿:

使用DesktopDuplication API捕获的实时游戏画面

总结与完整代码

通过本文的探讨,我们可以看到 Desktop Duplication API 为 Windows 平台上的高性能屏幕捕获提供了强大而稳定的底层支持。它完美解决了传统 GDI 截屏无法捕获硬件加速内容的痛点,是开发专业屏幕录制、游戏直播、远程协助等应用的基石技术。

尽管其编程模型稍显复杂,但一旦理解和封装成功,带来的性能和效果提升是质的飞跃。希望这个详细的 DesktopDuplicator 实现能为你自己的项目提供一个坚实的起点。

Desktop Duplication API完整C++实现代码

(上图展示了本文所述功能的完整 C++ 项目代码结构,供参考。)




上一篇:Java程序员迷茫想转行?五年老兵的肺腑之言与转行方向探讨
下一篇:X开源无手工特征推荐系统:基于Grok-1的For You Feed架构解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-2 22:03 , Processed in 0.411329 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表