原文
( _- w3 J( D5 S! m- l" Jhttp://blog.csdn.net/matlab2000/archive/2006/10/30/1356752.aspx; i3 B& _, N, D( N* P, N
8 h/ Q3 B! e2 V
常常我们想要编程捕获整个屏幕的内容,下面将解释如何做到这一点。典型的,我们的选择是使用GDI或者DirectX。另外一个值得考虑的方法是Windows Media API,直接将捕获的屏幕图像编码成码流输出,不过本文中不讨论该方法,主要讨论前两种技术。每一种方法中,我们得到了屏幕快照后,就可以用于保存或者编码输出。下面讨论了两种方法,并给出程序的部分代码和运行截图。
: K; ~9 u" F6 I% b; O, \$ E8 y) H
; {% C+ y6 X, Y1 P
: Y! B1 Q5 Z5 V4 J- O- ~4 Z7 f; g9 J! V" \( Q+ y: `
用GDI方法捕获
1 G6 L/ e! j4 ?& z4 M, d+ E2 ~
1 T" S4 Z/ k. w% R. L* d 如果性能不是问题,并且我们需要的只是屏幕快照,就可以考虑采用GDI方法。这种方法基于桌面也是一个窗口的基本思想(桌面也有窗口句柄和设备上下文),只要得到被捕获桌面的上下文,就可以用普通的方法把内容blit到我们应用定义的上下文中。如果知道桌面句柄(可以用函数GetDesktopWindow()函数得到),得到上下文是很直接的。步骤如下: m/ K1 G- _7 J3 ^ b" ?9 g
# O6 a- ~9 B6 `2 i" H1) 使用GetDesktopWindow获得桌面句柄
1 A6 o3 R# t+ u5 q' H/ Q0 j3 Q c; ?' i* w
2) 使用函数GetDC获得桌面窗口的DC# t: k- z+ |. d* l+ O/ y% [ Q
. ^" N$ r/ B) ?1 W( E8 V b, [+ i
3) 为桌面DC创建一个兼容的DC和一个用于选入兼容DC的兼容位图。可以用函数CreateCompatibleDC和CreateCompatibleBitmap;可以用SelectObject将位图选入我们的DC$ ]/ L/ R* |' W) U
) F" n$ l) L) x' x) p9 Z: O8 `4) 不管何时准备捕获屏幕,blit桌面DC内容到创建的兼容的DC,我们创建的兼容位图现在包含了捕获时刻的屏幕内容
2 c) \- X, I9 q6 R9 D" f% B- [6 z. `7 ?& O
5) 当完成捕获后,不要忘记释放对象$ T- s/ N1 g8 m2 n" H3 ~
8 g0 M4 h9 N w. h# o y
例子:8 b6 R1 e$ E. c. h* r0 K p
: p$ [3 y5 Y6 x' s" r5 R/ |- ]
Void CaptureScreen()7 n* @% }8 U& J
{: f7 n9 L4 s0 J R
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);
: a5 Q2 W) ^. z9 N int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);5 l" \5 W. q; C% |! M3 N/ i( E
HWND hDesktopWnd = GetDesktopWindow();
& q* S3 k& j2 l1 u' ?4 l+ l: l HDC hDesktopDC = GetDC(hDesktopWnd);
1 Y8 N: k* C* c" |! E3 `( _ HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);7 V4 q3 ~" C7 a6 }+ b+ q
HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC, . {' Z! e) |8 Y- q; R
nScreenWidth, nScreenHeight);7 H0 n1 z0 u9 Z' H7 ]$ t
SelectObject(hCaptureDC,hCaptureBitmap);
! L1 w, n: }, D* H- j BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,
1 C. c+ k0 j: i1 B7 z [ hDesktopDC,0,0,SRCCOPY|CAPTUREBLT);
: W; \6 A# P' }4 r; C& M9 F SaveCapturedBitmap(hCaptureBitmap); //占位符号,可以在此处放入自己的代码4 u4 c1 l8 S! T$ ? j% n
ReleaseDC(hDesktopWnd,hDesktopDC);) [* |( F9 A$ q; u% ^. F7 x7 [
DeleteDC(hCaptureDC);
0 o: O9 V% }( H4 [ DeleteObject(hCaptureBitmap);7 m& ?( ~1 q8 s( d
}) J9 Y" ^2 |* J# j" ^ [+ O2 E
上面的例子中,函数GetSystemMetrics()如果使用SM_CXSCREEN则返回屏幕宽,如果使用SM_CYSCREEN则返回屏幕的高。参考相关代码细节,我们很容易创建文件或者影片。
8 C. M S6 F/ P. n& _' M4 v; Q) Q) c4 X/ F! }/ |8 T
7 t/ `6 \& W# i8 C
( @3 u8 J- B8 O* G j& S' n/ X
采用DirectX的方法:* B5 R4 O/ L- \) k+ t; i, K
* _8 X$ Q6 w2 ?4 Y/ r" E2 f
由于DirectX提供了一个干净的方法,所以捕获屏幕快照在DirectX下非常容易。 k, v: b( q4 h* [* Z5 M' S
6 O y6 E& C! q+ X5 I2 [- u" Z5 P每一个DirectX应用都包含了我们称为buffer或者surface的包含了相应于应用的视频内存,被称为后备缓冲。一些程序可能有超过一个后备缓冲。另外有一个所有程序能够存取的前端缓冲,该前端缓冲包含了桌面内容相关的视频内存,所以就是屏幕图像。0 e+ }9 I9 A4 X" i
3 h$ F* [6 W3 o9 m {* V
5 C" {. ~& Z* S
% f2 e/ G! m& S' f* L# m
通过存取我们应用程序的前端缓冲,我们就可以捕获此刻的屏幕内容。存取应用的前端缓冲是简单直接的。接口IDirect3DDevice9提供了GetFrontBufferData()方法,带一个IDirect3DSurface9对象指针,拷贝前端缓冲的数据进入surface. IDirect3DSurfce9对象能够通过方法IDirect3DDevice8::CreateOffscreenPlainSurface()获得。一旦屏幕被捕获到surface,我们可以使用D3DXSaveSurfaceToFile()以位图方式保存表面到磁盘。捕获代码如下:
/ n# G. j+ o4 Q$ W0 J9 R; b9 m" ?- e2 f/ q0 p( Q! N9 n) l
extern IDirect3DDevice9* g_pd3dDevice;
" G3 H9 w0 C8 {Void CaptureScreen()
& e* _% k i: E2 ~: m/ H3 ^1 Y& {{
}, {: k. L4 G+ _+ a IDirect3DSurface9* pSurface;
) n# L ]+ W; A1 }2 { g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,3 d) u" d3 I6 p. O0 I( x/ S( R. y; [
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);
5 A$ ^" x! e4 S: u g_pd3dDevice->GetFrontBufferData(0, pSurface);
) J* J: B5 ]! Y+ } P D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL);3 ~; [4 Q; `. a3 l# x0 a4 A0 y3 h
pSurface->Release(); 4 h* h: a) t/ e' t; t+ @. [& U
}& B) ?( _6 V; Y! h8 d) e# b
上面代码中的g_pd3dDevice是一个IDirect3DDevice9对象,假定已经正确的初始化了。代码片断直接保存捕获图像到磁盘。然而,代替保存到磁盘,我们可能想要直接操作这个位图。我们可以通过使用方法IDirect3DSurface9::LockRect()来做到这点,获得一个指向表面的指针。我们可以拷贝图像到应用内存然后操作他们。接下来的代码片断表明了如何拷贝表面到我们的应用内存:1 o Z7 \* T& s [) y
! Q) Q; R# m$ R4 oextern void* pBits;
5 F( a, \$ ?" Mextern IDirect3DDevice9* g_pd3dDevice;
' f/ e* P2 g ~- y$ ]IDirect3DSurface9* pSurface;& v2 F2 S. \2 F' K/ }% H
g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,
& j7 K! a' K; D9 r* o D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH,
4 q0 J4 \* k r& S% W &pSurface, NULL);0 E8 U$ `# m2 q- m# h
g_pd3dDevice->GetFrontBufferData(0, pSurface);
9 A4 g8 ^" g& R7 E+ ]D3DLOCKED_RECT lockedRect;% `* }1 ?* E$ w) w# e& y
pSurface->LockRect(&lockedRect,NULL,
# A$ ~2 v8 N# R) d D3DLOCK_NO_DIRTY_UPDATE|
3 S% K Q/ M6 m: V& e J D3DLOCK_NOSYSLOCK|D3DLOCK_READONLY)));
2 Q0 F; A' h0 Q. u% `0 J' Cfor( int i=0 ; i < ScreenHeight ; i++)
+ J# _0 g+ w8 k. u" }) ~& W' M{
3 b- ^8 u1 H& z' ^2 z$ a2 V memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 ,
2 b5 A9 u( S7 P( Z (BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
* T6 Y2 f3 D# h- m ScreenWidth * BITSPERPIXEL / 8);
" t& B+ U2 L/ d c* [# I}7 ~% Y( _! ]. P5 e
g_pSurface->UnlockRect();
. @' ?4 {& v5 x' E; P: spSurface->Release();! p" A2 I' o) b' }
, R# J" T* z! v5 }2 ^+ D+ a* |
. x B+ B p7 [: I( }. [
上面的pBits是一个void *,在拷贝之前要确认我们已经分配了足够的内存。一个BITSPERPIXEL的典型值是32位,然后,依赖于你的显示器设置。重要的是注意到表面的宽度与捕获屏幕的宽度并不相同,由于这个问题涉及到内存对齐,表面可能加入附加的部分到每行的结束来对齐到字边界。lockedRect.Pitch给出两个行之间的字节数目,也就是说,为了前进到下一行正确的点,我们应当推进Pitch,而不是Width.下面的代码反向的拷贝表面的字节:' ?% U2 b1 g! ?8 N0 ^
1 \# e! {$ K4 z( x4 ]for( int i=0 ; i < ScreenHeight ; i++)- O7 } B# }4 d9 Q, |
{& H( _; F$ @2 l0 a- Q" V* l' u
memcpy((BYTE*) pBits +( ScreenHeight - i - 1) * 6 m& o3 l; V" l; U! N: g. P3 M
ScreenWidth * BITSPERPIXEL/8 ,
! t* V; F: n! x! D (BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
; Q0 _: L* L) f, H+ D ScreenWidth* BITSPERPIXEL/8);7 O- I" Z# B: M; |& j
}
$ h% I, }- r0 F- x4 D6 O v6 J% ]9 O 5 e. g7 A8 R1 A, X% j
3 _' L; O. g1 W1 A* |8 V
这点代码用于转换自顶而下和自底而上的位图。
- f0 t1 A( l; \" I f. |) _* Q6 K4 G" o
上面的技术中LockRect是IDirect3DSurface9中存取捕获图像的一个方法,我们还有另外一个更加复杂的方法,GetDC方法。IDirect3DSurface9::GetDC()方法用来获得GDI兼容的设备上下文,这样就可以直接blit表面内容到我们应用定义的DC。
5 b! Q! b1 P$ Z( ]9 v' w" d, j
: M5 f. ]! a" M1 U 1 \5 v* p+ M9 ~9 a- a Y
& H4 c8 t9 G( m5 i+ H然而,GetFrontBufferData是一个慢的方法,不应当用于性能临界的应用,GDI方法是更快的方法。
6 ^7 x# L6 z8 p+ @8 t6 }) w# L' w7 ]
- B" ^7 j g* A3 X3 ]- \" D9 v. j# o9 V" m# D
捕获程序要点:/ f% l# q2 R: e( u9 E) a/ g9 v
4 o A6 @: Q9 Q: n2 U& j 程序初始化的时候需要初始化相关资源,在WM_CREATE消息中初始化D3D,然后初始化FFMPEG,同时启动定时器;当用户选择开始捕获的时候,每次定时器到,处理WM_TIMER消息,锁定前端缓冲平面,然后拷贝数据到缓冲区,将该缓冲区传递给FFMPEG编码并输出;当用户停止的时候,停止定时器,清理D3D和FFMPEG,然后退出。下面是几个需要注意的技术点:1 b2 N! n" d2 }4 L% l+ }
: \" ?: {% s$ E+ Y! L' f( x, W
1)屏幕的颜色深度,由于屏幕可能处于不同的颜色深度下,所以需要仔细区分,当GetDeviceCaps函数返回颜色深度为16的时候,需要区分是否RGB555或者RGB565,根据颜色深度来设定传递给FFMPEG的像素格式。
' A7 f$ K3 t7 p2 V! ]0 `3 Y) w; R: i: p: m+ ]) p
2)编码参数的设定,由于编码库支持的图像大小不是任意的,所以需要仔细的设置编码参数,否则不能初始化编码器。3 M0 ?9 c" k8 j T0 ^2 d8 l
# N1 x2 n8 o' V1 h2 S5 K
3)图像转换的设定,由于原图像和目的图像的色彩空间和大小都不一样,所以需要仔细考虑图像转换,色彩空间的转换可以用img_convert,大小的转换可以用img_resample,注意到大小转换只针对YUV420P,所以在本案例中是先转换色彩空间,从RGBA32转换到YUV420P,然后调用重采样函数改变大小。
4 T- a1 w$ Z) D1 T2 B# |
! R# s! }: W5 D0 X* |2 P! O2 T4)编码图像的保存,调用avcodec_encode_video编码图像,然后直接写入文件即可,如果想要从网络输出,那么考虑rtp_callback函数回调,由库来做合适的分包,然后加上合适的头部发送
4 }# ^' s4 [( f! f
2 P7 Q9 ~" M) J J5 g' z5)抓屏过程中,程序需要转换成托盘图标。保存为bmp的时候图像行是颠倒的,要把表面的第一行作为bmp的最后一样。传给解码器的是正常坐标
/ n) A' }" ~* I1 A. `: j! j
6 c1 z2 ]6 _1 O( t6 I8 c6 }* y
+ V. D A% U! q
; u/ |1 b5 l }% k程序运行截图:
& T' E7 c! S, e0 F+ q
* G8 S. n' {3 m1 m6 h3 ?- G8 t6 q2 c3 C2 | U/ o: b7 U8 {9 _& J
该图是采用的捕获工具所截获的H263纯码流通过VLC播放然后用快照功能获得,是CIF的分辨率。2 o% J1 O" N/ u, c; F+ O9 X, r
" [4 u) U& Y4 Y k' } ~$ W' `. r
一些可能的改进:
7 D1 z- k1 s2 S7 X/ P- d6 R
* m5 r4 F4 X9 T1.针对DirectX和OpenGL的窗口捕获,这个需要采用hook技术,比较麻烦
3 T3 B- M, P5 x2 }9 p/ V7 F6 D6 @% Q% ^7 N* c
2.针对不同图像尺寸的编码,目前的库不能支持XGA的H263编码,4cif的编码出现了不规则捕获的问题,问题还没有查清楚,也许是能力问题,也许是程序设计问题。
+ l& M( n; Q* V `# X- d
0 Q1 ~5 D& N4 G1 @) G: N3.编程中的一些优化和保护措施没有做好,程序运行的时候占用的CPU资源比较大 |