ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Win32 AlphaBlend半透明绘制:从PNG到预乘DIB的完整实战

Win32 AlphaBlend半透明绘制:从PNG到预乘DIB的完整实战 简介以AlphaBlend函数为核心的Windows半透明绘制源码包面向初入Windows GDI编程或希望掌握位图Alpha混合效果的开发者。压缩包共15个文件大小约652KB文件类型涵盖cpp/h源码、sln/vcxproj工程配置、bmp位图素材、rc资源文件、可直接运行的exe以及说明文档与许可证便于在Visual Studio中打开并对照学习。内容以AlphaBlend调用为主线详细讲清Alpha通道原理、32位位图准备、BLENDFUNCTION结构体中blendOp、sourceConstantAlpha等关键字段含义以及目标与源设备上下文的参数设置逻辑帮助读者建立从像素透明度到最终混合效果的完整认识。包内附带的示例程序和配套位图可直观调节透明参数观察半透明变化适合通过改参数、读源码来深化理解。已有189人学习下载是学习Windows半透明绘制的实用参考。1. AlphaBlend 半透明绘制为什么资源里明明有 Alpha画到窗口却成了黑底做 Win32 界面、截屏工具或图片查看器时最常遇到的一个需求是把一张带透明区域的位图叠加到另一张背景上。很多人第一反应是 BitBlt但 BitBlt 只能不透明拷贝透明区域会按位图的 RGB 底色直接盖上去。AlphaBlend 是 GDI 里少有的原生透明混合函数它能读取 32 位位图里的 Alpha 通道实现真正的半透明绘制。不过照着文档调用时你很快会发现几个让人头疼的现象透明区域变成黑块、边缘出现白边、AlphaBlend 直接返回 FALSE。这些问题的根源通常集中在三件事源位图不是 32 位 DIB、BLENDFUNCTION 参数没用对、位图像素没有做预乘 Alpha。这篇实战笔记围绕这三件事展开适合需要维护老 MFC/Win32 项目、又不想为一张贴图引入整套 D2D 管线的开发者。标题里的“源码打包”也会一并讲清楚让拿到你源码的人能直接编译运行。2. AlphaBlend 的两个透明度来源SourceConstantAlpha 与像素 Alpha 通道AlphaBlend 的混合结果由两个透明度共同决定一个是 BLENDFUNCTION 结构体里的 SourceConstantAlpha一个是源位图每个像素自带的 Alpha 值。搞不清这两个谁先谁后是绝大多数半透明绘制翻车的起点。本章先把函数签名和参数含义讲透再给一个最小可运行片段。2.1 AlphaBlend 函数签名与 BLENDFUNCTION 结构AlphaBlend 声明在 wingdi.h实现在 msimg32.dll。函数原型很长但本质上就是把源 DC 上的一个矩形区域混合到目标 DC 的矩形区域BOOL AlphaBlend( HDC hdcDest, int xOriginDest, int yOriginDest, int wDest, int hDest, HDC hdcSrc, int xOriginSrc, int yOriginSrc, int wSrc, int hSrc, BLENDFUNCTION blendFunction );前十个参数和 StretchBlt 几乎一样最后多了一个 BLENDFUNCTION。真正决定透明方式的是这个结构体它只有四个字节typedef struct _BLENDFUNCTION { BYTE BlendOp; // 混合方式固定为 AC_SRC_OVER BYTE BlendFlags; // 保留必须为 0 BYTE SourceConstantAlpha; // 0~255全局不透明度 BYTE AlphaFormat; // 0 或 AC_SRC_ALPHA } BLENDFUNCTION;这里的每个字段都不能乱填。BlendOp 只能写 AC_SRC_OVER表示源覆盖到目标上BlendFlags 始终为 0。SourceConstantAlpha 是整个源图像的额外不透明度255 表示不额外衰减0 表示完全透明。AlphaFormat 是很多人忽略的重点设为 0 时AlphaBlend 完全忽略位图像素里的 Alpha 通道只按 SourceConstantAlpha 做整图均匀透明设为 AC_SRC_ALPHA 时才会使用 32 位位图每个像素自带的 Alpha 值并且这个像素 Alpha 还会和 SourceConstantAlpha 再乘一次。常见做法是 AlphaFormat 用 AC_SRC_ALPHASourceConstantAlpha 用 255意思就是“透明度全部交给位图像素自己说了算”。2.2 位图的 Alpha 通道从哪里来32 位 DIB 与预乘 AlphaAlphaBlend 不是任何 HBITMAP 都能处理。HBITMAP 分为 DDB设备相关位图和 DIB设备无关位图AlphaBlend 要求源位图是 32 位 DIB也就是每个像素 4 字节其中第四字节是 Alpha。普通 24 位位图没有 Alpha 通道16 位更没有。如果你在 Java 或 Android 里和 Bitmap 打过交道应该很熟悉这种像素数组的概念只不过 Windows GDI 里的 32 位 DIB 内存字节序是 BGRA不是 ARGB。还有一个绕不开的概念叫预乘 Alpha。标准透明混合公式是目标颜色 源颜色 × 源 Alpha 目标颜色 ×1 − 源 Alpha。AlphaBlend 在 AC_SRC_ALPHA 模式下默认认为源位图里的 RGB 分量已经预先乘过了 Alpha。比如一个半透明白色像素原本颜色是 (255, 255, 255)Alpha 是 128预乘后存进位图的值应该是 (128, 128, 128)Alpha 仍为 128。如果你把 (255, 255, 255, 128) 直接当作源混合AlphaBlend 会认为颜色分量已经带了 Alpha结果就是颜色过亮边缘发白。这就是为什么很多从 PNG 加载出来的位图不做预乘处理就直接 AlphaBlend会出现明显的亮边。2.3 最小可运行片段一张带 Alpha 的 DIB 画到窗口下面这个片段能在 WM_PAINT 里画出一个半透明红色方块。它演示了从创建 DIB、填充像素到 AlphaBlend 的完整流程是理解后面所有代码的基准。// WM_PAINT 中 PAINTSTRUCT ps; HDC hdcWin BeginPaint(hwnd, ps); // 1. 创建 100x100 的 32 位 DIBbiHeight 用负数表示自上而下行序 BITMAPINFO bmi {}; bmi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth 100; bmi.bmiHeader.biHeight -100; bmi.bmiHeader.biPlanes 1; bmi.bmiHeader.biBitCount 32; bmi.bmiHeader.biCompression BI_RGB; void* bits nullptr; HBITMAP hBmp CreateDIBSection(hdcWin, bmi, DIB_RGB_COLORS, bits, nullptr, 0); // 2. 填充像素32 位 DIB 的字节顺序是 B、G、R、A unsigned char* p static_castunsigned char*(bits); int pitch 100 * 4; for (int y 0; y 100; y) { unsigned char* line p y * pitch; for (int x 0; x 100; x) { line[x * 4 0] 0x00; // B line[x * 4 1] 0x00; // G line[x * 4 2] 0xFF; // R line[x * 4 3] 0x80; // A128半透明 } } // 3. 把 DIB 选入内存 DC HDC hdcMem CreateCompatibleDC(hdcWin); HGDIOBJ hOld SelectObject(hdcMem, hBmp); // 4. AlphaBlend 混合到窗口 DC BLENDFUNCTION bf {}; bf.BlendOp AC_SRC_OVER; bf.SourceConstantAlpha 255; // 全局不额外衰减 bf.AlphaFormat AC_SRC_ALPHA; // 使用像素 Alpha BOOL ok AlphaBlend(hdcWin, 20, 20, 100, 100, hdcMem, 0, 0, 100, 100, bf); // 5. 清理 SelectObject(hdcMem, hOld); DeleteDC(hdcMem); DeleteObject(hBmp); EndPaint(hwnd, ps);这段代码的逻辑顺序很关键。CreateDIBSection 用 hdcWin 作为参考 DC主要为了拿到和屏幕一致的色深格式也可以传 nullptr但在某些显卡上传屏幕 DC 更稳。biHeight 设为 -100 后内存里第一行就是图片顶部填充像素时不需要做行序翻转。填充时用字节数组直接赋值避免了大端小端带来的心智负担。AlphaBlend 中源矩形和目标矩形都写 100×100如果想缩放把目标矩形改成其他尺寸即可GDI 会做拉伸混合。如果 ok 返回 FALSE大概率是源位图或 DC 类型不对第四章会专门排查。3. 准备源位图从 PNG/文件到 32 位预乘 DIB 的三条路径实际工程里不会每次都在代码里手写像素更多是从 PNG、BMP 等文件加载。问题在于 AlphaBlend 只认 32 位 DIB而常见图片格式的像素数据和 DIB 格式并不一样。所以需要一条可靠的转换路径。这一章给出三条路径按可控性从高到低排列。3.1 路径一CreateDIBSection 手动填像素适合生成纹理如果你是在源码包里做演示或者临时生成一张渐变遮罩用 CreateDIBSection 手动填像素是最直接的选择。第二章的示例已经展示了整个过程这里补充几个容易被忽略的细节。CreateDIBSection 返回的 HBITMAP初始内容是不确定的甚至可能是随机内存。如果你创建后没有填充所有像素就交给 AlphaBlend透明区域会显示成一片杂色。我一般会先 memset 整块缓冲为 0再设置需要的像素。因为 Alpha 为 0 时颜色值不管是什么混合后都不该产生影响这能避免很多玄学花屏。另外CreateDIBSection 的第三个参数 DIB_RGB_COLORS 表示 bits 指向的颜色是 RGB 值不是调色板索引。32 位图不需要调色板所以这个参数固定写 DIB_RGB_COLORS。创建后如果不想要了直接 DeleteObjectGDI 会释放 bits 对应的内存不要手动 free。3.2 路径二LoadImage 加载 32 位 DIB 位图资源如果你的素材是 BMP并且你确认 BMP 的第四字节真的保存了 Alpha那么可以用 LoadImage 直接加载成 DIB。关键是要加 LR_CREATEDIBSECTION 标志否则系统返回的是 DDBAlphaBlend 依然不认。HBITMAP hBmp (HBITMAP)LoadImage( hInstance, MAKEINTRESOURCE(IDB_MYBITMAP), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION );这条路径的坑在于 BMP 的 Alpha 通道非常不可靠。多数制图工具导出的 32 位 BMP第四个字节存的是 0而不是透明值。实际表现就是位图能画出来但所有“透明”区域全是黑色。所以这条路径我只用于自己用代码生成、并且明确知道 Alpha 内容的 BMP。外部美术资源最好不要走 LoadImage直接用 GDI 更稳妥。3.3 路径三用 GDI 读取任意图片并转成预乘 Alpha DIBPNG 是带透明信息最常用的格式。Windows 的 LoadImage 不支持真正解析 PNG 的 Alpha 通道所以我一般用 GDI 做这件事。下面这个函数把任意外部图片读取出来转成 AlphaBlend 需要的 32 位预乘 DIB#include windows.h #include gdiplus.h #pragma comment(lib, msimg32.lib) #pragma comment(lib, gdiplus.lib) // 把外部图片文件转换成 32 位预乘 Alpha DIB bool ConvertImageToAlphaDib(const wchar_t* path, HBITMAP* outBmp) { Gdiplus::Bitmap src(path); if (src.GetLastStatus() ! Gdiplus::Ok) return false; UINT w src.GetWidth(); UINT h src.GetHeight(); Gdiplus::Rect rect(0, 0, (INT)w, (INT)h); Gdiplus::BitmapData data; if (src.LockBits(rect, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, data) ! Gdiplus::Ok) return false; BITMAPINFO bmi {}; bmi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth (LONG)w; bmi.bmiHeader.biHeight -(LONG)h; // 负值 自上而下 bmi.bmiHeader.biPlanes 1; bmi.bmiHeader.biBitCount 32; bmi.bmiHeader.biCompression BI_RGB; void* bits nullptr; HBITMAP hDib CreateDIBSection(nullptr, bmi, DIB_RGB_COLORS, bits, nullptr, 0); if (hDib bits) { const unsigned char* base (const unsigned char*)data.Scan0; unsigned char* dst (unsigned char*)bits; size_t dstPitch w * 4; for (UINT y 0; y h; y) { // 处理 GDI Stride 为正负两种行序 const unsigned char* srcRow; if (data.Stride 0) srcRow base y * data.Stride; else srcRow base (h - 1 - y) * (-data.Stride); unsigned char* dstRow dst y * dstPitch; for (UINT x 0; x w; x) { unsigned char b srcRow[x * 4 0]; unsigned char g srcRow[x * 4 1]; unsigned char r srcRow[x * 4 2]; unsigned char a srcRow[x * 4 3]; // 预乘 AlphaRGB 分量先乘 Alpha 再除 255 dstRow[x * 4 0] (unsigned char)(b * a / 255); dstRow[x * 4 1] (unsigned char)(g * a / 255); dstRow[x * 4 2] (unsigned char)(r * a / 255); dstRow[x * 4 3] a; } } } src.UnlockBits(data); if (outBmp) *outBmp hDib; return hDib ! nullptr; }这个函数做了两件重要的事。第一把 GDI 的 BitmapData 读成统一的 BGRA 字节数组第二在写入 DIB 前把 RGB 分量乘上 Alpha 再除以 255完成预乘。AlphaBlend 在 AC_SRC_ALPHA 模式下默认源数据是预乘过的这一步不做混合结果边缘就会过亮。由于 GDI LockBits 的 Stride 可能是负数这里的行指针做了正负判断避免图片上下颠倒。用的时候别忘了在程序启动时调用 GdiplusStartup退出时 GdiplusShutdown否则所有 GDI 调用都会像黑匣子一样没有反应。4. AlphaBlend 翻车排查5 个常见坑的现象、原因与解决这一章记录的是我自己和身边同事反复踩过的问题按现象、原因、解决三步排开。如果你照着前面的代码仍然画不对优先到这一章里找原因。4.1 AlphaBlend 返回 FALSE输出全是黑的现象AlphaBlend 返回 FALSE目标区域要么没变化要么整块变黑。原因最常见的是源 DC 里选中的位图不是 32 位 DIB。比如用 LoadBitmap 加载 DDB或者用 CreateCompatibleBitmap 创建的位图这两种位图都没有 Alpha 通道AlphaBlend 无法处理。其次是源 DC 不是内存 DC或者源 DC 和目标 DC 不是在同一个屏幕兼容模式下建立的。解决先确认位图格式。可以调用 GetObject 检查 BITMAP 的 bmBitsPixelBITMAP bm {}; GetObject(hBmp, sizeof(bm), bm); if (bm.bmBitsPixel ! 32) { // 不是 32 位AlphaBlend 没有正确工作的基础 }如果是通过 LoadImage 加载的确认加了 LR_CREATEDIBSECTION。如果仍然失败检查源 DC 是否用 CreateCompatibleDC 创建并且 SelectObject 是否成功不能把 HBITMAP 选入另一个进程的 DC 里。4.2 PNG 透明区域画出来是不透明黑色块现象用 PNG 做源图AlphaBlend 后原本透明的地方变成了黑色方块完全不透明。原因很多人用 LoadImage 加载 PNG 资源但 LoadImage 的 IMAGE_BITMAP 路径不会解析 PNG 的 Alpha 通道透明信息被丢弃第四字节变成 0 或 255。Alpha 为 0 的地方 RGB 仍然是原值AlphaBlend 认为这些像素是“完全透明”但因为位图本身已经是黑色底混合后自然露出黑色。如果第四字节被当成 255那透明区域就直接变成不透明黑块。解决不要用 LoadImage 处理 PNG。用第三章的 GDI 函数加载让 GDI 解出每个像素的 Alpha再写入 DIB。如果你只有 BMP 且透明区域是纯色底可以用代码把特定 RGB 值抠成 Alpha0但这样只能处理素材可控的场景不通用。4.3 半透明边缘出现黑边或亮边现象图标边缘有一圈发灰或者发亮的光晕尤其是白色、浅色图片特别明显。原因源位图没有做预乘 Alpha。AlphaBlend 在 AC_SRC_ALPHA 模式下认为 RGB 已经乘过了 Alpha而 PNG 解码出来的是非预乘值。边缘像素通常有过渡 Alpha比如 RGB255, A128混合时 AlphaBlend 直接把这个 RGB 当作已经带透明度的颜色参与计算导致边缘过饱和看起来像亮边。换成深色图片过饱和的部分就表现为黑边。解决在把像素写入 DIB 前做预乘dstRow[x * 4 0] (unsigned char)(b * a / 255); dstRow[x * 4 1] (unsigned char)(g * a / 255); dstRow[x * 4 2] (unsigned char)(r * a / 255); dstRow[x * 4 3] a;注意使用整数运算先乘后除避免 float 的精度和性能问题。这条规则对所有带 Alpha 的源图都成立我建议把预乘直接写进图片转换函数而不是每次调用 AlphaBlend 前临时处理。4.4 整张图变成均匀半透明想要的局部 Alpha 失效现象源图中间有半透明区域但画出来后整张图变成同一个透明度原来半透明的位置也按不透明颜色显示了。原因BLENDFUNCTION 里的 AlphaFormat 设成了 0或者结构体初始化不完整导致 AlphaFormat 为 0。AlphaFormat 为 0 时AlphaBlend 忽略像素 Alpha只用 SourceConstantAlpha 做整图均匀混合。很多人会漏掉这一项觉得“都设了 SourceConstantAlpha255应该就是原样”结果一看效果完全不对。解决初始化时显式写出所有字段BLENDFUNCTION bf {}; bf.BlendOp AC_SRC_OVER; bf.SourceConstantAlpha 255; bf.AlphaFormat AC_SRC_ALPHA;如果用了 { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA } 这种花括号写法也可以但明确逐字段赋值更容易检查。想验证像素 Alpha 是否生效可以在源图里画一个半透明圆形再用 AlphaFormatAC_SRC_ALPHA 看圆形的边缘过渡是否正常。4.5 目标 DC 是窗口 DC 时偶尔花屏或区域消失现象AlphaBlend 直接画到窗口 DC第一次显示正常窗口拖动或局部刷新后出现残留、花屏或者半透明区域变成空白。原因直接画在窗口 DC 上的内容不会自动保存系统在 WM_PAINT 之外触发的重绘可能覆盖掉你的混合结果。还有可能是窗口 DC 被剪裁绘制区域超出了当前有效重绘区。AlphaBlend 本身只是把像素混合到当前 DCDC 里的内容是什么取决于后续重绘流程它没有持久层。解决用双缓冲。先在内存 DC 里创建一块和客户区一样大的位图把背景和源图都画到内存 DC最后一次性 BitBlt 到窗口 DC。这样每次重绘都从干净状态开始半透明结果不会和上一次残留混在一起。AlphaBlend 在这种内存 DC 上表现也更稳定因为内部不需要额外处理窗口裁剪。5. 源码打包的收尾动作目录结构、链接库与淡入淡出验证标题里的“源码打包”不是把几个文件拖进压缩包那么简单重点是让拿到源码的人能直接编译。我一般会按固定结构整理并且在打包前跑一个淡入淡出验证。5.1 一个能被直接编译的源码包目录结构AlphaBlendDemo/ ├── src/ │ ├── main.cpp │ ├── alpha_bitmap.h │ └── alpha_bitmap.cpp ├── res/ │ └── sample.png ├── AlphaBlendDemo.vcxproj └── README.mdREADME 里写清楚三件事需要哪个 Visual Studio 版本、需要哪个 Windows SDK 版本、资源里的 sample.png 是不是带 Alpha 通道。很多源码包打不开不是因为代码有问题而是别人不知道用哪个工具链。工程文件配置里把 Win32 和 x64 两个平台都保留Debug 和 Release 共用同一份源码避免别人切换配置时还要手动改包含路径。5.2 容易漏掉的链接库msimg32.lib 与 GdiplusAlphaBlend 在 msimg32.dll使用它必须链接 msimg32.lib。GDI 则需要 gdiplus.lib。这两条我会直接写在源文件顶部#pragma comment(lib, msimg32.lib) #pragma comment(lib, gdiplus.lib)如果不加 msimg32.lib链接器会报 LNK2019说 AlphaBlend 解析失败如果用了 GDI 但没加 gdiplus.lib会报 GdiplusStartup 等一堆未解析符号。与其让同事去翻工程配置不如在代码里用 pragma 一次解决。5.3 用淡入淡出验证 AlphaBlend 是否打通验证源码包是否真的可用最好的方法是做一个淡入淡出效果。在定时器里把 SourceConstantAlpha 从 0 逐步加到 255每次用 AlphaBlend 重绘观察图片是否平滑出现、边缘是否干净// WM_TIMER 中 g_alpha min(255, g_alpha 5); BLENDFUNCTION bf {}; bf.BlendOp AC_SRC_OVER; bf.SourceConstantAlpha (BYTE)g_alpha; bf.AlphaFormat AC_SRC_ALPHA; // 用之前转换出来的 hBmp 做 AlphaBlend AlphaBlend(hdcMem, 0, 0, w, h, hdcSrc, 0, 0, w, h, bf); InvalidateRect(hwnd, nullptr, FALSE);这个技巧能同时验证三件事位图有没有 Alpha 通道、AlphaFormat 有没有写对、预乘有没有做对。如果淡入过程中图片边缘发黑或发白说明预乘步骤没到位如果图片整体透明度没变化说明 AlphaFormat 或 DC 有问题。我现在的习惯是任何带 Alpha 的位图进 AlphaBlend 之前先强制走一遍预乘源码包交付前一定在干净的虚拟机里按 README 编译一次这样那些翻车过的奇怪问题基本不会第二次出现。希望帮到你。本文还有配套的精品资源点击获取
返回列表