MFC捕获指定显示器截图写入剪贴板生成缩略图异常
实现目标
在对话框中展示一组显示器缩略图列表,用户点击对应缩略图即可触发预设操作。
已实现逻辑
显示器枚举
参考已有方案,通过EnumDisplayMonitors接口枚举获取所有显示器的矩形区域信息,额外扩展存储了每个显示器的设备名称,当前暂时通过CListBox控件展示显示器名称列表供用户选择,后续计划替换为显示器缩略图列表。
枚举实现代码:
struct MonitorRects { std::vector<RECT> rcMonitors; std::vector<CString> strMonitorNames; static BOOL CALLBACK MonitorEnum(HMONITOR hMon, HDC hdc, LPRECT lprcMonitor, LPARAM pData) { MonitorRects* pThis = reinterpret_cast<MonitorRects*>(pData); pThis->rcMonitors.push_back(*lprcMonitor); MONITORINFOEX sMI{}; sMI.cbSize = sizeof(MONITORINFOEX); GetMonitorInfo(hMon, &sMI); pThis->strMonitorNames.push_back(sMI.szDevice); return TRUE; } MonitorRects() { EnumDisplayMonitors(nullptr, nullptr, MonitorEnum, (LPARAM)this); } };
屏幕捕获初版实现
为验证捕获逻辑,最初计划将捕获结果复制到剪贴板,再粘贴到图像编辑器中验证内容正确性,实现了如下捕获函数:
BOOL CCenterCursorOnScreenDlg::CaptureRect(const CRect& rcCapture, CImage &rImage) { // 销毁当前已存储的位图,用于创建新位图 rImage.Destroy(); // 创建位图并关联到当前对象 if (!rImage.Create(rcCapture.Width(), rcCapture.Height(), 32, 0)) { AfxMessageBox(L"Cannot create image!", MB_ICONERROR); return FALSE; } // 创建虚拟屏幕设备上下文 CDC dcScreen; dcScreen.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr); // 从虚拟屏幕DC拷贝内容 BOOL bRet = ::BitBlt(rImage.GetDC(), 0, 0, rcCapture.Width(), rcCapture.Height(), dcScreen.m_hDC, rcCapture.left, rcCapture.top, SRCCOPY | CAPTUREBLT); // 资源清理并返回 dcScreen.DeleteDC(); rImage.ReleaseDC(); if (!(OpenClipboard() && EmptyClipboard() && ::SetClipboardData(CF_BITMAP, (HBITMAP)rImage))) AfxMessageBox(L"Error copying image to clipboard", MB_ICONERROR); CloseClipboard(); return bRet; }
调用代码如下:
CRect rcMonitor = m_monitors.rcMonitors.at(0); CImage img; CaptureRect(rcMonitor, img);
遇到的问题
运行初版代码时出现两个异常:
- 将剪贴板内容粘贴到HyperSnap时,得到的图像为空白,未显示预期捕获的对应显示器画面
- 执行粘贴操作后HyperSnap会卡死无响应,必须强制结束进程才能恢复
后续测试将捕获的CImage对象直接保存为本地JPG文件:
rImage.Save(L"d:\\testthumb.jpg", Gdiplus::ImageFormatJPEG);
打开保存的文件确认屏幕内容捕获完全正确,因此排除截图逻辑本身的问题,故障点定位在CImage对象写入剪贴板的环节。
故障原因
剪贴板写入逻辑存在两个核心错误:
- 直接将
CImage强转得到的HBITMAP传给SetClipboardData不符合接口规范。SetClipboardData要求传入的位图句柄所有权在调用成功后转移给系统,且优先接收设备无关位图(DIB);而CImage持有的位图是和对象生命周期绑定的设备相关位图(DDB),当CImage对象离开作用域析构时,对应的位图资源会被自动释放,剪贴板中存储的句柄变为无效悬空指针,直接导致粘贴时内容空白、目标程序卡死。 - 剪贴板操作未做完整的错误分支处理:
OpenClipboard传入空句柄时可能因其他程序占用剪贴板打开失败,此时直接执行后续操作会引发异常;SetClipboardData调用失败时也没有做对应资源回收。
修复方案
替换原有剪贴板操作逻辑,创建独立的DIB位图副本写入剪贴板,严格处理操作流程的资源归属和错误分支,修正后的完整捕获函数代码如下:
BOOL CCenterCursorOnScreenDlg::CaptureRect(const CRect& rcCapture, CImage &rImage) { rImage.Destroy(); if (!rImage.Create(rcCapture.Width(), rcCapture.Height(), 32, 0)) { AfxMessageBox(L"Cannot create image!", MB_ICONERROR); return FALSE; } CDC dcScreen; dcScreen.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr); BOOL bRet = ::BitBlt(rImage.GetDC(), 0, 0, rcCapture.Width(), rcCapture.Height(), dcScreen.m_hDC, rcCapture.left, rcCapture.top, SRCCOPY | CAPTUREBLT); dcScreen.DeleteDC(); rImage.ReleaseDC(); if (bRet) { HBITMAP hBmpSrc = (HBITMAP)rImage; BITMAP bmpInfo = {0}; GetObject(hBmpSrc, sizeof(BITMAP), &bmpInfo); HDC hMemDC = CreateCompatibleDC(nullptr); BITMAPINFO bmi = {0}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = bmpInfo.bmWidth; bmi.bmiHeader.biHeight = bmpInfo.bmHeight; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; void* pBits = nullptr; HBITMAP hBmpClip = CreateDIBSection(hMemDC, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0); if (hBmpClip) { HGDIOBJ hOldBmp = SelectObject(hMemDC, hBmpClip); BitBlt(hMemDC, 0, 0, bmpInfo.bmWidth, bmpInfo.bmHeight, rImage.GetDC(), 0, 0, SRCCOPY); rImage.ReleaseDC(); SelectObject(hMemDC, hOldBmp); if (OpenClipboard(m_hWnd)) { EmptyClipboard(); if (!SetClipboardData(CF_BITMAP, hBmpClip)) { AfxMessageBox(L"Error copying image to clipboard", MB_ICONERROR); DeleteObject(hBmpClip); } CloseClipboard(); } else { DeleteObject(hBmpClip); AfxMessageBox(L"Cannot open clipboard", MB_ICONERROR); } } DeleteDC(hMemDC); } return bRet; }
后续缩略图实现提示
如果仅需在对话框内展示显示器缩略图,不需要经过剪贴板中转:捕获到每个显示器的完整画面后,直接将CImage缩放到预设的缩略图尺寸,通过自绘控件或列表项展示即可,点击缩略图时关联之前存储的显示器设备名、矩形区域信息,即可触发对应预设操作。
内容的提问来源于stack exchange,提问作者Andrew Truckle
相关产品推荐
相关产品推荐

