拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Windows下C++实现DWM与DXGI桌面复制API的高性能屏幕截图

1. 项目概述为什么要在Windows下用DWM截图如果你在Windows平台上用C做过屏幕截图大概率用过BitBlt、PrintWindow这些经典的GDI函数。它们简单直接对付大多数窗口截图需求没问题。但当你需要截取带有Aero毛玻璃效果、DirectX/Direct3D渲染的游戏窗口或者遇到一些采用硬件加速渲染的现代应用比如某些基于Chromium的浏览器或UI框架时传统的GDI方法就失灵了——你截到的可能是一片黑色、一片灰色或者一个静止的、没有动画的窗口框架。这就是我们今天要聊的DWMDesktop Window ManagerAPI的价值所在。DWM桌面窗口管理器是Windows Vista及之后系统引入的核心组件负责管理窗口的合成与渲染。简单理解它就像一个舞台总监。每个窗口应用程序自己绘制好内容相当于演员在后台化妆准备但最终呈现在屏幕上的那个绚丽、带有透明、阴影和动画效果的画面是由DWM这个“总监”把所有窗口的内容拿过来在GPU上进行最终合成后再输出到显示器的。BitBlt这类GDI函数只能抓到“演员”自己画好的、还没经过“总监”后期处理的原始画面对于大量使用DirectX等硬件加速技术直接向DWM提交最终画面的“演员”GDI就无能为力了。而DWM API则提供了从“总监”那里直接拿到最终合成后画面的能力这就是实现“万能”屏幕截图的关键。所以这个项目的核心目标就是利用C调用Windows的DWM API实现一个能够捕获包括硬件加速窗口在内的、任意屏幕区域或窗口最终呈现画面的截图工具。这对于开发屏幕录制软件、远程协助工具、自动化测试脚本或者任何需要高保真捕捉屏幕内容的场景都是至关重要的底层技术。2. 核心原理与DWM API深度解析要理解如何用DWM截图得先搞明白DWM的工作模式特别是“桌面合成”这个概念。在DWM启用即Aero主题开启的情况下系统运行在“桌面合成”模式。此时应用程序通常不再直接向屏幕缓冲区绘制而是先绘制到一块离屏的纹理或表面Surface上这块表面被称为“交换链”的一部分。DWM则作为合成管理器收集所有窗口的这些表面按照Z序、透明度、特效等规则在GPU上进行混合最终将结果呈现到显示器。2.1 关键API函数DwmGetWindowAttribute微软提供了一系列DwmApi函数其中对我们截图最关键的一个是DwmGetWindowAttribute。这个函数可以查询一个窗口的各种DWM相关属性。我们需要的魔法属性值是DWMWA_EXTENDED_FRAME_BOUNDS和DWMWA_CLOAKED但最重要的是DWMWA_CLOAKED不对于截图另一个属性更关键但官方文档中并没有直接提供一个叫做“截图”的属性。实际上标准方法是结合DwmGetWindowAttribute来获取窗口在DWM管理下的准确位置信息而真正的截图动作需要另一个核心组件DXGIDirectX Graphics Infrastructure。DWM的桌面合成最终输出是一个DXGI表面。因此成熟的DWM截图方案通常是“DWM DXGI”的组合拳。不过有一个更直接的、利用了DWM合成结果的函数那就是BitBlt的“升级版”PrintWindow函数配合PW_RENDERFULLCONTENT标志。但请注意PrintWindow的行为因窗口而异并非所有窗口都支持。更底层、更可靠的方式是使用DXGI Desktop Duplication API。这才是Windows 8及以上版本中微软官方推荐的用于捕获桌面或窗口内容包括DirectX输出的高性能方法。它允许应用程序直接访问DWM的桌面合成输出缓冲区。虽然本项目标题聚焦“DWM”但在现代Windows编程实践中要实现其目标深入DXGI Duplication API是无法绕开的。我们可以将本项目理解为利用以DWM为核心的现代Windows图形架构包含DXGI来实现屏幕捕获。2.2 技术路线选择GDI vs. DWM/DXGI为了更清晰我们用一个表格对比不同技术路线的优劣技术方案核心API/原理优点缺点适用场景传统GDIBitBlt,GetDC(NULL)简单易用系统兼容性好Win95资源消耗低。无法捕获硬件加速窗口黑屏/灰屏无法捕获Aero玻璃等特效帧率较低。截取标准GDI控件窗口、静态桌面区域。增强型GDIPrintWindow可以捕获一些BitBlt抓不到的窗口内容如某些浏览器控件。行为不一致依赖窗口进程配合许多窗口尤其是游戏、UWP应用不支持或返回错误内容。不可靠。作为BitBlt的补充尝试捕获特定应用。DWM DXGI 桌面复制IDXGIOutputDuplication官方推荐能捕获所有DWM合成后的内容包括游戏、UWP、硬件加速UI高性能低延迟支持帧间差异更新。实现复杂仅支持Windows 8/8.1/10/11需要处理DXGI和Direct3D 11 API。屏幕录制、游戏直播、远程桌面、高性能截图工具。Windows Graphics Capture APIWindows.Graphics.Capture(WinRT)现代API更易用安全需要用户授权能捕获UWP和应用商店应用。仅限Windows 10 (1809) / 11需要C/WinRT或C#等语言支持进程外调用有一定开销。适用于现代Windows应用开发特别是UWP或WinUI3项目。对于本“C Windows下使用DWM实现屏幕截图”项目追求高可靠性和完整性我们选择DWM DXGI 桌面复制这条技术路线。它虽然复杂但能最彻底地实现标题所承诺的目标。3. 环境准备与项目配置在开始敲代码之前我们需要把开发环境搭建好。这里假设你使用Visual Studio 2019或2022进行开发。3.1 创建项目与配置属性首先创建一个新的“Windows桌面向导”C项目选择“桌面应用程序(.exe)”或“控制台应用程序”都可以因为我们最终是生成一个工具。项目创建好后右键点击项目选择“属性”进行关键配置常规 C语言标准设置为“ISO C17 标准”或更高。高级 字符集建议使用“使用多字节字符集”以简化字符串处理但“使用Unicode字符集”是更现代的做法。本文示例按Unicode设置。VC目录 包含目录确保默认的Windows SDK路径在内即可通常VS会自动配置好。链接器 输入 附加依赖项这里需要添加我们即将用到的库文件。对于DWM API需要dwmapi.lib。对于DXGI和Direct3D需要dxgi.lib和d3d11.lib。 因此在“附加依赖项”中填入dwmapi.lib; dxgi.lib; d3d11.lib;分号分隔。3.2 理解必要的头文件在我们的主源代码文件中需要包含以下头文件#include windows.h #include dwmapi.h // DWM API #include dxgi1_2.h // DXGI 桌面复制接口 (需要1.2版本) #include d3d11.h // Direct3D 11用于创建设备 #include wincodec.h // Windows 图像编码组件用于保存图片可选 #include vector #include string #include iostream #pragma comment(lib, dwmapi.lib) #pragma comment(lib, dxgi.lib) #pragma comment(lib, d3d11.lib)#pragma comment(lib, ...)是一种在代码中指定链接库的便捷方式效果和项目属性里设置一样。注意DXGI和D3D11相关API涉及COMComponent Object Model编程。我们需要使用Microsoft::WRL::ComPtr来自wrl/client.h来智能管理COM接口指针的生命周期避免内存泄漏。这是现代C处理COM对象的推荐方式。如果你使用的VS版本支持强烈建议采用。4. 分步实现使用DXGI Desktop Duplication API捕获屏幕DXGI桌面复制API的工作流程可以概括为初始化DXGI资源 - 获取桌面复制接口 - 进入捕获循环 - 获取一帧数据 - 处理并保存数据。下面我们分步拆解。4.1 初始化Direct3D设备与DXGI资源一切始于创建Direct3D设备。桌面复制功能需要D3D11设备。// 使用 ComPtr 智能指针管理COM接口 #include wrl/client.h using namespace Microsoft::WRL; HRESULT InitializeD3D11(ID3D11Device** ppDevice, ID3D11DeviceContext** ppContext) { HRESULT hr S_OK; D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_0 }; UINT creationFlags D3D11_CREATE_DEVICE_BGRA_SUPPORT; // 重要支持GDI兼容格式 // 通常不需要调试层除非在开发调试 #if defined(_DEBUG) creationFlags | D3D11_CREATE_DEVICE_DEBUG; #endif hr D3D11CreateDevice( nullptr, // 默认适配器显卡 D3D_DRIVER_TYPE_HARDWARE, // 使用硬件驱动 nullptr, // 非软件驱动 creationFlags, // 创建标志 featureLevels, // 特性等级 ARRAYSIZE(featureLevels), // 特性等级数量 D3D11_SDK_VERSION, // SDK版本 ppDevice, // 输出的设备指针 nullptr, // 不关心具体的特性等级 ppContext // 输出的设备上下文指针 ); if (FAILED(hr)) { std::cerr Failed to create D3D11 device. HRESULT: 0x std::hex hr std::endl; } return hr; }创建好设备后我们需要获取DXGI适配器、输出显示器等一系列接口最终得到IDXGIOutputDuplication接口这是我们进行桌面复制的核心。HRESULT InitializeDuplication(ID3D11Device* pDevice, IDXGIOutputDuplication** ppDuplication) { HRESULT hr S_OK; ComPtrIDXGIDevice dxgiDevice; ComPtrIDXGIAdapter dxgiAdapter; ComPtrIDXGIOutput dxgiOutput; ComPtrIDXGIOutput1 dxgiOutput1; // 1. 从D3D设备获取DXGI设备 hr pDevice-QueryInterface(__uuidof(IDXGIDevice), dxgiDevice); if (FAILED(hr)) return hr; // 2. 获取DXGI适配器代表显卡 hr dxgiDevice-GetAdapter(dxgiAdapter); if (FAILED(hr)) return hr; // 3. 获取第一个输出代表第一个显示器。多显示器需要遍历 hr dxgiAdapter-EnumOutputs(0, dxgiOutput); if (FAILED(hr)) return hr; // 4. 查询IDXGIOutput1接口它包含了桌面复制功能 hr dxgiOutput.As(dxgiOutput1); if (FAILED(hr)) return hr; // 5. 创建桌面复制接口 hr dxgiOutput1-DuplicateOutput(pDevice, ppDuplication); if (hr DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) { std::cerr Desktop duplication is not available (e.g., protected content fullscreen). std::endl; } else if (hr E_ACCESSDENIED) { std::cerr Access denied. Ensure no other application is duplicating the desktop. std::endl; } return hr; }4.2 捕获一帧桌面图像获得IDXGIOutputDuplication接口后我们就可以在一个循环中捕获帧了。这个过程涉及获取一个“帧资源”它可能包含桌面图像数据。HRESULT CaptureFrame(IDXGIOutputDuplication* pDuplication, ID3D11Device* pDevice, ID3D11Texture2D** ppCapturedTexture) { HRESULT hr S_OK; ComPtrIDXGIResource dxgiResource; DXGI_OUTDUPL_FRAME_INFO frameInfo; // 1. 获取一帧数据。DXGI_ERROR_WAIT_TIMEOUT表示桌面没有变化可以继续等待。 hr pDuplication-AcquireNextFrame(500, frameInfo, dxgiResource); // 超时500毫秒 if (hr DXGI_ERROR_WAIT_TIMEOUT) { // 桌面无变化不是错误是正常情况 return hr; } if (FAILED(hr)) { std::cerr Failed to acquire next frame. HRESULT: 0x std::hex hr std::endl; return hr; } // 2. 从资源中获取纹理接口 hr dxgiResource.As(ppCapturedTexture); if (FAILED(hr)) { pDuplication-ReleaseFrame(); // 如果失败必须释放帧 return hr; } // 3. 注意这里我们暂时不释放帧(ReleaseFrame)因为调用者还需要使用纹理。 // 调用者在处理完纹理后必须调用 pDuplication-ReleaseFrame(); return hr; }4.3 处理与保存捕获的纹理数据捕获到的ID3D11Texture2D通常存在于GPU显存中且格式可能是DXGI_FORMAT_B8G8R8A8_UNORM。为了保存为图片文件如PNG、BMP我们需要将其拷贝到CPU可访问的系统内存中。HRESULT SaveTextureToFile(ID3D11Device* pDevice, ID3D11DeviceContext* pContext, ID3D11Texture2D* pTexture, const wchar_t* filename) { HRESULT hr S_OK; // 1. 查询纹理描述了解其宽度、高度、格式等 D3D11_TEXTURE2D_DESC desc; pTexture-GetDesc(desc); // 2. 创建一个CPU可读的“暂存”纹理 desc.Usage D3D11_USAGE_STAGING; desc.BindFlags 0; // 不能绑定为渲染目标或着色器资源 desc.CPUAccessFlags D3D11_CPU_ACCESS_READ; desc.MiscFlags 0; ComPtrID3D11Texture2D pStagingTexture; hr pDevice-CreateTexture2D(desc, nullptr, pStagingTexture); if (FAILED(hr)) return hr; // 3. 将GPU纹理数据拷贝到CPU暂存纹理 pContext-CopyResource(pStagingTexture.Get(), pTexture); // 4. 映射暂存纹理获取CPU端的数据指针 D3D11_MAPPED_SUBRESOURCE mapped; hr pContext-Map(pStagingTexture.Get(), 0, D3D11_MAP_READ, 0, mapped); if (FAILED(hr)) return hr; // 5. 使用Windows Imaging Component (WIC) 保存为图片文件 // 这里以保存为PNG为例 ComPtrIWICImagingFactory wicFactory; hr CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(wicFactory)); if (SUCCEEDED(hr)) { ComPtrIWICBitmapEncoder encoder; ComPtrIWICBitmapFrameEncode frame; ComPtrIWICStream stream; // 创建编码器和流 hr wicFactory-CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder); if (SUCCEEDED(hr)) { hr wicFactory-CreateStream(stream); } if (SUCCEEDED(hr)) { hr stream-InitializeFromFilename(filename, GENERIC_WRITE); } if (SUCCEEDED(hr)) { hr encoder-Initialize(stream.Get(), WICBitmapEncoderNoCache); } if (SUCCEEDED(hr)) { hr encoder-CreateNewFrame(frame, nullptr); } if (SUCCEEDED(hr)) { hr frame-Initialize(nullptr); } // 设置帧的尺寸和像素格式 if (SUCCEEDED(hr)) { hr frame-SetSize(desc.Width, desc.Height); } WICPixelFormatGUID formatGUID GUID_WICPixelFormat32bppBGRA; // 匹配DXGI_FORMAT_B8G8R8A8_UNORM if (SUCCEEDED(hr)) { hr frame-SetPixelFormat(formatGUID); } // 写入像素数据 if (SUCCEEDED(hr)) { hr frame-WritePixels(desc.Height, mapped.RowPitch, desc.Height * mapped.RowPitch, static_castBYTE*(mapped.pData)); } if (SUCCEEDED(hr)) { hr frame-Commit(); } if (SUCCEEDED(hr)) { hr encoder-Commit(); } } // 6. 取消映射并释放资源 pContext-Unmap(pStagingTexture.Get(), 0); return hr; }4.4 整合完整的单次截图流程将以上步骤整合一个最小化的、捕获主显示器全屏并保存为PNG的流程如下void CaptureFullScreenAndSave(const wchar_t* outputPath) { HRESULT hr S_OK; ComPtrID3D11Device pDevice; ComPtrID3D11DeviceContext pContext; ComPtrIDXGIOutputDuplication pDuplication; ComPtrID3D11Texture2D pCapturedTexture; // 1. 初始化 hr InitializeD3D11(pDevice, pContext); if (FAILED(hr)) return; hr InitializeDuplication(pDevice.Get(), pDuplication); if (FAILED(hr)) return; // 2. 捕获一帧 hr CaptureFrame(pDuplication.Get(), pDevice.Get(), pCapturedTexture); if (hr DXGI_ERROR_WAIT_TIMEOUT) { std::cout Desktop hasnt changed since last capture. std::endl; // 可以在这里选择等待或重试 return; } if (FAILED(hr)) return; // 3. 保存到文件 hr SaveTextureToFile(pDevice.Get(), pContext.Get(), pCapturedTexture.Get(), outputPath); if (SUCCEEDED(hr)) { std::wcout LScreenshot saved to: outputPath std::endl; } else { std::cerr Failed to save screenshot. std::endl; } // 4. 必须释放当前帧否则无法捕获下一帧 pDuplication-ReleaseFrame(); } int main() { // 初始化COM库WIC需要 CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); CaptureFullScreenAndSave(LC:\\Screenshots\\capture.png); CoUninitialize(); return 0; }5. 进阶捕获特定窗口与性能优化上面的例子捕获了整个桌面主显示器。如何捕获特定窗口呢DXGI桌面复制API本身是针对显示输出的不是窗口。因此捕获特定窗口需要额外的步骤获取目标窗口句柄使用FindWindow或EnumWindows等API。获取窗口的边界矩形使用GetWindowRect。但注意这个矩形是窗口在屏幕上的坐标包含了非客户区边框、标题栏。对于DWM合成的内容你可能需要DwmGetWindowAttribute配合DWMWA_EXTENDED_FRAME_BOUNDS来获取包含阴影等特效的精确矩形。在全屏捕获的纹理中进行裁剪我们捕获的是整个桌面纹理。根据窗口矩形从这张大纹理中拷贝出对应的像素区域即可。这需要在SaveTextureToFile的映射数据步骤后手动计算偏移量和行宽只保存感兴趣区域ROI的数据。性能是另一个关键点。持续截图如录屏时频繁创建/销毁资源、保存文件会带来巨大开销。优化策略包括资源复用在初始化阶段创建好ID3D11Texture2D暂存纹理和WIC组件在循环中重复使用避免反复创建。内存映射优化对于持续捕获可以考虑将纹理数据拷贝到预先分配好的环形缓冲区中由另一个线程专门负责编码和保存实现生产-消费模型。帧差异处理DXGI_OUTDUPL_FRAME_INFO结构体中的LastPresentTime和AccumulatedFrames等信息结合AcquireNextFrame返回的DXGI_ERROR_WAIT_TIMEOUT可以判断桌面是否更新。无更新时跳过处理节省CPU/GPU资源。降低分辨率/帧率非实时性要求极高的场景可以每N帧捕获一次或先捕获到纹理后用D3D设备将其缩放到更小的纹理再保存。6. 常见问题与排查技巧实录在实际开发中你会遇到各种意想不到的问题。下面是我踩过的一些坑和解决方法问题1DuplicateOutput返回DXGI_ERROR_NOT_CURRENTLY_AVAILABLE(0x887A0027)原因这是最常见的问题之一。通常发生在以下情况当前桌面正在播放受数字版权保护DRM的内容比如某些流媒体网站的全屏视频。某个应用程序通常是游戏以“独占全屏”模式运行DXGI输出被该应用独占。已经有一个进程可能是你的程序另一个实例或其他录屏软件正在复制该输出。排查与解决关闭可能正在录屏或截图的软件如游戏加加、OBS、Xbox Game Bar。让目标游戏或应用切换到“窗口化”或“无边框窗口化”模式。对于DRM内容目前没有合法的方法绕过这是系统级别的保护。检查代码确保在每次捕获循环后正确调用了ReleaseFrame()否则资源会被一直占用。问题2捕获到的图片是黑屏或绿屏原因黑屏可能捕获到了错误的显示器输出或者纹理格式不匹配。确保你EnumOutputs的索引对应的是你想要捕获的显示器。绿屏或花屏纹理数据拷贝或映射出错。最常见的原因是RowPitch内存中一行像素的字节数使用错误。RowPitch由于内存对齐要求可能大于宽度 * 每像素字节数。在WritePixels或手动拷贝内存时必须使用mapped.RowPitch作为源数据的行跨度。排查检查D3D11_TEXTURE2D_DESC中的Width,Height,Format。Format应为DXGI_FORMAT_B8G8R8A8_UNORM。在保存文件前可以尝试将映射到的内存数据的前几个像素值打印出来看是否是预期的BGRA值而不是全0黑或异常值。确保在调用WritePixels时传入的cbStride参数是mapped.RowPitch而不是width * 4。问题3程序在第二次调用AcquireNextFrame时卡住或报错原因没有正确处理ReleaseFrame()。AcquireNextFrame和ReleaseFrame必须成对调用。在调用ReleaseFrame()之前不能再次调用AcquireNextFrame()获取同一复制接口的新帧。解决确保你的代码逻辑无论在成功还是失败捕获后保存文件失败的分支只要调用了AcquireNextFrame且没有返回DXGI_ERROR_WAIT_TIMEOUT最终都必须执行一次ReleaseFrame()。使用RAII资源获取即初始化思想封装这个接口是个好习惯。问题4如何捕获多显示器系统中的特定显示器方法IDXGIAdapter::EnumOutputs的参数UINT Output就是显示器的索引从0开始。你需要遍历所有输出并通过IDXGIOutput::GetDesc获取DXGI_OUTPUT_DESC其中的DesktopCoordinates字段可以告诉你该显示器在虚拟桌面坐标系中的位置。你可以根据这个位置信息来选择你想要捕获的显示器然后对其创建DuplicateOutput。问题5在Windows服务或没有交互式桌面的会话中运行失败原因DXGI桌面复制API需要运行在交互式桌面会话中即用户登录后的会话并且需要相应的图形设备上下文。在服务或Session 0中通常无法直接使用。解决这是该技术的本质限制。如果必须在无头环境中截图可能需要考虑其他方案如通过RDP虚拟通道或者将截图功能部署在用户会话的代理进程中。实操心得调试D3D/DXGI程序时启用D3D11调试层非常有用。在D3D11CreateDevice时加上D3D11_CREATE_DEVICE_DEBUG标志运行程序后输出窗口Output会打印详细的错误和警告信息比如资源泄漏、API调用错误等这对定位问题至关重要。记得只在调试版本开启因为会影响性能。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门