【MFC】截图编辑插件技术总结(3):实现无缝滚动截屏

时间:2024-02-21 15:17:40

今天,我们来说说如何实现滚动截图,就是截取带滚动条窗口的完整图片(网页除外)。之前有说过,一般的截图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