Windows DWM与DXGI桌面复制API实现精准屏幕截图
2026/8/24 7:55:18 网站建设 项目流程

1. 项目缘起:为什么不用BitBlt?

在Windows平台上做屏幕截图,很多人的第一反应是使用经典的GDI函数BitBlt。这确实是一个简单直接的方法,从桌面设备上下文(DC)中把像素数据“位块传输”到内存位图中。我最初的项目也是这么开始的,代码简洁,几行就能跑起来。但很快,我就遇到了一个几乎所有桌面应用开发者都会踩的坑:当目标窗口被其他窗口(比如一个置顶的视频播放器、一个半透明的聊天窗口)部分遮挡,或者窗口本身使用了DirectX/Direct2D等硬件加速渲染时,BitBlt捕获到的画面是“错误”的。

这个“错误”不是说程序崩溃,而是它捕获到的,是Windows在合成桌面之前,各个窗口自己提交的、未经最终混合的画面。对于被遮挡的区域,BitBlt抓到的是窗口原本的内容,而不是你最终在屏幕上看到的、被其他窗口盖住后的样子。更麻烦的是,对于那些使用WPF、UWP、甚至一些现代Qt应用(开启原生窗口模糊等效果),BitBlt抓到的可能是一片黑或者扭曲的图像。

这时,就需要请出Windows Vista之后引入的桌面窗口管理器——DWM(Desktop Window Manager)。DWM的核心工作就是负责所有窗口的合成,它掌握着最终呈现在屏幕上的每一帧完美画面。我们的目标,就是从DWM这里,拿到经过它合成后的、最“真实”的桌面图像。这就是本次项目的核心:绕过传统的GDI路径,直连DWM,实现一个健壮、准确的屏幕截图工具。

2. DWM截图的核心原理与API选择

要理解如何从DWM获取截图,首先得明白DWM的工作流程。在DWM启用的情况下(现代Windows默认开启),应用程序不再直接向屏幕缓冲区绘制,而是先绘制到一个离屏的“表面”(Surface),通常是Direct3D的纹理。DWM则作为合成器,收集所有窗口的表面,按照Z序、透明度、特效等进行混合,最终输出到显示设备。

因此,从DWM获取截图,本质上是请求DWM提供它当前用于合成输出的那个后备缓冲区(Back Buffer)的一份拷贝。微软为此提供了专门的API:DwmGetDxSharedSurface和更现代的DwmGetGraphicsStreamClient等。但对于我们C++开发者来说,最实用、文档最全、兼容性也足够好的,是DwmRegisterThumbnailDwmUpdateThumbnailProperties这一对组合,配合DXGI(DirectX Graphics Infrastructure)的桌面复制API。

为什么不直接用DwmGetDxSharedSurface这个API更底层,理论上更直接,但它对调用线程的COM初始化状态、DXGI设备创建有更严格的要求,且在不同Windows版本(如Win7, Win8, Win10)上行为可能有细微差异,文档示例也较少。对于截图这个单一任务,用桌面复制API是更“标准”和“安全”的路径。

我们的技术路线图如下:

  1. 初始化COM库:因为我们要使用DXGI,它构建在COM之上。
  2. 创建DXGI设备与输出:连接到默认的图形适配器和显示器输出。
  3. 创建桌面复制接口(IDXGIOutputDuplication):这是关键对象,它代表了我们对桌面输出图像的“复制”权限。
  4. 获取桌面帧:从复制接口中获取最新的桌面图像数据(IDXGIResource)。
  5. 转换与保存:将获取到的DXGI表面(通常是ID3D11Texture2D)中的数据,拷贝到CPU可访问的内存中,并转换为常见的位图格式(如BMP、PNG)保存。

这条路线的优势在于,它直接对接图形驱动,获取的是DWM合成后的最终帧,完美解决了窗口遮挡和硬件加速渲染的问题,并且性能极高,适合需要高帧率截图的场景(如录屏)。

3. 环境准备与项目配置

在开始敲代码之前,正确的开发环境配置是成功的一半。这里我以Visual Studio 2022为例,但核心思想适用于任何现代C++开发环境。

3.1 创建项目与设置属性

