我知道,标题不响亮一点你们是不会点进来看的(奸笑),好了言归正传,博主一直都想自己写一个屏幕录像软件,相信大家都用过屏幕录像软件了,专业级别或者商业级别的屏幕录像软件都是自己写驱动来获取显卡数据,或者自己写 Hook 来勾住一些图形函数来获取图形数据等等,不过博主没有这个能耐,唯一可以用的就是 Windows 自带的 GDI 函数了,以前看过一本游戏开发相关的书籍,里面讲解了如何使用 GetDIBits 函数来快速获取 HDC 里面的颜色数据,比起 GetPixel 快了上百个数量级不止,博主灵光一闪,那么是不是意味着只要我能够获取任意的窗体的 HDC,就可以用 GetDIBits 来获取窗体的颜色数据呢,基于这个念头,博主开始实施这个屏幕录像软件,当然,实践出真知,最终结果令人大吃一惊 —— 我成功了 !!我获取了目标窗体的颜色数据,然后保存到文件,然后通过一些代码进行录像回放,进而实现了一个简易的屏幕录像软件,接下来,让咱们看看整个软件的实现过程。
首先,我们来大概预览一下整个屏幕录像软件涉及到的技术点,然后逐一实现:
1. 如何获取全部窗口的信息,包括:句柄、HDC、标题、风格、窗体大小、客户区大小等等
2. 如何从目标窗体的 HDC 里面快速获取颜色数据
3. 如何将颜色数据快速绘制到预览区域
4. 如何将颜色数据保存到文件,并且进行录像回放
好,接下来咱们一个一个问题来解决,首先是第一个,获取全部窗口的信息,这个很简单,用 EnumWindows 函数就可以了,具体实现如下:
1 // 2 // 窗体信息 3 // 4 struct WINDOW_INFO 5 { 6 HWND handle; 7 DWORD style; 8 DWORD style_ex; 9 RECT rect_client; 10 RECT rect_window; 11 core::stringW title; 12 core::stringW class_name; 13 }; 14 15 unsigned int enumWindow( core::list< WINDOW_INFO > & window_list ) 16 { 17 EnumWindows( EnumThreadWndProc, ( LPARAM ) & window_list ); 18 19 return window_list.size( ); 20 } 21 22 BOOL CALLBACK EnumThreadWndProc( HWND hwnd, LPARAM lparam ) 23 { 24 WINDOW_INFO wi = { 0 }; 25 core::list< WINDOW_INFO > * window_list = ( core::list< WINDOW_INFO > * ) lparam; 26 27 wi.handle = hwnd; 28 wi.style = GetWindowLong( hwnd, GWL_STYLE ); 29 wi.style_ex = GetWindowLong( hwnd, GWL_EXSTYLE ); 30 31 wi.title.alloc( 1024, true ); 32 GetWindowTextW( hwnd, wi.title.getBuffer( ), 1024 ); 33 34 wi.class_name.alloc( 1024, true ); 35 GetClassNameW( hwnd, wi.class_name.getBuffer( ), 1024 ); 36 37 GetClientRect( hwnd, & wi.rect_client ); 38 GetWindowRect( hwnd, & wi.rect_window ); 39 40 if( window_list ) window_list->pushTail( wi ); 41 42 return TRUE; 43 }
在上面代码中,WINDOW_INFO 结构体包含了窗体的必须的一些信息,当然,你可以自己扩充一下这个结构体,比如加上 HDC 啊、子控件列表之类的;
enumWindow( ) 函数调用 Windows 的 EnumWindows( ) 函数,然后每当系统发现一个窗体,都会调用一次 EnumThreadWndProc( ) 回调函数,并且把窗体的句柄作为参数传递进去,这样子,我们就可以根据这个窗体句柄来获取窗体数据了,在这个例子里面,我用一个 list 链表保存了全部窗体的信息,这个 list 是我自己写的,所以和 STL 的 std::list 肯定有很大出入,大家不用纠结,知道是链表就可以了;
接下来就是怎么获取目标窗体的颜色数据了,假设我们要对桌面进行录像,那么获取桌面的 HWND 句柄就是 GetDesktopWindow( ) 函数,然后用 GetDC( ) 或者 GetWindowDC( ) 都可以拿到桌面的 HDC,接下来就是重点了 —— 使用 GetDIBits( ) 函数快速获取桌面 HDC 里面的颜色数据;
快速获取桌面 DC 的颜色数据的思路是这样子的,创建一个与桌面相互兼容的 HDC,称之为兼容 DC,创建一个与兼容 DC 互相兼容的位图对象,称之为兼容位图,将兼容位图选入到兼容 DC 内,然后把桌面 DC 的内容,通过 BitBlt( ) 函数快速传送到兼容 DC 内,然后通过 GetDIBits( ) 函数从兼容 DC 里面获取到颜色数据,这些颜色数据其实就是桌面的颜色数据了,说了这么多,还是直接上代码来的直观:
1 struct CAPTURE_INFO_INTERNAL 2 { 3 HDC dlg_dc; 4 HWND target_wnd; 5 HDC target_dc; 6 HDC comp_dc; 7 HBITMAP comp_bmp; 8 HBITMAP old_bmp; 9 int width; 10 int height; 11 BITMAPINFO bmp_info; 12 BYTE * buf; 13 }; 14 15 bool prepareForRecord( CAPTURE_INFO_INTERNAL & cii ) 16 { 17 BITMAPINFO bi = { 0 }; 18 19 cii.width = int( wi->rect_client.right - wi->rect_client.left ); 20 cii.height = int( wi->rect_client.bottom - wi->rect_client.top ); 21 22 cii.target_wnd = wi->handle; 23 if( NULL == cii.target_wnd ) 24 return false; 25 26 cii.target_dc = GetDC( cii.target_wnd ); 27 if( NULL == cii.target_dc ) 28 return false; 29 30 cii.comp_dc = CreateCompatibleDC( cii.target_dc ); 31 if( NULL == cii.comp_dc ) 32 return false; 33 34 cii.comp_bmp = CreateCompatibleBitmap( 35 cii.target_dc, 36 cii.width, 37 cii.height ); 38 if( NULL == cii.comp_bmp ) 39 return false; 40 41 cii.old_bmp = ( HBITMAP ) SelectObject( 42 cii.comp_dc, 43 cii.comp_bmp ); 44 45 cii.bmp_info.bmiHeader.biSize = sizeof( BITMAPINFOHEADER ); 46 cii.bmp_info.bmiHeader.biBitCount = 24; 47 cii.bmp_info.bmiHeader.biCompression = BI_RGB; 48 cii.bmp_info.bmiHeader.biWidth = cii.width; 49 cii.bmp_info.bmiHeader.biHeight = - cii.height; 50 cii.bmp_info.bmiHeader.biPlanes = 1; 51 52 cii.bmp_info.bmiColors[ 0 ].rgbBlue = 8; 53 cii.bmp_info.bmiColors[ 0 ].rgbGreen = 8; 54 cii.bmp_info.bmiColors[ 0 ].rgbRed = 8; 55 cii.bmp_info.bmiColors[ 0 ].rgbReserved = 0; 56 57 bi.bmiHeader.biSize = sizeof( BITMAPINFOHEADER ); 58 bi.bmiHeader.biBitCount = 24; 59 bi.bmiHeader.biCompression = BI_RGB; 60 bi.bmiHeader.biWidth = cii.width; 61 bi.bmiHeader.biHeight = - cii.height; 62 bi.bmiHeader.biPlanes = 1; 63 64 bi.bmiColors[ 0 ].rgbBlue = 8; 65 bi.bmiColors[ 0 ].rgbGreen = 8; 66 bi.bmiColors[ 0 ].rgbRed = 8; 67 bi.bmiColors[ 0 ].rgbReserved = 0; 68 69 GetDIBits( cii.comp_dc, cii.comp_bmp, 0, cii.height, NULL, & bi, DIB_RGB_COLORS ); 70 71 cii.buf = new BYTE[ bi.bmiHeader.biSizeImage ]; 72 73 return true; 74 }
上面代码中,CAPTURE_INFO_INTERNAL 结构体用来保存一些必须的变量,wi 变量保存了一个 WINDOW_INFO 窗体信息结构体,这个结构体我在枚举窗体的那一段已经讲解过了,prepareForRecord( ) 函数用于初始化 CAPTURE_INFO_INTERNAL 结构体,接下来我会在定时器回调函数中进行颜色数据的获取,请看下面代码:
1 void onTimer( CAPTURE_INFO_INTERNAL & cii ) 2 { 3 BitBlt( 4 preview_info.comp_dc, 5 0, 6 0, 7 preview_info.width, 8 preview_info.height, 9 preview_info.target_dc, 10 0, 11 0, 12 SRCCOPY ); 13 14 GetDIBits( 15 preview_info.comp_dc, 16 preview_info.comp_bmp, 17 0, 18 preview_info.height, 19 preview_info.buf, 20 & preview_info.bmp_info, 21 DIB_RGB_COLORS ); 22 }
没错,就是这么简单,只需要两个 GDI 函数就可以获取到桌面的颜色数据了,其中 cii 是一个 CAPTURE_INFO_INTERNAL 结构体,BitBlt( ) 函数负责将桌面 DC 的颜色数据传送到兼容 DC 里面,然后 GetDIBits( ) 函数负责将颜色数据从兼容 DC 里面取出来,这样子一次性进行数据传输以及数据获取,比起使用 GetPixel( ) 和 SetPixel( ) 快了不知道多少倍,利用这个技巧,甚至可以用来制作一些简单的 2D 游戏,在性能上差不多可以媲美 DirectDraw !!
基于 Windows GDI 的屏幕录像软件制作过程