Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Capturing Screen Images in WPF using GDI, Win32 and a little WPF Interop Help

Pete Brown - 08 February 2011

I keep a backlog of project/blog/video ideas on my machine here. One that I've had sitting there for a bit, and something that really interests me, is how to create a simple screen capture (still image) program using WPF.

I use a ton of screen shots in my applications. Before I joined Microsoft (when I was an MVP), I used Snag-it for just about everything. On this new machine, I haven't installed any screen capture apps at all. However, tools like Rooler by Pete Blois and Cropper up on CodePlex would be good candidates.

Of course, I want to build one myself anyway to learn how it's done. So, let's have at it.

Capturing a Region

The first task I decided to tackle is that of snap-shotting a region of the screen. Here's an example of a region which includes part of Visual Studio.

image

I wanted to keep it simple, so I didn't implement rubber-band selection or anything. A real app, of course, would need that. Instead, I decided just to hard-code the rectangle and test out the rest of the code from there.

There are three buttons, an Image and a Border in the Xaml. The one button we're concerned with for this example is the CaptureRegion button. It and the image are defined as follows:

<Button Content="Region"
Height="23"
HorizontalAlignment="Left"
Margin="10,68,0,0"
x:Name="CaptureRegion"
VerticalAlignment="Top"
Width="100"
Click="CaptureRegionButton_Click" />

<Border Background="LightGray"
BorderBrush="Black"
Grid.Column="1"
Margin="10">
<Image x:Name="CapturedImage"
RenderOptions.BitmapScalingMode="Fant"
Stretch="Uniform" />
</Border>

Note the RenderOptions.BitmapScalingMode setting on the Image. When displaying we're typically resizing an image by far more than 50%, so you want a more aggressive smoothing algorithm. See more on my posts about image resizing in WPF 4 here and here.

For this example, I implemented the call in code-behind.

private void CaptureRegionButton_Click(object sender, RoutedEventArgs e)
{
CapturedImage.Source = ScreenCapture.CaptureRegion(100, 100, 500, 500, true);
}

You can see that we're capturing a region starting at 100x100 with a size of 500,500. The last parameter says to copy to the clipboard -- more on that later.

Now for the important part. Capturing the region. This involves grabbing the desktop window, since a region may overlap windows, and rendering a portion of that to a bitmap. While much of it is Win32 API gunk (I'll leave out the Win32 API declarations; you can see them in the source code attached to this post), there's one really nice helper from WPF: the Imaging interop class.

// capture a region of a the screen, defined by the hWnd
public static BitmapSource CaptureRegion(
IntPtr hWnd, int x, int y, int width, int height, bool addToClipboard)
{
IntPtr sourceDC = IntPtr.Zero;
IntPtr targetDC = IntPtr.Zero;
IntPtr compatibleBitmapHandle = IntPtr.Zero;
BitmapSource bitmap = null;

try
{
// gets the main desktop and all open windows
sourceDC = User32.GetDC(User32.GetDesktopWindow());
//sourceDC = User32.GetDC(hWnd);
targetDC = Gdi32.CreateCompatibleDC(sourceDC);

// create a bitmap compatible with our target DC
compatibleBitmapHandle = Gdi32.CreateCompatibleBitmap(sourceDC, width, height);

// gets the bitmap into the target device context
Gdi32.SelectObject(targetDC, compatibleBitmapHandle);

// copy from source to destination
Gdi32.BitBlt(targetDC, 0, 0, width, height, sourceDC, x, y, Gdi32.SRCCOPY);

// Here's the WPF glue to make it all work. It converts from an
// hBitmap to a BitmapSource. Love the WPF interop functions
bitmap = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
compatibleBitmapHandle, IntPtr.Zero, Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());



}
catch (Exception ex)
{
throw new ScreenCaptureException(string.Format("Error capturing region {0},{1},{2},{3}", x, y, width, height), ex);
}
finally
{
Gdi32.DeleteObject(compatibleBitmapHandle);

User32.ReleaseDC(IntPtr.Zero, sourceDC);
User32.ReleaseDC(IntPtr.Zero, targetDC);
}

return bitmap;
}