首先,创建一个新的“控制台应用”或“桌面应用”项目。接下来是关键的属性页配置:

  1. 配置类型:确保是应用程序(.exe)
  2. C++语言标准:建议设置为C++17或更高,以获得更好的标准库支持。
  3. 字符集:使用Unicode字符集。这是现代Windows编程的标配。

3.2 链接库与头文件

DWM和DXGI相关的函数和接口,定义在特定的SDK头文件和库中。我们需要在项目属性中正确设置。

  • 附加包含目录:通常VS会自动配置Windows SDK的路径。如果没有,确保$(WindowsSDK_IncludePath)在包含目录中。
  • 附加库目录:同样,确保$(WindowsSDK_LibraryPath_x64)$(WindowsSDK_LibraryPath_x86)(根据你的目标平台)在库目录中。
  • 附加依赖项(链接器输入):这是核心。我们需要链接以下库:
    • dwmapi.lib:提供DwmRegisterThumbnail等DWM函数。
    • dxgi.lib:提供DXGI基础接口,如IDXGIFactory,IDXGIOutput
    • d3d11.lib:我们将使用Direct3D 11设备来操作纹理。这是与DXGI桌面复制API协作最常用的版本。

在你的stdafx.h或主源文件头部,需要包含这些头文件:

#include <windows.h> #include <dwmapi.h> #include <dxgi1_2.h> // 注意是1_2版本,它包含了IDXGIOutputDuplication #include <d3d11.h> #include <wincodec.h> // 用于使用Windows Imaging Component (WIC)保存图片 #include <vector> #include <string> #include <iostream> #pragma comment(lib, "dwmapi.lib") #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "d3d11.lib") #pragma comment(lib, "Windowscodecs.lib") // WIC库

注意dxgi1_2.h是关键。早期的dxgi.h不包含IDXGIOutputDuplication接口。确保你的Windows SDK版本足够新(例如随VS2022安装的SDK)。

3.3 处理COM智能指针

