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