一、 简介
屏幕抓图程序在处理图形中应用广泛。作为Windows XP及以后版本操作系统的图形处理内核,GDI+在二维几何图形处 理、图像显示与转换和字符排版等方面简直是传统GDI程序员的一种解脱。但是,至少在目前情况下,GDI+尚不能完全代替GDI。与GDI相比,它至少还 存在以下不足:
不支持从内存到屏幕的位传输操作;
不支持光栅“位运算”操作;
如果程序性能、速度要求比较严格,在图片输出方面的表现较差时,GDI往往能取代实现高性能的输出。
本文通过对流行的屏幕抓图程序工作原理的剖析,力图向读者阐明GDI+与GDI各自在图形处理方面的优缺点,并给出相应的VC++ .NET代码实现。
二、 GDI在抓图中的关键作用
要实现屏幕抓图,关键有两点:一是获取图片所在窗口的窗口句柄,即在何处捕获图片;二是保存抓取的图片,实现这一点正是GDI+的强项。
对于问题一,可以利用SetCapture函数,它能够追踪鼠标指针的移动(包括在屏幕抓图程序窗口之外的窗口)。在移动鼠标的过程中,它还可以根据鼠标 的指针所在位置来判断当前窗口的窗口句柄。我们还可以使用函数WindowFromPoint,这个函数能够找出鼠标指针当前位置所对应的窗口句柄。
使用过知名的抓图软件SnagIT的读者都知道,在选择抓图窗口时,鼠标指针所在位置的窗口都会出现加粗的红色边框,以提醒目前所选择的窗口,这个功能实现起来有些复杂。下面介绍在GDI中如何使这个红色边框出现。
【注意】正是由于这个红色边框的实现,读者才能发现GDI+在这方面的弱点。
在GDI 中,一个最基本的概念就是设备环境(DC),每一个窗口都具有自己的DC。如果能够找到窗口的DC,那么,用户就能够在该窗口的任何位置绘图。然而,在屏 幕抓图程序中,由于用户所选择的窗口不固定,所以,要想得到鼠标指针所处窗口的DC并不容易。这一问题的答案在于GetDC函数。下面是GetDC的函数 声明:
HDC GetDC(HWND hWnd);
这里,hWnd是DC对应的窗口句柄。注意,当hWnd为空时,该函数返回的是整个屏幕的设备环境句柄。这就意味着,开发人员可以在屏幕上的任何位置进行任意的绘图操作。
在鼠标指针所处的窗口绘图时,绘图的目的只是为了提醒用户目前所选择的窗口,所以,在绘图时,必须保证不会破坏窗口原有的画面。这时可将窗口的绘图模式设 为RS_NOTXORPEN,将画笔颜色与屏幕颜色进行异或运算之后,再对屏幕颜色取反即可。RS_NOTXORPEN运算方式的特点在于:对同一像素进 行两次RS_NOTXORPEN运算后,像素值并不会发生变化。这样,在同一个地方进行两次绘图后,窗口的画面并不会发生任何变化。
【注意】这些功能在GDI+中很难实现。
三、 编码实现
由上可知,屏幕抓图至少分为3个步骤:
(1) 启用鼠标指针捕获。
(2) 在鼠标指针所在处的窗口进行绘图,提示抓图的目标。
(3) 选定目标窗口时,将目标窗口的画面保存为自定义的位图并终止鼠标指针捕获。
以下是具体的编程步骤:
(1)在Visual C++ .NET中按照GDI+程序的框架新建一个基于对话框的项目ScreenCapture,然后准备好一个外形为相机的光标 文件(*.cur),将之引入资源管理器(IDC_CAMERA)。接着在CScreenCaptureDlg类中加入以下两个全局变量:
HWND hwndCapture;
Crect rectCapture;
(2)通过类向导加入对WM_MOUSEMOVE及WM_LBUTTONUP事件的响应函数,分别如下所示。
void CScreenCaptureDlg::OnMouseMove(UINT nFlags, CPoint point)
{
//如果用户按隹鼠标左键不放,则开始抓取图片
if(nFlags==MK_LBUTTON){
//隐藏程序窗口,以免影响在抓取时的“视野”
ShowWindow(SW_HIDE);
//载入“照相机”鼠标指针,开始追踪鼠标指针的移动
HCURSOR cur=LoadCursor(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDC_CAMERA));
SetCursor(cur);
SetCapture();
//获得鼠标指针所在窗口的句柄
this->ClientToScreen(&point);
hwndCapture=(HWND)::WindowFromPoint(point);
//取得屏幕的设备环境句柄,以便在屏幕的任何位置绘图
HDC hDC=::GetDC(NULL);
//建立一个红色的画笔
HPEN hPen=CreatePen(PS_INSIDEFRAME,6,RGB(255,0,0));
//将绘图模式设为R2_NOTXORPEN,在绘图时可以不破坏原有的背景
int nMode=SetROP2(hDC,R2_NOTXORPEN);
HPEN hpenOld=(HPEN)SelectObject(hDC,hPen);
//得到鼠标指针所在窗口的区域
::GetWindowRect(hwndCapture,&rectCapture);
//在鼠标指针所在处的窗口四周画一红色的矩形,做为选定时的提示
POINT pt[5];
pt[0]=CPoint(rectCapture.left,rectCapture.top);
pt[1]=CPoint(rectCapture.right,rectCapture.top);
pt[2]=CPoint(rectCapture.right,rectCapture.bottom);
pt[3]=CPoint(rectCapture.left,rectCapture.bottom);
pt[4]=CPoint(rectCapture.left,rectCapture.top);
::Polyline(hDC,pt,5);
//延时后再重绘红色矩形,这样不会破坏原有的内容
Sleep(100);
::Polyline(hDC,pt,5);
::SelectObject(hDC,hpenOld);
::ReleaseDC(NULL,hDC);
}
CDialog::OnMouseMove(nFlags, point);
}
void CScreenCaptureDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
// 得到鼠标指针所在窗口的区域宽、高
int nWidth=rectCapture.Width();
int nHeight=rectCapture.Height();
HDC hdcScreen,hMemDC;
HBITMAP hBitmap,hOldBitmap;
//建立一个屏幕设备环境句柄
hdcScreen=CreateDC("DISPLAY",NULL,NULL,NULL);
hMemDC=CreateCompatibleDC(hdcScreen);
//建立一个与屏幕设备环境句柄兼容、与鼠标指针所在窗口的区域等大的位图
hBitmap=CreateCompatibleBitmap(hdcScreen,nWidth,nHeight);
//把新位图选到内存设备描述表中
hOldBitmap=(HBITMAP)SelectObject(hMemDC,hBitmap);
//把屏幕设备描述表拷贝到内存设备描述表中
BitBlt(hMemDC,0,0,nWidth,nHeight,hdcScreen,rectCapture.left,rectCapture.top,SRCCOPY);
DeleteDC(hdcScreen);
DeleteDC(hMemDC);
//返回位图句柄
//打开剪贴板,并将位图拷到剪贴板上
OpenClipboard();
EmptyClipboard();
SetClipboardData(CF_BITMAP,hBitmap);
//关闭剪贴板
CloseClipboard();
MessageBox("屏幕内容已经拷到剪贴板!");
ReleaseCapture();
//恢复窗口显示模式
ShowWindow(SW_NORMAL);
CDialog::OnLButtonUp(nFlags, point);
}
至此,一个具有专业效果的屏幕抓图程序的核心已经搞定。
四、 用GDI+实现画面的保存
经过上面两步,如果用户在对话框中按住鼠标左键不放,程序便开始“抓图”。当选择好抓图的目标后,松开鼠标左键,抓图的目标窗口的画面就自动保存到剪贴板 中了。但是,把画面保存到文件中更为重要。如果用GDI的方式来操作,需要对各种类位图的结构有详尽的了解,极其麻烦。但如果用GDI+来实现之则极为容 易。下面介绍如何将已经抓到的图片保存到一个BMP文件中。
由上面知,抓图程序已经得到了所捕获的窗口的位图句柄,接下来要将位图句柄保存为相应的位图文件。这一切归功于GDI+的Bitmap类,详见下列代码。
void CScreenCaptureDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
//……省略
if(GetSaveFileName(&ofn))
{
CLSID pngClsid;
Bitmap bmp(hBitmap,NULL);
//获取BMP文件的编码方式
GetEncoderClsid(L"image/bmp",&pngClsid);//帮助函数
CString tmp(ofn.lpstrFile);
CStringW filename((LPCSTR)tmp);
//保存所截取的屏幕图片
bmp.Save(filename,&pngClsid);
}
ReleaseCapture();
MessageBox("屏幕内容已经保存到文件中!");
//恢复窗口显示模式
ShowWindow(SW_NORMAL);
CDialog::OnLButtonUp(nFlags, point);
}
五、 小结
本文通过一个专业的屏幕抓图程序的核心实现,对比分析了GDI与GDI+各自的优缺点。但我们相信,GDI+作为新一代图形引擎,随着版本的不断升级,其迟早要淘汰掉GDI。本人拙见,不足处还望读者指正。
另外,本文源码在Windows 2000/VC++.NET 2003环境中调试通过。调试过程中注意:
屏幕抓图程序在处理图形中应用广泛。作为Windows XP及以后版本操作系统的图形处理内核,GDI+在二维几何图形处 理、图像显示与转换和字符排版等方面简直是传统GDI程序员的一种解脱。但是,至少在目前情况下,GDI+尚不能完全代替GDI。与GDI相比,它至少还 存在以下不足:
不支持从内存到屏幕的位传输操作;
不支持光栅“位运算”操作;
如果程序性能、速度要求比较严格,在图片输出方面的表现较差时,GDI往往能取代实现高性能的输出。
本文通过对流行的屏幕抓图程序工作原理的剖析,力图向读者阐明GDI+与GDI各自在图形处理方面的优缺点,并给出相应的VC++ .NET代码实现。
二、 GDI在抓图中的关键作用
要实现屏幕抓图,关键有两点:一是获取图片所在窗口的窗口句柄,即在何处捕获图片;二是保存抓取的图片,实现这一点正是GDI+的强项。
对于问题一,可以利用SetCapture函数,它能够追踪鼠标指针的移动(包括在屏幕抓图程序窗口之外的窗口)。在移动鼠标的过程中,它还可以根据鼠标 的指针所在位置来判断当前窗口的窗口句柄。我们还可以使用函数WindowFromPoint,这个函数能够找出鼠标指针当前位置所对应的窗口句柄。
使用过知名的抓图软件SnagIT的读者都知道,在选择抓图窗口时,鼠标指针所在位置的窗口都会出现加粗的红色边框,以提醒目前所选择的窗口,这个功能实现起来有些复杂。下面介绍在GDI中如何使这个红色边框出现。
【注意】正是由于这个红色边框的实现,读者才能发现GDI+在这方面的弱点。
在GDI 中,一个最基本的概念就是设备环境(DC),每一个窗口都具有自己的DC。如果能够找到窗口的DC,那么,用户就能够在该窗口的任何位置绘图。然而,在屏 幕抓图程序中,由于用户所选择的窗口不固定,所以,要想得到鼠标指针所处窗口的DC并不容易。这一问题的答案在于GetDC函数。下面是GetDC的函数 声明:
HDC GetDC(HWND hWnd);
这里,hWnd是DC对应的窗口句柄。注意,当hWnd为空时,该函数返回的是整个屏幕的设备环境句柄。这就意味着,开发人员可以在屏幕上的任何位置进行任意的绘图操作。
在鼠标指针所处的窗口绘图时,绘图的目的只是为了提醒用户目前所选择的窗口,所以,在绘图时,必须保证不会破坏窗口原有的画面。这时可将窗口的绘图模式设 为RS_NOTXORPEN,将画笔颜色与屏幕颜色进行异或运算之后,再对屏幕颜色取反即可。RS_NOTXORPEN运算方式的特点在于:对同一像素进 行两次RS_NOTXORPEN运算后,像素值并不会发生变化。这样,在同一个地方进行两次绘图后,窗口的画面并不会发生任何变化。
【注意】这些功能在GDI+中很难实现。
三、 编码实现
由上可知,屏幕抓图至少分为3个步骤:
(1) 启用鼠标指针捕获。
(2) 在鼠标指针所在处的窗口进行绘图,提示抓图的目标。
(3) 选定目标窗口时,将目标窗口的画面保存为自定义的位图并终止鼠标指针捕获。
以下是具体的编程步骤:
(1)在Visual C++ .NET中按照GDI+程序的框架新建一个基于对话框的项目ScreenCapture,然后准备好一个外形为相机的光标 文件(*.cur),将之引入资源管理器(IDC_CAMERA)。接着在CScreenCaptureDlg类中加入以下两个全局变量:
HWND hwndCapture;
Crect rectCapture;
(2)通过类向导加入对WM_MOUSEMOVE及WM_LBUTTONUP事件的响应函数,分别如下所示。
void CScreenCaptureDlg::OnMouseMove(UINT nFlags, CPoint point)
{
//如果用户按隹鼠标左键不放,则开始抓取图片
if(nFlags==MK_LBUTTON){
//隐藏程序窗口,以免影响在抓取时的“视野”
ShowWindow(SW_HIDE);
//载入“照相机”鼠标指针,开始追踪鼠标指针的移动
HCURSOR cur=LoadCursor(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDC_CAMERA));
SetCursor(cur);
SetCapture();
//获得鼠标指针所在窗口的句柄
this->ClientToScreen(&point);
hwndCapture=(HWND)::WindowFromPoint(point);
//取得屏幕的设备环境句柄,以便在屏幕的任何位置绘图
HDC hDC=::GetDC(NULL);
//建立一个红色的画笔
HPEN hPen=CreatePen(PS_INSIDEFRAME,6,RGB(255,0,0));
//将绘图模式设为R2_NOTXORPEN,在绘图时可以不破坏原有的背景
int nMode=SetROP2(hDC,R2_NOTXORPEN);
HPEN hpenOld=(HPEN)SelectObject(hDC,hPen);
//得到鼠标指针所在窗口的区域
::GetWindowRect(hwndCapture,&rectCapture);
//在鼠标指针所在处的窗口四周画一红色的矩形,做为选定时的提示
POINT pt[5];
pt[0]=CPoint(rectCapture.left,rectCapture.top);
pt[1]=CPoint(rectCapture.right,rectCapture.top);
pt[2]=CPoint(rectCapture.right,rectCapture.bottom);
pt[3]=CPoint(rectCapture.left,rectCapture.bottom);
pt[4]=CPoint(rectCapture.left,rectCapture.top);
::Polyline(hDC,pt,5);
//延时后再重绘红色矩形,这样不会破坏原有的内容
Sleep(100);
::Polyline(hDC,pt,5);
::SelectObject(hDC,hpenOld);
::ReleaseDC(NULL,hDC);
}
CDialog::OnMouseMove(nFlags, point);
}
void CScreenCaptureDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
// 得到鼠标指针所在窗口的区域宽、高
int nWidth=rectCapture.Width();
int nHeight=rectCapture.Height();
HDC hdcScreen,hMemDC;
HBITMAP hBitmap,hOldBitmap;
//建立一个屏幕设备环境句柄
hdcScreen=CreateDC("DISPLAY",NULL,NULL,NULL);
hMemDC=CreateCompatibleDC(hdcScreen);
//建立一个与屏幕设备环境句柄兼容、与鼠标指针所在窗口的区域等大的位图
hBitmap=CreateCompatibleBitmap(hdcScreen,nWidth,nHeight);
//把新位图选到内存设备描述表中
hOldBitmap=(HBITMAP)SelectObject(hMemDC,hBitmap);
//把屏幕设备描述表拷贝到内存设备描述表中
BitBlt(hMemDC,0,0,nWidth,nHeight,hdcScreen,rectCapture.left,rectCapture.top,SRCCOPY);
DeleteDC(hdcScreen);
DeleteDC(hMemDC);
//返回位图句柄
//打开剪贴板,并将位图拷到剪贴板上
OpenClipboard();
EmptyClipboard();
SetClipboardData(CF_BITMAP,hBitmap);
//关闭剪贴板
CloseClipboard();
MessageBox("屏幕内容已经拷到剪贴板!");
ReleaseCapture();
//恢复窗口显示模式
ShowWindow(SW_NORMAL);
CDialog::OnLButtonUp(nFlags, point);
}
至此,一个具有专业效果的屏幕抓图程序的核心已经搞定。
四、 用GDI+实现画面的保存
经过上面两步,如果用户在对话框中按住鼠标左键不放,程序便开始“抓图”。当选择好抓图的目标后,松开鼠标左键,抓图的目标窗口的画面就自动保存到剪贴板 中了。但是,把画面保存到文件中更为重要。如果用GDI的方式来操作,需要对各种类位图的结构有详尽的了解,极其麻烦。但如果用GDI+来实现之则极为容 易。下面介绍如何将已经抓到的图片保存到一个BMP文件中。
由上面知,抓图程序已经得到了所捕获的窗口的位图句柄,接下来要将位图句柄保存为相应的位图文件。这一切归功于GDI+的Bitmap类,详见下列代码。
void CScreenCaptureDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
//……省略
if(GetSaveFileName(&ofn))
{
CLSID pngClsid;
Bitmap bmp(hBitmap,NULL);
//获取BMP文件的编码方式
GetEncoderClsid(L"image/bmp",&pngClsid);//帮助函数
CString tmp(ofn.lpstrFile);
CStringW filename((LPCSTR)tmp);
//保存所截取的屏幕图片
bmp.Save(filename,&pngClsid);
}
ReleaseCapture();
MessageBox("屏幕内容已经保存到文件中!");
//恢复窗口显示模式
ShowWindow(SW_NORMAL);
CDialog::OnLButtonUp(nFlags, point);
}
五、 小结
本文通过一个专业的屏幕抓图程序的核心实现,对比分析了GDI与GDI+各自的优缺点。但我们相信,GDI+作为新一代图形引擎,随着版本的不断升级,其迟早要淘汰掉GDI。本人拙见,不足处还望读者指正。
另外,本文源码在Windows 2000/VC++.NET 2003环境中调试通过。调试过程中注意:
确 保工程对GDI+库的正确引用:在头文件stdafx.h中要加入相应引用;在应用程序类的InitInstance成员函数前后及其析构函数中加适当的 操作;工程编译时要加入对gdiplus.lib的引用(“项目”|“添加现有项”,我的机器上是在C:\Program Files\ Microsoft Visual Studio.NET\vc7\platformSDK\lib下找到库文件)。
本文转自朱先忠老师51CTO博客,原文链接: http://blog.51cto.com/zhuxianzhong/60080,如需转载请自行联系原作者