在Windows操作系统中,经常需要进行屏幕截图。通常情况下,可能会使用一些常见的方法,如使用BitBlt函数。但是,如果需要排除特定的窗口,或者在Windows 7或Windows 8等较新的操作系统中,BitBlt函数可能无法满足需求。因此,需要寻找新的方法来实现这一功能。
本文将介绍一种使用WindowsVista及以上版本中的放大库(Magnification library)进行屏幕截图的方法。这种方法不适用于WindowsXP,但随着越来越多的用户从XP升级到更新的Windows版本,这并不是一个大问题。
示例程序中只包含一个“捕获”按钮。当用户点击此按钮时,程序将捕获屏幕截图,并弹出一个对话框,让用户指定文件名,并将捕获的图像保存为位图文件。
放大库的完整文档可在MSDN上找到。根据文档,屏幕捕获过程可以通过以下步骤简单完成:
使用MagInitialize函数初始化放大库。
if (!MagInitialize()) {
return FALSE;
}
创建具有分层属性的宿主对话框,并将其设置为全屏且不可见。使用MFC创建对话框,并将其用作宿主对话框。因为用它来存储捕获的图像,但不想将其显示给用户,所以将其隐藏。
// 获取屏幕分辨率
RECT rect;
HWND hDesktop = ::GetDesktopWindow();
::GetWindowRect(hDesktop, ▭);
// 设置窗口位置
SetWindowPos(NULL, 0, 0, rect.right, rect.bottom, SWP_HIDEWINDOW);
// 设置窗口分层属性
SetWindowLong(this->GetSafeHwnd(), GWL_EXSTYLE, GetWindowLong(this->GetSafeHwnd(), GWL_EXSTYLE) | WS_EX_LAYERED);
SetLayeredWindowAttributes(0, 255, LWA_ALPHA);
将放大窗口作为宿主窗口的子窗口创建。注意,窗口类是MagnifierWindow。如果想捕获鼠标光标,创建放大窗口时启用MS_SHOWMAGNIFIEDCURSOR。
hwndMag = CreateWindow(WC_MAGNIFIER, TEXT("MagnifierWindow"), WS_CHILD | MS_SHOWMAGNIFIEDCURSOR | WS_VISIBLE, 0, 0, m_ScreenX, m_ScreenY, hostDlg->GetSafeHwnd(), NULL, hInstance, NULL);
调用MagSetWindowFilterList函数,从捕获中排除指定窗口。由于这个强大的功能,这是想要使用放大库的主要原因。在这个程序中,过滤了主对话框,但可以根据需要过滤更多。
// 设置过滤列表以排除主窗口
pFilterList = new HWND[1];
pFilterList[0] = this->GetSafeHwnd();
if (!MagSetWindowFilterList(hwndMag, MW_FILTERMODE_EXCLUDE, 1, pFilterList)) {
return;
}
每当调用MagSetWindowSource函数时,整个桌面都会被捕获到放大窗口中。在上述代码中,将宿主窗口设置为不可见,但如果显示宿主窗口,将看到桌面的图像在其中。
// 获取屏幕矩形
RECT sourceRect;
sourceRect.top = 0;
sourceRect.left = 0;
sourceRect.right = m_ScreenX;
sourceRect.bottom = m_ScreenY;
if (!MagSetWindowSource(hwndMag, sourceRect)) {
return;
}
通常,可以通过使用BitBlt函数将窗口内容保存到文件中,或者将其内容复制到内存中。使用放大库捕获数据的主要问题是,无法像通常那样使用BitBlt函数访问宿主窗口或放大窗口的位图。因此,使用MagSetImageScalingCallback函数作为解决方案。
// 设置回调函数
if (!MagSetImageScalingCallback(hwndMag, (MagImageScalingCallback)MagImageScaling)) {
return FALSE;
}
回调函数的语法如下:
BOOL MagImageScaling(HWND hwnd, void *srcdata, MAGIMAGEHEADER srcheader, void *destdata, MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty)
srcdata参数是指向位图源的指针,srcheader包含捕获图像的信息,因此可以使用这两个参数来创建BITMAPINFOHEADER和BITMAPFILEHEADER。
BITMAPINFOHEADER bmif;
// 设置位图信息头
bmif.biSize = sizeof(BITMAPINFOHEADER);
bmif.biHeight = srcheader.height;
bmif.biWidth = srcheader.width;
bmif.biSizeImage = srcheader.cbSize;
bmif.biPlanes = 1;
bmif.biBitCount = (WORD)(bmif.biSizeImage / bmif.biHeight / bmif.biWidth * 8);
bmif.biCompression = BI_RGB;
位图文件头可以设置如下:
// 设置位图文件头
BITMAPFILEHEADER bmfh;
LONG offBits = sizeof(BITMAPFILEHEADER) + bmif.biSize;
bmfh.bfType = 0x4d42; // "BM"
bmfh.bfOffBits = offBits;
bmfh.bfSize = offBits + bmif.biSizeImage;
bmfh.bfReserved1 = 0;
bmfh.bfReserved2 = 0;
请注意,捕获的数据顺序与普通位图数据相反,因此必须在保存之前将其转换为正确的顺序,否则位图将水平翻转。
LONG lineSize = bmif.biWidth * bmif.biBitCount / 8;
BYTE* pLineData = new BYTE[lineSize];
BYTE* pStart;
BYTE* pEnd;
LONG lineStart = 0;
LONG lineEnd = bmif.biHeight - 1;
while (lineStart < lineEnd) {
// 获取交换行的地址
pStart = pData + (lineStart * lineSize);
pEnd = pData + (lineEnd * lineSize);
// 交换顶部与底部
memcpy(pLineData, pStart, lineSize);
memcpy(pStart, pEnd, lineSize);
memcpy(pEnd, pLineData, lineSize);
// 调整行索引
lineStart++;
lineEnd--;
}
delete pLineData;
最后,将上述所有内容保存到文件中。此示例仅支持BMP文件,但可以使用压缩库将其保存为其他格式。
// 打开文件
CFile pFile;
if (!pFile.Open((LPCTSTR)fileName, CFile::modeCreate | CFile::modeWrite)) {
return;
}
// 写入文件
pFile.Write(&bmfh, sizeof(BITMAPFILEHEADER)); // 位图文件头
pFile.Write(&bmif, sizeof(BITMAPINFOHEADER)); // 位图信息头
pFile.Write(pData, bmif.biSizeImage); // 转换后的位图数据
// 关闭文件
pFile.Close();