We use the Imaging interop class to create a WPF-compatible BitmapSource from a Windows hBitmap. Note that I'm not doing anything with the Clipboard parameter in this code. The clipboard portion is not yet working, but is included in the source code.

That's all there is to capturing a region. It's super simple once you realize there's this great helper functionality included in the WPF interop namespace.

Next, we'll expand this to capture the entire screen.

Capturing the Entire Screen

The second thing I decided to do is implement a simple Print-Screen function which snapshots the entire virtual screen space. Here's an example of a screen shot from this machine:

image

You can see from the above screenshot that I have two displays stacked on each other. Windows does a good job of providing both images, with appropriate transparency where no display occupies that space.

The nice part about this is it uses the existing CaptureRegion function to do all the heavy lifting:

public static BitmapSource CaptureFullScreen(bool addToClipboard)
{
return CaptureRegion(
User32.GetDesktopWindow(),
(int)SystemParameters.VirtualScreenLeft,
(int)SystemParameters.VirtualScreenTop,
(int)SystemParameters.VirtualScreenWidth,
(int)SystemParameters.VirtualScreenHeight,
addToClipboard);
}

The key thing to note in this chunk of code is the SystemParameters object which provides the size of the full screen.

The next task is to capture a single window. This is where things start getting hairy.

Capturing a Single Window

I'm not a fan of Alt-PrtScn to capture a single window. When you do that, you lose the nice shadow effect from Windows 7 and Vista, and get an ugly chopped image with dark corners (since it leave the shadow there, but doesn't mask the rounded corners). Here's an example:

image

Not only do we have the corner problem, but due to aero glass, background content shows through. On a full desktop, that content adds context, but in a screenshot, it just looks messy. Screenshots like that just look unprofessional. I did my best to make sure they don't show up in my books or on my blog.

The attached source code has the start of a blanking approach: I stick a blank window behind the target window before doing the screen shot. It's not stable and suffers from a race condition, so I'll not include that code in this blog post. However, I will show you how to capture the full window including the shadow.

Warning: Lark's Vomit!

(that was a Big Red Label. Look it up <g>)

Ok, seriously. This code has magic numbers. I hate magic numbers. There is likely a nice systemic way to retrieve the exact dimensions I need, including shadows and whatnot. However, it eludes me at the moment. Use the code if you want, but realize you're inheriting disease. Don't blame me if you implement a mission critical print-screen hospital patient monitoring system and someone gets +/- 5% dosage or something ;)

First, the main window capture function (with blanking window code removed):

public static BitmapSource CaptureWindow(IntPtr hWnd, bool recolorBackground, Color substituteBackgroundColor, bool addToClipboard)
{
Int32Rect rect = GetWindowActualRect(hWnd);

Window blankingWindow = null;

if (recolorBackground)
{
// ... blanking window gunk ...
}

// bring the to-be-captured window to capture to the foreground
// there's a race condition here where the blanking window
// sometimes comes to the top. Hate those. There is surely
// a non-WPF native solution to the blanking window which likely
// involves drawing directly on the desktop or the target window

User32.SetForegroundWindow(hWnd);

BitmapSource captured = CaptureRegion(
hWnd,
rect.X,
rect.Y,
rect.Width,
rect.Height,
true);

if (blankingWindow != null)
blankingWindow.Close();

return captured;
}

One function in there deserves calling-out: GetWindowActualRect. This is where the majority of fudging happens

// this accounts for the border and shadow. Serious fudgery here.
private static Int32Rect GetWindowActualRect(IntPtr hWnd)
{
Win32Rect windowRect = new Win32Rect();
Win32Rect clientRect = new Win32Rect();

User32.GetWindowRect(hWnd, out windowRect);
User32.GetClientRect(hWnd, out clientRect);

int sideBorder = (windowRect.Width - clientRect.Width)/2 + 1;

// sooo, yeah.
const int hackToAccountForShadow = 4;

Win32Point topLeftPoint = new Win32Point(windowRect.Left - sideBorder, windowRect.Top - sideBorder);

//User32.ClientToScreen(hWnd, ref topLeftPoint);

Int32Rect actualRect = new Int32Rect(
topLeftPoint.X,
topLeftPoint.Y,
windowRect.Width + sideBorder * 2 + hackToAccountForShadow,
windowRect.Height + sideBorder * 2 + hackToAccountForShadow);

return actualRect;
}

