摘 要:本文给出了设计一个抓图程序的完整过程,对一个抓图程序所应拥有的最基本的三大功能即:热键激活、屏幕抓图及存储所抓取的图形等功能的实现作了详细的介绍。
关键词:热键 屏幕抓图 DDB DIB
在游戏程序的设计中,抓图功能是比较重要的一个功能。其实抓图程序的设计并不太复杂,仔细分析一个典型的抓图程序,它主要有以下功能:①具有热键激活的功能;②屏幕抓图的功能;③存储所抓取的图形。以下介绍在VC6中抓图程序的实现过程。
在VC6中创建一单文档工程,可命名为BmpCapture,为简化编程不选取工具条、状态条、ActiveX支持、3D控件和打印及打印预览等选项。注意在AppWizard对话框的Step6中,选取视图类的基类为CScrollView,因为不同的机器上,屏幕分辨率有可能不一样,所以应该建立一滚动视图,以适应不同分辨率大小的图形。
当然要完全支持滚动视图并没有这么简单,我们还必须手工加入代码。
在文档类中加入一保护型成员变量protected: CSize m_SizeDoc,该成员变量用于保存实际图形的分辨率的大小,也就是文档的尺寸;因为该成员被设定为保护型成员,故不能由与该文档相连的视图直接处理,而为能够根据实际图形分辨率大小改变该成员变量,应在文档类中相应定义两成员函数:
①CSize GetDocSize() {return m_SizeDoc;}
该函数用以获取实际图形分辨率大小;
②void SetDocSize(CSize size) {m_SizeDoc=size;}
该函数用于根据实际图形分辨率大小来设置m_SizeDoc成员变量的值;
那么如何根据不同窗口对象客户区的大小来改变m_sizeDoc的大小呢?
用GetClientRect函数来获得窗口对象客户区的大小
CWnd *m_pWndShow=GetDesktopWindow();//得到桌面窗口
CRect rect;
m_pWndShow->GetClientRect(rect);
size.cx=rect.Width();
size.cy=rect.Height();
然后进行修改如下:
CBmpCaptureDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc->SetDocSize(size);
记得在每次修改了m_sizeDoc之后要调用视图类的SetScrollSizes成员函数以传递该尺寸。
SetScrollSizes(MM_TEXT,pDoc->GetDocSize());
除了这些工作以外,还需要重载CBmpCaptureView类的成员函数OnInitialUpdate,该函数在视图第一次与文档相连时被调用。通过重载这个函数,能把文档的尺寸在最初更新视图前通知给视图。
void CBmpCaptureView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal;
// TODO: calculate the total size of this view
sizeTotal.cx = sizeTotal.cy = 100;
SetScrollSizes(MM_TEXT, sizeTotal);
}
所抓取的图形应首先存于一内存DC的CBitmap对象中,然后在OnDraw函数中重绘。为此,在CBmpCaptureDoc类中加入一CBitmap型成员变量,如下:
public:
CBitmap m_bitmap;
完成了以上的工作以后,现在开始在程序中加入热键激活的功能。在抓图程序中所谓热键激活是指当程序被最小化后,按下热键后即可完成抓图工作,同时恢复抓图程序界面,并在框架窗口的客户区显示所抓取的图形。
由于在ClassWizard中并没有封装热键处理消息(WM_HOTKEY),所以必须手工加入所有的代码。
①首先在BmpCaptureView.h文件中,加入热键消息响应函数的声明:
protected:
//{{AFX_MSG(CBmpCaptureView)
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg LRESULT OnHotKey(WPARAM wParam,LPARAM lParam);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
②然后在BmpCaptureView.cpp文件中,找到消息映射的定义处,加入以下语句:
BEGIN_MESSAGE_MAP(CBmpCaptureView, CScrollView)
//{{AFX_MSG_MAP(CBmpCaptureView)
ON_WM_CREATE()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_HOTKEY,OnHotKey) //消息和函数发生关联
END_MESSAGE_MAP()
③下一步在OnCreate函数中加入初始化代码并用RegisterHotKey函数向系统登记热键。初始化部分包括将m_bitmap对象初始化为合适的大小,并与视图窗口相兼容同时将位图对象清空。重载OnCreate函数如下:
int CBmpCaptureView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CScrollView::OnCreate(lpCreateStruct) == -1)
return -1;
RegisterHotKey(m_hWnd,1001,MOD_CONTROL|MOD_ALT,’Y’);//此处登记热键
//为”Ctrl+Alt+Y”
// TODO: Add your specialized creation code here
CWnd *m_pWndShow=GetDesktopWindow();//得到桌面窗口
CClientDC windowDC(this);
CBmpCaptureDoc* pDoc = GetDocument();
CRect rect;
m_pWndShow->GetClientRect(rect);
int cx=rect.Width(),cy=rect.Height();
pDoc->m_bitmap.CreateCompatibleBitmap(&windowDC, cx,cy);
CDC memoryDC;
memoryDC.CreateCompatibleDC(&windowDC);
CBitmap* pOldBitmap =
memoryDC.SelectObject(&pDoc->m_bitmap);
CBrush* pWhiteBrush = new CBrush(RGB(255,255,255));
CRect rect1(0, 0, cx-1, cy-1);
memoryDC.FillRect(rect1, pWhiteBrush);//将位图清空
memoryDC.SelectObject(pOldBitmap);
delete pWhiteBrush;
return 0;
}
④接下来当然得编写响应热键消息的函数OnHotKey了,在这里首先通过检查参数wParam来判断是否是所期望的热键,然后获取桌面窗口DC并使用BitBlt函数将其内容拷至m_bitmap对象中,最后还有一段将被最小化的窗口恢复显示的代码。因为代码中用到了CMainFrame类指针,所以需要在BmpCaptureView.cpp文件中加入语句#include "MainFrm.h"
LRESULT CBmpCaptureView::OnHotKey(WPARAM wParam,LPARAM lParam)
{
if (wParam==1001)
{
CWnd *m_pWndShow=GetDesktopWindow();//得到桌面窗口
ASSERT(m_pWndShow!=NULL);
CDC *pdc_Showed=m_pWndShow->GetDC();
CRect rect;
m_pWndShow->GetClientRect(rect);
CSize size;
size.cx=rect.Width();
size.cy=rect.Height();
CBmpCaptureDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc->SetDocSize(size);
SetScrollSizes(MM_TEXT,pDoc->GetDocSize());
CClientDC cdc(this);
CDC memoryDC;
memoryDC.CreateCompatibleDC(pdc_Showed);
CBitmap* pOldBitmap =
memoryDC.SelectObject(&pDoc->m_bitmap);
size=pDoc->GetDocSize();
memoryDC.BitBlt (0,0,size.cx,size.cy,pdc_Showed,0,0,SRCCOPY);
Invalidate();//使窗口重画
memoryDC.SelectObject(pOldBitmap);
//以下重新显示被最小化的窗口
CMainFrame* pwnd=(CMainFrame*)AfxGetApp()->m_pMainWnd;
if(pwnd->SetForegroundWindow())
{
pwnd->ShowWindow(SW_SHOWNORMAL);
pwnd->UpdateWindow();
}
}
return 0;
}
以上OnHotKey函数中对 Invalidate() 的调用将迫使视图窗口重画,使视图类调用OnDraw函数,以下为OnDraw函数的代码,其功能为在视图客户区内显示m_bitmap对象:
void CBmpCaptureView::OnDraw(CDC* pDC)
{
CBmpCaptureDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CDC memoryDC;
memoryDC.CreateCompatibleDC(pDC);
CBitmap* pOldBitmap =
memoryDC.SelectObject(&pDoc->m_bitmap);
CSize size=pDoc->GetDocSize();
pDC->BitBlt(0, 0,size.cx, size.cy,
&memoryDC, 0, 0, SRCCOPY);
memoryDC.SelectObject(pOldBitmap);
}
到这里已经实现了:①热键激活的功能;②屏幕抓图的功能。剩下的工作是将所抓取的图形保存起来。
由于CBitmap类对象是一个DDB(设备相关位图),而我们将要保存的BMP位图则是DIB(设备无关位图)。所以首先得将DDB格式转换为DIB格式,然后再进行保存。
在CBmpCaptureDoc类中增加一个SaveBmp成员函数,代码如下:
BOOL CBmpCaptureDoc::SaveBmp(HBITMAP hBitmap, CFile& file)
{
if (hBitmap == NULL)
return FALSE;
HDC hDC;
int iBits;
//当前显示分辨率下每个像素所占字节数
WORD wBitCount;
//位图中每个像素所占字节数
//定义调色板大小, 位图中像素字节大小 ,
//位图大小
DWORD dwPaletteSize=0, dwBmBitsSize, dwDIBSize;
BITMAP Bitmap;
HANDLE hDib;
HPALETTE hPal,hOldPal=NULL;
//位图属性结构
BITMAPFILEHEADER bmfHdr; // 位图文件头
LPBITMAPINFOHEADER lpBI; // 位图头指针
BITMAPINFOHEADER bi;
//位图信息头结构
//DWORD dwDIBSize;
//计算位图文件每个像素所占字节数
hDC = CreateDC("DISPLAY",NULL,NULL,NULL);
iBits = GetDeviceCaps(hDC, BITSPIXEL) *
GetDeviceCaps(hDC, PLANES);
DeleteDC(hDC);
if (iBits<=1)
wBitCount=1;
else if (iBits<=4)
wBitCount=4;
else if (iBits<=8)
wBitCount=8;
else if (iBits<=16)
wBitCount=16;
else if (iBits<=24)
wBitCount=24;
//计算调色板大小
if (wBitCount<=8)
dwPaletteSize=(1<<wBitCount) *
sizeof(RGBQUAD);
GetObject(hBitmap, sizeof(BITMAP), (LPSTR)&Bitmap);
bi.biSize= sizeof(BITMAPINFOHEADER);
bi.biWidth= Bitmap.bmWidth;
bi.biHeight= Bitmap.bmHeight;
bi.biPlanes= 1;
bi.biBitCount= wBitCount;
bi.biCompression= BI_RGB;
bi.biSizeImage= 0;
bi.biXPelsPerMeter= 0;
bi.biYPelsPerMeter= 0;
bi.biClrUsed= 0;
bi.biClrImportant= 0;
dwBmBitsSize=((Bitmap.bmWidth *
wBitCount+31)/32)* 4
*Bitmap.bmHeight ;
hDib=GlobalAlloc(GHND,dwBmBitsSize+
dwPaletteSize+sizeof(BITMAPINFOHEADER));
lpBI=(LPBITMAPINFOHEADER)GlobalLock(hDib);
*lpBI=bi;
// 处理调色板
hPal=(HPALETTE)GetStockObject(DEFAULT_PALETTE);
if (hPal)
{
hDC=GetDC(NULL);
hOldPal=(HPALETTE)SelectPalette(hDC,hPal,FALSE);
RealizePalette(hDC);
}
// 获取该调色板下新的像素值
GetDIBits(hDC, hBitmap, 0, (UINT) Bitmap.bmHeight,
(LPSTR)lpBI+sizeof(BITMAPINFOHEADER)
+dwPaletteSize,(BITMAPINFO*)
lpBI, DIB_RGB_COLORS);
//恢复调色板
if (hOldPal)
{
SelectPalette(hDC,hOldPal, TRUE);
RealizePalette(hDC);
ReleaseDC(NULL, hDC);
}
/*
* 填写文件头
*/
/* 给文件类型成员赋值:BMP文件前两个字节必须为"BM"*/
bmfHdr.bfType = 0x4d42; // "BM"
dwDIBSize = sizeof(BITMAPINFOHEADER)
+ dwPaletteSize + dwBmBitsSize;
bmfHdr.bfSize = dwDIBSize+sizeof(BITMAPFILEHEADER);
bmfHdr.bfReserved1 = 0;
bmfHdr.bfReserved2 = 0;
bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER)
+ (DWORD)sizeof(BITMAPINFOHEADER)
+ dwPaletteSize;
TRY
{
// 写文件头
file.Write((LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER));
//
// 写DIB头及数据位
//
file.WriteHuge((LPSTR)lpBI, dwDIBSize);
}
CATCH (CFileException, e)
{
::GlobalUnlock((HGLOBAL) hDib);
THROW_LAST();
}
END_CATCH
::GlobalUnlock((HGLOBAL) hDib);
return TRUE;
}
只要熟悉了BMP位图的文件格式,以上的代码就不难理解。由于在很多书籍中对BMP位图的文件格式都有详细的介绍,故本文不再赘述。函数的最后是一段异常处理代码。
最后重载CBmpCaptureDoc类的OnSaveDocument成员函数,在该函数中调用以上的SaveBmp函数。代码如下:
BOOL CBmpCaptureDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
// TODO: Add your specialized code here and/or call the base class
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeCreate |
CFile::modeReadWrite | CFile::shareExclusive, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
TRUE, AFX_IDP_INVALID_FILENAME);
return FALSE;
}
BOOL bSuccess = FALSE;
TRY
{
BeginWaitCursor();
HBITMAP m_hBMP=(HBITMAP)m_bitmap;
bSuccess = SaveBmp(m_hBMP,file);
file.Close();
}
CATCH (CException, eSave)
{
file.Abort();
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eSave,
TRUE, AFX_IDP_FAILED_TO_SAVE_DOC);
return FALSE;
}
END_CATCH
EndWaitCursor();
SetModifiedFlag(FALSE);
if (!bSuccess)
{
CString strMsg;
strMsg.LoadString(IDS_CANNOT_SAVE_BMP);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
}
return bSuccess;
}
其中IDS_CANNOT_SAVE_BMP是字符串”Can not save bmp”的ID。可以看到OnSaveDocument中除了对SaveBmp函数的调用外,还有许多异常处理代码。
好了,现在可以Build这个工程来看看这个程序的功能了。
先将生成的执行程序最小化,然后按下热键”Ctrl+Alt+Y”,你会发现你的桌面已经被抓到BmpCapture中了,从“文件”菜单中选取“保存”子菜单就可以将你的桌面保存为Bmp位图了。
以上程序在VC6,WIN98中调试通过。
|