RenderTarget2D to clipboard

Hi to all,
is there a (quick) way to copy the content of a RenderTarget2D to the clipboard? (I’m talking about Windows.) I know there is the RenderTarget2D.SaveAsPng() method, so I was hoping there was a RenderTarget2D.CopyToClipboard() method too, but there isn’t.

1 Like

Have you tried storing that image to memory and exporting that image to clipboard?

You can convert the RenderTarget2D to a System.Drawing.Image, and then set it with System.Windows.Forms.Clipboard. Of course this is for Windows.

RenderTarget2D d;
System.Windows.Forms.Clipboard.SetImage(d.ToImage());

This is the code I use for the conversion

    public static System.Drawing.Image ToImage(this Texture2D texture)
    {
        if (texture == null) { return null; }

        byte[] textureData = new byte[4 * texture.Width * texture.Height];
        texture.GetData<byte>(textureData);

        // fix to swap R and B values
        byte r;
        for (int i = 0; i < textureData.Length; i += 4)
        {
            r = textureData[i];
            textureData[i] = textureData[i + 2];
            textureData[i + 2] = r;
        }
        
        System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(texture.Width, texture.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
        System.Drawing.Imaging.BitmapData bmpData = bmp.LockBits(new System.Drawing.Rectangle(0, 0, texture.Width, texture.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
        IntPtr safePtr = bmpData.Scan0;

        System.Runtime.InteropServices.Marshal.Copy(textureData, 0, safePtr, textureData.Length);
        bmp.UnlockBits(bmpData);

        return bmp;
    }

Edit: Added fix to swapping values

4 Likes

Your method works but it seems it swaps the Red component of the colors for the Blue one. Moreover, I don’t like too much the idea of having to add a reference to System.Drawing and System.Windows.Forms to my game project, but I suppose I have no alternatives.

Didn’t know about the red/blue swapping, I’ll look into it later.

I think that no matter what you do you’ll have to use Windows exclusive code seeing how you’re accessing their OS’s clipboard. You can still make your code platform independent with compiler flags. I like to put all these in a separate static class to keep track of them.

#if WINDOWS || LINUX

...exclusive code...

#endif

tl;dr use the suggested WinForms solution

You need platform-specific code because MonoGame does not expose clipboard functionality. However you don’t need System.Drawing or Windows Forms references (though that’s by far the easiest way to do it).

Instead you can directly use Win32 interop to set the clipboard data. It’s very painful compared to the WinForms solution, but since I looked into it I might as well write it up :stuck_out_tongue:
If nothing else, this post will make it clear that you’re better off using the WinForms + System.Drawing solution.

Clipboard Formats

The clipboard in Windows works by having a list of exposed formats linked to the data. For example if you copy some text from this webpage it might show up in the clipboard as HTML and as plain text. When pasting data the receiving program picks a format. For images Windows has different predefined bitmap formats, but there are custom formats as well. One custom format that’s used by some programs is PNG. MG can export textures to png so we can use that.
Note that the PNG clipboard format is not widely supported, so this method might not work depending on where you want to paste (works in GIMP and Inkscape, but not browsers, Discord, Paint 3D). Another way to handle this without bitmaps is to write the PNG to a (temporary) file and put the file handle in the clipboard instead, but that way you won’t be able to directly paste the image in most applications (works in Paint 3D, not in browsers, Discord, Inkscape). For good support you need to copy as bitmap data so you’ll need System.Drawing or an image encoding library that supports bitmap if the other options aren’t good enough for your use case (and they likely aren’t).

Interop Functions

Here’s the Win32 functions you’ll need:

[DllImport("user32.dll", SetLastError = true)]
public static extern bool OpenClipboard(IntPtr hwnd);

[DllImport("user32.dll", SetLastError = true)]
public static extern bool CloseClipboard();

[DllImport("user32.dll", SetLastError = true)]
public static extern bool EmptyClipboard();

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);

[DllImport("user32.dll", SetLastError = true)]
public static extern uint RegisterClipboardFormat(string lpszFormat);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalAlloc(uint uFlags, IntPtr dwBytes);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalLock(IntPtr hMem);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GlobalUnlock(IntPtr hMem);

Implementation

Windows clipboard data can be set on copy or on-demand when the data is requested (pasted). To set data on-demand you need to hook into the windows message loop which you don’t really want to do with MG because MG manages the message loop. Since we’ll only set the PNG format we can set the clipboard data on copy. To set clipboard data, we first have to open and empty the clipboard, then set our data and finally close the clipboard. Make sure you compile with unsafe enabled. The following code uses C# 8.

public static unsafe bool CopyTexture(Game game, int width, int height, Texture2D texture)
{
    var hwnd = game.Window.Handle;
    if (!OpenClipboard(hwnd))
        return false;

    try
    {
        if (!EmptyClipboard())
            return false;

        using var ms = new MemoryStream();
        texture.SaveAsPng(ms, width, height);

        // Clipboard data needs to be put in global memory
        const int GHND = 0x0042;
        var hmem = GlobalAlloc(GHND, ms.Length);
        var ptr = GlobalLock(hmem);

        using var ums = new UnmanagedMemoryStream((byte*) ptr.ToPointer(), ms.Length, ms.Length, FileAccess.Write);
        ms.CopyTo(ums);

        GlobalUnlock(hmem);

        var pngFormat = RegisterClipboardFormat("PNG");
        SetClipboardData(pngFormat, hmem);
    }
    finally
    {
        CloseClipboard();
    }

    return true;
}
2 Likes

I’m trying to use your solution (after fixing some mistyping), but the code “new UnmanagedMemoryStream((byte*)ptr.ToPointer(), ms.Length, ms.Length, FileAccess.Write)” gives me the error “Pointers and fixed-size buffers can only be used in an unsafe context” (repeated 4 times in the error list of Visual Studio).

Make sure you marked the method as unsafe and enabled unsafe compilation in your project.
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/unsafe-compiler-option#to-set-this-compiler-option-in-the-visual-studio-development-environment

But again, this will not work for most programs.

Perhaps:

?

I tried your fixed code and checked the Allow Unsafe Code option, but it doesn’t work anyway. If I paste into Paint or Gale, nothing happens. If I paste into paint.net, I get a PaintDotNet.Imaging.UnknownImageFormatException exception.
Anyway, this solution, as you warned, seems to be too much complicated so I will revert to the jamie_yello solution, but I have to find out why the Red & Blue color components are swapped.

Red and blue are swapped because MG uses ABGR while the bitmap in the suggested code has format System.Drawing.Imaging.PixelFormat.Format32bppArgb. So you’ll have to manually flip B and R channels in the array you fill with GetData.

Ok, thanks.

Now we just need a example for how to do this on linux mac android.
Then a wrapper around these that have a bunch of platform if defines and were good to go right ?

Should add this to a monogame class dunno how you deal with this for consoles.
I suppose just don’t define it?

I really wish system.drawing and drawing imaging were pure c# / .net classes then most of this would be painless. Like how many times have i needed something from winforms and just skipped it (Especially for text were you want to copy paste text into your monogame app.) cause its winblows only.

There’s the System.Drawing.Common nuget package which implements System.Drawing for Windows/Mac/Linux.

I updated my posted code to swap the R and B manually, thanks for pointing that out. I think I never noticed because I only ever used it on black and white things. Being colorblind doesn’t help either.

“Why isn’t the color changing? Oh yeah my eyes don’t work”

1 Like