我们将大量使用COM接口指针,如IDXGIOutputDuplication*ID3D11Device*等。手动管理它们的AddRefRelease极易出错。强烈建议使用Microsoft::WRL::ComPtr(需要包含#include <wrl/client.h>)。它能像std::unique_ptr一样自动管理COM对象生命周期,让代码安全又简洁。

4. 分步实现:从初始化到保存图片

现在,让我们进入核心的代码实现环节。我将把整个过程分解为几个清晰的函数。

4.1 初始化COM与创建D3D设备

一切始于COM和Direct3D设备。

bool InitializeDirectX(ID3D11Device** ppDevice, ID3D11DeviceContext** ppContext) { HRESULT hr = S_OK; // 1. 创建D3D11设备 D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0 }; UINT createDeviceFlags = 0; #ifdef _DEBUG createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; // 调试时启用调试层 #endif hr = D3D11CreateDevice( nullptr, // 默认适配器 D3D_DRIVER_TYPE_HARDWARE, // 使用硬件驱动 nullptr, // 没有软件驱动 createDeviceFlags, // 标志位 featureLevels, // 特性等级 1, // 特性等级数量 D3D11_SDK_VERSION, // SDK版本 ppDevice, // 返回的设备指针 nullptr, // 不关心特性等级 ppContext // 返回的设备上下文指针 ); if (FAILED(hr)) { std::cerr << "Failed to create D3D11 device. HRESULT: " << std::hex << hr << std::endl; return false; } return true; }

这个函数创建了Direct3D 11设备和上下文。设备上下文是执行渲染命令的“命令列表”,在拷贝纹理数据时会用到。

4.2 获取DXGI输出与创建桌面复制器

有了D3D设备,我们就可以获取代表显示器的DXGI输出,并创建关键的桌面复制接口。

bool CreateDesktopDuplication(ID3D11Device* pDevice, IDXGIOutputDuplication** ppDuplication) { HRESULT hr = S_OK; Microsoft::WRL::ComPtr<IDXGIFactory1> dxgiFactory; Microsoft::WRL::ComPtr<IDXGIAdapter> adapter; Microsoft::WRL::ComPtr<IDXGIOutput> dxgiOutput; Microsoft::WRL::ComPtr<IDXGIOutput1> dxgiOutput1; // 需要QueryInterface得到 // 1. 获取DXGI工厂 hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), &dxgiFactory); if (FAILED(hr)) return false; // 2. 获取默认适配器(显卡) hr = dxgiFactory->EnumAdapters(0, &adapter); if (FAILED(hr)) return false; // 3. 获取第一个输出(主显示器) hr = adapter->EnumOutputs(0, &dxgiOutput); if (FAILED(hr)) return false; // 4. 查询IDXGIOutput1接口,它包含DuplicateOutput方法 hr = dxgiOutput.As(&dxgiOutput1); if (FAILED(hr)) return false; // 5. 创建桌面复制接口 hr = dxgiOutput1->DuplicateOutput(pDevice, ppDuplication); if (hr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) { std::cerr << "DuplicateOutput failed: DXGI_ERROR_NOT_CURRENTLY_AVAILABLE. " << "This usually means the desktop is locked or another application is already duplicating." << std::endl; return false; } else if (FAILED(hr)) { std::cerr << "DuplicateOutput failed. HRESULT: " << std::hex << hr << std::endl; return false; } return true; }

DuplicateOutput是核心调用。它告诉系统:“我要复制这个显示输出的内容”。如果失败并返回DXGI_ERROR_NOT_CURRENTLY_AVAILABLE,通常是因为桌面被锁定了,或者已经有另一个进程(比如某些录屏软件)在复制了。一个健壮的程序应该处理这种情况,例如等待或提示用户。

4.3 捕获一帧桌面图像

复制器创建好后,我们就可以尝试从它那里“拉取”一帧图像。

bool AcquireDesktopFrame(IDXGIOutputDuplication* pDuplication, ID3D11Device* pDevice, ID3D11Texture2D** ppAcquiredTexture, DXGI_OUTDUPL_FRAME_INFO* pFrameInfo) { HRESULT hr = S_OK; Microsoft::WRL::ComPtr<IDXGIResource> dxgiResource; // 1. 获取一帧 hr = pDuplication->AcquireNextFrame(500, pFrameInfo, &dxgiResource); // 超时500毫秒 if (hr == DXGI_ERROR_WAIT_TIMEOUT) { // 在超时时间内没有新的帧 return false; } else if (FAILED(hr)) { // 其他错误,比如复制器被释放了 std::cerr << "AcquireNextFrame failed. HRESULT: " << std::hex << hr << std::endl; return false; } // 2. 从IDXGIResource查询到ID3D11Texture2D hr = dxgiResource.As(ppAcquiredTexture); if (FAILED(hr)) { pDuplication->ReleaseFrame(); // 记得释放帧! return false; } return true; }

AcquireNextFrame会等待直到有新的桌面帧可用,或者超时。成功获取后,我们得到一个IDXGIResource,需要将其转换为ID3D11Texture2D以便后续操作。非常重要的一点:如果AcquireNextFrame成功,你必须最终调用pDuplication->ReleaseFrame()来释放这一帧,否则后续的AcquireNextFrame会失败。这个释放操作通常放在处理完纹理数据之后。

4.4 将GPU纹理拷贝到CPU可读的内存

我们获取到的ID3D11Texture2D通常位于GPU显存中,CPU无法直接访问。为了保存为图片文件,需要创建一个位于系统内存(CPU可访问)的“暂存纹理”,并将GPU纹理的内容拷贝过去。

bool CopyTextureToSystemMemory(ID3D11Device* pDevice, ID3D11DeviceContext* pContext, ID3D11Texture2D* pSourceTexture, ID3D11Texture2D** ppStagingTexture, D3D11_TEXTURE2D_DESC* pTexDesc) { HRESULT hr = S_OK; // 1. 获取源纹理的描述信息(宽度、高度、格式等) pSourceTexture->GetDesc(pTexDesc); // 2. 修改描述,创建CPU可读的暂存纹理 D3D11_TEXTURE2D_DESC stagingDesc = *pTexDesc; stagingDesc.BindFlags = 0; // 不能绑定为渲染目标或着色器资源 stagingDesc.MiscFlags = 0; stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; // 关键:CPU可读 stagingDesc.Usage = D3D11_USAGE_STAGING; // 关键:用途是暂存 // 3. 创建暂存纹理 hr = pDevice->CreateTexture2D(&stagingDesc, nullptr, ppStagingTexture); if (FAILED(hr)) return false; // 4. 将GPU纹理数据拷贝到暂存纹理 pContext->CopyResource(*ppStagingTexture, pSourceTexture); return true; }

创建暂存纹理时,D3D11_USAGE_STAGINGD3D11_CPU_ACCESS_READ这两个标志是必须的。拷贝操作CopyResource是异步的,但在这个简单场景下,我们假设它立即完成。对于更复杂的应用,可能需要刷新命令队列或使用ID3D11DeviceContext::Map的同步机制。

4.5 映射内存并保存为图片文件

现在,暂存纹理在系统内存中,我们可以“映射”它,获取一个指向像素数据的指针,然后保存为文件。这里使用Windows Imaging Component (WIC)来保存为PNG格式,它比手动写BMP文件头更方便,也支持压缩。

bool SaveTextureToPNG(ID3D11DeviceContext* pContext, ID3D11Texture2D* pStagingTexture, const D3D11_TEXTURE2D_DESC& texDesc, const std::wstring& filename) { HRESULT hr = S_OK; D3D11_MAPPED_SUBRESOURCE mapped = {0}; // 1. 映射暂存纹理,获取CPU可读的指针 hr = pContext->Map(pStagingTexture, 0, D3D11_MAP_READ, 0, &mapped); if (FAILED(hr)) { std::cerr << "Failed to map staging texture." << std::endl; return false; } // 2. 使用WIC保存图片 Microsoft::WRL::ComPtr<IWICImagingFactory> wicFactory; Microsoft::WRL::ComPtr<IWICBitmapEncoder> encoder; Microsoft::WRL::ComPtr<IWICBitmapFrameEncode> frame; Microsoft::WRL::ComPtr<IWICStream> stream; // 创建WIC工厂 hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wicFactory)); if (FAILED(hr)) goto Cleanup; // 创建文件流 hr = wicFactory->CreateStream(&stream); if (FAILED(hr)) goto Cleanup; hr = stream->InitializeFromFilename(filename.c_str(), GENERIC_WRITE); if (FAILED(hr)) goto Cleanup; // 创建PNG编码器 hr = wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder); if (FAILED(hr)) goto Cleanup; hr = encoder->Initialize(stream, WICBitmapEncoderNoCache); if (FAILED(hr)) goto Cleanup; // 创建新帧并设置参数 hr = encoder->CreateNewFrame(&frame, nullptr); if (FAILED(hr)) goto Cleanup; hr = frame->Initialize(nullptr); if (FAILED(hr)) goto Cleanup; // 设置帧的宽度、高度、像素格式。DXGI_FORMAT_B8G8R8A8_UNORM 对应 GUID_WICPixelFormat32bppBGRA hr = frame->SetSize(texDesc.Width, texDesc.Height); if (FAILED(hr)) goto Cleanup; GUID pixelFormat = GUID_WICPixelFormat32bppBGRA; // 注意格式对应关系 hr = frame->SetPixelFormat(&pixelFormat); if (FAILED(hr)) goto Cleanup; // 3. 写入像素数据 // 计算步幅(每行字节数)。注意:mapped.RowPitch是纹理的行间距,可能大于宽度*4(由于内存对齐) UINT stride = texDesc.Width * 4; // 32bpp = 4 bytes per pixel // 如果纹理的行间距与计算的一致,可以直接写入 if (mapped.RowPitch == stride) { hr = frame->WritePixels(texDesc.Height, mapped.RowPitch, texDesc.Height * stride, static_cast<BYTE*>(mapped.pData)); } else { // 行间距不一致,需要逐行拷贝对齐 std::vector<BYTE> rowData(stride); BYTE* pSrc = static_cast<BYTE*>(mapped.pData); for (UINT y = 0; y < texDesc.Height; ++y) { memcpy(rowData.data(), pSrc, stride); hr = frame->WritePixels(1, stride, stride, rowData.data()); if (FAILED(hr)) break; pSrc += mapped.RowPitch; // 移动到下一行(按纹理行间距) } } if (FAILED(hr)) goto Cleanup; hr = frame->Commit(); if (FAILED(hr)) goto Cleanup; hr = encoder->Commit(); if (FAILED(hr)) goto Cleanup; Cleanup: // 4. 无论如何都要取消映射 pContext->Unmap(pStagingTexture, 0); if (FAILED(hr)) { std::cerr << "Failed to save PNG. HRESULT: " << std::hex << hr << std::endl; DeleteFile(filename.c_str()); // 尝试删除可能不完整的文件 return false; } std::wcout << L"Screenshot saved to: " << filename << std::endl; return SUCCEEDED(hr); }

这段代码有几个关键点:

  1. MapUnmap必须成对调用Unmap必须在释放任何资源前调用。
  2. 像素格式:桌面复制通常返回DXGI_FORMAT_B8G8R8A8_UNORM格式(BGRA顺序,每个通道8位)。WIC对应的格式是GUID_WICPixelFormat32bppBGRA
  3. 行间距(RowPitch):纹理内存是对齐的,mapped.RowPitch可能大于宽度 * 4。直接写入整个缓冲区会导致WIC读取到多余的填充数据,图片会错位。因此需要判断并可能进行逐行拷贝。这是新手最容易忽略的坑,会导致保存的图片出现斜向扭曲。

4.6 主函数流程串联

最后,我们将上述所有步骤串联在main函数中。

int main() { // 初始化COM(单线程公寓模型,STA) HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { std::cerr << "Failed to initialize COM." << std::endl; return -1; } ID3D11Device* pDevice = nullptr; ID3D11DeviceContext* pContext = nullptr; IDXGIOutputDuplication* pDeskDupl = nullptr; ID3D11Texture2D* pAcquiredTexture = nullptr; ID3D11Texture2D* pStagingTexture = nullptr; do { // 1. 初始化D3D if (!InitializeDirectX(&pDevice, &pContext)) break; // 2. 创建桌面复制器 if (!CreateDesktopDuplication(pDevice, &pDeskDupl)) break; // 3. 捕获一帧 DXGI_OUTDUPL_FRAME_INFO frameInfo = {0}; if (!AcquireDesktopFrame(pDeskDupl, pDevice, &pAcquiredTexture, &frameInfo)) { std::cout << "No new frame available or timeout." << std::endl; break; } // 4. 拷贝到系统内存 D3D11_TEXTURE2D_DESC texDesc = {0}; if (!CopyTextureToSystemMemory(pDevice, pContext, pAcquiredTexture, &pStagingTexture, &texDesc)) break; // 5. 保存为PNG std::wstring filePath = L"screenshot_dwm.png"; if (!SaveTextureToPNG(pContext, pStagingTexture, texDesc, filePath)) break; std::cout << "Screenshot captured successfully via DWM/DXGI." << std::endl; } while (false); // 只执行一次的“循环”,方便用break统一清理 // 6. 至关重要的清理工作 if (pDeskDupl && pAcquiredTexture) { // 必须在释放复制器接口前,释放已获取的帧 pDeskDupl->ReleaseFrame(); } if (pStagingTexture) pStagingTexture->Release(); if (pAcquiredTexture) pAcquiredTexture->Release(); if (pDeskDupl) pDeskDupl->Release(); if (pContext) pContext->Release(); if (pDevice) pDevice->Release(); CoUninitialize(); return 0; }

这个main函数清晰地展示了整个工作流。注意do...while(false)的用法,它允许我们在任何一步失败时,用break跳到统一的资源清理代码块,避免重复的清理逻辑。

5. 实战中的坑与进阶优化

把代码跑通只是第一步。在实际项目中应用,你会遇到更多问题。下面是我踩过的一些坑和对应的解决方案。

5.1 多显示器与指定屏幕截图

上面的例子只捕获了第一个显示器(EnumOutputs(0))。现代工作站多显示器很常见。如何截图所有显示器或指定某个显示器?

解决方案:遍历适配器的所有输出。

std::vector<IDXGIOutput*> GetAllOutputs(IDXGIAdapter* pAdapter) { std::vector<IDXGIOutput*> outputs; IDXGIOutput* pOutput = nullptr; for (int i = 0; ; ++i) { HRESULT hr = pAdapter->EnumOutputs(i, &pOutput); if (hr == DXGI_ERROR_NOT_FOUND) { // 没有更多输出了 break; } else if (SUCCEEDED(hr)) { outputs.push_back(pOutput); } } return outputs; }

你可以让用户选择显示器,或者为每个输出创建一个IDXGIOutputDuplication,分别截图,然后拼接或单独保存。

5.2 性能考量与帧率控制

AcquireNextFrame会等待新帧。如果你在一个循环中不断调用它来录屏,它会尽可能快地获取帧,这可能占用大量CPU/GPU。对于非实时性要求高的截图工具,你可能需要主动控制频率。

解决方案:在循环中增加Sleep或使用高精度计时器。

#include <chrono> #include <thread> //... auto frameInterval = std::chrono::milliseconds(33); // 约30FPS auto nextFrameTime = std::chrono::steady_clock::now(); while (!quit) { // ... 获取并处理帧 ... std::this_thread::sleep_until(nextFrameTime); nextFrameTime += frameInterval; }

更高级的做法是使用AcquireNextFrame的超时参数,并将其作为主要的等待机制。

5.3 错误处理与资源管理

我们的示例代码错误处理比较基础。在生产环境中,你需要更健壮的处理:

  • 设备丢失(DXGI_ERROR_DEVICE_REMOVEDDXGI_ERROR_DEVICE_RESET:当显卡被拔掉、驱动更新或发生严重图形错误时会发生。处理方式是释放所有D3D和DXGI资源,然后从头重新初始化
  • 访问被拒绝:如果桌面安全策略禁止(如远程桌面连接时),DuplicateOutput可能失败。需要有友好的用户提示。
  • 智能指针的全面使用:示例中为了清晰展示了裸指针,但强烈建议全部改用Microsoft::WRL::ComPtr,这样即使发生异常,资源也能自动释放,避免泄漏。

5.4 从全屏独占应用(如游戏)截图

当某个应用(如游戏)以全屏独占模式运行时,DWM桌面合成会被禁用。此时IDXGIOutputDuplication可能无法工作或抓不到正确内容。对于这种特殊情况,需要另辟蹊径:

  1. 使用游戏提供的API:如Steam Overlay、NVIDIA ShadowPlay/Ansel、AMD ReLive等。
  2. 注入DLL与Hook:高级技术,通过注入到目标进程,Hook其Present调用,直接从其交换链获取图像。这涉及复杂的逆向工程和稳定性问题,一般用于专业工具。
  3. 回退到BitBlt:对于全屏独占的非DXGI应用,有时BitBlt反而能工作,但这不可靠。

一个折中的方案是:先尝试DWM/DXGI路径,如果失败(例如获取到的纹理尺寸为0或全黑),再优雅地回退到BitBlt或给出明确错误。

6. 封装与实战:一个简单的命令行截图工具

将上面的代码模块化,封装成一个类,比如叫DwmScreenCapturer,会大大提高复用性。这个类可以负责生命周期管理(RAII),提供Initialize(),CaptureFrame(),SaveToFile()等方法。

这里分享一个我常用的技巧:如何生成不重复的文件名。一个简单的命令行工具,每次运行都覆盖screenshot.png并不友好。

std::wstring GenerateTimestampFilename(const std::wstring& prefix, const std::wstring& extension) { auto now = std::chrono::system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now); std::tm tm_buf; localtime_s(&tm_buf, &in_time_t); // 使用安全的localtime_s std::wstringstream wss; wss << prefix << std::put_time(&tm_buf, L"%Y%m%d_%H%M%S") << extension; return wss.str(); } // 使用 std::wstring fileName = GenerateTimestampFilename(L"screenshot_", L".png");

这样会生成像screenshot_20231027_143022.png这样的文件名。

最终,你可以编译这个程序,得到一个简单的命令行工具。运行它,就能在当前目录下生成一张精准的、反映当前桌面真实状态的PNG截图。相比于网上很多基于GDI的截图代码,这个方案在面对复杂桌面环境时显得无比可靠。

整个实现过程涉及了Windows图形系统的核心组件:从应用层对DWM的认知,到通过DXGI与图形驱动交互,再到使用Direct3D操作纹理,最后利用WIC编码成文件。它不仅仅是一个截图功能,更是一个理解现代Windows桌面图形栈的绝佳实践。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询