Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating a screenshot of a WPF window

Tags:

c#

wpf

In winforms, we can use DrawToBitmap. Is there a similar method to this in WPF?

like image 318
user496949 Avatar asked Feb 26 '11 03:02

user496949


2 Answers

Have you tried RenderTargetBitmap? https://msdn.microsoft.com/en-us/library/system.windows.media.imaging.rendertargetbitmap.aspx

There are a few "screenshot" methods around that use that, like this one taken from here:

    public static void CreateBitmapFromVisual(Visual target, string fileName)
    {
        if (target == null || string.IsNullOrEmpty(fileName))
        {
            return;
        }

        Rect bounds = VisualTreeHelper.GetDescendantBounds(target);

        RenderTargetBitmap renderTarget = new RenderTargetBitmap((Int32)bounds.Width, (Int32)bounds.Height, 96, 96, PixelFormats.Pbgra32);

        DrawingVisual visual = new DrawingVisual();

        using (DrawingContext context = visual.RenderOpen())
        {
            VisualBrush visualBrush = new VisualBrush(target);
            context.DrawRectangle(visualBrush, null, new Rect(new Point(), bounds.Size));
        }

        renderTarget.Render(visual);
        PngBitmapEncoder bitmapEncoder = new PngBitmapEncoder();
        bitmapEncoder.Frames.Add(BitmapFrame.Create(renderTarget));
        using (Stream stm = File.Create(fileName))
        {
            bitmapEncoder.Save(stm);
        }
    }
like image 139
almulo Avatar answered Oct 13 '22 08:10

almulo


Tested:

  • In use in an enterprise WPF app.
  • Tested on small part of an entire screen (can take a screenshot of any element on the screen).
  • Tested with multiple monitors.
  • Tested on a Window with WindowState=Normal and WindowState=Maximized.
  • Tested with 96 DPI.
  • Tested with 120 DPI (set font size to "125%" in Windows 10, then log out and log in).
  • Avoids any use of Windows-Form-Style pixel calculations which have scaling issues in different DPI settings.
  • Works even if part of the window is off the screen.
  • Works even if another rogue window is covering part of the current window.
  • Uses ClipToBounds so it is compatible with multiple docked windows in Infragistics.

Function:

/// <summary>
/// Take screenshot of a Window.
/// </summary>
/// <remarks>
/// - Usage example: screenshot icon in every window header.                
/// - Keep well away from any Windows Forms based methods that involve screen pixels. You will run into scaling issues at different
///   monitor DPI values. Quote: "Keep in mind though that WPF units aren't pixels, they're device-independent @ 96DPI
///   "pixelish-units"; so really what you want, is the scale factor between 96DPI and the current screen DPI (so like 1.5 for
///   144DPI) - Paul Betts."
/// </remarks>
public async Task<bool> TryScreenshotToClipboardAsync(FrameworkElement frameworkElement)
{
    frameworkElement.ClipToBounds = true; // Can remove if everything still works when the screen is maximised.

    Rect relativeBounds = VisualTreeHelper.GetDescendantBounds(frameworkElement);
    double areaWidth = frameworkElement.RenderSize.Width; // Cannot use relativeBounds.Width as this may be incorrect if a window is maximised.
    double areaHeight = frameworkElement.RenderSize.Height; // Cannot use relativeBounds.Height for same reason.
    double XLeft = relativeBounds.X;
    double XRight = XLeft + areaWidth;
    double YTop = relativeBounds.Y;
    double YBottom = YTop + areaHeight;
    var bitmap = new RenderTargetBitmap((int)Math.Round(XRight, MidpointRounding.AwayFromZero),
                                        (int)Math.Round(YBottom, MidpointRounding.AwayFromZero),
                                        96, 96, PixelFormats.Default);

    // Render framework element to a bitmap. This works better than any screen-pixel-scraping methods which will pick up unwanted
    // artifacts such as the taskbar or another window covering the current window.
    var dv = new DrawingVisual();
    using (DrawingContext ctx = dv.RenderOpen())
    {
        var vb = new VisualBrush(frameworkElement);
        ctx.DrawRectangle(vb, null, new Rect(new Point(XLeft, YTop), new Point(XRight, YBottom)));
    }
    bitmap.Render(dv);
    return await TryCopyBitmapToClipboard(bitmap);         
}        

private static async Task<bool> TryCopyBitmapToClipboard(BitmapSource bmpCopied)
{
    var tries = 3;
    while (tries-- > 0)
    {
        try
        {
            // This must be executed on the calling dispatcher.
            Clipboard.SetImage(bmpCopied);
            return true;
        }
        catch (COMException)
        {
            // Windows clipboard is optimistic concurrency. On fail (as in use by another process), retry.
            await Task.Delay(TimeSpan.FromMilliseconds(100));
        }
    }
    return false;
}   

In ViewModel:

 public ICommand ScreenShotCommand { get; set; }

Command:

 private async void OnScreenShotCommandAsync(FrameworkElement frameworkElement)
 {
     var result = await this.TryScreenshotToClipboardAsync(frameworkElement); 
     if (result == true)
     {
        // Success.
     }
 }

In constructor:

// See: https://stackoverflow.com/questions/22285866/why-relaycommand
// Or use MVVM Light to obtain RelayCommand.
this.ScreenShotCommand = new RelayCommand<FrameworkElement>(this.OnScreenShotCommandAsync);

And in XAML:

<Button Command="{Binding ScreenShotCommand, Mode=OneWay}"
        CommandParameter="{Binding ElementName=Content}"                                                        
        ToolTip="Save screenshot to clipboard">    
</Button>

The ElementName=Content points to a named element somewhere else on the same XAML page. If you want to screenshot an entire window, you cannot pass the Window in (as we cannot set ClipToBounds on a Window) but we can pass in a <Grid> inside the Window.

<Grid x:Name="Content">
    <!-- Content to take a screenshot of. -->
</Grid>
like image 24
Contango Avatar answered Oct 13 '22 10:10

Contango