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 the BitBlt or FromHbitmap 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 or GetWindowRect. 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

Filename Description Version Release Date
SimpleScreenshotCapture.zip
  • md5: d5feac405f75f196cbed10213fbb31bc

Sample project for the capturing screenshots using C# and p/invoke article.

27/08/2017 Download

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?

Styling with Markdown is supported