There are a couple problems related to the magic numbers:

  1. You can incorrect results with maximized windows. I can check for this and handle it in code.
  2. Some windows just don't cooperate with this. Again, Windows Live Writer is one that throws me. Most of the other Windows all behave nicely, but the WLW window ends up shifted over a few pixels.
  3. This hasn't been tested on Aero Basic
  4. When you click on child hWnds (like the web page inside the browser, or the editor inside Visual Studio) the fudge numbers no longer make sense.

Other than that, it does a decent job of capturing the window itself, including its aero borders and shadows.

Once we have the actual bounds of the window (including shadows), the code simply calls the same CaptureRegion code from before. Here's an example of capturing the Visual Studio window.

image

Without the white blanking window in the background, it looks ugly, I know. That is a solvable problem and will be dealt with.

But wait, how did I go about picking that particular window for capture? Ahh, that's the next part :)

Picking a Window with the Mouse

I didn't want this to be a completely academic exercise, so I figured I had better put in some realistic functionality. That comes in the form of using the mouse to select the window you want to capture.

private void CaptureWindowWithBackgroundButton_Click(object sender, RoutedEventArgs e)
{
PickWindowWithMouse();
}

private void PickWindowWithMouse()
{
Window captureWindow = new Window();

captureWindow.WindowStyle = WindowStyle.None;
captureWindow.Title = string.Empty;
captureWindow.ShowInTaskbar = false;
captureWindow.AllowsTransparency = true;
captureWindow.Background = new SolidColorBrush(Color.FromArgb(0x01, 0x00, 0x00, 0x00));
captureWindow.Topmost = true;

captureWindow.Left = SystemParameters.VirtualScreenLeft;
captureWindow.Top = SystemParameters.VirtualScreenTop;
captureWindow.Width = SystemParameters.VirtualScreenWidth;
captureWindow.Height = SystemParameters.VirtualScreenHeight;

Point mouseDown = new Point();

captureWindow.KeyUp += (s, e) =>
{
if (e.Key == Key.Escape)
{
// cancel the capture
captureWindow.Close();
this.Show();
}
};

captureWindow.MouseDown += (s, e) =>
{
mouseDown = e.GetPosition(captureWindow);
};

captureWindow.MouseUp += (s, e) =>
{
Point current = e.GetPosition(captureWindow);

if (Math.Abs(mouseDown.X - current.X) < 10.0 &&
Math.Abs(mouseDown.Y - current.Y) < 10.0)
{
// this is the window they want

Point screenCoords = captureWindow.PointToScreen(current);
captureWindow.Close();

IntPtr hWnd = User32.WindowFromPoint(new Win32Point((int)screenCoords.X, (int)screenCoords.Y));
User32.SetForegroundWindow(hWnd);

//CapturedImage.Source = ScreenCapture.CaptureWindow(hWnd, true, Colors.White, true);
CapturedImage.Source = ScreenCapture.CaptureWindow(hWnd, false, Colors.Transparent, true);

// show our app window again, and bring it to the foreground
WindowInteropHelper helper = new WindowInteropHelper(this);
this.Show();
User32.SetForegroundWindow(helper.Handle);

}
};

this.Hide();
captureWindow.Show();

}

To handle this, I create a full-screen WPF window and use it to capture the click. Windows don't have a click event, so I check to see if the MouseUp event is raised within 10 pixels of the MouseDown event. The window covers the entire screen, so I don't bother with capturing the mouse.

I then use the Win32 API to map that click to a hWnd in Windows. That hWnd doesn't always resolve to a top-level application. For example, IE8: if I click in the web page, the only part that is captured is the rendered web page (the portion viewable on-screen). I have to click in the title bar or tool bar to get the full IE window.

