原文7 z* U' _" B8 U! d" y; _! i
http://blog.csdn.net/matlab2000/archive/2006/10/30/1356752.aspx6 j7 H" D8 l6 [* a1 M8 x1 j
: Y9 h( j# A. B; a: z+ X 常常我们想要编程捕获整个屏幕的内容,下面将解释如何做到这一点。典型的,我们的选择是使用GDI或者DirectX。另外一个值得考虑的方法是Windows Media API,直接将捕获的屏幕图像编码成码流输出,不过本文中不讨论该方法,主要讨论前两种技术。每一种方法中,我们得到了屏幕快照后,就可以用于保存或者编码输出。下面讨论了两种方法,并给出程序的部分代码和运行截图。7 ` X; |$ s; o+ x0 a
+ }0 G' V+ c! g/ `5 ~ : u1 ]& y0 B+ U/ X
5 {1 E$ ^4 ]/ r K1 h- S3 h用GDI方法捕获
2 q3 g/ ]$ C' v/ w* F, S' B! B( s: @- D& v% l' [2 ]
如果性能不是问题,并且我们需要的只是屏幕快照,就可以考虑采用GDI方法。这种方法基于桌面也是一个窗口的基本思想(桌面也有窗口句柄和设备上下文),只要得到被捕获桌面的上下文,就可以用普通的方法把内容blit到我们应用定义的上下文中。如果知道桌面句柄(可以用函数GetDesktopWindow()函数得到),得到上下文是很直接的。步骤如下:
- @/ P P) _0 b1 z2 N! a: P4 T
{$ k; }6 }) V/ I1 x8 @1) 使用GetDesktopWindow获得桌面句柄
; ^* Z/ j( |6 U. P
" a n M& f2 F! B2) 使用函数GetDC获得桌面窗口的DC% Y1 k1 A6 {7 N+ s3 [, |/ T3 r2 n% p
) q( c1 G, f- o* F) `4 z6 [3) 为桌面DC创建一个兼容的DC和一个用于选入兼容DC的兼容位图。可以用函数CreateCompatibleDC和CreateCompatibleBitmap;可以用SelectObject将位图选入我们的DC
6 b; k6 M0 q+ ^# A9 a. u, q# n- P* `0 ?
4) 不管何时准备捕获屏幕,blit桌面DC内容到创建的兼容的DC,我们创建的兼容位图现在包含了捕获时刻的屏幕内容7 Q/ }( T b+ D3 Q* p/ X$ {+ L( t! S- v
) K+ t: D1 k+ U. f
5) 当完成捕获后,不要忘记释放对象
8 E2 n5 l) s: S; T, N$ a# ]
3 D7 U6 }8 J6 o( n. I2 m g1 `例子:
1 j! w! ?0 H. @! ^. J: b" `" v; g1 ~, j8 ?: L$ g4 B
Void CaptureScreen()
: U& I# Z( V) p- Q7 I' ~6 L{
\- t1 l2 D& K7 m7 s int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);* o& ?# {8 u" f7 i" L* p/ W( e- [
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
1 h2 \* q! G8 `$ N HWND hDesktopWnd = GetDesktopWindow();/ s1 U7 H& N5 O7 C3 c
HDC hDesktopDC = GetDC(hDesktopWnd);
' ^- C6 a; d) Q" L0 ? HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);% c4 @: q% X7 U1 t/ m" N
HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC,
+ n( G9 e5 ]) m8 T* a2 O nScreenWidth, nScreenHeight);
* i, F- t+ z4 v0 X SelectObject(hCaptureDC,hCaptureBitmap); 9 ^3 ^4 _! l1 T' D: m4 O
BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,
; g: e5 @8 W% }; \' O1 I: ~ hDesktopDC,0,0,SRCCOPY|CAPTUREBLT); ' f. ~; {8 N& |
SaveCapturedBitmap(hCaptureBitmap); //占位符号,可以在此处放入自己的代码
, d3 |% Z5 l6 ]. i6 X s ReleaseDC(hDesktopWnd,hDesktopDC);
3 k, {2 h, D6 K6 d, v! W: C" a DeleteDC(hCaptureDC);- P5 e1 l8 r1 w
DeleteObject(hCaptureBitmap);) `; h) P7 X" u! H0 p1 ]+ G L: d$ R
}4 y$ N& K& F8 b
上面的例子中,函数GetSystemMetrics()如果使用SM_CXSCREEN则返回屏幕宽,如果使用SM_CYSCREEN则返回屏幕的高。参考相关代码细节,我们很容易创建文件或者影片。
1 ?& W: ~8 r9 {# u" m$ j0 _6 P4 B' h1 }
' b0 S+ r# i1 H) T# `2 z: l
8 ]! i' L! {$ n
采用DirectX的方法:
# D) r g. T! m( ]8 E3 h1 W0 y: E7 w- b
由于DirectX提供了一个干净的方法,所以捕获屏幕快照在DirectX下非常容易。( T: a/ r! n6 V- L; b& X6 d5 n5 K
/ q% C7 f6 y% |) n每一个DirectX应用都包含了我们称为buffer或者surface的包含了相应于应用的视频内存,被称为后备缓冲。一些程序可能有超过一个后备缓冲。另外有一个所有程序能够存取的前端缓冲,该前端缓冲包含了桌面内容相关的视频内存,所以就是屏幕图像。
8 ^, W* V. v6 n% m3 _ b0 K" S3 F* j, D
& I- s3 Y, H4 T+ i" J0 i- C; ?5 j8 d: `. L% ~: Q7 J
通过存取我们应用程序的前端缓冲,我们就可以捕获此刻的屏幕内容。存取应用的前端缓冲是简单直接的。接口IDirect3DDevice9提供了GetFrontBufferData()方法,带一个IDirect3DSurface9对象指针,拷贝前端缓冲的数据进入surface. IDirect3DSurfce9对象能够通过方法IDirect3DDevice8::CreateOffscreenPlainSurface()获得。一旦屏幕被捕获到surface,我们可以使用D3DXSaveSurfaceToFile()以位图方式保存表面到磁盘。捕获代码如下:; X2 ], m5 k1 Z7 p# M2 i
4 U) @5 Z3 ~& rextern IDirect3DDevice9* g_pd3dDevice;- t: `( a, @2 G0 M" \( }) w& q5 {! R/ m
Void CaptureScreen()
4 }$ b+ }& _" d+ b: F7 Y) A- U5 m{
6 z! q! n( z. X# H6 i) F: H IDirect3DSurface9* pSurface;* g: C) ~" s* j$ O( S9 x. L6 @
g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,
$ b$ S& J+ O1 r D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);& z/ ]( z& L2 {( `( [
g_pd3dDevice->GetFrontBufferData(0, pSurface);9 e3 {7 u& R" |, F V
D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL);+ {; h9 j9 e- x1 i5 w. b0 d
pSurface->Release();
5 p3 `; ^* q" l! \) @}' ~5 V0 a2 N$ j! p1 [- G4 z
上面代码中的g_pd3dDevice是一个IDirect3DDevice9对象,假定已经正确的初始化了。代码片断直接保存捕获图像到磁盘。然而,代替保存到磁盘,我们可能想要直接操作这个位图。我们可以通过使用方法IDirect3DSurface9::LockRect()来做到这点,获得一个指向表面的指针。我们可以拷贝图像到应用内存然后操作他们。接下来的代码片断表明了如何拷贝表面到我们的应用内存:
& v8 X& F6 E7 G$ i: c; ?; s8 J/ U5 z3 g. k
extern void* pBits;
: N0 D q/ c5 yextern IDirect3DDevice9* g_pd3dDevice;$ p, i3 a8 w) N1 ~/ Z, b6 a
IDirect3DSurface9* pSurface; ?- u7 n7 Y# p5 z6 e' t1 _6 z
g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight, ?" l3 [5 i8 f
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, 4 s6 c* }2 H4 ?% r
&pSurface, NULL);# X2 K' U! z1 P+ {5 _
g_pd3dDevice->GetFrontBufferData(0, pSurface);
( b$ K2 [7 o- G1 z1 |# |D3DLOCKED_RECT lockedRect;
6 L( l7 V c* [2 U: d$ W. upSurface->LockRect(&lockedRect,NULL,2 c1 U. o! i' d) `; y% P- j
D3DLOCK_NO_DIRTY_UPDATE|6 x3 _0 Y. t. |+ D& X/ E
D3DLOCK_NOSYSLOCK|D3DLOCK_READONLY)));
! n/ b7 C1 n$ efor( int i=0 ; i < ScreenHeight ; i++)
8 L% s# t* ]0 r1 Y1 B+ D{- r. O3 \: R5 c: K/ Q
memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 , " j3 x* D- m0 U# i+ G. |
(BYTE*) lockedRect.pBits + i* lockedRect.Pitch , 7 G) B6 z' Q) I% U, X4 _% E6 `, P
ScreenWidth * BITSPERPIXEL / 8);
* P; j" a+ q. ]; \ M}
/ t5 c* A: m( l4 r- C3 q7 hg_pSurface->UnlockRect();
+ T% `8 R% V, N6 YpSurface->Release(); m* J9 l5 E6 V6 L9 ~- W6 D. L7 m
+ y! e5 R4 Y$ F/ c4 m; g
& e$ r2 _& {0 W* {上面的pBits是一个void *,在拷贝之前要确认我们已经分配了足够的内存。一个BITSPERPIXEL的典型值是32位,然后,依赖于你的显示器设置。重要的是注意到表面的宽度与捕获屏幕的宽度并不相同,由于这个问题涉及到内存对齐,表面可能加入附加的部分到每行的结束来对齐到字边界。lockedRect.Pitch给出两个行之间的字节数目,也就是说,为了前进到下一行正确的点,我们应当推进Pitch,而不是Width.下面的代码反向的拷贝表面的字节:
4 q$ d0 X! w+ q; m: W0 i9 b, L
- w( v" M, G; kfor( int i=0 ; i < ScreenHeight ; i++)
2 d8 [/ J" C P4 e! ~- V0 r{' v% h! Q/ m2 g
memcpy((BYTE*) pBits +( ScreenHeight - i - 1) * ' v( h& b$ \3 Q2 s* Z2 o& a
ScreenWidth * BITSPERPIXEL/8 ,
$ o5 ^6 ~, w! R, S" I7 a (BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
( ^2 c% K x6 A: D. [" b: g+ ~- K+ c ScreenWidth* BITSPERPIXEL/8);' T8 w3 t$ @) b, q" c n% o
}
7 V$ N3 F$ a% Q4 Y/ b # R1 w- k" L% }+ a, z# x0 T
: A7 y) f- G) S. a& ]0 m1 o这点代码用于转换自顶而下和自底而上的位图。6 L2 P7 p' k& P7 p, D5 c5 ~
) K4 K2 W7 |2 ~ r上面的技术中LockRect是IDirect3DSurface9中存取捕获图像的一个方法,我们还有另外一个更加复杂的方法,GetDC方法。IDirect3DSurface9::GetDC()方法用来获得GDI兼容的设备上下文,这样就可以直接blit表面内容到我们应用定义的DC。
! m: B( s+ g6 R0 l7 M3 a4 |' Q" f* e5 L( h o" G% m
5 m# ?. {1 a8 N, Q' Z y m/ O* N9 U
$ k0 L, O% O. ?0 ^2 t0 ]' Y然而,GetFrontBufferData是一个慢的方法,不应当用于性能临界的应用,GDI方法是更快的方法。# T% O6 y" T# w6 P4 f7 C- t
9 N' V6 C/ Z* O' S7 u
7 q, t0 r$ i$ {2 k- l
0 W# C5 ]6 @6 S' E捕获程序要点:
* D5 g0 j* R4 L/ y9 k# x: Y1 N+ L. J# ^5 A- G4 w+ L
程序初始化的时候需要初始化相关资源,在WM_CREATE消息中初始化D3D,然后初始化FFMPEG,同时启动定时器;当用户选择开始捕获的时候,每次定时器到,处理WM_TIMER消息,锁定前端缓冲平面,然后拷贝数据到缓冲区,将该缓冲区传递给FFMPEG编码并输出;当用户停止的时候,停止定时器,清理D3D和FFMPEG,然后退出。下面是几个需要注意的技术点:, T# j) {( G* m; t! ~" z5 V7 A
|! }# x* N4 b, S2 k
1)屏幕的颜色深度,由于屏幕可能处于不同的颜色深度下,所以需要仔细区分,当GetDeviceCaps函数返回颜色深度为16的时候,需要区分是否RGB555或者RGB565,根据颜色深度来设定传递给FFMPEG的像素格式。
) p) g( F+ h+ C' r3 L# T9 e
2 ^& p. }% v0 x# W2)编码参数的设定,由于编码库支持的图像大小不是任意的,所以需要仔细的设置编码参数,否则不能初始化编码器。( V$ i6 G% Q( Y4 d$ Q
1 g- ~0 A a. }' a0 [. T5 ]5 B3)图像转换的设定,由于原图像和目的图像的色彩空间和大小都不一样,所以需要仔细考虑图像转换,色彩空间的转换可以用img_convert,大小的转换可以用img_resample,注意到大小转换只针对YUV420P,所以在本案例中是先转换色彩空间,从RGBA32转换到YUV420P,然后调用重采样函数改变大小。5 q; M7 Y% a! j2 Q" z, p: C' _8 \* D
3 e* A( c, b7 c4)编码图像的保存,调用avcodec_encode_video编码图像,然后直接写入文件即可,如果想要从网络输出,那么考虑rtp_callback函数回调,由库来做合适的分包,然后加上合适的头部发送4 l+ r$ ~8 H) _! W2 Y$ @2 m
6 R8 Q3 R# e/ T5)抓屏过程中,程序需要转换成托盘图标。保存为bmp的时候图像行是颠倒的,要把表面的第一行作为bmp的最后一样。传给解码器的是正常坐标& J" y- a9 q1 q/ i, o& @
/ S3 Z8 V5 l5 s7 v7 P7 Z! g9 U' G$ |2 R
9 x$ K9 G, e0 N& G2 u& S2 w i& d' G& l Z0 N ?) O5 X! Q9 t( P
程序运行截图:
9 T8 t/ r7 H' S- K
' \# e" M# k9 ^: h) ?5 F
2 O7 m/ h( y/ q( ?) }该图是采用的捕获工具所截获的H263纯码流通过VLC播放然后用快照功能获得,是CIF的分辨率。3 e" q# k3 v6 H+ J) y2 Q/ u- ~
* w% ^4 F7 e7 r& E一些可能的改进:2 {" I& F- ?" j+ j) {9 O
- x3 T/ V5 ?+ y- f& b' O2 E& d1.针对DirectX和OpenGL的窗口捕获,这个需要采用hook技术,比较麻烦) P& e: z2 [3 W% z
! e; |4 j& [' F* A7 v1 e) @! f* T2.针对不同图像尺寸的编码,目前的库不能支持XGA的H263编码,4cif的编码出现了不规则捕获的问题,问题还没有查清楚,也许是能力问题,也许是程序设计问题。" U4 i$ U/ J+ M$ O/ g
; E! s. G+ m% d: G& n6 M3.编程中的一些优化和保护措施没有做好,程序运行的时候占用的CPU资源比较大 |