原文" T/ _+ N. g" G0 T* ^5 ]8 y
http://blog.csdn.net/matlab2000/archive/2006/10/30/1356752.aspx& w2 b5 J2 A. V: c# m( U6 T7 H
: @# t8 ^ C! X8 @- Q. X 常常我们想要编程捕获整个屏幕的内容,下面将解释如何做到这一点。典型的,我们的选择是使用GDI或者DirectX。另外一个值得考虑的方法是Windows Media API,直接将捕获的屏幕图像编码成码流输出,不过本文中不讨论该方法,主要讨论前两种技术。每一种方法中,我们得到了屏幕快照后,就可以用于保存或者编码输出。下面讨论了两种方法,并给出程序的部分代码和运行截图。# t& |6 a- q1 q& z
* {! L/ r) N3 d, d0 m, u" D9 g! X" N
- ~* `1 P0 ]% s; v3 s) q t5 G5 ^/ ^. I# Z5 u( C0 D9 i
用GDI方法捕获
, y# z9 N8 B" b/ X& O+ ~" @ q8 h4 K+ ?! [7 Q/ k6 J; N q
如果性能不是问题,并且我们需要的只是屏幕快照,就可以考虑采用GDI方法。这种方法基于桌面也是一个窗口的基本思想(桌面也有窗口句柄和设备上下文),只要得到被捕获桌面的上下文,就可以用普通的方法把内容blit到我们应用定义的上下文中。如果知道桌面句柄(可以用函数GetDesktopWindow()函数得到),得到上下文是很直接的。步骤如下:# P+ ]4 A+ V5 t r, b/ [
7 m0 ]9 Q _! @# V |, K) j" p
1) 使用GetDesktopWindow获得桌面句柄
* ~6 K! F2 s' A" w- ?) ^' k, W+ l- |9 B; H; [5 f' @/ @% m8 b8 Z
2) 使用函数GetDC获得桌面窗口的DC
( r+ S+ V" H, w* l0 I
" n% M4 {7 z* O5 p4 b z7 Z" a3) 为桌面DC创建一个兼容的DC和一个用于选入兼容DC的兼容位图。可以用函数CreateCompatibleDC和CreateCompatibleBitmap;可以用SelectObject将位图选入我们的DC+ a6 Q$ K7 Z2 e( G* \
# l7 O' s( K# @ {; V; _+ {4) 不管何时准备捕获屏幕,blit桌面DC内容到创建的兼容的DC,我们创建的兼容位图现在包含了捕获时刻的屏幕内容
" p6 b/ C2 F) `4 g& ]# j7 V- w, u/ w5 J5 f' \! Y2 [
5) 当完成捕获后,不要忘记释放对象
& B8 o( e, V8 d" G5 D( I. A0 l5 C: F6 P) I2 K7 f- c0 E& i4 M
例子:
4 G) w4 q' c U- _6 {) g5 Z6 r1 i5 x
Void CaptureScreen()" p9 A, j& F2 V( A
{
' {* K% h# Y* A+ a; x Y int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);6 G4 G+ e7 J) l$ r5 p8 |* y
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
) O$ p. u) X- V/ n7 A. I# [ HWND hDesktopWnd = GetDesktopWindow();9 H- E+ p+ E. ^+ w( P: q9 y3 F# j
HDC hDesktopDC = GetDC(hDesktopWnd);
& t5 ]8 x7 d& K2 S HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);
/ [- h! s- ?9 t) p1 j HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC,
( _! p5 A, q5 D% z0 ?9 T( @ nScreenWidth, nScreenHeight);
+ g6 w3 ^: ~( G( h1 w. i* a SelectObject(hCaptureDC,hCaptureBitmap); 1 O* V2 L# z0 {- ]% m& F. F
BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,' j$ Z: M4 w( m0 G; U# n. `9 B, h2 v
hDesktopDC,0,0,SRCCOPY|CAPTUREBLT);
2 m3 w5 P) e+ R# ]$ c SaveCapturedBitmap(hCaptureBitmap); //占位符号,可以在此处放入自己的代码& w+ v! E6 ^1 h1 R
ReleaseDC(hDesktopWnd,hDesktopDC);
1 P( m M: ]0 r$ m6 o/ f DeleteDC(hCaptureDC);& F7 m' Q, v2 P! e8 a
DeleteObject(hCaptureBitmap);
: v$ _; n- c; i* K; e& \$ z}8 w7 l1 s, j; y/ D* J8 z
上面的例子中,函数GetSystemMetrics()如果使用SM_CXSCREEN则返回屏幕宽,如果使用SM_CYSCREEN则返回屏幕的高。参考相关代码细节,我们很容易创建文件或者影片。
+ z3 b- S" q. p, b$ ]
- _: ?5 {/ @' [# T ! f) M" K( T. Q1 _
% e! M7 q' u1 O6 a" p, Y采用DirectX的方法:
, _: b& F' a7 C9 G$ Z7 p; @$ i4 s& R" p4 Z4 Q; Y0 j
由于DirectX提供了一个干净的方法,所以捕获屏幕快照在DirectX下非常容易。
7 u1 M& u6 v& D) B0 ^" K" j7 D# B, S9 G
每一个DirectX应用都包含了我们称为buffer或者surface的包含了相应于应用的视频内存,被称为后备缓冲。一些程序可能有超过一个后备缓冲。另外有一个所有程序能够存取的前端缓冲,该前端缓冲包含了桌面内容相关的视频内存,所以就是屏幕图像。; c d. I+ n9 P* R' L
+ K% |, m, M6 d( r/ C# y V k) n* V j8 K0 z4 e
5 W! S* k4 W7 ]; z! w4 F通过存取我们应用程序的前端缓冲,我们就可以捕获此刻的屏幕内容。存取应用的前端缓冲是简单直接的。接口IDirect3DDevice9提供了GetFrontBufferData()方法,带一个IDirect3DSurface9对象指针,拷贝前端缓冲的数据进入surface. IDirect3DSurfce9对象能够通过方法IDirect3DDevice8::CreateOffscreenPlainSurface()获得。一旦屏幕被捕获到surface,我们可以使用D3DXSaveSurfaceToFile()以位图方式保存表面到磁盘。捕获代码如下:
+ x( s; ^! d: r! h h) f
5 M9 g8 a( I: q2 T. d6 C- _extern IDirect3DDevice9* g_pd3dDevice;- T/ ^1 {- i/ U- t; I+ a
Void CaptureScreen()
' B) e* }& t* ^{: k: l# d; N; \6 ~" A" Y2 X# n2 P
IDirect3DSurface9* pSurface;4 s8 C$ S/ F' O; t, Q: E/ }
g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,, S5 {. j6 C3 x6 y
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);
3 x5 Q6 y3 t0 T- Q& e$ b g_pd3dDevice->GetFrontBufferData(0, pSurface);! ^3 L% |2 `/ {
D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL);8 S+ ]8 E. j& L9 X/ p2 X7 O6 Y* Z$ r
pSurface->Release();
7 M5 e# T) e$ d}
4 h8 x4 N" D, c: R! T2 |上面代码中的g_pd3dDevice是一个IDirect3DDevice9对象,假定已经正确的初始化了。代码片断直接保存捕获图像到磁盘。然而,代替保存到磁盘,我们可能想要直接操作这个位图。我们可以通过使用方法IDirect3DSurface9::LockRect()来做到这点,获得一个指向表面的指针。我们可以拷贝图像到应用内存然后操作他们。接下来的代码片断表明了如何拷贝表面到我们的应用内存:% q9 C/ z3 v7 A4 h5 [. u' m
* a7 V( _' i& t7 Xextern void* pBits;
. b0 P j* X8 e8 Yextern IDirect3DDevice9* g_pd3dDevice;" R* ^; O& K& E5 q; v, N" q* B! u
IDirect3DSurface9* pSurface;* z! g* t. S( \
g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,& p# n4 C% R$ }+ W& l
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH,
2 `" P% [5 B+ \+ _ &pSurface, NULL);
8 ~$ b( d/ @) Q+ j2 Jg_pd3dDevice->GetFrontBufferData(0, pSurface);) t3 I; }% [% v6 \
D3DLOCKED_RECT lockedRect;9 y% c) M" |" U1 V+ U
pSurface->LockRect(&lockedRect,NULL,
4 @" }5 a9 v4 f" o D3DLOCK_NO_DIRTY_UPDATE|3 M% g6 V' B* N7 Y3 B! Q
D3DLOCK_NOSYSLOCK|D3DLOCK_READONLY)));
6 n) }" a9 U2 O/ b* ~for( int i=0 ; i < ScreenHeight ; i++): {+ T) | n9 A. h# n3 k3 C" }
{
% ?* y0 @1 g) I/ {& w0 c+ g memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 , " }- H) ~6 P" ]3 J' h. ~
(BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
5 ]1 A7 \' ~) |+ Q6 [ ScreenWidth * BITSPERPIXEL / 8);
" @9 `( I1 k: y6 J$ T; H' _}1 A4 k( {! `0 H8 [# L
g_pSurface->UnlockRect();
7 K- ^ G, L7 Y, x( l. spSurface->Release();
( @# m: |& U/ V/ Q. P. j* S 0 t+ ?+ r- V, V* Z9 O0 o6 t
% k7 v, {' L4 d; [$ `) C1 ]
上面的pBits是一个void *,在拷贝之前要确认我们已经分配了足够的内存。一个BITSPERPIXEL的典型值是32位,然后,依赖于你的显示器设置。重要的是注意到表面的宽度与捕获屏幕的宽度并不相同,由于这个问题涉及到内存对齐,表面可能加入附加的部分到每行的结束来对齐到字边界。lockedRect.Pitch给出两个行之间的字节数目,也就是说,为了前进到下一行正确的点,我们应当推进Pitch,而不是Width.下面的代码反向的拷贝表面的字节:
7 u, `8 a# Y g1 E: x F
3 D) z$ M( j! \8 z5 o2 e! |for( int i=0 ; i < ScreenHeight ; i++)2 x/ v* l. @! W( x
{
0 |& c# `) s9 f/ d1 p5 y memcpy((BYTE*) pBits +( ScreenHeight - i - 1) * ' z: g7 f4 u9 j( d" ~; y* @
ScreenWidth * BITSPERPIXEL/8 ,
6 m; G3 A) x% n: W! J' k# A& B4 X (BYTE*) lockedRect.pBits + i* lockedRect.Pitch , 4 n3 w8 F8 |( j+ Z6 a& j3 U
ScreenWidth* BITSPERPIXEL/8);
5 B. Z" S, P! ^' f7 {& j$ C}
: S; w6 ?6 E. c) N
8 D/ X* R8 V1 j1 @. N* }: p
: h" q4 S9 r- O9 O& k9 N6 W I这点代码用于转换自顶而下和自底而上的位图。
/ ]1 n7 ~. N R/ e* a
0 ^8 Y% F3 W; i( c/ X上面的技术中LockRect是IDirect3DSurface9中存取捕获图像的一个方法,我们还有另外一个更加复杂的方法,GetDC方法。IDirect3DSurface9::GetDC()方法用来获得GDI兼容的设备上下文,这样就可以直接blit表面内容到我们应用定义的DC。
: M# u3 P/ j. A$ A* }8 p Q
1 V, V; s& I) y/ Q0 c: L
V2 f+ l3 U0 I0 B) I! l" q5 t0 i, v& F y7 r; q2 E5 A
然而,GetFrontBufferData是一个慢的方法,不应当用于性能临界的应用,GDI方法是更快的方法。- K N0 j+ a. ]# k( e# F' j$ ~
, _: j3 Y, e7 e# z# A
6 D' m4 F% f9 V% ^1 [9 d2 H8 X/ d+ _; e1 w/ e
捕获程序要点:0 E+ u8 y& {: |# z9 e; G- f5 e+ X2 G
' s% i0 q8 J4 p/ {* e 程序初始化的时候需要初始化相关资源,在WM_CREATE消息中初始化D3D,然后初始化FFMPEG,同时启动定时器;当用户选择开始捕获的时候,每次定时器到,处理WM_TIMER消息,锁定前端缓冲平面,然后拷贝数据到缓冲区,将该缓冲区传递给FFMPEG编码并输出;当用户停止的时候,停止定时器,清理D3D和FFMPEG,然后退出。下面是几个需要注意的技术点: k0 \1 i# Z# r( N- l
, o$ S7 _& b) i( i n5 `. u7 V4 \/ M& J \
1)屏幕的颜色深度,由于屏幕可能处于不同的颜色深度下,所以需要仔细区分,当GetDeviceCaps函数返回颜色深度为16的时候,需要区分是否RGB555或者RGB565,根据颜色深度来设定传递给FFMPEG的像素格式。
5 c' |( `; Z, e7 }. G5 I1 `
; ^- b+ b9 |3 u2)编码参数的设定,由于编码库支持的图像大小不是任意的,所以需要仔细的设置编码参数,否则不能初始化编码器。
$ z2 Y) B& t R, M, U$ P; v
0 ]! W) V' K5 J1 |3)图像转换的设定,由于原图像和目的图像的色彩空间和大小都不一样,所以需要仔细考虑图像转换,色彩空间的转换可以用img_convert,大小的转换可以用img_resample,注意到大小转换只针对YUV420P,所以在本案例中是先转换色彩空间,从RGBA32转换到YUV420P,然后调用重采样函数改变大小。
) K) `2 A* R) k* W, ]+ V" o( K/ q& ^
# U1 W! h/ q+ ^9 R" i% i. Q; |, l4)编码图像的保存,调用avcodec_encode_video编码图像,然后直接写入文件即可,如果想要从网络输出,那么考虑rtp_callback函数回调,由库来做合适的分包,然后加上合适的头部发送
7 V0 Q. j3 u ]! x* q; _( N
* {8 ~# L1 s( B) \4 m7 p5)抓屏过程中,程序需要转换成托盘图标。保存为bmp的时候图像行是颠倒的,要把表面的第一行作为bmp的最后一样。传给解码器的是正常坐标
" T3 M$ f( W* n4 @. q2 s8 q" ? A1 p9 e5 ~
p' r9 x) W& E# V) {9 f2 b
. ~0 `! |; _- N4 m程序运行截图:
& y3 G" y+ }0 {# u9 @
; f" N' j/ D5 G( n, F A6 B# i- J# l, g
该图是采用的捕获工具所截获的H263纯码流通过VLC播放然后用快照功能获得,是CIF的分辨率。
; v6 O* [) y9 D/ s5 m3 l0 c ?7 d) b
! j8 O. s% |/ N, D) w; g一些可能的改进:+ Y8 d$ S7 j. v# c# o2 `
) Y, n) m! S3 b& P* i! L
1.针对DirectX和OpenGL的窗口捕获,这个需要采用hook技术,比较麻烦2 I- C$ N9 Z: N# m$ }2 i! ]; }
! G" m+ |+ b0 s
2.针对不同图像尺寸的编码,目前的库不能支持XGA的H263编码,4cif的编码出现了不规则捕获的问题,问题还没有查清楚,也许是能力问题,也许是程序设计问题。4 P) Z0 C8 |/ N
7 G4 ?5 G- S( d# S, f: G2 X
3.编程中的一些优化和保护措施没有做好,程序运行的时候占用的CPU资源比较大 |