Search code examples
windowswinapiclipboardgdi

Why can't I put a DIB section on the clipboard?


I am trying to capture the screen contents, modify the bits of the grabbed image directly, and then put the result on the clipboard. (Actually, I'm not ultimately interested in the clipboard, but am using it as a testing step.)

I started with the example from one of the answers to this question. However, it uses CreateCompatibleBitmap, and from what I understand, there is no way to directly access the bits of bitmaps created with that function, so I am trying to use CreateDIBSection instead. Here is what I have so far:

void GetScreenShot(void)
{
    int x1, y1, w, h;

    // get screen dimensions
    x1  = GetSystemMetrics(SM_XVIRTUALSCREEN);
    y1  = GetSystemMetrics(SM_YVIRTUALSCREEN);
    w  = GetSystemMetrics(SM_CXVIRTUALSCREEN);
    h  = GetSystemMetrics(SM_CYVIRTUALSCREEN);

    // copy screen to bitmap

    HDC hScreen = GetDC(NULL);

    HDC hDC = CreateCompatibleDC(hScreen);
    if( !hDC )
        throw 0;

    // This works:
    //HBITMAP hBitmap = CreateCompatibleBitmap(hScreen, w, h);

    BITMAPINFO BitmapInfo;
    BitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    BitmapInfo.bmiHeader.biWidth = w;
    BitmapInfo.bmiHeader.biHeight = h;
    BitmapInfo.bmiHeader.biPlanes = 1;
    BitmapInfo.bmiHeader.biBitCount = 24;   // assumption; ok for our use case
    BitmapInfo.bmiHeader.biCompression = BI_RGB;
    BitmapInfo.bmiHeader.biSizeImage = ((w * 3 + 3) & ~3) * h;
    BitmapInfo.bmiHeader.biXPelsPerMeter = (int)(GetDeviceCaps( hScreen, LOGPIXELSX ) * 39.3701 + 0.5);
    BitmapInfo.bmiHeader.biYPelsPerMeter = (int)(GetDeviceCaps( hScreen, LOGPIXELSY ) * 39.3701 + 0.5);
    BitmapInfo.bmiHeader.biClrUsed = 0;
    BitmapInfo.bmiHeader.biClrImportant = 0;
    BitmapInfo.bmiColors[0].rgbBlue = 0;
    BitmapInfo.bmiColors[0].rgbGreen = 0;
    BitmapInfo.bmiColors[0].rgbRed = 0;
    BitmapInfo.bmiColors[0].rgbReserved = 0;

    void *pBits;
    // This does not work:
    HBITMAP hBitmap = CreateDIBSection( hScreen, &BitmapInfo, DIB_RGB_COLORS, &pBits, NULL, 0 );
    if( !hBitmap )
        throw 0;

    HGDIOBJ old_obj = SelectObject(hDC, hBitmap);
    if( !old_obj )
        throw 0;

    if( !BitBlt(hDC, 0, 0, w, h, hScreen, x1, y1, SRCCOPY) )
        throw 0;

    if( !SelectObject(hDC, old_obj) )
        throw 0;

    if( !GdiFlush() )
        throw 0;

    // this is where we would modify the image

    // save bitmap to clipboard

    if( !OpenClipboard(NULL) )
        throw 0;

    if( !EmptyClipboard() )
        throw 0;

    if( !SetClipboardData( CF_BITMAP, hBitmap ) )   // CF_DIB causes the throw
        throw 0;

    if( !CloseClipboard() )
        throw 0;

    // clean up
    DeleteDC(hDC);
    ReleaseDC(NULL, hScreen);
    DeleteObject(hBitmap);
}

However, this does not work. All of the calls report success, but the image does not end up on the clipboard.

When I run this in a debugger, I can see what looks like image data at pBits after the call to BitBlt, although it's a bit suspicious in that the first bunch of values have R,G,B all the same, but the bottom-left corner of my screen actually has a bluish colour. Anyway, even if the actual bits are wrong, I should get something of an image on the clipboard, but I don't.

I've tried using CF_DIB as the first argument to SetClipboardData instead of CF_BITMAP, but then the call fails.

If I comment out the call to CreateDIBSection and uncomment the call to CreateCompatibleBitmap, then it works, but I have no opportunity to modify the image bits directly.

I guess I could capture my DIB section first, modify it, then call CreateCompatibleBitmap and blit from the DIB section into the "compatible bitmap", but it seems kind of asinine to copy the bits again for no apparent reason.

Why can't I pass my DIB section to SetClipboardData?

(I must say I hate working with GDI etc. It's generally clear as mud.)


Solution

  • Figured it out, finally, when I found this. The API documentation at MSDN is rather vague about this, probably because it itself dates back as far, but it looks like the clipboard functions all use the Windows 3.x style memory allocation system (GlobalAlloc etc.).

    It makes sense for a system clipboard to expose shared memory to the application directly as opposed to the OS having to copy data into internal buffers. But clipboard functionality dates back far enough that the newer page file based schemes for shared memory didn't exist, so they had to use GlobalAlloc memory. When 32-bit Windows came along, it made more sense to just emulate that mechanism rather than break existing application code.

    I strongly suspect that for similar reasons most GDI handles are actually GlobalAlloc handles as well, and that's why you can pass the return from CreateCompatibleBitmap to the clipboard. By contrast, CreateDIBSection does not fully use the old-style allocation, which is obvious from the fact that you can tell it to store the bits in a file mapping. (I suspect that the handle it returns is still from GlobalAlloc but that the block so allocated in turn contains a direct pointer to virtual memory for the image data, and SetClipboardData tests for this because it's an obvious "gotcha".)

    So I fixed everything by just letting CreateDIBSection allocate wherever it wants, because one way or another it's not going to be possible to hand that to SetClipboardData anyway, and then doing this when I want to send to the clipboard:

    void CScreenshot::SendToClipboard( void )
    {
        HGLOBAL hClipboardDib = GlobalAlloc( GMEM_MOVEABLE | GMEM_SHARE, cbDib );
        if( !hClipboardDib )
            throw 0;
    
        void *pClipboardDib = GlobalLock( hClipboardDib );
        memcpy( pClipboardDib, &BitmapInfo, sizeof(BITMAPINFOHEADER) );
        memcpy( (BITMAPINFOHEADER*)pClipboardDib+1, pBits, BitmapInfo.bmiHeader.biSizeImage );
        GlobalUnlock( hClipboardDib );
    
        if( !OpenClipboard( NULL ) )
        {
            GlobalFree( hClipboardDib );
            throw 0;
        }
    
        EmptyClipboard();
        SetClipboardData( CF_DIB, hClipboardDib );
        CloseClipboard();
    }
    

    It's unfortunate I have to make a redundant copy here, but on the bright side, I strongly suspect that the application reading the clipboard will see that same copy, as opposed to Windows doing any further copying internally.

    If I wanted to be a total efficiency junkie, I suspect that the handle returned from CreateCompatibleBitmap could be used in a call to GlobalLock and then you could get at the bits directly without incurring the copy in CScreenshot::SendToClipboard, because you could just pass it straight to SetClipboardData. However, I also strongly suspect that would be undocumented behaviour (but correct me if I'm wrong!), so a pretty bad idea. You'd also have to keep track of whether you passed that into the clipboard or not, and if you did, not call DeleteObject on it. But I'm not sure. I also suspect SetClipboardData would have to make a copy of it anyway, because it probably isn't allocated with GMEM_SHARE.

    Thanks to the commenters for getting me a little closer to figuring it out.