views:

371

answers:

4

I need help setting a transparent image to the clipboard. I keep getting "handle is invalid". Basically, I need a "second set of eyes" to look over the following code. (The complete working project at ftp://missico.net/ImageVisualizer.zip.)

This is an image Debug Visualizer class library, but I made the included project to run as an executable for testing. (Note that window is a toolbox window and show in taskbar is set to false.) I was tired of having to perform a screen capture on the toolbox window, open the screen capture with an image editor, and then deleting the background added because it was a screen capture. So I thought I would quickly put the transparent image onto the clipboard. Well, the problem is...no transparency support for Clipboard.SetImage. Google to the rescue...not quite.

This is what I have so far. I pulled from a number of sources. See the code for the main reference. My problem is the "invalid handle" when using CF_DIBV5. Do I need to use BITMAPV5HEADER and CreateDIBitmap?

Any help from you GDI/GDI+ Wizards would be greatly appreciated.

    public static void SetClipboardData(Bitmap bitmap, IntPtr hDC) {

        const uint SRCCOPY = 0x00CC0020;
        const int CF_DIBV5 = 17;
        const int CF_BITMAP = 2;

        //'reference
        //'http://social.msdn.microsoft.com/Forums/en-US/winforms/thread/816a35f6-9530-442b-9647-e856602cc0e2

        IntPtr memDC = CreateCompatibleDC(hDC);
        IntPtr memBM = CreateCompatibleBitmap(hDC, bitmap.Width, bitmap.Height);

        SelectObject(memDC, memBM);

        using (Graphics g = Graphics.FromImage(bitmap)) {

            IntPtr hBitmapDC = g.GetHdc();
            IntPtr hBitmap = bitmap.GetHbitmap();

            SelectObject(hBitmapDC, hBitmap);

            BitBlt(memDC, 0, 0, bitmap.Width, bitmap.Height, hBitmapDC, 0, 0, SRCCOPY);

            if (!OpenClipboard(IntPtr.Zero)) {
                throw new System.Runtime.InteropServices.ExternalException("Could not open Clipboard", new Win32Exception());
            }

            if (!EmptyClipboard()) {
                throw new System.Runtime.InteropServices.ExternalException("Unable to empty Clipboard", new Win32Exception());
            }

            //IntPtr hClipboard = SetClipboardData(CF_BITMAP, memBM); //works but image is not transparent

            //all my attempts result in SetClipboardData returning hClipboard = IntPtr.Zero
            IntPtr hClipboard = SetClipboardData(CF_DIBV5, memBM);


            //because 
            if (hClipboard == IntPtr.Zero) {

                //    InnerException: System.ComponentModel.Win32Exception
                //         Message="The handle is invalid"
                //         ErrorCode=-2147467259
                //         NativeErrorCode=6
                //         InnerException: 

                throw new System.Runtime.InteropServices.ExternalException("Could not put data on Clipboard", new Win32Exception());
            }

            if (!CloseClipboard()) {
                throw new System.Runtime.InteropServices.ExternalException("Could not close Clipboard", new Win32Exception());
            }

            g.ReleaseHdc(hBitmapDC);

        }

    }

    private void __copyMenuItem_Click(object sender, EventArgs e) {

        using (Graphics g = __pictureBox.CreateGraphics()) {

            IntPtr hDC = g.GetHdc();

            MemoryStream ms = new MemoryStream();

            __pictureBox.Image.Save(ms, ImageFormat.Png);

            ms.Seek(0, SeekOrigin.Begin);

            Image imag = Image.FromStream(ms);

            // Derive BitMap object using Image instance, so that you can avoid the issue 
            //"a graphics object cannot be created from an image that has an indexed pixel format"

            Bitmap img = new Bitmap(new Bitmap(imag));

            SetClipboardData(img, hDC);

            g.ReleaseHdc();

        }

    }
A: 

Bitmap.GetHbitmap() will actually composite the bitmap to an opaque background, losing the alpha channel. This question addressed how to get an HBITMAP with the alpha channel intact.

Chris Ostler
Same problem of "invalid handle". Moreover, if I use CF_BITMAP the background is Black instead of "control grey" used by the PictureBox control.
AMissico
The documentation for the clipboard formats (see http://msdn.microsoft.com/en-us/library/ms649013.aspx) states that when using CF_DIB, the handle is to be a buffer allocated using GlobalAlloc, which contains a BITMAPINFO structure, followed by the bitmap bits. I would expect that you would have to marshall this manually.When using CF_BITMAP, you may pass an HBITMAP, but there is not the associated metadata to specify the use of the alpha channel, so it is ignored.
Chris Ostler
A: 

I wonder if you need to create a screen compatible DC and select the bitmap into that first?

Steve Sheldon
+1  A: 

I see three problems:

  1. The invalid handle error might come from leaving memBM selected into memDC. You should always select the bitmap out of a DC before passing it anywhere else.

  2. BitBlt is a GDI call (not GDI+). GDI doesn't preserve the alpha channel. On newer versions of Windows, you can use AlphaBlend to composite a bitmap with alpha onto a background, but the composite won't have an alpha channel.

  3. You've created a compatible bitmap, which means the bitmap has the same color format as the DC you passed in (which I'm assuming is the same as the screen). So your bitmap could end up as 16-bit, 24-bit, 32-bit, or even 8-bit, depending on how the screen is set. If BitBlt had preserved the alpha channel of the original, you'd likely lose it when converting to the screen format.

Adrian McCarthy
AlphaBlend has been around for quite a while.
Chris O
+1  A: 

There are a few things you can do to tighten up the codebase a bit, and I've done some homework on CF_DIBV5 that you may find helpful.

First off, in __copyMenuItem_Click(), we have four complete copies of your image, which is much more than necessary.

  1. __pictureBox.Image
  2. Image imag = Image.FromStream(ms);
  3. new Bitmap(imag)
  4. Bitmap img = new Bitmap(new Bitmap(imag)); (the outer bitmap)

Furthermore, your MemoryStream, imag, new Bitmap(imag), and img do not get disposed, which could result in problems.

Without changing the intent of the code (and probably without solving the handle issue), you could rewrite the method like this:

private void __copyMenuItem_Click(object sender, EventArgs e)
{
    var image =  __pictureBox.Image;
    using (var g = __pictureBox.CreateGraphics())
    using (var bmp = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb))
    using (var bmpg = Graphics.FromImage(bmp))
    {
        IntPtr hDC = g.GetHdc();
        bmpg.DrawImage(image, 0, 0, image.Width, image.Height);
        SetClipboardData(bmp, hDC);
        g.ReleaseHdc();
    }
}

