在 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;
}
关键点提醒:
- 管理员权限检查:由于该 API 涉及系统底层图形资源,在某些配置下(特别是捕获安全桌面时)必须使用管理员权限运行。代码开头对此进行了检查并给出明确提示。
- 初始化后延迟:在
Initialize() 和第一次 CaptureFrame() 之间,我们故意添加了一个短暂的延迟(如100毫秒)。
std::this_thread::sleep_for(std::chrono::milliseconds(100));
这是为了解决一个常见问题:初始化后立即抓取,可能会得到一张未更新的黑色或旧图像。这个延迟是为了“等待”一次桌面更新发生。这虽然是个有效的 workaround,但并非最优雅的方案。一个更优的实践可能是循环调用 AcquireNextFrame 直到它返回一个非超时的成功状态,表示真正获取到了新帧。欢迎大家在 云栈社区 分享更好的解决方案。
运行效果展示
编译并运行上述程序后,控制台会输出初始化信息,如下图所示:

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

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

总结与完整代码
通过本文的探讨,我们可以看到 Desktop Duplication API 为 Windows 平台上的高性能屏幕捕获提供了强大而稳定的底层支持。它完美解决了传统 GDI 截屏无法捕获硬件加速内容的痛点,是开发专业屏幕录制、游戏直播、远程协助等应用的基石技术。
尽管其编程模型稍显复杂,但一旦理解和封装成功,带来的性能和效果提升是质的飞跃。希望这个详细的 DesktopDuplicator 实现能为你自己的项目提供一个坚实的起点。

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