So, in a nutshell:

  • Hide this app window
  • Show the capture window
  • Capture the mouse click
  • Hide the capture window
  • Map the mouse click to an app
  • Bring that app to the front (so whole window is visible)
  • Capture the image
  • Show my app's window again
  • Bring my app's window to the front

The user can press escape at any time to close the capture window.

The full-screen WPF window is not the only available option. However, I'll want to have that available for rubber band selection and other possible enhancements in the future, so using it here imposes no hard tax.

Features that are Not Quite Working

As mentioned, the code has some additional stuff not shown here. It's not shown because it's not working that well. The first is the ability to add a blanking window behind the window being captured. This is good for getting nice clean screenshots, typically on a white background. When taking normal screen shots using Print-Screen, I manually do this by hovering the window over notepad when using print screen. Crude, but works. I want a better solution.

The second bit is the clipboard. I wrestled with the clipboard tonight but couldn't get it to put a good default format in that was recognized by Windows Live Writer. As written, it will paste into MS Word and even MS Paint. WLW, on the other hand, doesn't recognize what's there. Obviously something's amiss.

So, expect a follow-up should I come up with decent solutions for these issues. Feel free to tinker with the code on your own as well.

       

Source Code and Related Media

Download /media/73897/petebrown.screencapture.zip
posted by Pete Brown on Tuesday, February 8, 2011
filed under:        

14 comments for “Capturing Screen Images in WPF using GDI, Win32 and a little WPF Interop Help”

  1. Petesays:
    @Aaron

    I encode comments on the blog. Here's the pastie link: http://pastie.org/1546855

    Truthfully, I didn't realize the CopyFromScreen method existed. Thanks for the tip :)

    I'll try that out instead of using the Win32 API methods

    Pete
  2. BHARAT BABASO MANEsays:
    Looks very nice, i am going try this out..

    It would be really great if you could guide me in one of my recent issues that i am working on.

    I am importing an SVG in my WPF application and showing as a vector graphics. Now my user wants a copy functionality for the imported image. I mean after the import user will select the part of the image and say copy and that copied part shall be displayed in another WPF window. If you could give some pointers it would be real good help for me.

    Thanks in advance
    Bharat Mane
    bharat.mane@gmail.com
  3. sanjaysays:
    Good program it helped me to implement a similar kind of requirement. There is a specific functionality that i'm finding a bit difficult to implement. Let me explain it.

    I need to bring up a rectangle box on the screen that should have a option to drag and resize the rectangle area and get the co-ordinates of the rectangle.

    These co-ordinates is used to record a video within in.

    Can you guide me to achieve this, please.

    Thanks,
    Sanjay
  4. Bobsays:

    Thanks!

    But running this code inside a Windows Service gives a black image (jpg), or white image (png). Not the screen shot I was looking for...

    The idea is to "monitor" a unattended computer by installing a service that captures screen shots at intervals and saves them to a web server.

    Any ideas?

    -Bob-
  5. Adam Bsays:
    Hi, i've used your CaptureRegion method and it worked quite well, the only thing i noticed is it opened a memory leak which would inevitably cause my program to crash.


    the problem was in the following part of the code:

    finally
    {
    Gdi32.DeleteObject(compatibleBitmapHandle);

    User32.ReleaseDC(IntPtr.Zero, sourceDC);
    User32.ReleaseDC(IntPtr.Zero, targetDC);
    }

    shouldn't it be:

    finally
    {
    Gdi32.DeleteObject(compatibleBitmapHandle);

    User32.ReleaseDC(IntPtr.Zero, sourceDC);
    User32.DeleteDC(targetDC);
    }

    this fixed it for me. just FYI

    anyways, great article :)
  6. Anassays:
    hiiz ..
    it is a great work..
    i wanna ask u if i can run this WPF application inside my silverlight 5 (out of browser ) website..
    how i can use Pinvoke in such application since i am be able to run the code in th silverlight application
  7. Softdessays:
    Hello i'm triying to use your code but i have a problem.
    Im triying to make a region screen of my current app, bot the image capture not include my current app... there is a way to solve that?

Comment on this Post

Remember me