VC 抓屏的各種方法
目錄:
1 導言
2 用GID函數抓屏
3 用DirectX方式抓屏
4 用Windows Media API抓屏
1 導言
有時候我們需要編程抓取整個螢幕上的內容,下面我將介紹抓屏是如何?的。典型地,我們可以用GID和DirectX來完成,另外一個選擇是 Windows Media API,在這篇文章我會逐一加以分析。在每一種方法裡,一旦我們把螢幕的內容儲存到了程式定義的記憶體塊或bitmap檔案裡,我們就可以進一步利用它們來產生動畫和電影,這個過程你可以參考“從HBitmap建立電影”一文中,以獲得更多的協助。
2 用GDI函數抓屏
如果我們不太在意抓屏的效率,並且我們想要的只是一個螢幕快照的話,可以考慮使用GDI方式。這種抓屏機制是以“案頭也是一個視窗,案頭也有一個視窗控制代碼(HWND)”這個簡單的常識為基礎的,如果我們得到了案頭的裝置上下文(DC),就可以利用blit(複製)它的內容到我們建立的DC中。我們可以用 GetDeskWindow()得到案頭的視窗控制代碼,從控制代碼得到DC也是很容易的。具體的實現步驟為:
1. 通過GetDesktopWindow()函數得到案頭的視窗控制代碼
2. 用GetDC()取得桌面視窗的DC
3. 建立和螢幕DC相容的位元影像和DC(CreateCompatibleBitmap ()和CreateCompatibleDC()),並把這個位元影像選進該DC(SelectObject())
4. 當你準備好抓屏時,就複製桌面視窗DC的內容到相容DC,你就完成的抓屏過程,相容位元影像中就是抓屏時刻的螢幕內容
5. 完成後別忘了釋放你建立的對象,記憶體是寶貴的(對別的程式來說)
範例程式碼:
void CaptureScreen()
{
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
HWND hDesktopWnd = GetDesktopWindow();
HDC hDesktopDC = GetDC(hDesktopWnd);
HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);
HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC,
nScreenWidth, nScreenHeight);
SelectObject(hCaptureDC,hCaptureBitmap);
BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,hDesktopDC,0,0,SRCCOPY);
SaveCapturedBitmap(hCaptureBitmap); //Place holder - Put your code
//here to save the captured image to disk
ReleaseDC(hDesktopWnd,hDesktopDC);
DeleteDC(hCaptureDC);
DeleteObject(hCaptureBitmap);
}
上面程式碼片段中,GetSystemMetrics()返回螢幕的寬度(SM_CXSCREEN)和高度(SM_CYSCREEN)。關於如何儲存抓到的位元影像到檔案和如何置到剪貼簿,請參看附帶的原始碼,很簡單的。範例程式碼每隔一段時間就通過上述技術抓屏,並把映像序列儲存到動畫。
3 DirectX方式
用DreictX進行抓屏也是很簡單的,DirectX提供了很優雅的實現。
每個DirectX程式都包含一個被我們稱作緩衝的記憶體地區,其中儲存了和該程式有關的顯存內容,這在程式中被稱作後台緩衝(Back Buffer),有些程式有不止一個的後台緩衝。還有一個緩衝,在預設情況下每個程式都可以訪問-前台緩衝。前台緩衝儲存了和案頭相關的顯存內容,實質上就是螢幕映像。
我們的程式通過訪問前台緩衝就可以捕捉到當前螢幕的內容。由DirectX的底層最佳化機製做保證,我們的抓屏效率是很高的,至少比GDI方式高。
在 DirectX程式中訪問前台緩衝是很簡單的,IDirect3DDevice8介面提供了GetFrontBuffer()方法,它接收一個 IDirect3DSurface8對象指標做參數,並複製前台緩衝的內容到該Surface。IDirect3DSurfce8對象可以用 IDirect3DDevice8::CreateImageSurface()得到。一旦螢幕內容被儲存到了這個surface,我們就可以用 D3DXSaveSurfaceToFile()方法直接把內容儲存到磁碟bmp檔案。範例程式碼如下:
extern IDirect3DDevice8* g_pd3dDevice;
Void CaptureScreen()
{
IDirect3DSurface8 * pSurface;
g_pd3dDeviceàCreateImageSurface(ScreenWidth,ScreenHeight,
D3DFMT_A8R8G8B8,&pSurface);
g_pd3dDevice->GetFrontBuffer(pSurface);
D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,
NULL,NULL);
pSurface->Release();
}
上面,g_pd3dDevice 是一個初始化好的IDirect3DDevice對象,這個例子直接把捕捉到的映像儲存到檔案。然而,有時候我們想訪問直接這個映像中的各個位,我們可以使用IDirect3DSurface8::LockRect(),它給我們一個執行surface記憶體的指標,也就是捕捉到的映像的資料。我們複製這些資料到程式定義的記憶體中就可以操作它了。看下面的代碼:
extern void* pBits;
extern IDirect3DDevice8* g_pd3dDevice;
IDirect3DSurface8 * pSurface;
g_pd3dDeviceàCreateImageSurface(ScreenWidth,ScreenHeight,
D3DFMT_A8R8G8B8,&pSurface);
g_pd3dDevice->GetFrontBuffer(pSurface);
D3DLOCKED_RECT lockedRect;
pSurfaceàLockRect(&lockedRect,NULL,
D3DLOCK_NO_DIRTY_UPDATE|D3DLOCK_NOSYSLOCK|
D3DLOCK_READONLY)));
for( int i=0 ; i < ScreenHeight ; i++)
{
memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 ,
(BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
ScreenWidth * BITSPERPIXEL / 8);
}
g_pSurface->UnlockRect();
pSurface->Release();
上面的pBits是一個void*,請保證為先為它分配組足夠的記憶體空間。BITSPERPIXEL一般用32位色即可,它也取決於你的顯示器當前配置。一個需要注意的是,surface的寬度和被捕捉的螢幕寬度不一樣。由於記憶體對齊的原因(按WORD對齊的記憶體通常在訪問時效率較高),surface在每行結尾處可能會有多餘的bits以使它對齊到word邊界上。lockedRect.Pitch給我們提供了兩個連續行的開端之間的位元組數。也就是說我們在讀取一行時要向後移動指標Pitch位元組而不是Width位元組。你可以用下面的代碼反序複製surface:
for( int i=0 ; i < ScreenHeight ; i++)
{
memcpy((BYTE*) pBits +( ScreenHeight - i - 1) *
ScreenWidth * BITSPERPIXEL/8 ,
(BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
ScreenWidth* BITSPERPIXEL/8);
}
這對於從top-down位元影像到bottom-up位元影像很有用。
我們還可以使用IDirect3DSurface9的 GetDC()方法取得DirectX surface的GDI相容DC,然後複製它的內容到我們的相容DC。如果你用的是DirectX9,試試吧。
最後,需要注意的一點,文檔提到:FrontBuffer是一個比較慢的操作,設計就是如此,所以在效率很關鍵的程式中應避免使用。已經警告你了!本文附帶的原始碼用這種技術定時捕捉螢幕,並儲存為動畫。
4 用Windows Media API抓屏
Windows Media 9.0 支援用Windows Media Encoder 9 API來抓屏。它有一個編碼器叫 Windows Media Video 9 Screen codec,特別為抓屏最佳化過。Windows Media Encoder API提供了一個IWMEncoder2介面可以用來高效地捕捉螢幕映像。
用這種技術進行抓屏也很簡單,首先我們用CoCreateInstance()建立一個IWMEncoder2對象:
IWMEncoder2* g_pEncoder=NULL;
CoCreateInstance(CLSID_WMEncoder,NULL,CLSCTX_INPROC_SERVER,
IID_IWMEncoder2,(void**)&g_pEncoder);
這個Encoder對象包含了捕捉螢幕的所需的全部操作,然而為了正確地工作,編碼器對象的行為取決於被稱作profile的設定。一個profile只是一個包含了所有控制編碼操作設定的檔案,我們可以根據被捕捉的資料的特性在運行時建立包含自訂設定的profile。為了在你的抓屏程式中使用 profile,我們基於Windows Media Video 9 Screen codec來建立自訂的profile。自訂的 profile對象從IWMEncProfile2開始就被支援了。我們可以用 CoCreateInstance來建立自訂profile
IWMEncProfile2* g_pProfile=NULL;
CoCreateInstance(CLSID_WMEncProfile2,NULL,CLSCTX_INPROC_SERVER,
IID_IWMEncProfile2,(void**)&g_pProfile);
我需要在profile裡指定編碼器的聽眾(audience)。每個profile可以包含多個聽眾配置,它們是IWMEncAudienceObj介面對象。這裡我們為profile使用一個聽眾。我們可以通過IWMEncProfile::AddAudience()為我們的profile建立聽眾,這個函數返回一個IWMEncAudienceObj指標,可以用來配置視頻編碼器 (IWMEncAudienceObj::put_VideoCodec()),視訊框架對象 (IWMEncAudienceObj::put_VideoHeight() 和
IWMEncAudienceObj::put_VideoWidth())我們用下面的代碼來配置視頻編碼器:
extern IWMEncAudienceObj* pAudience;
#define VIDEOCODEC MAKEFOURCC('M','S','S','2')
//MSS2 is the fourcc for the screen codec
long lCodecIndex=-1;
g_pProfile->GetCodecIndexFromFourCC(WMENC_VIDEO,VIDEOCODEC,
&lCodecIndex); //Get the Index of the Codec
pAudience->put_VideoCodec(0,lCodecIndex);
fourcc 是針對每個編碼器的唯一的標識,Windows Media Video 9 Screen codec的fourcc為MSS2。 IWMEncAudienceObj::put_VideoCodec()接受profile索引來組織一個 profile,索引可以用 IWMEncProfile::GetCodecIndexFromFourCC()取得。
一旦我們配置完畢一個profile對象,我們就可以用IWMEncSourceGroup :: put_Profile()選擇這個profile到我們的編碼器。一個源組(SourceGruop)是一組視頻流來源或音頻流來源,或html來源。每個編碼器可以使用許多源組,並從中取得輸入資料。由於我們的程式僅僅使用視頻流中是視頻來源。這個視頻來源需要用 IWMEncVideoSource2::SetInput(BSTR) Screen Device來配置為輸入來源:
extern IWMEncVideoSource2* pSrcVid;
pSrcVid->SetInput(CComBSTR("ScreenCap://ScreenCapture1");
目的輸出可以用IWMEncFile::put_LocalFileName()配置為儲存到視頻檔案(wmv檔案)。IWMEncFile對象可以用IWMEncoder::get_File()得到:
IWMEncFile* pOutFile=NULL;
g_pEncoder->get_File(&pOutFile);
pOutFile->put_LocalFileName(CComBSTR(szOutputFileName);
現在,一旦編碼器對象的一切所需配置都完成後,我們就可以用IWMEncoder::Start()開始抓屏。IWMEncoder::Stop() 和 IWMEncoder::Pause可以用來停止和暫停捕捉。
這些適用於全屏捕捉,我們也可以通過調整輸入視頻來源流的屬性來選擇一個地區進行捕捉。我們可以用IWmEnVideoSource2的IPropertyBag 介面來實現:
#define WMSCRNCAP_WINDOWLEFT CComBSTR("Left")
#define WMSCRNCAP_WINDOWTOP CComBSTR("Top")
#define WMSCRNCAP_WINDOWRIGHT CComBSTR("Right")
#define WMSCRNCAP_WINDOWBOTTOM CComBSTR("Bottom")
#define WMSCRNCAP_FLASHRECT CComBSTR("FlashRect")
#define WMSCRNCAP_ENTIRESCREEN CComBSTR("Screen")
#define WMSCRNCAP_WINDOWTITLE CComBSTR("WindowTitle")
extern IWMEncVideoSource2* pSrcVid;
int nLeft, nRight, nTop, nBottom;
pSrcVid->QueryInterface(IID_IPropertyBag,(void**)&pPropertyBag);
CComVariant varValue = false;
pPropertyBag->Write(WMSCRNCAP_ENTIRESCREEN,&varValue);
varValue = nLeft;
pPropertyBag->Write( WMSCRNCAP_WINDOWLEFT, &varValue );
varValue = nRight;
pPropertyBag->Write( WMSCRNCAP_WINDOWRIGHT, &varValue );
varValue = nTop;
pPropertyBag->Write( WMSCRNCAP_WINDOWTOP, &varValue );
varValue = nBottom;
pPropertyBag->Write( WMSCRNCAP_WINDOWBOTTOM, &varValue );
本文附帶的源碼實現此中技術的抓屏。除去產生的動畫品質很好外,一個有意思的地方是滑鼠指標也被抓到了(GDI和DirectX預設是不抓取滑鼠指標的)。
注意,為了適用WindowMedia9.0 API,你的電腦必須安裝Windows Media9.0 SDK,你可以用下面地址下載:
http://msdn.microsoft.com/library/default.asp?url=/downloads/list/winmedia.asp
終端使用者必須安裝Windows Media Encoder 9 系列才能運行你的程式。在發布基於Windows Media Encoder SDK 的程式時,Windows Media Encoder軟體也必須附帶上去,要麼在你的軟體安裝時自動安裝Windows Media Encoder要麼讓使用者自己下載安裝。
Windows Encoder 9.0可以從下面地址下載:
http://www.microsoft.com/windows/windowsmedia/9series/encoder/default.aspx
結論
上面討論的各種方法都是基於一個目標-抓取螢幕的內容。然而適用不同的技術,得到的結果也不一樣。如果我們需要的只是偶爾的抓屏,GDI方式是個好的選擇,因為它簡單。然而如果你想得到更專業的結果,可以使用Windows Media。一個可能沒有意義的要點是,這些技術捕捉到的內容的品質很大程度上決於你的系統設定,比如進位硬體加速會大大提高抓屏的品質和程式的運行效率。