今天,我们来说说如何实现滚动截图,就是截取带滚动条窗口的完整图片(网页除外)。之前有说过,一般的截图API不能截取带滚动条窗口的完整图片,要实现这个功能就要另谋出路了。不过,实现的思路其实很简单,就是让带滚动条的窗口滚动起来,并且不断截图,最后将这些图片拼接起来。说起来简单,做起来却不容易,下面先讨论几个可能遇到的问题,最后提出我的解决方案。
1、如何让窗口滚动?
这个问题其实很简单,只需要向目标窗口发送消息就可以实现。首先要获取目标窗口的窗口句柄,然后使用SendMessage或者PostMessage函数。让窗口滚动的消息有很多,比如WM_MOUSEWHEEL、WM_VSCROLL、WM_HSCROLL等。
2、如何确定每次滚动了多少像素?
这个问题是实现滚动截图的重点,下面让我们先看一段普通情况下带滚动条窗口的处理函数。
1 void CScrollHelper::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 2 3 { 4 5 if ( m_attachWnd == NULL ) 6 7 return; 8 9 const int lineOffset = 60; 10 11 // Compute the desired change or delta in scroll position. 12 13 int deltaPos = 0; 14 15 switch( nSBCode ) 16 17 { 18 19 case SB_LINEUP: 20 21 // Up arrow button on scrollbar was pressed. 22 23 deltaPos = -lineOffset; 24 25 break; 26 27 case SB_LINEDOWN: 28 29 // Down arrow button on scrollbar was pressed. 30 31 deltaPos = lineOffset; 32 33 break; 34 35 case SB_PAGEUP: 36 37 // User clicked inbetween up arrow and thumb. 38 39 deltaPos = -m_pageSize.cy; 40 41 break; 42 43 case SB_PAGEDOWN: 44 45 // User clicked inbetween thumb and down arrow. 46 47 deltaPos = m_pageSize.cy; 48 49 break; 50 51 case SB_THUMBTRACK: 52 53 // Scrollbar thumb is being dragged. 54 55 deltaPos = Get32BitScrollPos(SB_VERT, pScrollBar) - m_scrollPos.cy; 56 57 break; 58 59 case SB_THUMBPOSITION: 60 61 // Scrollbar thumb was released. 62 63 deltaPos = Get32BitScrollPos(SB_VERT, pScrollBar) - m_scrollPos.cy; 64 65 break; 66 67 default: 68 69 // We don\'t process other scrollbar messages. 70 71 return; 72 73 } 74 75 // Compute the new scroll position. 76 77 int newScrollPos = m_scrollPos.cy + deltaPos; 78 79 // If the new scroll position is negative, we adjust 80 81 // deltaPos in order to scroll the window back to origin. 82 83 if ( newScrollPos < 0 ) 84 85 deltaPos = -m_scrollPos.cy; 86 87 // If the new scroll position is greater than the max scroll position, 88 89 // we adjust deltaPos in order to scroll the window precisely to the 90 91 // maximum position. 92 93 int maxScrollPos = m_displaySize.cy - m_pageSize.cy; 94 95 if ( newScrollPos > maxScrollPos ) 96 97 deltaPos = maxScrollPos - m_scrollPos.cy; 98 99 // Scroll the window if needed. 100 101 if ( deltaPos != 0 ) 102 103 { 104 105 m_scrollPos.cy += deltaPos; 106 107 m_attachWnd->SetScrollPos(SB_VERT, m_scrollPos.cy, TRUE); 108 109 m_attachWnd->ScrollWindow(0, -deltaPos); 110 111 } 112 113 }
以上这段代码摘自codeproject上的一个项目,是对WM_VSCROLL消息的处理函数,逻辑很简单,首先判断是哪种滚动,然后确定要滚动的像素,最后调用SetScrollPos和ScrollWindow。ScrollWindow就是实际上滚动窗口的API函数,实现类似功能的API还有ScrollDC和ScrollWindowEx。从这段代码中,我们可以发现windows并不会规定每次滚动的像素大小,需要滚动多少全部由程序自己控制,如上面代码中lineOffset就是作者自己硬编码到代码中的。
那就是说,如果我们向不同的窗口发送同样的WM_VSCROLL消息,它们实际滚动了多少像素是不确定的,那么,有办法获取这个lineOffset的值吗?或者说,能获取ScrollWindow的参数吗?
说到这里或许有人已经猜到了,要获取ScrollWindow的参数可以通过钩子来实现,钩子要怎么用?限于篇幅这里就不细述了,网上资料很多可以自己找找。这里说两点我做的时候遇到的问题:
第一,如何定义全局变量?比如,我需要一个标志和记录滚动了多少像素的变量。因为全局钩子会注入到当前所有运行的进程中,即每个进程都会有钩子的一个实例,普通方式声明的全局变量实际上也会存在多个实例,并不是实际意义上的全局变量。解决办法就是使用#pragma data_seg("MyShared")声明全局变量 #pragma data_seg()将变量的声明包含起来,用这个方法就可以定义一个公共的存储空间,这样声明的变量就只会有一个实例了。
第二,如何修改或者获取这些变量?解决方法是自己编写API接口并暴露出来。
3、如何判断滚动到了底部?
这个判断方法还是挺多的,可以监听目标窗口的WM_PAINT消息或者判断滚动长度是否为0等等。
好的,接下来就介绍下我的解决方案,通过上面的三个问题,相信大家已经有一定的解决思路了,废话不多说,上代码:
1 hhookSysMsg=SetHook(0,m_ptWnd,pdc);//设置全局钩子,钩取WM_PAINT消息和ScrollDC()API函数 2 3 BOOL flag=0; 4 5 int i=0; 6 7 m_paintFlag=FALSE; 8 9 int line_Width=16; 10 11 CRect rect; 12 13 ::GetClientRect(m_ptWnd,&rect); 14 15 HDC hMenDC=::CreateCompatibleDC(pdc); 16 17 HDC hSrcDC=::CreateCompatibleDC(pdc); 18 19 HBITMAP hSrcBitmap=::CreateCompatibleBitmap(pdc,rect.Width(),rect.Height()); 20 21 int totalHeight=rect.Height(); 22 23 HBITMAP hTotalBitmap=::CreateCompatibleBitmap(pdc,rect.Width(),totalHeight); 24 25 ::SelectObject(hMenDC,hTotalBitmap); 26 27 ::SelectObject(hSrcDC,hSrcBitmap); 28 29 CPoint pointStart; 30 31 pointStart.x=0; pointStart.y=0; 32 33 while(TRUE){ 34 35 //截图 36 37 if(m_paintFlag) {SaveToFile(_T(""),hTotalBitmap);::SystemParametersInfo(SPI_SETWHEELSCROLLLINES,oldLines,0,0);break;} //到底,保存图片到文件并跳出循环 38 39 ::PrintWindow(m_ptWnd,hSrcDC,PW_CLIENTONLY);//将窗口拷贝到内存中 40 41 ::BitBlt(hMenDC,pointStart.x,pointStart.y,rect.Width(),rect.Height(),hSrcDC,0,0,SRCCOPY); 42 43 //滚动滚动条到合适的位置 44 45 int countMove=0; 46 47 //while(!m_paintFlag&&countMove<count){ 48 49 SetFlag(TRUE); 50 51 ::SetFocus(m_ptWnd); 52 53 flag= ::SendMessage(m_ptWnd,WM_MOUSEWHEEL,-WHEEL_DELTA<<16 ,0); 54 55 //::PostMessage(m_ptWnd,WM_MOUSEWHEEL,-WHEEL_DELTA<<16 ,0); 56 57 Sleep(100);//为了确保窗口已经向下滚动,否则会出问题 58 59 line_Width=0-GetYWidth(); 60 61 countMove++; 62 63 m_paintFlag=GetFlag(); 64 65 //} 66 67 if(m_paintFlag) 68 69 { 70 71 countMove--; 72 73 } 74 75 //先保存图片副本,再扩大图片 76 77 int tempHeight=totalHeight; 78 79 HDC htempDC=::CreateCompatibleDC(pdc); 80 81 HBITMAP htempBitmap=::CreateCompatibleBitmap(pdc,rect.Width(),totalHeight); 82 83 ::SelectObject(htempDC,htempBitmap); 84 85 ::BitBlt(htempDC,0,0,rect.Width(),totalHeight,hMenDC,0,0,SRCCOPY); 86 87 totalHeight+=line_Width*countMove; 88 89 DeleteObject(hTotalBitmap); 90 91 hTotalBitmap=::CreateCompatibleBitmap(pdc,rect.Width(),totalHeight); 92 93 ::SelectObject(hMenDC,hTotalBitmap); 94 95 ::BitBlt(hMenDC,0,0,rect.Width(),tempHeight,htempDC,0,0,SRCCOPY); 96 97 DeleteObject(htempBitmap); 98 99 DeleteDC(htempDC); 100 101 //根据实际滚动的行数,移动开始贴图的位置 102 103 pointStart.y+=(line_Width*countMove); 104 105 } 106 107 DeleteObject(hTotalBitmap); 108 109 DeleteDC(pdc);
下面是DLL中的代码:
1 static BOOL (WINAPI* TrueScrollWindow)(HWND hWnd,int XAmount,int YAmount,const RECT*lpRect,const RECT*lpClipRect)=ScrollWindow; 2 3 static int (WINAPI* TrueScrollWindowEx)(HWND hWnd,int dx,int dy,const RECT *prcScroll,const RECT *prcClip,HRGN hrgnUpdate,LPRECT prcUpdate,UINT flags)=ScrollWindowEx; 4 5 static BOOL (WINAPI* TrueScrollDC)(HDC hDC,int dx,int dy,const RECT *lprcScroll,const RECT *lprcClip,HRGN hrgnUpdate,LPRECT lprcUpdate)=ScrollDC; 6 7 // CMyHook 成员函数 8 9 { 10 11 BOOL WINAPI MyScrollWindow(HWND hWnd,int XAmount,int YAmount,const RECT*lpRect,const RECT*lpClipRect) 12 13 { 14 if(hWnd==m_hwnd)//如果目标窗口调用了该API函数 15 { 16 17 //获取位移量 18 19 xWidth=XAmount; 20 21 yWidth=YAmount; 22 23 } 24 25 return TrueScrollWindow(hWnd,XAmount,YAmount,lpRect,lpClipRect); 26 27 } 28 29 BOOL WINAPI MyScrollWindowEx(HWND hWnd,int dx,int dy,const RECT *prcScroll,const RECT *prcClip,HRGN hrgnUpdate,LPRECT prcUpdate,UINT flags) 30 31 { 32 33 if(hWnd==m_hwnd)//如果目标窗口调用了该API函数 34 35 { 36 37 //获取位移量 38 39 xWidth=dx; 40 41 yWidth=dy; 42 43 } 44 45 return TrueScrollWindowEx(hWnd,dx,dy,prcScroll,prcClip,hrgnUpdate,prcUpdate,flags); 46 47 } 48 49 BOOL WINAPI MyScrollDC(HDC hDC,int dx,int dy,const RECT *lprcScroll,const RECT *lprcClip,HRGN hrgnUpdate,LPRECT lprcUpdate) 50 51 { 52 53 xWidth=dx; 54 55 yWidth=dy; 56 57 return TrueScrollDC(hDC,dx,dy,lprcScroll,lprcClip,hrgnUpdate,lprcUpdate); 58 59 } 60 61 static HMODULE ModuleFromAddress(PVOID pv) 62 63 { 64 65 MEMORY_BASIC_INFORMATION mbi; 66 67 if(::VirtualQuery(pv, &mbi, sizeof(mbi)) != 0) 68 69 { 70 71 return (HMODULE)mbi.AllocationBase; 72 73 } 74 75 else 76 77 { 78 79 return NULL; 80 81 } 82 83 } 84 85 DLL_API HHOOK SetHook(DWORD ThreadProcessId,HWND hwnd,HDC hdc) 86 87 { 88 89 m_hwnd=hwnd; 90 91 m_hdc=hdc; 92 93 //m_hook = SetWindowsHookEx(WH_CALLWNDPROC,CallWndProc, ModuleFromAddress(CallWndProc),0); 94 95 m_hook = SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc, ModuleFromAddress(GetMsgProc),ThreadProcessId); 96 97 return m_hook; 98 99 } 100 101 DLL_API BOOL GetFlag() 102 103 { 104 105 return m_paintflag; 106 107 } 108 109 DLL_API void SetFlag(BOOL flag) 110 111 { 112 113 m_paintflag=flag; 114 115 } 116 117 DLL_API void SetStart(long start) 118 119 { 120 121 m_start=start; 122 123 } 124 125 DLL_API long GetTimeSpan() 126 127 { 128 129 return m_timespan; 130 131 } 132 133 DLL_API POINT GetCaretPoint() 134 135 { 136 137 return m_caretPos; 138 139 } 140 141 DLL_API int GetYWidth() 142 143 { 144 145 return yWidth; 146 147 } 148 149 BOOL APIENTRY DllMain( HINSTANCE hModule, 150 151 DWORD ul_reason_for_call, 152 153 LPVOID lpReserved 154 155 ) 156 157 { 158 159 int error=0; 160 161 switch (ul_reason_for_call) 162 163 { 164 165 case DLL_PROCESS_ATTACH: 166 167 DetourTransactionBegin(); 168 169 DetourUpdateThread(::GetCurrentThread()); 170 171 DetourAttach(&(PVOID&)TrueScrollWindow, MyScrollWindow); 172 173 DetourAttach(&(PVOID&)TrueScrollDC, MyScrollDC); 174 175 DetourAttach(&(PVOID&)TrueScrollWindowEx, MyScrollWindowEx); 176 177 error = DetourTransactionCommit(); 178 179 if(NO_ERROR!=error){ 180 181 ::MessageBox(NULL,_T("Error!"),_T("Error in Detours!"),MB_OK); 182 183 } 184 185 break; 186 187 case DLL_THREAD_ATTACH: 188 189 break; 190 191 case DLL_THREAD_DETACH: 192 193 break; 194 195 case DLL_PROCESS_DETACH: 196 197 DetourTransactionBegin(); 198 199 DetourUpdateThread(GetCurrentThread()); 200 201 DetourDetach(&(PVOID&)TrueScrollWindow, MyScrollWindow); 202 203 DetourDetach(&(PVOID&)TrueScrollDC, MyScrollDC); 204 205 DetourDetach(&(PVOID&)TrueScrollWindowEx, MyScrollWindowEx); 206 207 error = DetourTransactionCommit(); 208 209 break; 210 211 } 212 213 return TRUE; 214 215 }
我的解决方案在Office2007 Word、Visual Studio2008的代码窗口、解决方案窗口及系统文件夹窗口上测试过是可行的,但在Adobe Reader及WPS上却失败了,原因暂时还不太确定,可能是它们并没有调用ScrollWindow等3个API函数来实现滚动(还没发现有什么方法能够不调用这3个函数来实现滚动的),也可能是这些软件采用了一些防止钩子注入的技术。由于本人对钩子的使用还是初学者,也有可能是钩子的使用方法有误导致。总之,时间关系,不允许我继续研究下去了,以后有时间再完善吧!如果谁要是解决了这个问题麻烦告知一声!!
测试工程在这里:http://pan.baidu.com/s/1pJ6teJp