截屏程序源代码进阶:从GDI到Desktop Duplication API

发布时间:2026/9/10 14:57:50
截屏程序源代码进阶:从GDI到Desktop Duplication API 简介这份C截屏程序源代码压缩包是一套面向初、中级开发者的完整工程实例用于演示如何通过图形设备接口捕获屏幕画面并保存为图像文件。压缩包共46个文件以9个头文件和7个源文件为主体覆盖对话框界面、自定义编辑控件、程序入口等模块同时包含位图、图标、光标等界面素材以及工程配置和说明文档整体大小为145KB目录简练便于学习者按模块阅读。目前已有283人学习。源码的核心逻辑涉及获取活动窗口句柄、取得屏幕设备上下文、建立内存设备上下文、创建兼容位图、执行像素块复制以及保存结果图像等步骤借助这个项目可以理解设备上下文与图形对象之间的关系熟悉在窗口程序中组织资源与代码的方式并能在此基础上扩展出区域截图、定时截图或屏幕录制功能。适合希望用完整小项目巩固Windows编程能力的读者。1. 拿到一份 C 截屏程序源代码.zip先别急着双击 exeC 截屏程序源代码.zip 在网络上的出现频率几乎和“怎么判断质数”一样高但它远比一段排序代码值得拆开看。zip 后缀说明作者交付的不只是单个 cpp而是包含工程文件、头文件、资源文件在内的源码集合截屏程序意味着它要同时处理窗口体系、显存纹理、位图编码和异常返回。对一个有五年经验的桌面开发这份源码应该能回答四个问题它走的是哪条采集路径性能边界在哪重新编译需要什么依赖扩展成定时工具要改哪里。以下内容不依赖某个特定压缩包里的原始代码而是依据这类工具最常见的工程实践把完整链路和你需要的判断标准讲清楚。2. 截屏程序源代码包的技术选型GDI、DirectX 还是 Desktop Duplication API2.1 为什么老源码包多用 GDI以及它的边界在哪在收到的 C 截屏程序源代码里老一批示例通常用 GDI 实现。核心流程是 GetDC(NULL) 拿到桌面设备上下文CreateCompatibleDC 建一个内存 DCBitBlt 把像素复制过去最后 GetDIBits 转成 DIB 保存。这段代码在 Windows 95 时代就在用资料多、依赖少只要链接 gdi32 和 user32不用初始化 COM也不用理解 D3D 设备所以非常适合作教学入口。它的问题要到真实场景才会暴露。第一个是全屏独占游戏和硬件覆盖层这类画面不经过普通窗口消息机制GDI 读到的桌面图像里没有它们的内容第二个是性能BitBlt 每次都是全屏同步复制4K 分辨率下 CPU 占用能到 10% 以上。第三个是 DPI 缩放在 150% 缩放的 Windows 上GetSystemMetrics 拿到的分辨率可能只是逻辑值截出来是半张桌面。HDC screen GetDC(nullptr); HDC memory CreateCompatibleDC(screen); int w GetSystemMetrics(SM_CXSCREEN); int h GetSystemMetrics(SM_CYSCREEN); HBITMAP bmp CreateCompatibleBitmap(screen, w, h); SelectObject(memory, bmp); BitBlt(memory, 0, 0, w, h, screen, 0, 0, SRCCOPY); // 保存 bmp 时需要先 GetDIBits ReleaseDC(nullptr, screen); DeleteDC(memory);这段代码的边界在于SM_CXSCREEN 在系统 DPI 改变时返回的是逻辑坐标如果程序没有声明 PerMonitorV2 DPI Aware截出来就是模糊或裁切的。要做兼容需要调用 SetProcessDpiAwarenessContext 之后再取真实像素宽高。这也解释了为什么很多老代码在新笔记本上表现异常不是原理解错了而是没有随系统版本演进做适配。2.2 Desktop Duplication API 凭什么成为现代 C 截屏的默认选择从 Windows 8 开始桌面窗口管理器把输出交给 DXGI 统一管理微软因此提供了 Desktop Duplication API也就是 IDXGIOutputDuplication。它复制的不是应用层窗口而是显示器收到的最终合成画面内容包括硬件游标、DirectX 渲染结果、硬件加速视频。这个层级的截图最接近用户真实所见也正好是远程协作和录制工具需要的原始素材。这个 API 和 GDI 最大的区别是增量帧。AcquireNextFrame 返回的 DXGI_OUTDUPL_FRAME_INFO 里带 DirtyRects 和 MoveRects工具可以只传输变化区域而不是每次全屏重拷。对比 GDI 实现代码量大约多出 200 行但换来的是两个关键能力对遮挡内容的正确捕获以及对高分辨率显示器的可靠支持。选型判断并不复杂如果只是做一个演示用的小工具GDI 足够如果要在别人电脑长期跑或者有录屏、远程查看需求直接上 Desktop Duplication API 更合适。现在多数项目使用 GDI 实现低频率任务比如闪烁检测、应用状态监控使用 DXGI 实现高频录屏两者并不冲突。2.3 一份完整源码包的典型模块划分一份值得阅读的 C 截屏程序源代码.zip 不会把所有逻辑堆在一个 main.cpp 里。无论是 Windows SDK 示例样式还是 CMake 管理的小工程它大概率会包含下面这些文件模块对应文件核心职责捕获器capture.h / capture.cpp创建 D3D 设备和输出复制管理帧获取编码器encoder.h / encoder.cpp把 BGRA 像素写成 BMP 或交给 PNG 库参数解析args.h / args.cpp解析延时、输出路径、显示器编号入口调度main.cpp初始化、循环调用、错误处理拿到 zip 后我建议先检查编码模块是否存在。有很多源码包里压根没有编码器只有捕获内存缓冲运行后什么都不输出这类源码通常只是技术验证不适合作为交付物。另一步是看头文件里用的是dxgi1_2.h还是d3d9.h前者说明走现代路径后者多半是旧项目。只看这两个文件就能判断源码质量的一半。3. 手写一套可编译的 C 截屏程序源代码附参数说明为了让编译、打包和排错章节有具体对象这一章我给出一个基于 Desktop Duplication API 的最小可编译实现。这个方案不需要窗口句柄不依赖 MFC纯 Win32 加 COM适合转成 DLL 或命令行工具。3.1 初始化 D3D11 设备和 DXGI 工厂采集的第一步是建立 D3D11 设备。这不只是为了显示图形而是让 GPU 提供“生成桌面纹理副本”的能力。如果你在虚拟机或远程桌面里运行D3D11CreateDevice 可能返回 E_FAIL必须先检查返回码并对无 GPU 场景降级到 GDI。#include windows.h #include d3d11.h #include dxgi1_2.h #include vector #include fstream #pragma comment(lib, d3d11.lib) #pragma comment(lib, dxgi.lib) HRESULT InitDuplication(ID3D11Device** device, ID3D11DeviceContext** context, IDXGIOutputDuplication** duplication, UINT monitorIndex) { D3D_FEATURE_LEVEL level; HRESULT hr D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION, device, level, context); if (FAILED(hr)) return hr; IDXGIDevice* dxgiDevice nullptr; hr (*device)-QueryInterface(__uuidof(IDXGIDevice), (void**)dxgiDevice); if (FAILED(hr)) return hr; IDXGIAdapter* adapter nullptr; hr dxgiDevice-GetAdapter(adapter); if (FAILED(hr)) return hr; IDXGIOutput* output nullptr; hr adapter-EnumOutputs(monitorIndex, output); if (FAILED(hr)) return hr; IDXGIOutput1* output1 nullptr; hr output-QueryInterface(__uuidof(IDXGIOutput1), (void**)output1); if (FAILED(hr)) return hr; hr output1-DuplicateOutput((IUnknown*)(*device), duplication); dxgiDevice-Release(); adapter-Release(); output-Release(); output1-Release(); return hr; }逻辑说明D3D11_CREATE_DEVICE_BGRA_SUPPORT 是强制要求缺少它时 DuplicateOutput 会返回 DXGI_ERROR_UNSUPPORTED因为输出复制内部的纹理格式需要 BGRA 支持。EnumOutputs 的 monitorIndex 从 0 开始0 是主输出多显示器环境下要遍历取得全部索引。查询 IDXGIOutput1 是调用 DuplicateOutput 的前提DXGI 1.1 的 IDXGIOutput 接口没有这个方法。代码里手动 Release 容易漏实际工程我会把中间指针封进Microsoft::WRL::ComPtr退出路径上统一交给 RAII 释放。对于截屏程序这种一次性初始化场景漏一次两次问题不大但如果放到循环里反复创建泄漏就会很快累积。3.2 采集一帧并转成位图保存初始化之后每帧调用 AcquireNextFrame。超时参数给 0 表示不等待、立即返回适合“手动按一下截一张”的交互模式给 100 毫秒则适合循环截屏避免没有画面更新时空转 CPU。bool AcquirePixels(IDXGIOutputDuplication* duplication, ID3D11Device* device, ID3D11DeviceContext* context, unsigned char* outPixels, int outWidth, int outHeight) { DXGI_OUTDUPL_FRAME_INFO frameInfo {}; IDXGIResource* desktopResource nullptr; HRESULT hr duplication-AcquireNextFrame(100, frameInfo, desktopResource); if (hr DXGI_ERROR_WAIT_TIMEOUT) return false; if (FAILED(hr)) return false; ID3D11Texture2D* acquired nullptr; hr desktopResource-QueryInterface(__uuidof(ID3D11Texture2D), (void**)acquired); if (FAILED(hr)) { desktopResource-Release(); duplication-ReleaseFrame(); return false; } D3D11_TEXTURE2D_DESC desc; acquired-GetDesc(desc); D3D11_TEXTURE2D_DESC stagingDesc {}; stagingDesc.Width desc.Width; stagingDesc.Height desc.Height; stagingDesc.MipLevels 1; stagingDesc.ArraySize 1; stagingDesc.Format DXGI_FORMAT_B8G8R8A8_UNORM; stagingDesc.SampleDesc.Count 1; stagingDesc.Usage D3D11_USAGE_STAGING; stagingDesc.CPUAccessFlags D3D11_CPU_ACCESS_READ; stagingDesc.BindFlags 0; ID3D11Texture2D* staging nullptr; hr device-CreateTexture2D(stagingDesc, nullptr, staging); if (FAILED(hr)) { acquired-Release(); desktopResource-Release(); duplication-ReleaseFrame(); return false; } context-CopyResource(staging, acquired); D3D11_MAPPED_SUBRESOURCE mapped {}; if (FAILED(context-Map(staging, 0, D3D11_MAP_READ, 0, mapped))) { staging-Release(); acquired-Release(); desktopResource-Release(); duplication-ReleaseFrame(); return false; } outWidth (int)desc.Width; outHeight (int)desc.Height; size_t dstRowPitch size_t(outWidth) * 4; outPixels new unsigned char[dstRowPitch * outHeight]; for (int y 0; y outHeight; y) { memcpy(outPixels size_t(y) * dstRowPitch, (const unsigned char*)mapped.pData size_t(y) * mapped.RowPitch, dstRowPitch); } context-Unmap(staging, 0); staging-Release(); acquired-Release(); desktopResource-Release(); duplication-ReleaseFrame(); return true; }这里有一个极易踩的坑mapped.RowPitch 不等于 outWidth * 4。显卡对纹理每行会做对齐RowPitch 通常比逻辑行大直接拿一整行长度做 memcpy 会导致图像错位。上面代码通过逐行拷贝正确处理了这一点改成直接把整块内存连续复制就会出现右侧 1/4 区域是花的情况。保存 BMP 的代码相对独立。BMP 的 DIB 头加上像素缓冲就可以了因为捕获纹理是 BGRA和 BMP 的存储顺序一致不需要交换通道。void SaveAsBmp(const char* path, const unsigned char* pixels, int width, int height) { BITMAPFILEHEADER bf {}; BITMAPINFOHEADER bi {}; bf.bfType 0x4D42; bf.bfOffBits sizeof(BITMAPFILEHEADER) sizeof(BITMAPINFOHEADER); bf.bfSize bf.bfOffBits DWORD(size_t(width) * height * 4); bi.biSize sizeof(BITMAPINFOHEADER); bi.biWidth width; bi.biHeight -height; bi.biPlanes 1; bi.biBitCount 32; bi.biCompression BI_RGB; std::ofstream out(path, std::ios::binary); out.write(reinterpret_castconst char*(bf), sizeof(bf)); out.write(reinterpret_castconst char*(bi), sizeof(bi)); out.write(reinterpret_castconst char*(pixels), std::streamsize(size_t(width) * height * 4)); }参数说明biHeight 使用负值是“顶行优先”标志不加负号保存出来的图片会是上下颠倒的。如果后续要用 GDI 函数 StretchDIBits 显示这张图负高度还要继续保持否则显示层会再次取反。3.3 多次截屏的时序与内存回收AcquireNextFrame 成功拿到帧后必须成对调用 ReleaseFrame。有些代码把 ReleaseFrame 放在保存操作之后这没错但要注意保存操作里不要再访问由 desktopResource 取得的叠代纹理。正确顺序是复制纹理到 StagingUnmap释放 staging释放 acquired释放 desktopResource最后 ReleaseFrame。顺序颠倒会出现 DXGI_ERROR_ACCESS_LOST之后的 AcquireNextFrame 全部失败。截屏频率也要控制。桌面画面的产生是以刷新率为周期的从上次 ReleaseFrame 到下次 AcquireNextFrame 至少要隔一个显示周期。连续无限循环、中间不 Sleep会把 GPU 的 DMA 队列打满反而让采集延迟增大。我一般把最小间隔设为 30 毫秒这已经能支持 30 FPS 的录制需求。4. 让源代码.zip 在别人电脑上跑起来编译、打包与排错代码完成后源码包的价值又回到了“交付”这件事。这里不是指上传到网盘而是让拿到压缩包的同事或用户在 10 分钟内重新构建并能运行。4.1 Visual Studio 与 MinGW 的编译命令差异Visual Studio 下最直接的编译方式是使用原生命令行工具链。链接时需要 d3d11.lib 和 dxgi.lib这两者在 Windows SDK 里随 Visual Studio 一并提供。cl /std:c17 /EHsc /O2 /DUNICODE main.cpp capture.cpp encoder.cpp /link d3d11.lib dxgi.lib如果你拿到的是一个带解决方案文件的项目那么直接在 IDE 里面切换 Release x64 构建即可。命令行方式的好处是不依赖项目组件的选择适合在 CI/CD 环境复现。MinGW-w64 在 Windows 下也能编译但命令语法略有不同g -stdc17 -O2 -DUNICODE main.cpp capture.cpp encoder.cpp -ld3d11 -ldxgi -o screenshot.exe注意 MinGW 自带的 dxgi1_2.h 可能版本较旧如果头文件缺少 IDXGIOutputDuplication 的定义要么升级 mingw-w64 的捆绑 Windows SDK要么把 MSVC 头文件目录加到 g 的 -I 前面。这个问题的典型报错是IDXGIOutputDuplication was not declared in this scope。4.2 该往 zip 里放哪些文件不该放哪些所谓源代码.zip交付物的核心是源代码。下面是合理的目录结构screenshot/ ├── src/ │ ├── main.cpp │ ├── capture.cpp │ ├── capture.h │ ├── encoder.cpp │ └── encoder.h ├── CMakeLists.txt ├── README.md └── .gitignore编译产物如 .exe、.obj、.pdb、.ilk 不应该出现在 zip 里。原因有两个一是这些文件与具体机器、具体编译选项绑定换了 Windows SDK 版本就要重新编译二是 .pdb 会泄露源码路径Debug 信息里带上D:\Users\xxx这类本机路径被二次分发出去对源码作者不是好事。除非 CMakeLists.txt 配置了完整依赖获取逻辑否则 README 里要写明系统要求。截屏程序最少需要 Windows 8 或 Windows Server 2012系统低于这个版本时 dxgi1_2.h 里的接口无法运行。还应写明编译器要求MSVC 2019 或更高版本。4.3 运行时错误排查缺少 VCRUNTIME、当前不会命中断点编译好的 exe 换个电脑运行经常会碰到“找不到 VCRUNTIME140.dll”的弹窗。这是开发机上装了 Visual C Redistributable目标机器没有。解决办法是在 README 里写一句“请安装对应架构的 运行库”或者在安装脚本里检测注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64不存在就去官网下载。调试时另一个高频提示是“当前不会命中断点没有为该文档加载任何符号”。这个问题的原因是 exe 引用的 pdb 文件与当前运行的 exe 不是同一次编译或者调试器没有被正确指定可执行文件。从源码.zip 进一个新工程时最容易犯的错是手动拷贝 exe 到桌面运行再回来启动调试此时 exe 和 pdb 已经处于两个副本符号自然断不上。解决方式是不要在外部运行程序把 VS Code 或 Visual Studio 的调试目标直接指向源码目录下的 exe。如果你更喜欢 VS Code 而不是完整 IDE需要自己在 c_cpp_properties.json 里写入 Windows SDK 头文件路径。这里有一个配置示例重点在 includePath{ environments: [ { name: Windows, includePath: [ ${workspaceFolder}/src, C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/ucrt, C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/um, C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/shared ] } ], configurations: [ { name: Win32, includePath: [${workspaceFolder}/src], defines: [_DEBUG, UNICODE, _UNICODE], compilerPath: cl.exe, intelliSenseMode: windows-msvc-x64 } ], version: 4 }这段配置里的版本号 10.0.26100.0 是 Windows 11 24H2 SDK 的默认版本老机器上可以dir C:\Program Files (x86)\Windows Kits\10\Include查看实际存在哪个目录再改路径。另一个容易忽略的项是 defines 里的 UNICODE 和 _UNICODE如果源码用到 tstring必须保持这两个宏一致否则编译器会混入 ANSI 版本函数声明。5. 进阶验证给 C 截屏程序加延时与显示器参数到这里源码的基础功能已经具备最后给它加两个实用参数延时截屏和显示器选择。这个升级方案不依赖界面直接在命令行完成适合接入 CI 或自动化脚本。5.1 用命令行参数控制截屏延迟、输出路径与显示器编号使用 wmain 的好处是可以直接读宽字符参数避免处理中文路径时出现编码转换。参数解析逻辑不需要引入 getopt在 Windows 下命令行解析用 std::wstring 就能快速做完int wmain(int argc, wchar_t* argv[]) { int delayMs 0; int monitor 0; std::wstring output Lscreenshot.bmp; for (int i 1; i argc; i) { std::wstring arg argv[i]; if (arg L-delay i 1 argc) delayMs _wtoi(argv[i]); else if (arg L-monitor i 1 argc) monitor _wtoi(argv[i]); else if (arg L-output i 1 argc) output argv[i]; else if (arg L-h) { /* 打印用法 */ return 0; } } if (delayMs 0) Sleep(delayMs); ID3D11Device* device nullptr; ID3D11DeviceContext* context nullptr; IDXGIOutputDuplication* duplication nullptr; HRESULT hr InitDuplication(device, context, duplication, monitor); if (FAILED(hr)) return 2; unsigned char* pixels nullptr; int w 0, h 0; if (AcquirePixels(duplication, device, context, pixels, w, h)) { SaveAsBmp(screen.bmp, pixels, w, h); delete[] pixels; } if (duplication) duplication-Release(); if (context) context-Release(); if (device) device-Release(); return 0; }参数解释-delay 用毫秒适合用户在按下回车后切到目标窗口-monitor 从 0 开始-output 指定输出路径。实际使用中我还会再加一个 -format png 参数但当前结构只保留最小集合让读者能一眼看懂启动流程。5.2 验证截图正确性的两种低成本方法验证分两层。第一层是肉眼验证确认截图内容与显示器一致第二层是自动化验证用脚本计算预期结果与实际结果的差异适合当你修改了采集代码时做回归测试。用 Python 做像素对比是最快的方法import sys from PIL import Image img_a Image.open(sys.argv[1]).convert(RGB) img_b Image.open(sys.argv[2]).convert(RGB) if img_a.size ! img_b.size: print(resolution mismatch:, img_a.size, img_b.size) diff_count 0 for y in range(img_a.height): for x in range(img_a.width): if img_a.getpixel((x, y)) ! img_b.getpixel((x, y)): diff_count 1 print(diff pixels:, diff_count)这个脚本对双屏幕验证同样有效。先截主屏再截第二屏把两张图和系统显示设置中的分辨率对比能确认 EnumOutputs 索引是否正确。执行逻辑是先把参数保存到一个文件里再用脚本循环读取。验证场景期望结果失败时的常见原因单显示器 BMP 尺寸正确与系统分辨率一致未声明 DPI Aware左上角像素颜色一致R/B 通道顺序正确Staging 格式用了 R8G8B8A8播放视频时截屏不黑屏画面完整使用了 GDI 而不是 DXGI连续截 10 张无泄漏内存不变ReleaseFrame 漏调或顺序错使用上述场景回归一遍基本能覆盖截屏程序的典型问题。剩下要处理的就只是编码格式、压缩级别这类外围选项不再影响核心捕获链路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询