ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC/VS屏幕截图完整指南:BitBlt核心实现与多显示器、DPI兼容处理

MFC/VS屏幕截图完整指南:BitBlt核心实现与多显示器、DPI兼容处理 简介面向MFC/C开发者的屏幕截图功能实现在Visual Studio环境下演示如何捕获全屏或指定窗口并保存为位图或JPEG。这一例程围绕GDI图形设备接口展开覆盖CDC设备上下文、CBitmap位图对象、GetDC窗口设备句柄获取、BitBlt像素块复制、SelectObject选入位图等核心API的组合用法对学习Windows图形编程和MFC消息处理具有很好的参考价值。资源包共22个文件包含头文件、cpp源码、资源脚本、图标以及完整工程文件sln/vcxproj压缩后仅136KB代码量精炼便于直接编译运行或迁移到现有项目。已有635人学习适合需要快速掌握GDI/CDC/CBitmap/BitBlt等Windows图形接口的初学者。通过阅读源码可梳理截屏标准流程获取屏幕设备上下文、创建兼容位图、BitBlt复制像素、释放GDI资源同时也可参考其窗口截取、文件保存等扩展思路是学习MFC图形编程和Windows屏幕捕获机制的实用例程。1. MFC/VS 截取屏幕图片这组词背后是桌面工具的第一道坎MFC/VS 截取屏幕图片这组词在桌面开发里很常见但真做起来总有意外全黑、只截主屏、分辨率不对都是截屏工具最常见的翻车现场。看起来是 BitBlt 一行代码的事背后却连着 DC 生命周期、DPI 虚拟化和多显示器三套逻辑。这篇笔记想做的事很简单把一个 MFC 工程里“点按钮存一张屏幕图”的完整路径拆透——怎么选截屏对象、怎么写核心代码、怎么保存成 BMP以及多显示器和 DPI 缩放这些边界条件怎么处理。适用对象是做截图工具、界面存档、自动化验证的桌面端开发者。新手照步骤能跑通熟手可以参考参数边界和清理时机少走一遍我已经走过的弯路。2. 动手前先想清楚全屏、窗口还是区域决定你用的是哪套 API2.1 三种截图对象它们背后是两套完全不同的 API写 MFC 截屏最忌讳一上来就搜代码。先把自己要截的东西归类全屏、窗口、区域。这三类在日常工具里出现频率差不多可代码差别很大选错了轻则多写代码重则截出来根本不是要的内容。全屏最简单GetDC(NULL)拿到的就是整个虚拟桌面的 DC直接用 BitBlt 按屏幕宽高拷像素。窗口截图不能直接对窗口 DC 做 BitBlt因为窗口 DC 只在窗口没被遮挡时有意义通常做法是先GetWindowRect拿窗口外框再用PrintWindow或 WM_PRINT 让窗口自己画进一个兼容 DC。区域截图其实是全屏截图的参数化版本把 BitBlt 的目标宽高固定成区域的宽高源坐标从区域左上角开始。截图对象常见实现路径典型场景最容易踩的坑全屏截图GetDC(NULL) BitBlt 全屏尺寸存档留证、录屏单帧多显示器下只截主屏窗口截图GetWindowRect PrintWindow控件快照、窗口留档被遮挡、DPI 缩放后黑图区域截图BitBlt 限定宽高和源坐标表格区、局部校验坐标基准混乱导致偏位为什么这么分因为 MFC 里的 GDI 截屏核心就是 BitBlt 一个函数它把屏幕上指定矩形内的像素原封不动地拷贝到内存 DC。全屏只是区域等于虚拟屏大小窗口则是让窗口自己重新绘制一遍而不是从屏幕抓现成的像素。有个容易混的点区域截图用的坐标到底是以屏幕左上角为基准还是以客户区左上角为基准如果坐标来自GetWindowRect就是相对虚拟屏幕左上角的坐标如果来自GetClientRect必须先ClientToScreen转换。混用坐标系是截屏偏位的第一大来源。提示区域截图的坐标基准要么统一用虚拟屏坐标要么统一用客户区坐标混用是截屏偏位最常见的原因。2.2 在 VS 里新建 MFC 截屏工程从向导到按钮这里的 VS 指 Visual Studio不是 VS CodeMFC 工程只能在 Visual Studio 里建。打开 Visual Studio新建项目选“MFC 应用”应用类型选“基于对话框”语言用 C。创建好后在资源视图里打开对话框模板从工具箱拖一个按钮进去把按钮 ID 改成IDC_BTN_CAPTURE标题改成“截屏”。双击这个按钮VS 会自动生成消息处理函数框架void CCaptureDlg::OnBnClickedBtnCapture() { // TODO: 在此添加按钮处理代码 // 截屏代码会写在这个函数里 }这个名字是 VS 根据按钮 ID 自动映射的。MFC 的消息映射表把IDC_BTN_CAPTURE和这个函数绑定所以函数名不能随便改。如果希望在别处触发截屏也可以自己在消息映射里加一行ON_BN_CLICKED(IDC_BTN_CAPTURE, CCaptureDlg::OnBnClickedBtnCapture)。工程配置上有两个点值得注意。第一项目属性里的字符集建议选“使用 Unicode 字符集”这样代码里的Lxxx.bmp字符串不会出现转换问题。第二如果同一个工程里加了多个 .cpp 文件不要在每个文件里都定义同一个消息处理函数否则链接时会报 LNK2005 符号重复定义这个错误和截屏本身没关系纯粹是工程组织问题。MFC 向导默认会链接 user32 和 gdi32截屏需要用到的那几个 Win32 API 不需要额外加库这点比新手想的省事。2.3 为什么在 MFC 里优先用 GDI 而不是 GDI 或 DXGIMFC 工程里做静态截图我默认用 GDI 的 BitBlt 系列而不是 GDI。GDI 不是不能用它保存成 PNG、JPEG 确实方便但要在 MFC 里先用GdiplusStartup初始化、退出时再GdiplusShutdown管线比 GDI 长一截。截屏本身只是把屏幕像素拷出来GDI 的BitBlt加GetDIBits已经覆盖整个流程代码量更少也不容易引入额外的状态管理。DXGI Desktop Duplication 更适合连续采集比如录屏或远程画面推送。它要求创建 DXGI 设备和共享资源状态机复杂为一个按钮截屏引入它太重。如果以后要往录屏方向走再单独抽一个 DXGI 采集模块和现有 GDI 截屏并存完全来得及。也有人会问“桌面开发用 MFC 还是 Qt”。Qt 的QScreen::grabWindow底层同样走系统抓屏如果哪天把工具迁移到 Qt这个需求的表现形式会变但“屏幕 DC → 内存 DC → 位图 → 文件”这个模型是不变的。本文代码以 MFC 为主思路可以平移。3. 核心实现用 GDI 的 BitBlt 把屏幕像素搬进 BMP 文件3.1 理解屏幕 DC、内存 DC 和位图这三件套屏幕 DC 是截屏的“源头”。GetDC(NULL)返回的屏幕 DC 代表整个虚拟桌面所有显示器的像素都可以从它上面拷。注意这个 DC 用完后必须ReleaseDC(NULL, hdc)释放否则每次截屏都会泄漏一个 GDI 对象这个问题在定时连续截屏时尤其致命。内存 DC 是中间画板。屏幕 DC 不能直接拿来当写入目标BitBlt 需要一个兼容 DC 作为目标CreateCompatibleDC(hScreen)创建的就是这种画板。它本身没有可画的表面必须配合一张位图。先用CreateCompatibleBitmap(hScreen, w, h)创建一张和屏幕颜色格式一致的位图再通过SelectObject(hMemDC, hBitmap)把它选入内存 DC。选入时SelectObject的返回值是上一个旧位图保存好后面清理时再选回去否则直接删除位图容易删掉 DC 里正在用的对象导致后续异常。整个流程的顺序是GetDC(NULL)→CreateCompatibleDC→CreateCompatibleBitmap→SelectObject→BitBlt→GetDIBits→SelectObject(旧位图)→DeleteObject→DeleteDC→ReleaseDC对象创建方式清理方式常见误用屏幕 DCGetDC(NULL)ReleaseDC(NULL, hdc)只开不关连续截屏内存暴涨内存 DCCreateCompatibleDCDeleteDC和屏幕 DC 搞混位图CreateCompatibleBitmapDeleteObject选入 DC 后直接删除这三件套的创建和释放顺序决定了这个函数能不能扛住长时间运行。后面第 5 章有一节专门讲连续截屏的内存泄漏就是从这里来的。3.2 完整代码从屏幕 DC 到 BMP 文件这是最常用的一个函数输入一个屏幕矩形输出 BMP 文件。坐标用虚拟屏坐标单位是像素。BOOL CaptureRectToFile(LPCTSTR lpszFileName, int nLeft, int nTop, int nRight, int nBottom) { BOOL bOk FALSE; HDC hScreen GetDC(NULL); // 整个虚拟桌面的DC HDC hMemDC CreateCompatibleDC(hScreen); // 兼容内存DC int nWidth nRight - nLeft; int nHeight nBottom - nTop; HBITMAP hBitmap CreateCompatibleBitmap(hScreen, nWidth, nHeight); HBITMAP hOldBitmap (HBITMAP)SelectObject(hMemDC, hBitmap); // 把屏幕指定矩形拷进内存DC if (BitBlt(hMemDC, 0, 0, nWidth, nHeight, hScreen, nLeft, nTop, SRCCOPY)) { BITMAPINFO bmi {0}; bmi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth nWidth; bmi.bmiHeader.biHeight -nHeight; // 负值从上到下存储免翻转 bmi.bmiHeader.biPlanes 1; bmi.bmiHeader.biBitCount 32; // 32位BGRA bmi.bmiHeader.biCompression BI_RGB; DWORD dwBitsSize ((nWidth * 32 31) / 32) * 4 * nHeight; BYTE* pBits new BYTE[dwBitsSize]; // 把位图内容读到内存缓冲区 if (GetDIBits(hMemDC, hBitmap, 0, nHeight, pBits, bmi, DIB_RGB_COLORS)) { BITMAPFILEHEADER bfh {0}; bfh.bfType 0x4D42; // BM bfh.bfSize sizeof(BITMAPFILEHEADER) sizeof(BITMAPINFOHEADER) dwBitsSize; bfh.bfOffBits sizeof(BITMAPFILEHEADER) sizeof(BITMAPINFOHEADER); HANDLE hFile CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile ! INVALID_HANDLE_VALUE) { DWORD dwWritten 0; WriteFile(hFile, bfh, sizeof(bfh), dwWritten, NULL); WriteFile(hFile, bmi.bmiHeader, sizeof(BITMAPINFOHEADER), dwWritten, NULL); WriteFile(hFile, pBits, dwBitsSize, dwWritten, NULL); CloseHandle(hFile); bOk TRUE; } } delete[] pBits; } // 清理先选回旧位图再删位图、删DC、释放屏幕DC if (hOldBitmap) SelectObject(hMemDC, hOldBitmap); DeleteObject(hBitmap); DeleteDC(hMemDC); ReleaseDC(NULL, hScreen); return bOk; }调用方式全屏时传入虚拟屏范围区域时传入目标矩形。// 截全屏虚拟屏坐标 int vx GetSystemMetrics(SM_XVIRTUALSCREEN); int vy GetSystemMetrics(SM_YVIRTUALSCREEN); int vw GetSystemMetrics(SM_CXVIRTUALSCREEN); int vh GetSystemMetrics(SM_CYVIRTUALSCREEN); CaptureRectToFile(Lfullscreen.bmp, vx, vy, vx vw, vy vh);代码里有两个细节值得说明。第一biHeight用负值表示 BMP 像素行从上到下存储和显卡内存排列一致省去保存前做行翻转。第二dwBitsSize的公式确保每行字节数按 4 字节对齐这是 BMP 文件格式的硬性要求宽高随意的时候也安全。注意GetDC 必须配 ReleaseDCCreateCompatibleDC 必须配 DeleteDC。两条链漏一条长时间跑就会出问题。3.3 BitBlt 核心参数设置尺寸、坐标、光栅操作BitBlt 的函数签名分布在 wingdi.h 里最常用的有六个参数BitBlt(HDC hdcDest, int nx, int ny, int nWidth, int nHeight, HDC hdcSrc, int xs, int ys, DWORD dwRop)这里nx/ny是目标 DC 的左上角通常传0, 0nWidth/nHeight是要截的宽高必须大于 0xs/ys是源 DC屏幕里拷取的起点这是屏幕坐标系中的位置dwRop是光栅操作码平常只用SRCCOPY表示直接把源像素覆盖到目标不做任何混合。坐标最容易错的不是 BitBlt 本身而是“这个坐标是哪套坐标系”。如果区域来自GetWindowRect得到的是相对虚拟屏幕左上角的坐标可以直接当作xs/ys用如果来自GetClientRect那是客户区坐标必须先ClientToScreen转成屏幕坐标再传进来。窗口有边框、标题栏、滚动条的时候客户区和窗口区可以差出几十个像素。多显示器场景下还有一个负坐标问题。副屏放在主屏左边时屏幕矩形左上角的 x 是负数比如-1920。BitBlt 是接受负数源坐标的只要在屏幕 DC 的有效范围内就能正常拷贝。怕就怕代码里写死了0起点。dwRop还有一个CAPTUREBLT很多人把它当万能钥匙。实际上默认 BitBlt 不会把分层窗口WS_EX_LAYERED的像素拷进来加SRCCOPY | CAPTUREBLT能把这类窗口包含进去。但它只是解决分层窗口这一小类问题遇到硬件加速渲染的窗口照样黑图那种情况要交给第 4 章的方案。4. 高频坑位排查多显示器、DPI 缩放和窗口遮挡让截屏集体翻车截屏代码跑通一次很容易换台机器就现原形。下面这三类是我在 MFC 截屏工具里踩过的真问题按“现象 → 原因 → 解决”记录。4.1 多显示器为什么全屏截图只截到主屏现象双屏扩展模式下截屏程序输出一张图片只有主屏那么宽副屏内容完全没进去或者图片最右侧能看到一部分副屏其余区域是黑的。单屏机器上一切正常一接副屏就出问题。原因代码里全屏范围写成了GetSystemMetrics(SM_CXSCREEN)和SM_CYSCREEN这两个值只返回主屏尺寸。GetDC(NULL)返回的却是整个虚拟桌面 DC。副屏内容在超出主屏的矩形里BitBlt 在参数超出屏幕 DC 有效范围时要么空白要么被裁剪。解决用虚拟屏那组系统指标而不是主屏尺寸int vx GetSystemMetrics(SM_XVIRTUALSCREEN); int vy GetSystemMetrics(SM_YVIRTUALSCREEN); int vw GetSystemMetrics(SM_CXVIRTUALSCREEN); int vh GetSystemMetrics(SM_CYVIRTUALSCREEN); // 虚拟屏区域可能是负坐标原样传进去即可 CaptureRectToFile(Lall_screens.bmp, vx, vy, vx vw, vy vh);BitBlt 的源坐标用虚拟屏坐标不需要额外换算。副屏在左边时SM_XVIRTUALSCREEN返回的是负数直接把vx传进去就行。GetWindowRect返回的坐标也是这套坐标系所以窗口截图在多屏场景下反而不需要太多处理只有区域截图要确认自己的坐标基准是虚拟屏还是客户区。4.2 DPI 缩放截出来的图变小、模糊、鼠标对不上现象一台 100% 缩放的机器正常另一台 150% 或 200% 缩放的机器上同一份代码截全屏生成图片的分辨率只有实际物理分辨率的约三分之二。比如 3840x2160 的屏截出来变成 2560x1440。用鼠标坐标做像素定位时位置和图像内容对不上。窗口截出来也比真实窗口窄一圈。原因Windows 对没声明 DPI 感知的程序做了 DPI 虚拟化。程序里GetSystemMetrics、GetWindowRect返回的是逻辑像素而屏幕 DC 在 BitBlt 时实际拷贝的是物理像素。用逻辑宽高去拷物理屏结果自然是分辨率缩水。截图变模糊是后续拉伸放大造成的。解决在创建任何窗口之前把进程设为 DPI 感知。MFC 工程放在InitInstance最前面BOOL CCaptureApp::InitInstance() { // 放在这个函数最前面窗口创建之前 typedef HRESULT(WINAPI* PFN_SetProcessDpiAwareness)(int); HMODULE hShcore LoadLibrary(_T(Shcore.dll)); if (hShcore) { PFN_SetProcessDpiAwareness pFn (PFN_SetProcessDpiAwareness)GetProcAddress(hShcore, SetProcessDpiAwareness); if (pFn) pFn(2); // 2 PROCESS_PER_MONITOR_DPI_AWARE } else { SetProcessDPIAware(); // Win7/Win8 老路径让系统按系统DPI感知处理 } // ...后续原来的代码 }设置之后GetSystemMetrics返回物理像素屏幕 DC 尺寸和物理分辨率一致。要注意的是这一步会改变整个进程的 UI 布局老工程用默认字体时控件可能显小对话框资源和字体大小得跟着调整。多显示器之间 DPI 不一致的环境要进一步用SetProcessDpiAwarenessContext走 Per-Monitor V2但通用的单分辨率缩放场景上面的代码已经够用。4.3 窗口遮挡与透明窗口BitBlt 不灵PrintWindow 也有边界现象一截某个窗口目标窗口被另一个窗口盖住一半。BitBlt 截出的图片里被盖住的区域显示的是遮挡窗口的内容或桌面背景而不是目标窗口本身的内容。现象二用PrintWindow截窗口整个区域是黑块或者截透明窗口WS_EX_LAYERED时背景区域是黑色的。原因BitBlt 抓的是屏幕合成后的真实像素遮挡窗口先画在屏幕上所以像素也被盖住了。PrintWindow 是让窗口自己往指定 DC 里重绘如果窗口用了 DirectComposition 或硬件加速合成普通 WM_PRINT 路径拿不到合成后的缓冲。透明窗口的绘制逻辑类似普通路径不会把背景合成进来。解决BitBlt 路径在截窗口时不成立改用PrintWindowBOOL CaptureWindowToFile(HWND hWnd, LPCTSTR lpszFileName) { RECT rc; GetWindowRect(hWnd, rc); int nW rc.right - rc.left; int nH rc.bottom - rc.top; HDC hScr GetDC(NULL); HDC hMem CreateCompatibleDC(hScr); HBITMAP hBmp CreateCompatibleBitmap(hScr, nW, nH); HBITMAP hOld (HBITMAP)SelectObject(hMem, hBmp); // 优先带 PW_RENDERFULLCONTENT多数窗口会把完整合成内容画进去 DWORD dwFlags PW_RENDERFULLCONTENT; if (!PrintWindow(hWnd, hMem, dwFlags)) PrintWindow(hWnd, hMem, 0); // 老了系统回退到普通路径 // 保存 BMP 的流程复用 3.2 的 GetDIBits WriteFile 逻辑 // 这里从 hMem 中读取位图并写入文件 if (hOld) SelectObject(hMem, hOld); DeleteObject(hBmp); DeleteDC(hMem); ReleaseDC(NULL, hScr); return TRUE; }PrintWindow 的 DC 要用 CreateCompatibleDC 创建的兼容 DC而且最好先把位图选进去否则部分窗口会拒绝绘制。PW_RENDERFULLCONTENT 这个标志在 Win8.1 以上可用Windows 10/11 下对绝大多数窗口都有效但遇到真正走 DirectComposition 的视频窗口或浏览器硬件加速层它可能仍然输出黑块。对这种情况我一般会分成两步走。第一试试“隐藏遮挡窗口”的土办法枚举目标窗口上方的顶层窗口临时SetWindowPos把它们移开截完再恢复这一招能解决不少重叠场景。第二真搞不定再引入 DXGI Desktop Duplication 做桌面采集那套东西是一个完整的采集管线适合后面要录屏、推流的场景为单张截图专门迁移不划算。5. 进阶做法定时连续截屏、区域直截和 GDI 对象的内存回收5.1 定时截屏每 500ms 抓一张内存却不涨截屏不只是点一下按钮。很多时候要定期留档每隔 500ms 抓一张连起来就是屏幕变化的记录。MFC 里实现很简单对话框上放一个“开始”按钮点击后启动定时器。void CCaptureDlg::OnBnClickedBtnStart() { // 定时器ID 1间隔500ms SetTimer(1, 500, NULL); } void CCaptureDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { static int nSeq 0; CString strPath; strPath.Format(_T(capture_%04d.bmp), nSeq); // 用虚拟屏范围兼容多显示器 int vx GetSystemMetrics(SM_XVIRTUALSCREEN); int vy GetSystemMetrics(SM_YVIRTUALSCREEN); int vw GetSystemMetrics(SM_CXVIRTUALSCREEN); int vh GetSystemMetrics(SM_CYVIRTUALSCREEN); CaptureRectToFile(strPath, vx, vy, vx vw, vy vh); } CDialogEx::OnTimer(nIDEvent); }OnTimer每被系统调度一次就执行一次截屏。文件按序号命名capture_0001.bmp这样排下去。500ms 间隔相当于 2 帧每秒对操作留痕、状态监控已经足够。参数上有一个需要提前算的账一张 1080p 的 32 位 BMP 约 8.3MB按 2fps 存一小时大约 60MB。磁盘占用会很快上来实际项目里一般会建议压缩成 PNG或者只截关键区域不然跑一晚上就是几十万个小文件。如果确实需要高帧率连续采集GDI 截屏加 BMP 落盘这条路很快会遇到瓶颈那时候应该先换压缩格式再考虑 DXGI。5.2 区域截图直接剪裁 BitBlt 参数而不是截全屏再裁有些需求只关心屏幕上某个区域比如表格里的某一行、按钮的图标、报表的一角。常见的错误做法是先截全屏再在内存里做一次矩形裁剪白白多一次大图的分配和拷贝。正确做法是把区域矩形直接传给CaptureRectToFileBitBlt 只拷贝目标区域不会多占内存。比如要截一个控件// 拿控件在屏幕上的位置 RECT rcWnd; GetDlgItem(IDC_REPORT)-GetWindowRect(rcWnd); // 相对虚拟屏坐标 // 直接截这个区域不用先截全屏再裁剪 CaptureRectToFile(_T(area.bmp), rcWnd.left, rcWnd.top, rcWnd.right, rcWnd.bottom);GetWindowRect返回的是屏幕坐标可以直接传给CaptureRectToFile。如果截的是控件的客户区步骤是GetClientRect拿到客户区大小再ClientToScreen把左上角转成屏幕坐标。这一步是区域截图最容易出错的点很多人拿客户区宽高直接当屏幕坐标用截出来的图整体向右下偏移。为什么不建议截全屏再裁一张 4K 全屏位图要占约 33MB 内存裁剪时又是一次逐像素的整图拷贝。直接对目标区域 BitBlt内存占用只有区域大小差距非常明显连续截屏时这个差距会直接影响帧率。5.3 连续截屏时的一次内存泄漏事故现象截屏程序开着跑了一晚上内存从 30MB 涨到 2GB 以上最后 GDI 对象耗尽截出来的图全黑。原因CaptureRectToFile里创建了屏幕 DC、内存 DC、位图如果中途提前 return没有走到后面的清理代码所有对象都会被丢弃。定时器每 500ms 触发一次每次泄漏几 MB几个小时后内存就爆了。我自己的惨痛经历有一次在GetDIBits失败时直接加了 return没走清理代码第二天跑批发现截图全黑任务管理器里内存已经顶到 2.7GB。从那以后我写截屏函数第一件事就是把所有出口都统一到同一段清理逻辑上。清理顺序是固定的先SelectObject选回旧位图再DeleteObject删除位图然后DeleteDC删除内存 DC最后ReleaseDC释放屏幕 DC。任何分支都不能漏。更稳妥的办法是写一个小的 RAII 类让析构函数兜底class CScreenCaptureGuard { public: HDC hScreen; HDC hMemDC; HBITMAP hBitmap; HBITMAP hOldBmp; CScreenCaptureGuard(int nW, int nH) { hScreen GetDC(NULL); hMemDC CreateCompatibleDC(hScreen); hBitmap CreateCompatibleBitmap(hScreen, nW, nH); hOldBmp (HBITMAP)SelectObject(hMemDC, hBitmap); } ~CScreenCaptureGuard() { if (hOldBmp) SelectObject(hMemDC, hOldBmp); DeleteObject(hBitmap); DeleteDC(hMemDC); ReleaseDC(NULL, hScreen); } };这个类把三件套的创建和释放收拢到构造和析构函数里。哪怕中间有异常、有提前 return析构函数都会执行不会“漏一次就泄漏一点”。实际工程里还应该在构造函数里判断每一步的返回值创建失败就抛异常或置标志这里为可读性省略了。6. 验证截屏结果用像素级对比代替“肉眼看着没问题”6.1 一个简单的像素级校验流程肉眼看不靠谱尤其是截屏偶尔差一两个像素的情况。我现在的验证流程是先用 Windows 自带截图工具截一张基准图再让自己写的函数截同一区域逐像素对比 RGB。差异像素占比低于 0.1%基本可以交付。// 比较两张32位BGRA位图返回差异像素数量 DWORD CountDiffPixels(const BYTE* a, const BYTE* b, DWORD width, DWORD height) { DWORD diff 0; for (DWORD y 0; y height; y) { const BYTE* pa a y * width * 4; const BYTE* pb b y * width * 4; for (DWORD x 0; x width; x) { // BGRA顺序三通道任一个超过阈值就算差异 if (abs(pa[x * 4] - pb[x * 4]) 3 || abs(pa[x * 4 1] - pb[x * 4 1]) 3 || abs(pa[x * 4 2] - pb[x * 4 2]) 3) { diff; } } } return diff; }阈值 3 以内通常是肉眼不易察觉的色差字体边缘的抗锯齿和部分渲染差异通常落在这个范围。如果差异像素大量集中在固定区域且差值超过几十基本可以断定是坐标偏移或者位图格式没对齐。6.2 我留给自己的几个验证习惯我现在常备一台双显示器 150% 缩放的测试机专门用来跑截屏回归。每次改动后至少确认三件事全屏截图的尺寸和物理分辨率一致窗口截图的内容和窗口实际位置吻合定时器连续运行 20 分钟任务管理器里内存曲线平稳。如果这次改动只动了保存逻辑我也会至少完整跑一遍上面的流程。老实说我见过太多“能跑就以为没问题”的做法最后都在 DPI 缩放或遮挡问题上栽跟头。截屏工具就是这样边界条件不测到换台机器就是另一副面孔。希望这套思路和代码能让你在 MFC/VS 截屏这件事上少走一点弯路也希望帮到你。本文还有配套的精品资源点击获取
返回列表