起初我并不想发表答案,毕竟你已经有了一个足够好的答案。但是我被迫这样做了,另外,当你搜索GetClipboardData CF_DIB
时,这是最主要的问题,因此最好尝试提供一个更完整的解决方案。
不幸的是,剪贴板格式是个雷区。而GDI位图是一个更大的雷区。CF_DIB
给你一个“打包的DIB”,如果你想对其进行有意义的操作,你确实需要在某种程度上解析它。这是布局(伪代码):
struct PACKED_DIB {
struct BITMAPINFO {
BITMAPINFOHEADER bih;
DWORD optional_RGB_bitmaks[];
DWORD optional_color_table[];
}
BYTE pixel_data_array[];
}
整个结构的总大小由
GlobalSize()
给出。任何进一步处理所需的关键信息是从
BITMAPINFO
结构的开头到像素数据数组的开头的偏移量(以字节为单位)。如果可选的位掩码和颜色表不存在,则此偏移量为常数
40
(
sizeof(BITMAPINFOHEADER)
)。
是否如此取决于应用程序将位图放入剪贴板的方式。大多数应用程序之所以这样做,是因为这是最简单的方法。
该代码计算了该偏移量:
static INT GetPixelDataOffsetForPackedDIB(const BITMAPINFOHEADER *BitmapInfoHeader)
{
INT OffsetExtra = 0;
if (BitmapInfoHeader->biSize == sizeof(BITMAPINFOHEADER) )
{
if (BitmapInfoHeader->biBitCount > 8)
{
if (BitmapInfoHeader->biCompression == BI_BITFIELDS)
{
OffsetExtra += 3 * sizeof(RGBQUAD);
}
else if (BitmapInfoHeader->biCompression == 6 )
{
OffsetExtra += 4 * sizeof(RGBQUAD);
}
}
}
if (BitmapInfoHeader->biClrUsed > 0)
{
OffsetExtra += BitmapInfoHeader->biClrUsed * sizeof(RGBQUAD);
}
else
{
if (BitmapInfoHeader->biBitCount <= 8)
{
OffsetExtra += sizeof(RGBQUAD) << BitmapInfoHeader->biBitCount;
}
}
return BitmapInfoHeader->biSize + OffsetExtra;
}
下面是一个演示如何使用此偏移量进行多种操作的程序:
- 将剪贴板图像写入 .bmp 文件
- 使用 SFML 从内存中加载它(
sf::Image::loadFromMemory
)
- 从 SFML 图像中将其放回剪贴板
- 将其转换为
HBITMAP
(以便在 GDI 中使用)
- 从 GDI
HBITMAP
将其放回剪贴板
#include <sdkddkver.h>
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#if DEMO_SFML
#include <SFML/Graphics.hpp>
#endif
static BOOL OpenClipboard_ButTryABitHarder(HWND ClipboardOwner);
static INT GetPixelDataOffsetForPackedDIB(const BITMAPINFOHEADER *BitmapInfoHeader);
static void PutBitmapInClipboard_AsDIB(HBITMAP hBitmap);
static void PutBitmapInClipboard_From32bppTopDownRGBAData(INT Width, INT Height, const void *Data32bppRGBA);
int wmain(int argc, wchar_t *argv[])
{
if (!OpenClipboard_ButTryABitHarder(NULL))
{
return 1;
}
HGLOBAL ClipboardDataHandle = (HGLOBAL)GetClipboardData(CF_DIB);
if (!ClipboardDataHandle)
{
CloseClipboard();
return 0;
}
BITMAPINFOHEADER *BitmapInfoHeader = (BITMAPINFOHEADER *)GlobalLock(ClipboardDataHandle);
assert(BitmapInfoHeader);
SIZE_T ClipboardDataSize = GlobalSize(ClipboardDataHandle);
assert(ClipboardDataSize >= sizeof(BITMAPINFOHEADER));
INT PixelDataOffset = GetPixelDataOffsetForPackedDIB(BitmapInfoHeader);
size_t TotalBitmapFileSize = sizeof(BITMAPFILEHEADER) + ClipboardDataSize;
wprintf(L"BITMAPINFOHEADER size: %u\r\n", BitmapInfoHeader->biSize);
wprintf(L"Format: %hubpp, Compression %u\r\n", BitmapInfoHeader->biBitCount, BitmapInfoHeader->biCompression);
wprintf(L"Pixel data offset within DIB: %u\r\n", PixelDataOffset);
wprintf(L"Total DIB size: %zu\r\n", ClipboardDataSize);
wprintf(L"Total bitmap file size: %zu\r\n", TotalBitmapFileSize);
BITMAPFILEHEADER BitmapFileHeader = {};
BitmapFileHeader.bfType = 0x4D42;
BitmapFileHeader.bfSize = (DWORD)TotalBitmapFileSize;
BitmapFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + PixelDataOffset;
HANDLE FileHandle = CreateFileW(L"test.bmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (FileHandle != INVALID_HANDLE_VALUE)
{
DWORD dummy = 0;
BOOL Success = true;
Success &= WriteFile(FileHandle, &BitmapFileHeader, sizeof(BITMAPFILEHEADER), &dummy, NULL);
Success &= WriteFile(FileHandle, BitmapInfoHeader, (DWORD)ClipboardDataSize, &dummy, NULL);
Success &= CloseHandle(FileHandle);
if (Success)
{
wprintf(L"File saved.\r\n");
}
}
#if DEMO_SFML
BYTE *BitmapFileContents = (BYTE *)malloc(TotalBitmapFileSize);
assert(BitmapFileContents);
memcpy(BitmapFileContents, &BitmapFileHeader, sizeof(BITMAPFILEHEADER));
memcpy(BitmapFileContents + sizeof(BITMAPFILEHEADER), BitmapInfoHeader, ClipboardDataSize);
sf::Image image;
image.loadFromMemory(BitmapFileContents, TotalBitmapFileSize);
free(BitmapFileContents);
image.flipHorizontally();
PutBitmapInClipboard_From32bppTopDownRGBAData(image.getSize().x, image.getSize().y, image.getPixelsPtr());
#else
BYTE *PixelDataFromClipboard = (BYTE *)BitmapInfoHeader + PixelDataOffset;
BYTE *PixelDataNew;
HBITMAP hBitmap = CreateDIBSection(NULL, (BITMAPINFO *)BitmapInfoHeader, DIB_RGB_COLORS, (void **)&PixelDataNew, NULL, 0);
assert(hBitmap);
BITMAP BitmapDesc = {};
GetObjectW(hBitmap, sizeof(BitmapDesc), &BitmapDesc);
SIZE_T PixelDataBytesToCopy = (SIZE_T)BitmapDesc.bmHeight * BitmapDesc.bmWidthBytes;
SIZE_T PixelDataBytesAvailable = ClipboardDataSize - PixelDataOffset;
if (PixelDataBytesAvailable < PixelDataBytesToCopy)
{
PixelDataBytesToCopy = PixelDataBytesAvailable;
}
memcpy(PixelDataNew, PixelDataFromClipboard, PixelDataBytesToCopy);
PixelDataNew[7] = 0;
PixelDataNew[11] = 100;
HDC hdc = CreateCompatibleDC(NULL);
assert(hdc);
SelectObject(hdc, hBitmap);
RECT rc = { 0, 0, BitmapDesc.bmWidth / 2, BitmapDesc.bmHeight / 2 };
HBRUSH brush = CreateSolidBrush(RGB(250, 100, 0));
FillRect(hdc, &rc, brush);
DeleteObject(brush);
DeleteDC(hdc);
PutBitmapInClipboard_AsDIB(hBitmap);
#endif
GlobalUnlock(ClipboardDataHandle);
CloseClipboard();
return 0;
}
static BOOL OpenClipboard_ButTryABitHarder(HWND hWnd)
{
for (int i = 0; i < 20; ++i)
{
if (OpenClipboard(hWnd)) return true;
Sleep(10);
}
return false;
}
static INT GetPixelDataOffsetForPackedDIB(const BITMAPINFOHEADER *BitmapInfoHeader)
{
INT OffsetExtra = 0;
if (BitmapInfoHeader->biSize == sizeof(BITMAPINFOHEADER) )
{
if (BitmapInfoHeader->biBitCount > 8)
{
if (BitmapInfoHeader->biCompression == BI_BITFIELDS)
{
OffsetExtra += 3 * sizeof(RGBQUAD);
}
else if (BitmapInfoHeader->biCompression == 6 )
{
OffsetExtra += 4 * sizeof(RGBQUAD);
}
}
}
if (BitmapInfoHeader->biClrUsed > 0)
{
OffsetExtra += BitmapInfoHeader->biClrUsed * sizeof(RGBQUAD);
}
else
{
if (BitmapInfoHeader->biBitCount <= 8)
{
OffsetExtra += sizeof(RGBQUAD) << BitmapInfoHeader->biBitCount;
}
}
return BitmapInfoHeader->biSize + OffsetExtra;
}
static void PutBitmapInClipboard_From32bppTopDownRGBAData(INT Width, INT Height, const void *Data32bppRGBA)
{
assert(Width > 0);
assert(Height > 0);
assert(Data32bppRGBA);
DWORD PixelDataSize = 4 * Width * Height;
size_t TotalSize = sizeof(BITMAPINFOHEADER) + PixelDataSize;
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, TotalSize);
assert(hGlobal);
void *mem = GlobalLock(hGlobal);
assert(mem);
BITMAPINFOHEADER *bih = (BITMAPINFOHEADER *)mem;
bih->biSize = sizeof(BITMAPINFOHEADER);
bih->biWidth = Width;
bih->biHeight = -Height;
bih->biPlanes = 1;
bih->biBitCount = 32;
bih->biCompression = BI_RGB;
bih->biSizeImage = PixelDataSize;
BYTE *PixelData = (BYTE *)mem + sizeof(BITMAPINFOHEADER);
DWORD NumPixels = Width * Height;
for (DWORD i = 0; i < NumPixels; ++i)
{
DWORD tmp = ((DWORD *)Data32bppRGBA)[i];
DWORD tmp2 = tmp & 0xff00ff00;
tmp2 |= (tmp >> 16) & 0xff;
tmp2 |= (tmp & 0xff) << 16;
((DWORD *)PixelData)[i] = tmp2;
}
GlobalUnlock(hGlobal);
EmptyClipboard();
SetClipboardData(CF_DIB, hGlobal);
}
static void PutBitmapInClipboard_AsDIB(HBITMAP hBitmap)
{
BITMAP desc = {};
int tmp = GetObjectW(hBitmap, sizeof(desc), &desc);
assert(tmp != 0);
DWORD PixelDataSize = 4 * desc.bmWidth * desc.bmHeight;
assert(desc.bmWidth > 0);
assert(desc.bmHeight > 0);
size_t TotalSize = sizeof(BITMAPINFOHEADER) + PixelDataSize;
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, TotalSize);
assert(hGlobal);
void *mem = GlobalLock(hGlobal);
assert(mem);
BITMAPINFOHEADER *bih = (BITMAPINFOHEADER *)mem;
bih->biSize = sizeof(BITMAPINFOHEADER);
bih->biWidth = desc.bmWidth;
bih->biHeight = desc.bmHeight;
bih->biPlanes = 1;
bih->biBitCount = 32;
bih->biCompression = BI_RGB;
bih->biSizeImage = PixelDataSize;
HDC hdc = CreateCompatibleDC(NULL);
assert(hdc);
HGDIOBJ old = SelectObject(hdc, hBitmap);
assert(old != nullptr);
void *PixelData = (BYTE *)mem + sizeof(BITMAPINFOHEADER);
tmp = GetDIBits(hdc, hBitmap, 0, desc.bmHeight, PixelData, (BITMAPINFO *)bih, DIB_RGB_COLORS);
assert(tmp != 0);
DeleteDC(hdc);
GlobalUnlock(hGlobal);
EmptyClipboard();
SetClipboardData(CF_DIB, hGlobal);
}
我希望这段代码大部分是可以直接使用的,因为我需要它来满足自己的需求,如果有人发现了问题,我很乐意听取反馈。
以下是一些额外的参考说明:
- 已在Win10上测试
- 已在WinXP上测试(除SFML外),尽管较旧的CRT中
%zu
无法工作,谁知道呢
- 错误处理不适用于生产环境。
CF_DIB
的解释实际上是指“它是打包的DIB”。没有官方保证它将是一个普通的BITMAPINFOHEADER
,即biSize == 40
,尽管很可能情况是如此。
BITMAPFINO
文档解释了该结构实际上长度是可变的,并且需要考虑BITMAPINFOHEADER.biClrUsed
,但它未提到BI_BITFIELDS
。
BITMAPINFOHEADER
有更多细节,但未提及BI_ALPHABITFIELDS
或位掩码仅出现在多态BITMAPINFOHEADER
结构实际上是一个普通的BITMAPINFOHEADER
(即biSize == 40
)的事实。后续版本,如BITMAPV5HEADER
,无条件地包括位掩码。
- 总而言之,BMP文件格式的维基百科文章包含了一个更简明、更连贯的DIB内存布局解释。
- Paint的早期版本处理剪贴板,包括在
GetDIBPixelDataOffset
中所做的偏移量计算的方式非常类似(显然我不能在这里逐字张贴)。它不会假设biSize == 40
。Paint的新版本使用COleServerItem
处理剪贴板。
- 最后,由GIMP和其他跨平台软件使用的GTK源代码实现了CF_DIB的非常相似。该代码甚至处理Web浏览器特定格式,因此更难跟踪。它是transmute_cf_dib_to_image_bmp函数。函数的
length
参数来自GlobalSize
。请注意,它也不会假设biSize == 40
。
- 如果使用
CF_BITMAP
调用SetClipboardData
,它需要使用DDB(如果将其传递给真正的DIB,则会默默失败)。隐式转换为DIB的CF_BITMAP
使用BI_BITFIELDS
,这也适用于屏幕截图(至少如果原始DDB与屏幕DC兼容)。
- 将位图放入剪贴板是一个全新的困扰。如果使用
CF_BITMAP
将DDB放入剪贴板,则不会复制该位图(至少最初不会)。如果任何程序对其进行操作,则所有访问剪贴板的程序都将看到受到操纵的位图。但是,只要有一个应用程序请求它作为CF_DIB
,Windows就会施加一些魔术,那就不再正确了,位图现在已经是副本。这不适用于作为CF_DIB
放入剪贴板的位图,这些位图立即免疫其他程序的操作。相比之下,CF_DIB
似乎具有较少的令人不快的含义和惊
BITMAPINFO
是一个可变长度的结构体,要想确定它的长度需要仔细检查BITMAPINFOHEADER
。 - dialer