我最初不打算发布答案,毕竟你已经有了一个足够好的答案。不过我想我还是被逼着这么做的,另外,这是你搜索GetClipboardData CF_DIB时弹出的主要问题,所以不妨尝试提出一个更完整的解决方案。
不幸的是,剪贴板格式是一个雷区。 GDI 位图是一个更大的雷区。 CF_DIB 给你一个“打包的 DIB”,如果你真的想用它做任何有意义的事情,你确实需要在某种程度上解析它。这是布局(伪代码):
struct PACKED_DIB {
struct BITMAPINFO {
BITMAPINFOHEADER bih; // 40 bytes
DWORD optional_RGB_bitmaks[]; // (variable size)
DWORD optional_color_table[]; // (variable size)
}
BYTE pixel_data_array[]; // (variable size)
}
结构的总大小由GlobalSize() 给出。任何进一步处理所需的关键信息位是从BITMAPINFO 结构的开头到像素数据数组的开头的偏移量(以字节为单位)。如果可选位掩码和颜色表不存在,则此偏移量为常量40 (sizeof(BITMAPINFOHEADER))。 是否是这种情况完全取决于应用程序如何将位图放入剪贴板。大多数应用程序都这样做是因为它是最简单的方法。
此代码计算该偏移量:
// Returns the offset, in bytes, from the start of the BITMAPINFO, to the start of the pixel data array, for a packed DIB.
static INT GetPixelDataOffsetForPackedDIB(const BITMAPINFOHEADER *BitmapInfoHeader)
{
INT OffsetExtra = 0;
if (BitmapInfoHeader->biSize == sizeof(BITMAPINFOHEADER) /* 40 */)
{
// This is the common BITMAPINFOHEADER type. In this case, there may be bit masks following the BITMAPINFOHEADER
// and before the actual pixel bits (does not apply if bitmap has <= 8 bpp)
if (BitmapInfoHeader->biBitCount > 8)
{
if (BitmapInfoHeader->biCompression == BI_BITFIELDS)
{
OffsetExtra += 3 * sizeof(RGBQUAD);
}
else if (BitmapInfoHeader->biCompression == 6 /* BI_ALPHABITFIELDS */)
{
// Not widely supported, but technically a valid DIB format.
// You *can* get this in the clipboard, although neither GDI nor stb_image will like it.
OffsetExtra += 4 * sizeof(RGBQUAD);
}
}
}
if (BitmapInfoHeader->biClrUsed > 0)
{
// We have no choice but to trust this value.
OffsetExtra += BitmapInfoHeader->biClrUsed * sizeof(RGBQUAD);
}
else
{
// In this case, the color table contains the maximum number for the current bit count (0 if > 8bpp)
if (BitmapInfoHeader->biBitCount <= 8)
{
// 1bpp: 2
// 4bpp: 16
// 8bpp: 256
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))
{
// Could not open clipboard. This usually indicates that another application is permanently blocking it.
return 1;
}
HGLOBAL ClipboardDataHandle = (HGLOBAL)GetClipboardData(CF_DIB);
if (!ClipboardDataHandle)
{
// Clipboard object is not a DIB, and is not auto-convertible to DIB
CloseClipboard();
return 0;
}
BITMAPINFOHEADER *BitmapInfoHeader = (BITMAPINFOHEADER *)GlobalLock(ClipboardDataHandle);
assert(BitmapInfoHeader); // This can theoretically fail if mapping the HGLOBAL into local address space fails. Very pathological, just act as if it wasn't a bitmap in the clipboard.
SIZE_T ClipboardDataSize = GlobalSize(ClipboardDataHandle);
assert(ClipboardDataSize >= sizeof(BITMAPINFOHEADER)); // Malformed data. While older DIB formats exist (e.g. BITMAPCOREHEADER), they are not valid data for CF_DIB; it mandates a BITMAPINFO struct. If this fails, just act as if it wasn't a bitmap in the clipboard.
INT PixelDataOffset = GetPixelDataOffsetForPackedDIB(BitmapInfoHeader);
// ============================================================================================================
// ============================================================================================================
//
// Example 1: Write it to a .bmp file
//
// The clipboard contains a packed DIB, whose start address coincides with BitmapInfoHeader, and whose total size is ClipboardDataSize.
// By definition, we can jam the whole DIB memory into a BMP file as-is, except that we need to prepend a BITMAPFILEHEADER struct.
// The tricky part is that for BITMAPFILEHEADER.bfOffBits, which must be calculated using the information in BITMAPINFOHEADER.
// The BMP file layout:
// @offset 0: BITMAPFILEHEADER
// @offset 14 (sizeof(BITMAPFILEHEADER)): BITMAPINFOHEADER
// @offset 14 + BitmapInfoHeader->biSize: Optional bit masks and color table
// @offset 14 + DIBPixelDataOffset: pixel bits
// @offset 14 + ClipboardDataSize: EOF
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; // Will fail if bitmap size is nonstandard >4GB
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
// ============================================================================================================
// ============================================================================================================
//
// Example 2: Load it from memory in SFML
//
// SFML expects a whole bitmap file, including its BITMAPFILEHEADER, in memory.
// So this is similar to Example 1, except in memory.
BYTE *BitmapFileContents = (BYTE *)malloc(TotalBitmapFileSize);
assert(BitmapFileContents);
memcpy(BitmapFileContents, &BitmapFileHeader, sizeof(BITMAPFILEHEADER));
// Append DIB
memcpy(BitmapFileContents + sizeof(BITMAPFILEHEADER), BitmapInfoHeader, ClipboardDataSize);
sf::Image image;
image.loadFromMemory(BitmapFileContents, TotalBitmapFileSize);
// The memory can be freed once the image has been loaded in SFML.
free(BitmapFileContents);
// Manipulate it:
image.flipHorizontally();
// Put it back in the clipboard:
PutBitmapInClipboard_From32bppTopDownRGBAData(image.getSize().x, image.getSize().y, image.getPixelsPtr());
#else
// ============================================================================================================
// ============================================================================================================
//
// Example 3: Convert to HBITMAP for GDI
//
BYTE *PixelDataFromClipboard = (BYTE *)BitmapInfoHeader + PixelDataOffset;
// This will only work if the DIB format is supported by GDI. Not all formats are supported.
BYTE *PixelDataNew;
HBITMAP hBitmap = CreateDIBSection(NULL, (BITMAPINFO *)BitmapInfoHeader, DIB_RGB_COLORS, (void **)&PixelDataNew, NULL, 0);
assert(hBitmap);
// Need to copy the data from the clipboard to the new DIBSection.
BITMAP BitmapDesc = {};
GetObjectW(hBitmap, sizeof(BitmapDesc), &BitmapDesc);
SIZE_T PixelDataBytesToCopy = (SIZE_T)BitmapDesc.bmHeight * BitmapDesc.bmWidthBytes;
SIZE_T PixelDataBytesAvailable = ClipboardDataSize - PixelDataOffset;
if (PixelDataBytesAvailable < PixelDataBytesToCopy)
{
// Malformed data; doesn't contain enough pixels.
PixelDataBytesToCopy = PixelDataBytesAvailable;
}
memcpy(PixelDataNew, PixelDataFromClipboard, PixelDataBytesToCopy);
// NOTE: While it is possible to create a DIB section without copying the pixel data, in general you'd want to
// copy it anyway because the clipboard needs to be closed asap.
// Draw something on it.
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);
// ============================================================================================================
// ============================================================================================================
//
// Copy it back to the clipboard.
//
PutBitmapInClipboard_AsDIB(hBitmap);
#endif // DEMO_SFML
GlobalUnlock(ClipboardDataHandle);
CloseClipboard();
return 0;
}
static BOOL OpenClipboard_ButTryABitHarder(HWND hWnd)
{
for (int i = 0; i < 20; ++i)
{
// This can fail if the clipboard is currently being accessed by another application.
if (OpenClipboard(hWnd)) return true;
Sleep(10);
}
return false;
}
// Returns the offset, in bytes, from the start of the BITMAPINFO, to the start of the pixel data array, for a packed DIB.
static INT GetPixelDataOffsetForPackedDIB(const BITMAPINFOHEADER *BitmapInfoHeader)
{
INT OffsetExtra = 0;
if (BitmapInfoHeader->biSize == sizeof(BITMAPINFOHEADER) /* 40 */)
{
// This is the common BITMAPINFOHEADER type. In this case, there may be bit masks following the BITMAPINFOHEADER
// and before the actual pixel bits (does not apply if bitmap has <= 8 bpp)
if (BitmapInfoHeader->biBitCount > 8)
{
if (BitmapInfoHeader->biCompression == BI_BITFIELDS)
{
OffsetExtra += 3 * sizeof(RGBQUAD);
}
else if (BitmapInfoHeader->biCompression == 6 /* BI_ALPHABITFIELDS */)
{
// Not widely supported, but valid.
OffsetExtra += 4 * sizeof(RGBQUAD);
}
}
}
if (BitmapInfoHeader->biClrUsed > 0)
{
// We have no choice but to trust this value.
OffsetExtra += BitmapInfoHeader->biClrUsed * sizeof(RGBQUAD);
}
else
{
// In this case, the color table contains the maximum number for the current bit count (0 if > 8bpp)
if (BitmapInfoHeader->biBitCount <= 8)
{
// 1bpp: 2
// 4bpp: 16
// 8bpp: 256
OffsetExtra += sizeof(RGBQUAD) << BitmapInfoHeader->biBitCount;
}
}
return BitmapInfoHeader->biSize + OffsetExtra;
}
// Helper function for interaction with libraries like stb_image.
// Data will be copied, so you can do what you want with it after this function returns.
static void PutBitmapInClipboard_From32bppTopDownRGBAData(INT Width, INT Height, const void *Data32bppRGBA)
{
// Nomenclature: Data at offset 0 is R top left corner, offset 1 is G top left corner, etc.
// This is pretty much the opposite of what a HBITMAP normally does.
assert(Width > 0);
assert(Height > 0);
assert(Data32bppRGBA);
// GDI won't help us here if we want to preserve the alpha channel. It doesn't support BI_ALPHABITFIELDS, and
// we can't use BI_RGB directly because BI_RGB actually means BGRA in reality.
// That means, unfortunately it's not going to be a simple memcpy :(
DWORD PixelDataSize = 4/*32bpp*/ * Width * Height;
// We need BI_BITFIELDS for RGB color masks here.
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; // Negative height means top-down bitmap
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)
{
// Convert RGBA to BGRA
DWORD tmp = ((DWORD *)Data32bppRGBA)[i];
DWORD tmp2 = tmp & 0xff00ff00; // assumes LE
tmp2 |= (tmp >> 16) & 0xff;
tmp2 |= (tmp & 0xff) << 16;
((DWORD *)PixelData)[i] = tmp2;
}
GlobalUnlock(hGlobal);
EmptyClipboard();
SetClipboardData(CF_DIB, hGlobal);
// The hGlobal now belongs to the clipboard. Do not free it.
}
// Bitmap will be copied, so you can do what you want with it after this function returns.
static void PutBitmapInClipboard_AsDIB(HBITMAP hBitmap)
{
// Need this to get the bitmap dimensions.
BITMAP desc = {};
int tmp = GetObjectW(hBitmap, sizeof(desc), &desc);
assert(tmp != 0);
// We need to build this structure in a GMEM_MOVEABLE global memory block:
// BITMAPINFOHEADER (40 bytes)
// PixelData (4 * Width * Height bytes)
// We're enforcing 32bpp BI_RGB, so no bitmasks and no color table.
// NOTE: SetClipboardData(CF_DIB) insists on the size 40 version of BITMAPINFOHEADER, otherwise it will misinterpret the data.
DWORD PixelDataSize = 4/*32bpp*/ * desc.bmWidth * desc.bmHeight; // Correct alignment happens implicitly.
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); // This can fail if the hBitmap is still selected into a different DC.
void *PixelData = (BYTE *)mem + sizeof(BITMAPINFOHEADER);
// Pathologial "bug": If the bitmap is a DDB that originally belonged to a device with a different palette, that palette is lost. The caller would need to give us the correct HDC, but this is already insane enough as it is.
tmp = GetDIBits(hdc, hBitmap, 0, desc.bmHeight, PixelData, (BITMAPINFO *)bih, DIB_RGB_COLORS);
assert(tmp != 0);
// NOTE: This will correctly preserve the alpha channel if possible, but it's up to the receiving application to handle it.
DeleteDC(hdc);
GlobalUnlock(hGlobal);
EmptyClipboard();
SetClipboardData(CF_DIB, hGlobal);
// The hGlobal now belongs to the clipboard. Do not free it.
}
我打算将此代码大部分用于生产,因为我自己需要它,如果有人发现问题,我很乐意听到。
一些补充说明供参考:
- 在 Win10 上测试
- 在 WinXP(SFML 除外)上测试,虽然
%zu 在旧 CRT 中不起作用,谁知道
- 错误处理尚未准备好生产。
-
the explanation of
CF_DIB 真正想说的是“这是一个打包的 DIB”。没有官方保证它将是一个普通的BITMAPINFOHEADER,即biSize == 40,尽管很可能是这种情况。
-
BITMAPFINO documentation 解释说结构长度确实是可变的,BITMAPINFOHEADER.biClrUsed 需要考虑在内,但没有提到BI_BITFIELDS。
-
BITMAPINFOHEADER 对此有更多详细信息,但未提及 BI_ALPHABITFIELDS 或仅当多态 BITMAPINFOHEADER 结构实际上是普通 BITMAPINFOHEADER(即 biSize == 40)时才存在位掩码的事实。更高版本,如 BITMAPV5HEADER,无条件包含位掩码。
- 总而言之,wikipedia article on the BMP file format 包含对 DIB 内存布局的更简洁和连贯的解释。
- 旧版本的 Paint 处理剪贴板,包括偏移计算,其方式与我在上面的
GetDIBPixelDataOffset 中所做的非常相似(显然我不能在这里逐字发布)。它不假定biSize == 40。较新版本的 Paint 使用 COleServerItem 进行剪贴板处理。
- 作为最终参考,GIMP 和其他跨平台软件使用的 GTK 的源代码实现 CF_DIB 非常相似。该代码甚至可以处理特定于 Web 浏览器的格式,因此有点难以理解。这是transmute_cf_dib_to_image_bmp function。该函数的
length 参数来自GlobalSize。请注意,它也不假定 biSize == 40。
- 如果用
CF_BITMAP 调用SetClipboardData,它需要一个DDB(如果你传递一个HBITMAP,它会默默地失败,这实际上是一个DIB)。隐式转换为 DIB 的 CF_BITMAP 使用 BI_BITFIELDS,这也适用于屏幕截图(至少在原始 DDB 与屏幕 DC 兼容的情况下)。
- 将位图放入剪贴板是一种全新的蠕虫。如果使用
CF_BITMAP 将 DDB 放入剪贴板,则不会复制该位图(至少最初不会复制)。如果有任何程序对其进行操作,则所有访问剪贴板的程序都将看到被操作的位图。但是,一旦任何应用程序以CF_DIB 的形式请求它,Windows 就会应用一堆魔法,而这不再是真的,位图现在是一个副本。这不适用于作为CF_DIB 放入剪贴板的位图,这些位图会立即不受其他程序的操作。 CF_DIB 似乎减少了令人不快的影响和意外,而且似乎也被大多数应用程序使用。虽然您可以尝试在将原始位图放入剪贴板时保留其格式,但我选择对传出数据使用固定的包罗万象的格式,因为它已经够疯狂了。
-
SetClipboardData 暗示 CF_DIB 不适用于 Windows 应用商店应用程序,但我无法证实这一说法。再说一次,关于 NULL 所有者的段落也是不正确的。
-
PutBitmapInClipboard_AsDIB 和PutBitmapInClipboard_From32bppTopDownRGBAData 都尽可能保留 alpha 通道,尽管 GDI 绘图函数不支持 alpha 并且会破坏它(SFML 可以很好地处理它)。将 alpha 通道放在 MSB 中并使用BI_RGB 似乎是在 DIB 中存储 alpha 的事实标准。