Capturing screenshots using C# and p/invoke
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.
IntPtr desktophWnd = GetDesktopWindow();
IntPtr desktopDc = GetWindowDC(desktophWnd);
// TODO
ReleaseDC(desktophWnd, desktopDc);
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.
IntPtr memoryDc = CreateCompatibleDC(desktopDc);
// TODO
DeleteDC(memoryDc);
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.
IntPtr bitmap = CreateCompatibleBitmap(desktopDc, width, height);
IntPtr oldBitmap = SelectObject(memoryDc, bitmap);
// TODO
SelectObject(memoryDc, oldBitmap);
DeleteObject(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.
const int SRCCOPY = 0x00CC0020;
const int CAPTUREBLT = 0x40000000;
bool success = BitBlt(memoryDc, 0, 0, width, height, desktopDc, left, top, SRCCOPY | CAPTUREBLT);
if (!success)
{
throw new Win32Exception();
}
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.
Bitmap result = Image.FromHbitmap(bitmap);
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.
[DllImport("gdi32.dll")]
static extern bool BitBlt(IntPtr hdcDest, int nxDest, int nyDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll")]
static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int nHeight);
[DllImport("gdi32.dll")]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
static extern IntPtr DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll")]
static extern IntPtr DeleteObject(IntPtr hObject);
[DllImport("user32.dll")]
static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);
[DllImport("gdi32.dll")]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
const int SRCCOPY = 0x00CC0020;
const int CAPTUREBLT = 0x40000000;
public Bitmap CaptureRegion(Rectangle region)
{
IntPtr desktophWnd;
IntPtr desktopDc;
IntPtr memoryDc;
IntPtr bitmap;
IntPtr oldBitmap;
bool success;
Bitmap result;
desktophWnd = GetDesktopWindow();
desktopDc = GetWindowDC(desktophWnd);
memoryDc = CreateCompatibleDC(desktopDc);
bitmap = CreateCompatibleBitmap(desktopDc, region.Width, region.Height);
oldBitmap = SelectObject(memoryDc, bitmap);
success = BitBlt(memoryDc, 0, 0, region.Width, region.Height, desktopDc, region.Left, region.Top, SRCCOPY | CAPTUREBLT);
try
{
if (!success)
{
throw new Win32Exception();
}
result = Image.FromHbitmap(bitmap);
}
finally
{
SelectObject(memoryDc, oldBitmap);
DeleteObject(bitmap);
DeleteDC(memoryDc);
ReleaseDC(desktophWnd, desktopDc);
}
return result;
}
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.
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
public Bitmap CaptureWindow(IntPtr hWnd)
{
RECT region;
GetWindowRect(hWnd, out region);
return this.CaptureRegion(Rectangle.FromLTRB(region.Left, region.Top, region.Right, region.Bottom));
}
public Bitmap CaptureWindow(Form form)
{
return this.CaptureWindow(form.Handle);
}
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.
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
public Bitmap CaptureActiveWindow()
{
return this.CaptureWindow(GetForegroundWindow());
}
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.
public Bitmap CaptureMonitor(Screen monitor)
{
return this.CaptureMonitor(monitor, false);
}
public Bitmap CaptureMonitor(Screen monitor, bool workingAreaOnly)
{
Rectangle region;
region = workingAreaOnly ? monitor.WorkingArea : monitor.Bounds;
return this.CaptureRegion(region);
}
public Bitmap CaptureMonitor(int index)
{
return this.CaptureMonitor(index, false);
}
public Bitmap CaptureMonitor(int index, bool workingAreaOnly)
{
return this.CaptureMonitor(Screen.AllScreens[index], workingAreaOnly);
}
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.
public Bitmap CaptureDesktop()
{
return this.CaptureDesktop(false);
}
public Bitmap CaptureDesktop(bool workingAreaOnly)
{
Rectangle desktop;
Screen[] screens;
desktop = Rectangle.Empty;
screens = Screen.AllScreens;
for (int i = 0; i < screens.Length; i++)
{
Screen screen;
screen = screens[i];
desktop = Rectangle.Union(desktop, workingAreaOnly ? screen.WorkingArea : screen.Bounds);
}
return this.CaptureRegion(desktop);
}
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
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
SimpleScreenshotCapture.zip
|
Sample project for the capturing screenshots using C# and p/invoke article. |
27/08/2017 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Ram S
#
I love your code above - but i need the code to be in VBScript. Not sure how i can save the above as .vb and then convert it to .vbs (Vbscript code)? Any help?
Richard Moss
#
Hello,
Thanks for the question. Unfortunately however, this isn't easily achieved. Although there are many C# to VB.NET conversion utilities out the (the oldest one I know of is http://www.developerfusion.com/tools/convert/csharp-to-vb/) and the above code can be used in VB.NET, it isn't possible to do this from VB Script via pure code. VB Script is from the VB6 era so objects like
Screen
don't exist. The main sticking point is that VB Script doesn't support p/invoke, so you wouldn't be able to make API calls.It has been many years since I've used VB Script so I might be wrong in the particulars, but as far as I'm aware the only way to make this would would be to make a COM DLL (you could create one using C#, VB.NET, VB6 or probably several other languages) which can handle the actual capturing of screenshots using p/invoke (or whatever features the language itself offers) and calling that from VB6.
Personally I would probably just write a command line tool and call that from VB Script or a batch file rather than deal with COM again.
Hope this helps!
Regards;
Richard Moss