Skip to content


Taking WPF “Screenshots”

I was recently working on a Surface project at Microsoft (that will be shown at BETT :-) ) and one of the requirements was to provide an external “administration console”.  As part of that console I wanted to show an “screenshot” of the current game running on the Surface unit; after playing around for a while it turned out it was pretty straightforward.

We did consider sending over the RAW XAML from the Surface to the Console, but that would potentially have issues when resources weren’t available, so the approach that was taken was to create a JPG screenshot and send it over as a byte array via WCF.

Rendering to a BitmapFrame

The key to this approach is RenderTargetBitmap which allows us to render any WPF Visual to a BitmapFrame as follows:

RenderTargetBitmap renderTarget = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Pbgra32);
renderTarget.Render(myVisual);
BitmapFrame bitmapFrame = BitmapFrame.Create(renderTarget);

Then from there we can use JpegBitmapEncoder to create a JPG from that BitmapFrame:

JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder();
jpgEncoder.Frames.Add(bitmapFrame);

Then we can output that JPG to a stream of our choice using the Save() method.

Problems

While this works for many cases, and indeed worked perfectly for the Surface application, we do encounter problems when the source we are rendering has Transforms applied or when it’s not positioned at 0,0.  When this occurs the screenshots we take will have the content shifted“out of frame” resulting in black borders, or content missing altogether.  The following screenshot demonstrates the problem:

Shifted Content Problem

Workaround

To work around the problem we can use a VisualBrush to “draw” our source element onto a new Visual, and render that with our RenderTargetBitmap:

DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();

using (drawingContext)
{
    drawingContext.DrawRectangle(sourceBrush, null, new Rect(new Point(0, 0), new Point(200, 200)));
}
renderTarget.Render(drawingVisual);

It’s not ideal, but I’ve yet to find a better workaround for it.

Putting it all Together

To make it more useful, we can wrap the whole lot up into a Extension Method.  Rather than extending Visual, I’ve chosen to use UIElement so I have access to the RenderSize to calculate the required size of the output bitmap.  I’ve also included parameters to scale the resulting bitmap and to set the JPG quality level:

///
/// Gets a JPG "screenshot" of the current UIElement
///
/// UIElement to screenshot
/// Scale to render the screenshot
/// JPG Quality
/// Byte array of JPG data
public static byte[] GetJpgImage(this UIElement source, double scale, int quality)
{
    double actualHeight = source.RenderSize.Height;
    double actualWidth = source.RenderSize.Width;

    double renderHeight = actualHeight * scale;
    double renderWidth = actualWidth * scale;

    RenderTargetBitmap renderTarget = new RenderTargetBitmap((int) renderWidth, (int) renderHeight, 96, 96, PixelFormats.Pbgra32);
    VisualBrush sourceBrush = new VisualBrush(source);

    DrawingVisual drawingVisual = new DrawingVisual();
    DrawingContext drawingContext = drawingVisual.RenderOpen();

    using (drawingContext)
    {
        drawingContext.PushTransform(new ScaleTransform(scale, scale));
        drawingContext.DrawRectangle(sourceBrush, null, new Rect(new Point(0, 0), new Point(actualWidth, actualHeight)));
    }
    renderTarget.Render(drawingVisual);

    JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder();
    jpgEncoder.QualityLevel = quality;
    jpgEncoder.Frames.Add(BitmapFrame.Create(renderTarget));

    Byte[] _imageArray;

    using (MemoryStream outputStream = new MemoryStream())
    {
        jpgEncoder.Save(outputStream);
        _imageArray = outputStream.ToArray();
    }

    return _imageArray;
}

I’ve bundled the extension method class in with a small demo app and you can grab the source to both from here:

screenshotdemo.zip

Share:
  • Twitter
  • DotNetKicks
  • DotNetShoutout
  • Google Bookmarks
  • Digg
  • del.icio.us
  • Live
  • Technorati
  • StumbleUpon
  • email
  • Netvibes
  • Ping.fm
  • Print
  • Reddit

Technorati Tags: , ,

Posted in Surface, WPF.

Tagged with , , .