The next thing that looked like it would require attention was the line:

IntPtr hClipboard = SetClipboardData(CF_DIBV5, memBM);

I am fairly certain that you must marshal out the BITMAPV5HEADER structure to pass bits to the clipboard when using CF_DIBV5. I've been wrong before, but I would verify that memBM actually contains the header. A good indicator is whether the first DWORD (UInt32) has the value 124 (the size of the header in bytes).

My final remarks are more recommendation than a second pair of eyes. I found that photo applications like GIMP, Paint.NET, Fireworks, and PhotoScape appear to have poor or non-existant support for CF_DIBV5 (Format17) pasting. you might consider copying to the clipboard the PNG format, with an opaque bitmap as backup just in case the target application does not support PNG. I use an extension method to facilitate this:

public static void CopyMultiFormatBitmapToClipboard(this Image image)
{
    using (var opaque = image.CreateOpaqueBitmap(Color.White))
    using (var stream = new MemoryStream())
    {
        image.Save(stream, ImageFormat.Png);

        Clipboard.Clear();
        var data = new DataObject();
        data.SetData(DataFormats.Bitmap, true, opaque);
        data.SetData("PNG", true, stream);
        Clipboard.SetDataObject(data, true);
    }
}

With the extension method in hand, your __copyMenuItem_Click() method could be reduced to the following, and the SetClipboardData() method could be removed altogether:

private void __copyMenuItem_Click(object sender, EventArgs e)
{
    __pictureBox.Image.CopyMultiFormatBitmapToClipboard();
}

Now, as we already discussed on another thread, PNG support may not cut it for you. I've tested this approach on a few applications; however, it will be up to you to determine whether this is sufficient transparency support for your requirements.

  • GIMP: transparency supported
  • Fireworks (3.0): transparency supported
  • PhotoScape: white background
  • Paint.NET: white background
  • MS PowerPoint 2007: transparency supported
  • MS Word 2007: white background
  • MS Paint (Win7): white background

Discussion of everything I looked into would be too lengthy for Stack Overflow. I have additional sample code and discussion available at my blog: http://www.notesoncode.com/articles/2010/08/16/HandlingTransparentImagesOnTheClipboardIsForSomeReasonHard.aspx

Good luck!

kbrimington
I do not have time to try out your code, but I am giving you the bounty for all your hard work. The posted code is throw-away code that I gathered from numerous sources in order to get anything to work. So your "second set of eyes" and recommendations is a great help. As soon as I get caught up, I will try out your suggestions.
AMissico