2

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.)

Kevin
  • 1,179
  • 7
  • 18
  • 2
    `CF_DIB` wants a packed bitmap, i.e. a single chunk of memory containing a `BITMAPINFO` followed by the bitmap bits - not a GDI handle. `CreateDIBSection` lets you specify where the bitmap is placed using a file mapping handle and an offset. Create a memory mapped file that's large enough to contain the header and the bits, and set `dwOffset` appropriately to point past the header. – Jonathan Potter May 13 '18 at 07:14
  • @JonathanPotter Thanks for the tip.. one thing not clear is what to pass as the second argument of `SetClipboardData`. I have tried the return from `CreateDIBSection`, the return from `CreateFileMapping`, and `(HANDLE)(((BITMAPINFO*)pBits) - 1)`, but none of them work. When using `CF_DIB`, they all cause the error `0x00000006 The handle is invalid.` I've copied the header into the mapping (`CreateDIBSection` apparently doesn't), but it still doesn't work. – Kevin May 13 '18 at 08:18
  • Another strange thing is that `CF_BITMAP` _never_ generates an error. Even `SetClipboardData( CF_BITMAP, (HANDLE)0xCCCCCCCC )` purports to succeed! This API seems to be a bit of a mess. – Kevin May 13 '18 at 08:20
  • `CreateCompatibleBitmap` lets you directly access the bits. What's wrong with `GetDIBits`? – Barmak Shemirani May 13 '18 at 09:01
  • @BarmakShemirani Thank you for the tip. If I understand correctly, `GetDIBits` _copies_ the bits, so then once I make the changes I'd have to call `SetDIBits` to put them back. But I guess I'll have to try this as I don't know what else to do. Kind of frustrating I have to incur 4 copies for someting that should only have needed 2... no wonder modern programs are so slow! – Kevin May 13 '18 at 09:27
  • Use MapViewOfFile to get a memory address from your file mapping and pass that. – Jonathan Potter May 13 '18 at 11:25
  • @JonathanPotter I'm pretty sure that wouldn't work; see my answer. But you got me on the right track; thanks! – Kevin May 13 '18 at 15:19
  • Did you try OLE? OleSetClipboard etc. – Anders May 13 '18 at 18:37
  • @Anders Interesting suggestion, no I didn't even think of it. Just looked at the API docs and it seems that one is pretty complex.. involves delayed rendering and negotiation between the producer and consumer regarding how the data will be transferred. Ultimately I solved it a much simpler way. – Kevin May 14 '18 at 10:14
  • @Kevin I think it is possible to return your data as an IStream on top of your memory view and that would avoid a copy but yes, it is complex. – Anders May 14 '18 at 10:44
  • @Kevin [This answer](https://stackoverflow.com/a/46424800/395685) covers it in c#. I never bothered using BITMAPINFO; I just constructed it in a byte array, because all `struct` methods are technically system-endianness-dependent. The normal method for putting DIB on the clipboard in c# is to use a `MemoryStream`. – Nyerguds May 16 '18 at 13:26

1 Answers1

2

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.

Kevin
  • 1,179
  • 7
  • 18
  • You always have to call `DeleteObject(hbitmap)` Note that `SetClipboardData` creates separate copies (or I could be wrong!) `CreateCompatibleBitmap` creates DDB hbitmap, `SetClipboardData(CF_BITMAP...)` converts that DDB to DIB. Also `GMEM_SHARE` is obsolete and ignored. If you use `GMEM_FIXED` then you don't need lock and unlock. – Barmak Shemirani May 14 '18 at 04:41
  • @BarmakShemirani From [MSDN](https://msdn.microsoft.com/en-us/library/windows/desktop/ms649051(v=vs.85).aspx): "If `SetClipboardData` succeeds, the system owns the object identified by the `hMem` parameter. The application may not write to or free the data once ownership has been transferred to the system ... If the `hMem` parameter identifies a memory object, the object must have been allocated using the function with the `GMEM_MOVEABLE` flag." It's possible `GMEM_FIXED` might work (undocumented!) but this would likely force the system to make a copy. – Kevin May 14 '18 at 09:59
  • Yes I got confused. You are making the copy yourself with `memcpy` Documentation says don't call `GlobalFree( hClipboardDib )` unless `SetClipboardData` fails, or `OpenClipboard` fails before it. `DeleteObject(hbitmap)` is still required. In the last part you propose something weird, which I think involves replacing `void *pBits;` with `GlobalAlloc` I don't see how that's going to work. Don't worry about that part. Modern CPUs are very good at allocating and copying large blocks of memory. – Barmak Shemirani May 14 '18 at 14:48
  • @BarmakShemirani Yeah, it's mostly academic, although I'm still of the opinion that every time CPUs get faster we just slow them back down with our increasing (albeit sensible) laziness. :) I wasn't suggesting to `GlobalAlloc` after `CreateCompatibleBitmap`, I was suggesting that the handle it returns is probably a "global memory handle" that you could then `GlobalLock` to get at the bits directly, but it's probably undocumented so a little risky. – Kevin May 14 '18 at 15:45
  • Plus, it might not avoid work anyway if as you say `CF_BITMAP` gets converted to DIB by `SetClipboardData`, although I seem to remember somewhere that the clipboard could simultaneously have multiple formats such as `CF_BITMAP` _and_ `CF_DIB`. – Kevin May 14 '18 at 15:48
  • *"The API documentation at MSDN is rather vague about this"* - No, it is not. [Standard Clipboard Formats](https://msdn.microsoft.com/en-us/library/windows/desktop/ff729168.aspx) explains: *"CF_DIB: A **memory object** containing a `BITMAPINFO` structure followed by the bitmap bits."* And [Clipboard Operations: Memory and the Clipboard](https://msdn.microsoft.com/en-us/library/windows/desktop/ms649014.aspx#_win32_Memory_and_the_Clipboard) points out: *"A memory object that is to be placed on the clipboard should be allocated by using the `GlobalAlloc` function with the `GMEM_MOVEABLE` flag."* – IInspectable May 14 '18 at 17:10
  • @IInspectable Good catch. They're _usually_ pretty good about mentioning this kind of thing on [the page for the function itself](https://msdn.microsoft.com/en-us/library/windows/desktop/ms649051(v=vs.85).aspx), so when I didn't see it there I didn't think to dig through the conceptual overviews. But "memory object" is a pretty vague term unless you do so... I actually just sent them feedback as such, since their pages make a point of asking me. :) – Kevin May 14 '18 at 17:36
  • On the other hand, it's information you already know without reading any documentation at all. You are passing ownership of a memory blob to the clipboard. Since the API has no way of specifying the allocator, the allocator needs to be implied. – IInspectable May 15 '18 at 19:10
  • @IInspectable True, but since I knew it accepted GDI objects, ideally I didn't need to question this further, and the link between GDI objects and the Windows 3.x allocator remained obscure (though not surprising in retrospect). I'd have figured it out much sooner had I been trying to paste text, because then I'd have been forced to question what the allocator was rather than just passing an opaque handle from another API. – Kevin May 15 '18 at 21:35
  • Odd. The c# clipboard functions have a specific option to make the clipboard make its own copy of the data so it's no longer your concern and you can clean up everything related to the creation. – Nyerguds May 16 '18 at 13:30