18 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. abi says

    Very good post, i too had the same problem, however when i published my xbap application under partial trust, it throw a Security Exception, Is DrawingVisual and DrawingContext will work only on full trust,

    Thank Your

    • srobbins says

      @abi I’ve just had a look through, and it won’t work in partial trust because of the JpegBitmapEncoder – it’s not supported in that situatation. I guess it’s to stop you creating a bitmap on the fly and bypassing the security for where you can and can’t grab images from.

  2. abi says

    Thanks srobbins for the reply , Encoders wont work in partial trust, do you have any suggetions to take the bytes of RenderTargetBitmap, im really stuck here.

    Thanks

    • srobbins says

      The only other suggestion I can think of is to render to a WritableBitmap (WriteableBitmap rb = new WriteableBitmap(renderTarget);) then try grabbing the pixels from that. You will need to find another library to encode the data though, unless you want to move it around in a raw format.

  3. Prerak Sanghvi says

    This repeatably causes low level exceptions on my machine (as you read the stack trace, know that I’ve ensured my code runs on UI thread). I’m using .NET 3.0 (I know I could update, but I have other constraints).

    Unhandled Exception on Dispatcher (System.Windows.Threading.Dispatcher). Handled=False, Exception=System.Runtime.InteropServices.COMException (0x8000FFFF): Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))
    at MS.Internal.HRESULT.Check(Int32 hr)
    at System.Windows.Media.Composition.DUCE.Channel.SendCommand(Byte* pCommandData, Int32 cSize)
    at System.Windows.Media.Composition.DUCE.CompositionNode.SetAlpha(ResourceHandle hCompositionNode, Double alpha, Channel channel)
    at System.Windows.Media.Visual.DisconnectBitmapEffectPropertiesOnAllChannels()
    at System.Windows.Media.Visual.set_VisualBitmapEffect(BitmapEffect value)
    at System.Windows.UIElement.OnBitmapEffectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    at System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    at System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
    at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, OperationType operationType)
    at System.Windows.DependencyObject.InvalidateProperty(DependencyProperty dp)
    at System.Windows.StyleHelper.InvalidateDependents(Style ownerStyle, FrameworkTemplate frameworkTemplate, DependencyObject container, DependencyProperty dp, FrugalStructList`1& dependents, Boolean invalidateOnlyContainer)
    at System.Windows.StyleHelper.OnTriggerSourcePropertyInvalidated(Style ownerStyle, FrameworkTemplate frameworkTemplate, DependencyObject container, DependencyProperty dp, DependencyPropertyChangedEventArgs changedArgs, Boolean invalidateOnlyContainer, FrugalStructList`1& triggerSourceRecordFromChildIndex, FrugalMap& propertyTriggersWithActions, Int32 sourceChildIndex)
    at System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
    at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, OperationType operationType)
    at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, OperationType operationType, Boolean isInternal)
    at System.Windows.DependencyObject.SetValue(DependencyPropertyKey key, Object value)
    at System.Windows.DependencyObject.SetValue(DependencyPropertyKey dp, Boolean value)
    at System.Windows.ReverseInheritProperty.FirePropertyChangeInAncestry(DependencyObject element, Boolean oldValue, DeferredElementTreeState treeState)
    at System.Windows.ReverseInheritProperty.FirePropertyChangeInAncestry(DependencyObject element, Boolean oldValue, DeferredElementTreeState treeState)
    at System.Windows.ReverseInheritProperty.FirePropertyChangeInAncestry(DependencyObject element, Boolean oldValue, DeferredElementTreeState treeState)
    at System.Windows.ReverseInheritProperty.FirePropertyChangeInAncestry(DependencyObject element, Boolean oldValue, DeferredElementTreeState treeState)
    at System.Windows.ReverseInheritProperty.OnOriginValueChanged(DependencyObject oldOrigin, DependencyObject newOrigin, DeferredElementTreeState& oldTreeState)
    at System.Windows.Input.MouseDevice.ChangeMouseOver(IInputElement mouseOver, Int32 timestamp)
    at System.Windows.Input.MouseDevice.PreNotifyInput(Object sender, NotifyInputEventArgs e)
    at System.Windows.Input.InputManager.ProcessStagingArea()
    at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
    at System.Windows.Input.InputProviderSite.ReportInput(InputReport inputReport)
    at System.Windows.Interop.HwndMouseInputProvider.ReportInput(IntPtr hwnd, InputMode mode, Int32 timestamp, RawMouseActions actions, Int32 x, Int32 y, Int32 wheel)
    at System.Windows.Interop.HwndMouseInputProvider.FilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at System.Windows.Interop.HwndSource.InputFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
    at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
    at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)

    • Kess says

      I am getting the COMException as well. Does any one know of a solution? Thanks.

      • srobbins says

        Haven’t got a clue I’m afraid. If you put the sample code somewhere I can try it here to see if it’s a “your machine” issue if that’s any help.

  4. cristi says

    Excelent post!

    But how it works if the UIElement is not visible? Taking screenshot from a hidden windows

    • srobbins says

      @cristi Good question, I have no idea :-) If the Window itself is invisible, and isn’t rendering, then you could always render whatever “root” content you have in there (grid, stackpanel, canvas etc.)

  5. Moritz says

    Thanks a lot for this great piece of code! Saved me a lot of research. ;)

    Best regards from Germany

  6. AAron says

    Very nice, thank you for posting. Saved lots of time here too.

  7. Aaron says

    Thank you very much for the code sample – it’s excellent! One question – I’m loading a usercontrol into a tabcontrol with a FlowDirection of RightToLeft and when the code grabs the screenshot of the usercontrol, it’s always backwards as a result of the tabcontrol’s flow layout. Any ideas how to get around that?

    • srobbins says

      I haven’t tested it, but you could possibly create a grid (or another container), set the flow direction, add your user control to it and screenshot the grid. Then re-parent the user control back where it came from.

      • Aaron says

        I gave that a try this morning, but didn’t have any luck with it. When I figure out a solution, I’ll be sure to post it. Thanks for your suggestion!

      • Aaron says

        I used a style for the TabControl and set the TabPanel’s FlowDirection to RightToLeft to get the tabs on the right. Then I left the FlowDirection of the TabControl itself as LeftToRight. Here’s the TabPanel tag that I have within the ControlTemplate:

  8. Kenneth says

    Excellent post! Solves what I needed to do (which is to screen capture the WPF window).
    Thank you!

  9. Keith says

    Great post! I was really hoping it would help me with what I am trying to do, but it doesn’t work. I am trying to use the code to get a screenshot of the contents of a wpf WebBrowser control. All I am getting is a black image. I was hoping to use the screenshot in an Image control because the WebBrowser control does not allow any other control to overlay it.

Continuing the Discussion

  1. WPF UIElement extension methods for ’screenshot’ and location linked to this post on 20/04/2010

    [...] a bunch of WPF pages. In WPF this is an easy task. Especially when somenone already has created an extension method for it:. The extension method is in C#, so i have converted it to VB.NET and added another extension method [...]



Some HTML is OK

or, reply to this post via trackback.