windows - 为什么我不能将 DIB 部分放入剪贴板?

标签 windows winapi clipboard gdi

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

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

但是,这不起作用。所有调用都报告成功,但图像最终并未出现在剪贴板上。

当我在调试器中运行它时,我可以在调用 BitBlt 之后在 pBits 看到看起来像图像数据的内容,尽管这有点可疑,因为第一个一堆值的 R、G、B 都相同,但我屏幕的左下角实际上是蓝色的。不管怎样,即使实际的位是错误的,我也应该在剪贴板上得到图像的一些东西,但我没有。

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

如果我注释掉对 CreateDIBSection 的调用并取消注释对 CreateCompatibleBitmap 的调用,那么它可以工作,但我没有机会直接修改图像位。

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

为什么我无法将 DIB 部分传递给 SetClipboardData

(我必须说我讨厌使用 GDI 等工具。它通常很清晰。)

最佳答案

当我发现 this 时,终于明白了。 MSDN 上的 API 文档对此相当模糊,可能是因为它本身可以追溯到很早以前,但看起来剪贴板函数都使用 Windows 3.x 风格的内存分配系统(GlobalAlloc 等) .

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

我强烈怀疑,出于类似的原因,大多数 GDI 句柄实际上也是 GlobalAlloc 句柄,这就是为什么您可以将 CreateCompatibleBitmap 的返回值传递到剪贴板。相比之下,CreateDIBSection 确实没有完全使用旧式分配,这一点从您可以告诉它将位存储在文件映射中的事实中可以明显看出。 (我怀疑它返回的句柄仍然来自 GlobalAlloc,但如此分配的 block 又包含指向图像数据虚拟内存的直接指针,并且 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

感谢评论者让我更接近弄清楚这一点。

关于windows - 为什么我不能将 DIB 部分放入剪贴板?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50313607/

相关文章:

c++ - APC 可以中断 WSAWaitForMultipleEvents 吗?

c++ - Win32编程 TextOut WM_Paint

c# - 如何在剪贴板中存储对象集合?

windows - Powershell - 使桌面背景更改立即生效

c# - 程序启动时如何请求管理员权限?

windows - 颜色管理 : Monitor profile changed notification for Windows?

javascript - 如何使用 JavaScript 检测图像是否被粘贴或放入内容可编辑区域?

windows - 如何调整 TCP 以实现高性能的单向传输?

Windows Docker 桌面 Linux 模式 - docker 容器时间偏差

css - flexbox 将换行符添加到剪贴板