Windows 剪贴板 DIB 格式转 PNG 实战指南

1 阅读3分钟

Windows 剪贴板 DIB 格式转 PNG 实战指南

Windows 开发中,剪贴板的图片常以 DIB 格式存在。DIB 适合内存中的位图表示,但不便存储和传输。本文介绍三种语言(C++、C#、Python)下将 CF_DIB 转为 PNG 的完整方案。


背景:什么是 DIB?

DIB(Device-Independent Bitmap)是 Windows 的位图标准格式。与设备相关位图(DDB)不同,DIB 自带颜色表,跨设备显示一致。

剪贴板中的 DIB 以 CF_DIBCF_DIBV5 存在,结构分三部分:

区域说明
位图信息头BITMAPINFOHEADERBITMAPV5HEADER
颜色表可选,位深 ≤ 8bit 时存在
像素数据位图的实际像素值

方案一:C++ + GDI+

最原生的 Windows API 方案,适合集成到 C++ 桌面应用中。

1. 从剪贴板获取 DIB

#include <windows.h>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")

using namespace Gdiplus;

HBITMAP GetDIBFromClipboard()
{
    if (!OpenClipboard(NULL)) return NULL;

    HANDLE hData = GetClipboardData(CF_DIB);
    if (!hData) {
        CloseClipboard();
        return NULL;
    }

    BITMAPINFO* pBMI = (BITMAPINFO*)GlobalLock(hData);
    if (!pBMI) {
        CloseClipboard();
        return NULL;
    }

    HDC hDC = GetDC(NULL);
    BYTE* pBits;
    HBITMAP hBitmap = CreateDIBSection(hDC, pBMI, DIB_RGB_COLORS,
                                        (void**)&pBits, NULL, 0);
    if (hBitmap) {
        // 计算颜色表大小
        int colorTableSize = 0;
        if (pBMI->bmiHeader.biBitCount <= 8) {
            colorTableSize = pBMI->bmiHeader.biClrUsed * sizeof(RGBQUAD);
            if (colorTableSize == 0)
                colorTableSize = (1 << pBMI->bmiHeader.biBitCount) * sizeof(RGBQUAD);
        }

        int headerSize = pBMI->bmiHeader.biSize;
        BYTE* pSrcBits = (BYTE*)pBMI + headerSize + colorTableSize;

        // 计算位图数据大小
        int dataSize = pBMI->bmiHeader.biSizeImage;
        if (dataSize == 0) {
            int width  = pBMI->bmiHeader.biWidth;
            int height = abs(pBMI->bmiHeader.biHeight);
            int stride = ((width * pBMI->bmiHeader.biBitCount + 31) / 32) * 4;
            dataSize = stride * height;
        }

        CopyMemory(pBits, pSrcBits, dataSize);
    }

    GlobalUnlock(hData);
    ReleaseDC(NULL, hDC);
    CloseClipboard();
    return hBitmap;
}

2. HBITMAP → PNG

int GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
    UINT num = 0, size = 0;
    GetImageEncodersSize(&num, &size);
    if (size == 0) return -1;

    ImageCodecInfo* pCodecs = (ImageCodecInfo*)malloc(size);
    if (!pCodecs) return -1;

    GetImageEncoders(num, size, pCodecs);
    for (UINT j = 0; j < num; ++j) {
        if (wcscmp(pCodecs[j].MimeType, format) == 0) {
            *pClsid = pCodecs[j].Clsid;
            free(pCodecs);
            return j;
        }
    }
    free(pCodecs);
    return -1;
}

bool SaveHBITMAPToPNG(HBITMAP hBitmap, const wchar_t* filename)
{
    GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    Bitmap* bitmap = Bitmap::FromHBITMAP(hBitmap, NULL);
    if (!bitmap) {
        GdiplusShutdown(gdiplusToken);
        return false;
    }

    CLSID pngClsid;
    bool ok = false;
    if (GetEncoderClsid(L"image/png", &pngClsid) != -1)
        ok = (bitmap->Save(filename, &pngClsid, NULL) == Ok);

    delete bitmap;
    GdiplusShutdown(gdiplusToken);
    return ok;
}

3. 完整调用

int main()
{
    HBITMAP hBitmap = GetDIBFromClipboard();
    if (!hBitmap) {
        MessageBox(NULL, L"剪贴板中没有 DIB 格式的图片", L"错误", MB_OK);
        return 1;
    }

    if (SaveHBITMAPToPNG(hBitmap, L"clipboard_image.png"))
        MessageBox(NULL, L"PNG 保存成功!", L"成功", MB_OK);
    else
        MessageBox(NULL, L"PNG 保存失败!", L"错误", MB_OK);

    DeleteObject(hBitmap);
    return 0;
}

方案二:C# + .NET

.NET 封装了底层细节,实现最简洁:

using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;

class ClipboardImageConverter
{
    public static void ConvertDIBToPNG(string outputPath)
    {
        if (Clipboard.ContainsImage())
        {
            using Image image = Clipboard.GetImage();
            image.Save(outputPath, ImageFormat.Png);
            Console.WriteLine("PNG 保存成功!");
        }
        else
        {
            Console.WriteLine("剪贴板中没有图片!");
        }
    }
}

需要添加 System.Drawing.Common NuGet 包(.NET 6+)。


方案三:Python + Pillow + pywin32

import win32clipboard
from PIL import Image
import io

def dib_to_png(output_path):
    try:
        win32clipboard.OpenClipboard()

        if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
            data = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
            image = Image.open(io.BytesIO(data))
            image.save(output_path, "PNG")
            print(f"PNG 已保存到: {output_path}")
        else:
            print("剪贴板中没有 DIB 格式的图片")

    except Exception as e:
        print(f"错误: {e}")
    finally:
        win32clipboard.CloseClipboard()

# 使用
dib_to_png("clipboard_output.png")

安装依赖:pip install pillow pywin32


方案对比

C++C#Python
代码量极少
性能最高
依赖GDI+(系统自带).NET RuntimePillow + pywin32
适用场景桌面应用集成.NET 应用脚本 / 批处理

注意事项

  1. 剪贴板独占 — 调用 OpenClipboard 后尽快操作并 CloseClipboard,避免阻塞其他程序
  2. GDI+ 生命周期 — C++ 方案务必配对调用 GdiplusStartup / GdiplusShutdown
  3. 资源释放 — HBITMAP 用完立删,GDI 资源有限
  4. DIB 变体CF_DIBV5 头部更大,需按 biSize 判断头大小后再定位像素数据
  5. PNG 压缩 — 可调整 GDI+ 的 EncoderParameters 控制压缩级别

本文覆盖了从底层 Win32 API 到高层脚本三种实现路径,可根据项目技术栈选择最合适的方案。