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 teft; public int top; public int bight; 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.
Downloads
- SimpleScreenshotCapture.zip (28.55 KB)
All content Copyright (c) by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is https://www.cyotek.com/blog/capturing-screenshots-using-csharp-and-p-invoke?source=rss.