I was recently updating some documentation and wanted to programmatically capture some screenshots of the application in different states. This article describes how you can easily capture screenshots in your own applications.
Using the Win32 API
This article makes use of a number of Win32 API methods. Although you may not have much call to use them directly in day to day .NET (not to mention Microsoft wanting everyone to use universal "apps" these days), they are still extraordinarily useful and powerful.
This article does assume you know the basics of platform invoke so I won't cover it here. In regards to the actual API's I'm using, you can find lots of information about them either on MSDN, or PInvoke.net.
A number of the API's used in this article are GDI calls. Generally, when you're using the Win32 GDI API, you need to do things in pairs. If something is created (pens, brushes, bitmaps, icons etc.), then it usually needs to be explicitly destroyed when finished with (there are some exceptions just to keep you on your toes). Although there haven't been GDI limits in Windows for some time now (as far as I know!), it's still good not to introduce memory leaks. In addition, device contexts always have a number of objects associated with them. If you assign a new object to a context, you must restore the original object when you're done. I'm a little rusty with this so hopefully I'm not missing anything out.
Setting up a device context for use with BitBlt
To capture a screenshot, I'm going to be using the BitBlt
API.
This copies information from one device context to another,
meaning I'm going to need a source and destination context to
process.
The source is going to be the desktop, so first I'll use the
GetDesktopWindow
and GetWindowDC
calls to obtain this. As
calling GetWindowDC
essentially places a lock on it, I also
need to release it when I'm finished with it.
Now for the destination - for this, I'm going to create a memory
context using CreateCompatibleDC
. When you call this API, you
pass in an existing DC and the new one will be created based on
that.
There's still one last step to perform - by itself, that memory
DC isn't hugely useful. We need to create and assign a GDI
bitmap to it. To do this, first create a bitmap using
CreateCompatibleBitmap
and then attach it to the DC using
SelectObject
. SelectObject
will also return the relevant old
object which we need to restore (again using SelectObject
)
when we're done. We also use DeleteObject
to clean up the
bitmap.
Although this might seem like a lot of effort, it's not all that
different from using objects implementing IDisposable
in C#,
just C# makes it a little easier with things like the using
statement.
Calling BitBlt to capture a screenshot
With the above setup out the way, we have a device context which
provides access to a bitmap of the desktop, and we have a new
device context ready to transfer data to. All that's left to do
is make the BitBlt
call.
If you've ever used the DrawImage
method of a Graphics
object before, this call should be fairly familiar - we pass in
the DC to write too, along with the upper left corner where data
will be copied (0, 0
in this example), followed by the width
and height
of the rectangle - this applies to both the source
and destination. Finally, we pass in the source device context,
and the upper left corner where data will be copied from, along
with flags that detail how the data will be copied.
In my old VB6 days, I would just use SRCCOPY
(direct copy),
but in those days windows were simpler things. The CAPTUREBLT
flag ensures the call works properly with layered windows.
If the call fails, I throw a new Win32Exception
object without
any parameters - this will take care of looking up the result
code for the BitBlt
failure and filling in an appropriate
message.
Now that our destination bitmap has been happily "painted" with
the specified region from the desktop we need to get it into
.NET-land. We can do this via the FromHbitmap
static method of
the Image
class - this method accepts a GDI bitmap handle and
return a fully fledged .NET Bitmap
object from it.
Putting it all together
As the above code is piecemeal, the following helper method will
accept a Rectangle
which describes which part of the desktop
you want to capture and will then return a Bitmap
object
containing the captured information.
Note the
try ... finally
block used to try and free GDI resources if theBitBlt
orFromHbitmap
calls fail. Also note how the clean-up is the exact reverse of creation/selection.
Now that we have this method, we can use it in various ways as demonstrated below.
Capturing a single window
If you want to capture a window in your application, you could
call Capture
with the value of the Bounds
property of your
Form
. But if you want to capture an external window then
you're going to need to go back to the Win32 API. The
GetWindowRect
function will return any window's boundaries.
Win32 has its own version of .NET's Rectangle
structure, named
RECT
. This differs slightly from the .NET version in that it
has right
and bottom
properties, not width
and height
.
The Rectangle
class has a helper method, FromLTRB
which
constructs a Rectangle
from left, top, right and bottom
properties which means you don't need to perform the subtraction
yourself.
Depending on the version of Windows you're using, you may find that you get slightly unexpected results when calling
Form.Bounds
orGetWindowRect
. As I don't want to digress to much, I'll follow up why and how to resolve in another post (the attached sample application includes the complete code for both articles).
Capturing the active window
As a slight variation on the previous section, you can use the
GetForegroundWindow
API call to get the handle of the active
window.
Capturing a single monitor
.NET offers the Screen
static class which provides access to
all monitors on your system via the AllScreens
property. You
can use the FromControl
method to find out which monitor a
form is hosted on, and get the region that represents the
monitor - with or without areas covered by the task bar and
other app bars. This means it trivial to capture the contents of
a given monitor.
Capturing the entire desktop
It is also quite simple to capture the entire desktop without
having to know all the details of monitor arrangements. We just
need to enumerate the available monitors and use
Rectangle.Union
to merge two rectangles together. When this is
complete, you'll have one rectangle which describes all
available monitors.
There is one slight problem with this approach - if the resolutions of your monitors are different sizes, or are misaligned from each other, the gaps will be filled in solid black. It would be nicer to make these areas transparent, however at this point in time I don't need to capture the whole desktop so I'll leave this either as an exercise for the reader, or a subsequent update.
Capturing an arbitrary region
Of course, you could just call CaptureRegion
with a custom
rectangle to pick up some arbitrary part of the desktop. The
above helpers are just that, helpers!
A note on display scaling and high DPI monitors
Although I don't have a high DPI monitor, I did temporarily scale the display to 125% to test that the correct regions were still captured. I tested with a manifest stating that the application supported high DPI and again without, in both cases the correct sized images were captured.
The demo program
A demonstration program for the techniques in this article is available from the links below. It's also available on GitHub.
Update History
- 2017-08-27 - First published
- 2020-11-22 - Updated formatting
Like what you're reading? Perhaps you like to buy us a coffee?
# Ram S
# Richard Moss