原文
; [3 E8 T; o6 K( r$ Ehttp://blog.csdn.net/matlab2000/archive/2006/10/30/1356752.aspx
3 q0 L& m. {- \2 |" i
f* S' C& x. }- v2 \ 常常我们想要编程捕获整个屏幕的内容,下面将解释如何做到这一点。典型的,我们的选择是使用GDI或者DirectX。另外一个值得考虑的方法是Windows Media API,直接将捕获的屏幕图像编码成码流输出,不过本文中不讨论该方法,主要讨论前两种技术。每一种方法中,我们得到了屏幕快照后,就可以用于保存或者编码输出。下面讨论了两种方法,并给出程序的部分代码和运行截图。; _5 H, m% Q# M( {) V( V# c* p8 u
; h& ?6 f/ t" u, S; E; u
* q# [' l _& V* N4 y& e
4 h" ]3 Q4 c/ v9 G0 Z( L% c! b用GDI方法捕获5 `& Q2 Z* ~: g4 J9 \7 ?/ N
3 ~1 ^8 m1 {$ C' T# t2 K 如果性能不是问题,并且我们需要的只是屏幕快照,就可以考虑采用GDI方法。这种方法基于桌面也是一个窗口的基本思想(桌面也有窗口句柄和设备上下文),只要得到被捕获桌面的上下文,就可以用普通的方法把内容blit到我们应用定义的上下文中。如果知道桌面句柄(可以用函数GetDesktopWindow()函数得到),得到上下文是很直接的。步骤如下:
5 Z& N7 Q9 a1 H2 F9 S5 n- K( P9 i/ x7 v7 c; n$ k* y
1) 使用GetDesktopWindow获得桌面句柄4 i2 N! ~+ z% @' K8 e
6 R5 h. }0 c [6 t* o
2) 使用函数GetDC获得桌面窗口的DC
! T5 B# z# z9 ~2 Y* ^; b9 ^
: x# M2 v- R7 D8 |2 s+ h3) 为桌面DC创建一个兼容的DC和一个用于选入兼容DC的兼容位图。可以用函数CreateCompatibleDC和CreateCompatibleBitmap;可以用SelectObject将位图选入我们的DC
" S# p: a0 o$ n) g7 d# |+ k5 |) `+ {0 m, h' [1 W' K& v
4) 不管何时准备捕获屏幕,blit桌面DC内容到创建的兼容的DC,我们创建的兼容位图现在包含了捕获时刻的屏幕内容
- u* @( @2 c0 C- I% w H2 F# m
: D# i5 o2 m/ R2 U5) 当完成捕获后,不要忘记释放对象* b9 @& C/ ^! ~$ L. j7 w7 K4 s
6 l- j e+ J, ^例子:
: X- [2 y) u) k* ] S& g, n
' N* n- I: ?1 [0 M3 eVoid CaptureScreen()
- S% s% o, r* z! r' w8 y$ ?/ L{0 \) E6 P0 m' I1 m( d- U% q$ S
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN); }2 R: \, _+ t! h6 s
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
2 `4 U8 H/ u/ z+ `+ k- P# i HWND hDesktopWnd = GetDesktopWindow();
/ q! V1 Z5 V; }/ s4 x8 ]5 t" V HDC hDesktopDC = GetDC(hDesktopWnd);
5 p& p; R' K* S" \* y HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);8 a4 n6 q8 v2 q; {
HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC, # o+ o ?" @5 H( q' o
nScreenWidth, nScreenHeight);2 x- e: J; i. G2 D8 Z( z; e Q5 p
SelectObject(hCaptureDC,hCaptureBitmap); ; ~- Q( L" s9 Q) i, P
BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,
2 `3 S- Q0 l5 }* `# N4 X) i hDesktopDC,0,0,SRCCOPY|CAPTUREBLT); $ _6 q7 p& B% w1 m
SaveCapturedBitmap(hCaptureBitmap); //占位符号,可以在此处放入自己的代码
, l2 X& S6 n! x. i ReleaseDC(hDesktopWnd,hDesktopDC);& ~9 ]9 J; ~! X7 S
DeleteDC(hCaptureDC);( d( U4 o# w2 P3 h
DeleteObject(hCaptureBitmap);
@7 X8 y8 Z% A}
3 Y) r# r. k1 I- U1 w% A上面的例子中,函数GetSystemMetrics()如果使用SM_CXSCREEN则返回屏幕宽,如果使用SM_CYSCREEN则返回屏幕的高。参考相关代码细节,我们很容易创建文件或者影片。2 V& B: D9 T) Y- ?' ?$ D
' F- V/ R% i t! U. S! w
- {- I; ^; Y' I! b0 r9 r+ U
* u- p. Y8 z t4 q采用DirectX的方法:/ X9 p/ `# G( C5 d8 C% Z2 r
7 Z/ |5 B4 A3 h6 Z9 R 由于DirectX提供了一个干净的方法,所以捕获屏幕快照在DirectX下非常容易。
0 b1 x, l" t3 ^# X; h4 }* F2 Y, U! ^0 }& T. O
每一个DirectX应用都包含了我们称为buffer或者surface的包含了相应于应用的视频内存,被称为后备缓冲。一些程序可能有超过一个后备缓冲。另外有一个所有程序能够存取的前端缓冲,该前端缓冲包含了桌面内容相关的视频内存,所以就是屏幕图像。
! R" z. c2 g* }2 u/ q9 P& A L) [) g9 \/ |
9 w6 K9 M U [$ K" K* [8 I+ {( R& V9 V6 Z* [/ U1 h( u& P
通过存取我们应用程序的前端缓冲,我们就可以捕获此刻的屏幕内容。存取应用的前端缓冲是简单直接的。接口IDirect3DDevice9提供了GetFrontBufferData()方法,带一个IDirect3DSurface9对象指针,拷贝前端缓冲的数据进入surface. IDirect3DSurfce9对象能够通过方法IDirect3DDevice8::CreateOffscreenPlainSurface()获得。一旦屏幕被捕获到surface,我们可以使用D3DXSaveSurfaceToFile()以位图方式保存表面到磁盘。捕获代码如下:
9 _- n- {2 l6 Y& Z) {" X+ |* O- t+ B
extern IDirect3DDevice9* g_pd3dDevice;2 U+ Y! n3 H& m1 k
Void CaptureScreen()
% H6 Q! H2 `' l! F6 V% V{6 u; p; \1 a' A/ q0 i8 z+ A
IDirect3DSurface9* pSurface;
9 z( k% O3 ]& ~$ I& ^ g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,
) n6 K6 H" g( x$ ^* j# t D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);
- E% _8 }1 F+ z. ^ g_pd3dDevice->GetFrontBufferData(0, pSurface);6 Q7 `& E( [; Y. ?' u4 g! i- P! o
D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL); l* m1 k! I: C3 p4 o( |$ j M
pSurface->Release();
3 m" B4 z4 Z7 G} W* I6 l5 G" E* J3 p# d) W
上面代码中的g_pd3dDevice是一个IDirect3DDevice9对象,假定已经正确的初始化了。代码片断直接保存捕获图像到磁盘。然而,代替保存到磁盘,我们可能想要直接操作这个位图。我们可以通过使用方法IDirect3DSurface9::LockRect()来做到这点,获得一个指向表面的指针。我们可以拷贝图像到应用内存然后操作他们。接下来的代码片断表明了如何拷贝表面到我们的应用内存:, t8 P' b* u' d0 m$ I& J
; W# C, K, N; w* j8 n
extern void* pBits;. `8 H8 f: e4 A' h9 X5 a6 t
extern IDirect3DDevice9* g_pd3dDevice;
8 g% W* W+ d: S) k# b, YIDirect3DSurface9* pSurface;! E2 q6 k1 ^. U. q7 t
g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight," ^. a& \) b+ i- s+ e; b
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, . ]3 k6 }' h% m) ?2 C6 i( m
&pSurface, NULL);
7 u- D, o; M7 Kg_pd3dDevice->GetFrontBufferData(0, pSurface);
$ W& k" W& K0 H! t, I, bD3DLOCKED_RECT lockedRect;
# f3 @' d, Q, A9 R6 b5 RpSurface->LockRect(&lockedRect,NULL,
. O0 y% M/ s8 n* j D3DLOCK_NO_DIRTY_UPDATE|
- B& l: l+ j5 t' s" q/ [ D3DLOCK_NOSYSLOCK|D3DLOCK_READONLY)));
: K% j% c) |' t- r( `9 b/ _for( int i=0 ; i < ScreenHeight ; i++)8 T( J) S7 S: `% g$ m
{ K$ c$ y9 G n: V; P
memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 , 6 j8 ]* p; B( {4 T
(BYTE*) lockedRect.pBits + i* lockedRect.Pitch , * U5 h! X- x {9 k* k
ScreenWidth * BITSPERPIXEL / 8);! e8 ~+ a* f1 g! P! q
}
( ?; i4 k/ ~ p9 G2 ~2 ]g_pSurface->UnlockRect();3 k5 u/ U" B+ c* ?- g, ~
pSurface->Release();: Z1 i& [( B U" Z! |
3 y {* i* ~7 B& {! ~) t
7 [( w4 v- A& q/ g& c: v& p上面的pBits是一个void *,在拷贝之前要确认我们已经分配了足够的内存。一个BITSPERPIXEL的典型值是32位,然后,依赖于你的显示器设置。重要的是注意到表面的宽度与捕获屏幕的宽度并不相同,由于这个问题涉及到内存对齐,表面可能加入附加的部分到每行的结束来对齐到字边界。lockedRect.Pitch给出两个行之间的字节数目,也就是说,为了前进到下一行正确的点,我们应当推进Pitch,而不是Width.下面的代码反向的拷贝表面的字节:
. S( _2 m; p# C; {% {: e. k
1 v9 d' W2 D0 o" g+ ffor( int i=0 ; i < ScreenHeight ; i++)
- e* x5 l; Z. k# l7 O{
5 {& h* \) L. Z5 v memcpy((BYTE*) pBits +( ScreenHeight - i - 1) * ; y) X0 y) R! b, N
ScreenWidth * BITSPERPIXEL/8 , 7 t. \- } S, z" U
(BYTE*) lockedRect.pBits + i* lockedRect.Pitch , 8 _ k$ K' {8 u! O" j: {& ?! U: t& T
ScreenWidth* BITSPERPIXEL/8);
1 d) U% j, [3 E! j}
1 G3 }6 n0 W6 x9 L& [3 e
+ b, W; S2 H" \6 U2 s
+ |+ s( v3 \. f% ~这点代码用于转换自顶而下和自底而上的位图。
6 j3 C, N% r3 ?2 I9 I& w6 h& `4 P
7 R+ z2 J# X) Z3 b. c上面的技术中LockRect是IDirect3DSurface9中存取捕获图像的一个方法,我们还有另外一个更加复杂的方法,GetDC方法。IDirect3DSurface9::GetDC()方法用来获得GDI兼容的设备上下文,这样就可以直接blit表面内容到我们应用定义的DC。1 i' P4 X ?2 ~$ v; c6 D( t
0 I! k+ T7 v5 ~: ]7 ]* B. a % p2 D: h+ V& C2 N" S
8 T+ ^2 ~5 C- N O0 {然而,GetFrontBufferData是一个慢的方法,不应当用于性能临界的应用,GDI方法是更快的方法。
) i( C; }. p" i9 s. l* E7 n
3 k* R8 c. S& c \
: I/ @2 Y( g f$ Y) e+ K) \( t$ s6 }& M) n) j# D
捕获程序要点:
8 T- U8 [8 T E, l9 Q2 _4 t0 a5 e. j3 Y: M
程序初始化的时候需要初始化相关资源,在WM_CREATE消息中初始化D3D,然后初始化FFMPEG,同时启动定时器;当用户选择开始捕获的时候,每次定时器到,处理WM_TIMER消息,锁定前端缓冲平面,然后拷贝数据到缓冲区,将该缓冲区传递给FFMPEG编码并输出;当用户停止的时候,停止定时器,清理D3D和FFMPEG,然后退出。下面是几个需要注意的技术点:; a/ F+ ?( ^& I4 @. ?+ ], b1 }
. Z }% }7 v7 f$ C9 v, W) ~1)屏幕的颜色深度,由于屏幕可能处于不同的颜色深度下,所以需要仔细区分,当GetDeviceCaps函数返回颜色深度为16的时候,需要区分是否RGB555或者RGB565,根据颜色深度来设定传递给FFMPEG的像素格式。
/ Z* \$ z/ @9 B
1 q) [0 `4 C8 R% E2)编码参数的设定,由于编码库支持的图像大小不是任意的,所以需要仔细的设置编码参数,否则不能初始化编码器。2 N* e$ d* r7 O8 a; T# i) b' A+ N
T1 V+ u& H3 H6 o3 O2 i3)图像转换的设定,由于原图像和目的图像的色彩空间和大小都不一样,所以需要仔细考虑图像转换,色彩空间的转换可以用img_convert,大小的转换可以用img_resample,注意到大小转换只针对YUV420P,所以在本案例中是先转换色彩空间,从RGBA32转换到YUV420P,然后调用重采样函数改变大小。9 _0 Y7 a$ ?" w1 S4 O) t' Y
) n" U4 c. e% o4 Z8 { T
4)编码图像的保存,调用avcodec_encode_video编码图像,然后直接写入文件即可,如果想要从网络输出,那么考虑rtp_callback函数回调,由库来做合适的分包,然后加上合适的头部发送
+ V9 F5 J2 B, J5 ^/ |- {4 s0 |1 X% t- {) d( I" P# m5 E% a( Z
5)抓屏过程中,程序需要转换成托盘图标。保存为bmp的时候图像行是颠倒的,要把表面的第一行作为bmp的最后一样。传给解码器的是正常坐标
6 E8 C& O/ y( w% l/ K7 Z5 ~- J% g$ @+ H" v
8 w8 p; L+ g% U7 }1 H
/ Z# q* \5 p; w( a! L5 [程序运行截图:
1 r7 j% V$ W" W7 G1 L0 \3 l2 |7 u2 ^
( n1 Q1 Q; \0 E+ ^6 i$ p( {# B8 O% B
* @% I! y# C& u该图是采用的捕获工具所截获的H263纯码流通过VLC播放然后用快照功能获得,是CIF的分辨率。3 y5 R8 |; k# f* T# V/ M: x& c
" w6 S& a/ L3 K9 s+ m一些可能的改进:3 m" v, _9 }% ?9 N* |) h
; k4 `; {# b9 i( o! A3 a( x
1.针对DirectX和OpenGL的窗口捕获,这个需要采用hook技术,比较麻烦
- m& q5 c# i& o G, f: O8 L8 s0 _) f
2.针对不同图像尺寸的编码,目前的库不能支持XGA的H263编码,4cif的编码出现了不规则捕获的问题,问题还没有查清楚,也许是能力问题,也许是程序设计问题。
5 ]5 Y' t3 v. O( v6 _# K. y6 Y2 x. b7 m7 x4 A
3.编程中的一些优化和保护措施没有做好,程序运行的时候占用的CPU资源比较大 |