原文
2 h" g n9 S bhttp://blog.csdn.net/matlab2000/archive/2006/10/30/1356752.aspx
* {0 @3 X# v9 I, J: `' N( }7 M1 }
常常我们想要编程捕获整个屏幕的内容,下面将解释如何做到这一点。典型的,我们的选择是使用GDI或者DirectX。另外一个值得考虑的方法是Windows Media API,直接将捕获的屏幕图像编码成码流输出,不过本文中不讨论该方法,主要讨论前两种技术。每一种方法中,我们得到了屏幕快照后,就可以用于保存或者编码输出。下面讨论了两种方法,并给出程序的部分代码和运行截图。
# ?( _: ~" @9 e" N6 v6 B
/ H: r9 k' A. d7 h* ^$ Q
# c! V& O, u5 w
* S' O! c6 a( Z) k: S# ^5 i1 w& Z" s% ^用GDI方法捕获
7 g7 k8 ~* i/ B" S- V6 ^8 f, z
% d" E# I$ b7 L7 V 如果性能不是问题,并且我们需要的只是屏幕快照,就可以考虑采用GDI方法。这种方法基于桌面也是一个窗口的基本思想(桌面也有窗口句柄和设备上下文),只要得到被捕获桌面的上下文,就可以用普通的方法把内容blit到我们应用定义的上下文中。如果知道桌面句柄(可以用函数GetDesktopWindow()函数得到),得到上下文是很直接的。步骤如下:
' F5 G2 M' j/ h/ w* F7 y: p' e" j
1) 使用GetDesktopWindow获得桌面句柄, k1 L$ ^; ^: U8 e3 Z5 m
5 b5 N; L; y/ R5 F/ |& x2) 使用函数GetDC获得桌面窗口的DC" X1 q1 G- d2 T' O1 J3 h W% G* p. ^
7 ~1 a& l; \. n7 s# M1 J, o3) 为桌面DC创建一个兼容的DC和一个用于选入兼容DC的兼容位图。可以用函数CreateCompatibleDC和CreateCompatibleBitmap;可以用SelectObject将位图选入我们的DC
6 L( H. x q3 z$ c. m1 f4 i- o8 q' u) G! k8 z- @" a% o
4) 不管何时准备捕获屏幕,blit桌面DC内容到创建的兼容的DC,我们创建的兼容位图现在包含了捕获时刻的屏幕内容
5 n% |6 v! x( {/ \- F
. ~+ f6 O U( y! Y' ~* H5) 当完成捕获后,不要忘记释放对象4 F$ D; }( a/ F: }+ }
2 r9 e0 ^( D2 k4 M8 P. C, l例子:# G* \" o, y! ^' j& m2 F
$ d& F# P& W& c6 j1 a0 B1 c- U8 ZVoid CaptureScreen()
7 P; t2 i8 ~ ?. D# i{
# U! r2 M8 Z. L2 ? int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);2 n/ h6 j9 I7 r. m2 }" k% G
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);9 G) t6 K4 U: Q4 H/ y( |& n$ r
HWND hDesktopWnd = GetDesktopWindow();
: N( S& j. Q& D HDC hDesktopDC = GetDC(hDesktopWnd);7 q5 }9 N" [2 w; \. ~1 C7 O: q9 ^9 @$ S
HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);8 Q+ j" n+ _' f$ W& z* S$ r
HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC, ( ]4 X( G, a) q4 z- E+ l
nScreenWidth, nScreenHeight);; D: Z7 G) i" C* m
SelectObject(hCaptureDC,hCaptureBitmap); " ~* w/ `" a# s/ S5 e
BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,& p, i6 K4 s' Z
hDesktopDC,0,0,SRCCOPY|CAPTUREBLT);
* [1 \! h, t* R0 u* ]& Y; H SaveCapturedBitmap(hCaptureBitmap); //占位符号,可以在此处放入自己的代码
1 K+ c1 r. S1 n! }7 ] ReleaseDC(hDesktopWnd,hDesktopDC);: S( Z2 \" j! A2 | e, h
DeleteDC(hCaptureDC);
3 r# s) r3 d! ^ DeleteObject(hCaptureBitmap);
) Z0 y- ?- k" `6 w( z; o L0 `}4 \7 V% R$ A2 F u( k
上面的例子中,函数GetSystemMetrics()如果使用SM_CXSCREEN则返回屏幕宽,如果使用SM_CYSCREEN则返回屏幕的高。参考相关代码细节,我们很容易创建文件或者影片。9 v% Y* O" p# z7 ~
) M! V' _4 U, t2 L
/ g C t' S( K Y5 E
$ O4 q( i7 x% q1 q. a ^6 U) k采用DirectX的方法:
5 y3 a, \- U) s
- i/ U& r, |" R- X) ^ 由于DirectX提供了一个干净的方法,所以捕获屏幕快照在DirectX下非常容易。, J' [ b5 P2 ?; a- Q, \
2 B9 c8 \5 a" c( |6 j s0 U
每一个DirectX应用都包含了我们称为buffer或者surface的包含了相应于应用的视频内存,被称为后备缓冲。一些程序可能有超过一个后备缓冲。另外有一个所有程序能够存取的前端缓冲,该前端缓冲包含了桌面内容相关的视频内存,所以就是屏幕图像。
" n7 r# D$ g& J, R- K5 I3 ?9 T
4 ~% L! s* o( P
3 r7 X/ ^ } z, J& Z- g2 n
5 i) r" I$ P) n2 ?; M0 X4 v通过存取我们应用程序的前端缓冲,我们就可以捕获此刻的屏幕内容。存取应用的前端缓冲是简单直接的。接口IDirect3DDevice9提供了GetFrontBufferData()方法,带一个IDirect3DSurface9对象指针,拷贝前端缓冲的数据进入surface. IDirect3DSurfce9对象能够通过方法IDirect3DDevice8::CreateOffscreenPlainSurface()获得。一旦屏幕被捕获到surface,我们可以使用D3DXSaveSurfaceToFile()以位图方式保存表面到磁盘。捕获代码如下:
, M) B* e3 V3 m8 r+ b) E" A% r% h
extern IDirect3DDevice9* g_pd3dDevice;
9 c6 [! R7 o( q) Y, ~! t8 m4 W& H" XVoid CaptureScreen()& n: ~; l# m d+ s" i
{* f2 s/ a0 N7 T7 k4 m
IDirect3DSurface9* pSurface;
5 `+ Q1 \/ A3 H# O Q' N& h g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,1 G4 o5 D( d; P4 }3 Z
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);
" I i2 }) h; b5 l, @ g_pd3dDevice->GetFrontBufferData(0, pSurface);
. I" _2 |- L$ x/ C7 g1 @# @ D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL);
% ^0 N5 L: P8 f/ B( Y. M' y pSurface->Release(); $ M! Y/ y9 p" t6 N* e
}
. a) T5 \8 |% Q/ v上面代码中的g_pd3dDevice是一个IDirect3DDevice9对象,假定已经正确的初始化了。代码片断直接保存捕获图像到磁盘。然而,代替保存到磁盘,我们可能想要直接操作这个位图。我们可以通过使用方法IDirect3DSurface9::LockRect()来做到这点,获得一个指向表面的指针。我们可以拷贝图像到应用内存然后操作他们。接下来的代码片断表明了如何拷贝表面到我们的应用内存:4 K, q4 x* X+ ^$ C+ X; m
" |8 W' i& N1 N
extern void* pBits;
& u5 v% v0 X" ~" Fextern IDirect3DDevice9* g_pd3dDevice;$ e2 F5 P5 j t( a: T0 F7 `& J
IDirect3DSurface9* pSurface;
3 ~% c* @# U. E7 J. P% Og_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,
! F% w A2 G0 E0 r; s' c' H+ L9 S D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH,
- X: K0 `+ w) e, m+ O9 b &pSurface, NULL);
& b) \6 o3 |1 n3 ^g_pd3dDevice->GetFrontBufferData(0, pSurface);
# T; k; O# v4 e/ G) L: n/ c& }D3DLOCKED_RECT lockedRect;
2 l& w. F; S- c& N" u2 m$ EpSurface->LockRect(&lockedRect,NULL,
& A4 v( \% H& ^ Y. z) O D3DLOCK_NO_DIRTY_UPDATE|
8 n8 M3 t" n% }: W, o0 M D3DLOCK_NOSYSLOCK|D3DLOCK_READONLY)));* L2 ]! f1 W& R) R' l) y
for( int i=0 ; i < ScreenHeight ; i++)
, }9 u4 h4 r2 ?+ R9 T{2 |4 l% f2 P6 W
memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 ,
. C3 Y/ s% H+ C# p& Z1 f (BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
J. o' @" N+ J2 A* I& ? s$ f ScreenWidth * BITSPERPIXEL / 8);
0 w6 L* I) }8 n1 _) B( J6 W}& }0 K8 b8 O( r0 f
g_pSurface->UnlockRect();7 G4 ?$ T) u. M3 @7 d
pSurface->Release();1 v& d# L2 S8 ?9 P* z3 D
+ G4 J' V! |9 K) Y* @" D9 U
9 \: z% E0 m, o' D. H0 K
上面的pBits是一个void *,在拷贝之前要确认我们已经分配了足够的内存。一个BITSPERPIXEL的典型值是32位,然后,依赖于你的显示器设置。重要的是注意到表面的宽度与捕获屏幕的宽度并不相同,由于这个问题涉及到内存对齐,表面可能加入附加的部分到每行的结束来对齐到字边界。lockedRect.Pitch给出两个行之间的字节数目,也就是说,为了前进到下一行正确的点,我们应当推进Pitch,而不是Width.下面的代码反向的拷贝表面的字节:- I' D* H+ |. b) w2 l
9 {( Z- J0 x- d
for( int i=0 ; i < ScreenHeight ; i++)
0 k8 [$ E/ W- e* @/ w{1 e0 W/ N+ S; [" j( |% k
memcpy((BYTE*) pBits +( ScreenHeight - i - 1) * , Y2 p! l, g7 D6 _1 s
ScreenWidth * BITSPERPIXEL/8 ,
) l+ T- N) v$ }1 _ (BYTE*) lockedRect.pBits + i* lockedRect.Pitch ,
8 I; z6 |* b r/ B+ j ScreenWidth* BITSPERPIXEL/8);# B' ]1 Q3 z Z. T
}+ m* X( l. q% p5 l) G8 [
8 X" ^' A) |# n) w' q5 v
2 [" d( ~- H9 w' f4 T" S1 ^这点代码用于转换自顶而下和自底而上的位图。4 r5 v5 x" q, z0 D$ H
& O0 R* ~& M) d
上面的技术中LockRect是IDirect3DSurface9中存取捕获图像的一个方法,我们还有另外一个更加复杂的方法,GetDC方法。IDirect3DSurface9::GetDC()方法用来获得GDI兼容的设备上下文,这样就可以直接blit表面内容到我们应用定义的DC。
2 d/ L; Y1 a, _3 A
( U) _2 e6 G& _6 x: x" o ! f* u& O" e! u6 `( Y
7 [! J! M7 T9 _# m% y: d. s6 V然而,GetFrontBufferData是一个慢的方法,不应当用于性能临界的应用,GDI方法是更快的方法。
; O7 \/ B5 F# h$ Z4 v; g, p8 ^ o1 |
2 P" _' h! C, d8 @* w" h2 h2 g4 V' ^: U, Z% o% N
捕获程序要点:) k$ u5 s) z2 ~( y9 v% B
( y/ ^3 N4 L9 _, Y) n3 \% Y 程序初始化的时候需要初始化相关资源,在WM_CREATE消息中初始化D3D,然后初始化FFMPEG,同时启动定时器;当用户选择开始捕获的时候,每次定时器到,处理WM_TIMER消息,锁定前端缓冲平面,然后拷贝数据到缓冲区,将该缓冲区传递给FFMPEG编码并输出;当用户停止的时候,停止定时器,清理D3D和FFMPEG,然后退出。下面是几个需要注意的技术点:/ ]& h. _. v$ A$ \
3 q, G$ f9 f$ c0 B) V5 A1 _
1)屏幕的颜色深度,由于屏幕可能处于不同的颜色深度下,所以需要仔细区分,当GetDeviceCaps函数返回颜色深度为16的时候,需要区分是否RGB555或者RGB565,根据颜色深度来设定传递给FFMPEG的像素格式。7 Z v/ A$ O* U
5 U, U+ f; A0 c& H0 [2 T
2)编码参数的设定,由于编码库支持的图像大小不是任意的,所以需要仔细的设置编码参数,否则不能初始化编码器。, K" `7 z" [2 w- W+ H6 E, u
1 S4 y: H$ Y' \( t( l. D3)图像转换的设定,由于原图像和目的图像的色彩空间和大小都不一样,所以需要仔细考虑图像转换,色彩空间的转换可以用img_convert,大小的转换可以用img_resample,注意到大小转换只针对YUV420P,所以在本案例中是先转换色彩空间,从RGBA32转换到YUV420P,然后调用重采样函数改变大小。 i$ A' ~3 w9 c
4 {) @+ E- w# H% k1 \* R
4)编码图像的保存,调用avcodec_encode_video编码图像,然后直接写入文件即可,如果想要从网络输出,那么考虑rtp_callback函数回调,由库来做合适的分包,然后加上合适的头部发送( |6 x9 j! j6 k/ e; A
- H7 H/ `5 J$ Y. I. p! [. Z5)抓屏过程中,程序需要转换成托盘图标。保存为bmp的时候图像行是颠倒的,要把表面的第一行作为bmp的最后一样。传给解码器的是正常坐标
$ \: Z' |' y% p' M4 J5 [2 }( T6 R: F- d4 J0 V6 y' H5 d
9 w2 _* K3 R) Y2 f/ c
' A- H% [1 h% ^0 w7 O: y5 g- K
程序运行截图:' B' k: V- C' |& Z/ \) r: j; o
7 [/ D& F; J# s2 I2 W# N q
3 j+ q6 r9 y# A1 F该图是采用的捕获工具所截获的H263纯码流通过VLC播放然后用快照功能获得,是CIF的分辨率。
) }% n+ d- D9 R* @
$ }8 S1 t5 _% H J% A一些可能的改进:
) a% {8 {; |; |, b
/ L* }2 B6 C. w5 A. W& E k* w" k1.针对DirectX和OpenGL的窗口捕获,这个需要采用hook技术,比较麻烦
9 V/ W7 p( D. S; O3 |* c
; o0 H# {2 A1 j! T$ d( y T- g" u$ D2.针对不同图像尺寸的编码,目前的库不能支持XGA的H263编码,4cif的编码出现了不规则捕获的问题,问题还没有查清楚,也许是能力问题,也许是程序设计问题。9 Q/ |9 ~( R- {% j- ]7 h1 c7 _
: T. P; O2 e# E' c
3.编程中的一些优化和保护措施没有做好,程序运行的时候占用的CPU资源比较大 |