Introduction
While developing MogoTest, a service for detecting Web browser rendering issues, I found it necessary to be able to capture the contents of the entire browser canvas rather than just the current viewport. A full page screenshot lets the user see how their content looks from a holistic perspective. Unfortunately, capturing all of the content clipped by the scrollable viewport is not a very straightforward process.
One approach to grabbing the full canvas is to take a screenshot and then scroll the viewport to display all the previously hidden sections, stitching all of the images together to form one composite that represents the full canvas contents. While this generally works, it fails if there are any fixed position elements on the page or if there is ECMAScript in place to modify the DOM on scroll events because in both cases the very act of scrolling modifies the canvas’s contents. It would be better then to capture the entire canvas without scrolling.
The Problem with Making Windows Large Enough to Remove Scrollbars
Windows does not allow windows to be larger than the virtual screen resolution by default. The virtual screen resolution is defined as the vertical and horizontal span of all connected displays. If you have a single display, the current screen resolution will have a 1:1 match with the virtual screen resolution and if you have two displays side-by-side, the virtual screen resolution will be the height of the lowest resolution by the sum of the two horizontal screen resolutions.
Resizing the window to display the entire canvas contents, thus, requires some trickery in handling the virtual screen dimensions. As it turns out, every window is sent a WM_GETMINMAXINFO
message just before the window’s screen coordinates or size is changed. WM_GETMINMAXINFO
passes as its lParam
a MINMAXINFO
value which contains the virtual screen dimensions as the the ptMaxTrackSize
member. Modifying the lParam
before the window receives the message would allow us to effectively change the virtual screen resolutions on a per-process basis.
If you have access to the source code of the application you’d like to capture the full canvas contents of, the solution is simple: just modify your message processing loop to handle WM_GETMINMAXINFO
messages and modify the lParam
as necessary. In the general case, however, you won’t be able to modify the binary so you’ll have to modify the executing process’s address space to inject your own message handler.
While the general concept is rather straightforward, the implementation is fairly convoluted. At the core of it, the complexity is caused by the WM_GETMINMAXINFO
message being sent rather than retrieved by the window.
Tricking Windows into Letting you Resize the Window Larger than the Screen
The Win32 API allows applications to monitor global event message traffic by setting up a hook procedure via the SetWindowsHookEx
function. This is how utilities like Spy++ are able to tell what messages are being sent to a program. As of this writing, there are thirteen different types of hooks, each with its own context and execution point in the global chain.
If you’re like me, you’d probably try to register a WH_GETMESSAGE
hook with GetMsgProc
. On the outset it seems logical enough: intercept the WM_GETMINMAXINFO
message before GetMessage
or PeekMessage
is called and modify accordingly. This will not work, however, and you will waste a lot of time trying to make it work. The nuance here is that WM_GETMINMAXINFO
is sent to the window – the window does not poll for it – and as such a WH_GETMESSAGE
hook will never see the message.
The next seemingly logical hook type to try is WH_CALLWNDPROC
, which you register with the CallWndProc
function. This type of hook will indeed intercept the WM_MINMAXINFO
message before the window will, but unfortunately, the hook procedure cannot modify the message. This is by design and Windows will enforce it; trying to get a reference to the lParam
to modify the value in memory will not work.
And so it goes with all the hook types. Many look like they’ll do what you need, but will fail in some way. It seems that modifying the WM_GETMINMAXINFO
message from out of process is not possible. And largely that’s true. However, we can get creative by supplanting the process’s window procedure, which executes in process, by using SetWindowLongPtr
from the WH_CALLWNDPROC
hook. Example 1 shows what that interaction may look like.
SetWindowLongPtr
is an amazing feature of Windows that lets you supply a new function pointer for a restricted set of functions in a Windows process. The new function can then call out to the original function through a handle to that function. One of the functions allowed to be replaced is the window procedure. By supplying our own we will be able to finally modify that WM_MINMAXINFO
message. In Example 1
we showed how to call SetWindowLongPtr
. Example 2
shows what the custom window procedure looks like:
Note that we only handle the WM_GETMINMAXINFO
message and delegate all others to the original window procedure. Additionally, we uninstall the custom procedure as soon as we’ve accomplished what we need to.
We modify the ptMaxTrackSize
component of the MINMAXINFO
struct, which is itself a POINT
struct, having an x
and a y
component. These should be set large enough to handle the full canvas plus the window chrome that surrounds the main client area. Once this is done, you should be able to size the window large enough to obviate the need for scrollbars.
Fig. 1 shows how this all ties together between a theoretical screenshot.exe process taking full canvas screenshots in Internet Explorer.
Capturing the Canvas Contents
Now that your application can be sized large enough to capture the canvas contents, you must resize the window to that maximum size. This calculation is application dependent. For Internet Explorer much of the work is done with the IWebBrowser2 interface, for example.
One caveat is that if the window is already maximized, Windows will not send it a sizing message. My solution to this problem is to first check if the window is already maximized and if so note that fact, change the maximized state to restored, then resize the window to be large enough for the full canvas contents. Once done, I then re-maximize the window if it was previously maximized, effectively restoring the window to its original dimensions. It is a bit kludgy, but I haven’t been able to come up with a better solution. I suspect there is a way by intercepting a different window message, but I couldn’t figure out which one if it is in fact possible. This process can be seen in Example 3
.
All that remains now is to capture the contents, unregister the WH_CALLWNDPROC
hook, and resize the window to its original dimensions so the user doesn’t have to deal with a massive window. Example 3
pulls all this code together.
Conclusion
Taking full page or full canvas screenshots in Windows can be tricky, but the method discussed in this article should be widely applicable. In my particular case I was enhancing the SnapsIE utility. My SnapsIE fork illustrates how I use all of these techniques. Note that SnapsIE is written as an ActiveX control for Internet Explorer, so the code is likely more complex than is warranted in many cases.
Acknowledgments
- Haw-Bin Chai for SnapsIE, which served as a basis for much of the work I did.
- Jim Evans for more IE screenshot work on selenium, which handled IE8 a bit more gracefully than SnapsIE did.
- sunnyandy, who had the closest answer on how to take full screen screenshots that I was able to find.
- Igor Tandetnik, who knows VC++ better than any human I’m aware of.
- Jeff Rafter, who helped me debug all sorts of issues while I was developing the foundation for this article and then served as a peer reviewer of the content.
- MogoTest for allowing me to spend all this time solving the problem.