为什么我不能在剪贴板上放置DIB部分?

时间:2018-05-13 07:04:54

标签: windows winapi clipboard gdi

我正在尝试捕获屏幕内容,直接修改抓取图像的位,然后将结果放在剪贴板上。 (实际上,我最终并不对剪贴板感兴趣,而是将其用作测试步骤。)

我从this question的答案中的一个开始。但是,它使用CreateCompatibleBitmap,根据我的理解,无法直接访问使用该函数创建的位图位,因此我尝试使用CreateDIBSection。以下是我到目前为止的情况:

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);
}

然而,这不起作用。所有调用都报告成功,但图像不会在剪贴板上结束。

当我在调试器中运行时,我可以在调用pBits之后看到BitBlt处的图像数据看起来像什么,虽然有点怀疑第一组值有R, G,B都一样,但我的屏幕左下角实际上是蓝色。无论如何,即使实际位是错误的,我也应该在剪贴板上得到某个的图像,但我没有。

我尝试使用CF_DIB作为SetClipboardData的第一个参数而不是CF_BITMAP,但随后调用失败。

如果我注释掉CreateDIBSection的来电并取消注释CreateCompatibleBitmap,那么它可以正常工作,但我没有机会直接修改图片位。

我想我可以首先捕获我的DIB部分,修改它,然后从DIB部分调用CreateCompatibleBitmap和blit进入“兼容位图”,但是再次复制这些位似乎有点愚蠢原因。

为什么我不能将我的DIB部分传递给SetClipboardData

(我必须说我讨厌与GDI等合作。它通常很清楚。)

1 个答案:

答案 0 :(得分:1)

最后,当我找到this时想出来。 MSDN上的API文档对此非常模糊,可能是因为它本身可以追溯到目前为止,但看起来剪贴板函数都使用Windows 3.x样式的内存分配系统(GlobalAlloc等)。

系统剪贴板将共享内存直接暴露给应用程序是有意义的,而不是必须将数据复制到内部缓冲区的操作系统。但剪贴板功能可以追溯到远远不足以使基于较新页面文件的共享内存方案不存在,因此它们必须使用GlobalAlloc内存。当32位Windows出现时,仅仅模拟该机制而不是破坏现有的应用程序代码更有意义。

我强烈怀疑由于类似的原因,大多数GDI句柄实际上也是GlobalAlloc句柄,这就是为什么你可以将CreateCompatibleBitmap的返回传递给剪贴板的原因。相比之下,CreateDIBSection使完全使用旧式分配,这可以告诉它将位存储在文件映射中。 (我怀疑它返回的句柄仍然来自GlobalAlloc,但是这样分配的块依次包含指向图像数据的虚拟内存的直接指针,SetClipboardData测试它是因为它显而易见“疑难杂症”。)

所以我通过让CreateDIBSection分配到任何地方来修复所有内容,因为无论如何不可能将其交给SetClipboardData,然后在我想要的时候这样做发送到剪贴板:

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();
}

很遗憾,我必须在这里制作一份冗余副本,但从好的方面来看,我强烈怀疑阅读剪贴板的应用程序会看到同一份副本,而不是Windows在内部进行任何进一步的复制。

如果我想成为一名效率最高的玩家,我怀疑CreateCompatibleBitmap返回的句柄可用于调用GlobalLock然后你就可以这些位直接不会在CScreenshot::SendToClipboard中产生副本,因为您可以直接将其传递给SetClipboardData。然而,我也强烈怀疑这将是无证的行为(但如果我错了,请纠正我!),所以这是一个非常糟糕的主意。您还必须跟踪是否将其传递到剪贴板,如果没有,请不要在其上调用DeleteObject。但我不确定。我还怀疑SetClipboardData无论如何都必须复制它,因为它可能没有用GMEM_SHARE分配。

感谢评论者让我更接近想出来。