wpfclipboardbitmapsource

How to output an image of a WPF control to the clipboard


I have the following code that takes a Control (as a Visual object), uses a Visual Brush to pain the control into a RenderTargetBitmap which can then be saved to disk. This is successful.

I would like to use the same code to place the image in the clipboard. This does not seem to work; although the clipboard accepts the data, it does not accept that the data is an image. This is clearly a formatting issue but I have no idea how to sort it...

The code follows:-

public void CopyToClipBoard(Visual forDrawing)
{           
    RenderTargetBitmap bmp = ControlToImage(forDrawing, 96, 96);
    CopyToClipBoard(bmp);
}
private void CopyToClipBoard(BitmapSource bmp)
{
   Thread thread = new Thread(() =>
   {                
      Clipboard.SetImage(bmp);
   });
   thread.SetApartmentState(ApartmentState.STA); //Set the thread to STA
   thread.Start();
}
private static RenderTargetBitmap ControlToImage(Visual target, double dpiX, double dpiY)
{
   if (target == null)
   {
      return null;
   }
   // render control content
   Rect bounds = VisualTreeHelper.GetDescendantBounds(target);
   RenderTargetBitmap rtb = new RenderTargetBitmap((int)(bounds.Width * dpiX / 96.0),
                                                   (int)(bounds.Height * dpiY / 96.0),
                                                  dpiX, dpiY,
                                                  PixelFormats.Pbgra32);
   DrawingVisual dv = new DrawingVisual();
   using (DrawingContext ctx = dv.RenderOpen())
   {
       VisualBrush vb = new VisualBrush(target);
       ctx.DrawRectangle(vb, null, new Rect(new System.Windows.Point(), bounds.Size));
    }
    rtb.Render(dv);
    return rtb;           
}

Solution

  • After reviewing your code, I would expect a cross thread exception to be thrown in your thread delegate (CopyToClipBoard(BitmapSource):void). BitmapSource is a DispatcherObject and created on a different dispatcher thread (on the main thread) than it is handled on. You can't pass DispatcherObject instances between threads. Except the instance extends Freezable and Freezable.Freeze() was called on this instance.

    This means in order to pass the bmp reference to the thread that handles the clipboard (or any other thread than the associated thread in general), you must call bmp.Freeze() before.

    I assume the exception is swallowed by the Thread and you never ran your application in debug mode. That's why you never recognized the exception.

    As I wrote before, you don't need to spawn a new STA thread to access the clipboard. I don't recommend this.

    In case you keep using a dedicated thread, simply freeze the Freezable before passing it to the thread:

    public void CopyToClipBoard(Visual forDrawing)
    {           
        RenderTargetBitmap bmp = ControlToImage(forDrawing, 96, 96);
       
        // Freeze the freezable DispatcherObject so that it can be consumed by other threads
        bmp.Freeze();
    
        CopyToClipBoard(bmp);
    }