
简介面向VC初学者的图形图像处理源码范例演示如何利用掩码位图实现透明图片的合成与显示适用于Windows GUI开发中需要做不规则窗口或半透明控件的场景。资源为RAR压缩包共24个文件包含5个C源文件、6个头文件、3张BMP与2个ICO素材以及sln/vcproj/rc等工程配置文件整体仅550KB目录结构紧凑便于在Visual Studio中直接打开编译。已有666人浏览学习适合正在学习MFC位图操作与GDI绘图机制的读者。通过源码可掌握颜色位图与掩码位图的分离处理方法理解Alpha通道判断、Bitmap::FromFile加载、DrawImage与BitBlt组合绘制的关键技巧代码中模块划分清晰可直接复用资源加载和掩码生成逻辑帮助快速实现透明按钮、异形窗体等常见界面效果。 掩码位图这个东西现在很多做图像处理的年轻人可能都没碰过了。大家一提起透明图片第一反应都是PNG带Alpha通道或者Direct2D、GDI里现成的TransparentBlt。但在早年Windows GDI时代在VC里做透明贴图掩码位图Mask Bitmap是绕不开的核心方案。哪怕到今天在某些需要极致性能、不想引入GDI依赖、或者需要处理老位图资源的场景里这套手艺依然有它的生存空间。这篇就来把掩码位图做透明图片的来龙去脉、原理和实操细节一次讲透。1. 为什么需要掩码位图GDI贴图的透明困局先从一个最基础的问题说起GDI的BitBlt函数做的是整块位图拷贝。它不认识什么叫透明它只会把一个矩形区域的像素原样搬到另一个矩形区域。这意味着如果你有一张背景是白色或纯色的卡通形象位图想把它贴到一个彩色背景的窗口上贴出来就是一块难看的白底或色底方块。有人可能会说那我用TransparentBlt不就行了吗TransparentBlt确实是Windows 2000/XP之后引入的API它能把指定的一种颜色变成透明。但它有两个痛点第一它依赖msimg32.dll在某些精简系统或特殊环境下可能缺失第二它在透明处理时只做单色抠图边缘处理粗糙容易出现色边Color Fringe而且性能在大量贴图时并不算优秀。掩码位图方案解决的是更底层的问题在纯GDI环境下用位运算的方式让一块区域要么显示源图内容要么显示目标背景内容。它不依赖任何额外DLL只要GDI能用它就能用。核心思想就是用一张黑白位图作为模板告诉GDI哪些像素该留、哪些像素该透。这里需要先建立一个认知掩码位图本身不是用来看的它是用来算的。真正实现透明效果靠的是掩码位图与源位图、目标位图之间的三次位运算。理解了这一点后面所有代码都不会再让你迷惑。2. 三次位运算的本质AND与OR怎么变出透明要彻底搞懂掩码位图必须先把位运算这个数学基础吃透。假设我们有一张源位图Src即要贴的带背景的图、一张掩码位图Mask黑白图和一块目标区域Dst窗口背景。整个过程分三步第一步把目标区域中要显示源图的部分清成黑色。BitBlt(dst, ... SRCAND)即把Dst和Mask做按位与AND。由于Mask中要显示源图的区域是白色像素值0xFFFFFF要透明的区域是黑色像素值0x000000与运算的结果是白色掩码区域Dst像素与0xFFFFFF做AND结果仍是Dst原样没变。黑色掩码区域Dst像素与0x000000做AND结果变成黑色。也就是说这一步把所有将来要被源图覆盖的像素点全部清空为黑。第二步把源图中要显示的部分与目标区域合并同时把不要的部分挡掉。BitBlt(dst, ... SRCAND)但这次用Src和Mask的反色做运算结果存入一个临时DC或者直接用一个巧妙方式处理。更规范的做法是双重AND再OR但经典三步法里这一步通常是将源图Src与掩码Mask做AND源图中对应掩码黑色的区域变成黑色掩码白色的区域保留源图原色。这样得到的是一张除去透明区域的源图。再将这个结果OR到已经清好黑的Dst上。第三步把第二步的结果与第一步的结果做OR合并。BitBlt(dst, ... SRCPAINT)OR运算遵循有1就1的原则。第一步清出来的黑色区域遇上第二步保留的源图像素最终显示的就是源图内容。而第一步保留的Dst原背景区域因为第二步对应位置是黑色0OR后仍是Dst原样。黑跟任何颜色OR结果都是那个颜色本身所以两边的信息都完整保留了下来。把这三步翻译成大白话就是先用掩码把目标区域需要盖住的地方掏成黑洞再把源图上不要的地方也涂成黑色用反掩码最后把两张图叠在一起黑对黑互相不干扰有内容的地方各自保留。这就是RGBA透明通道普及之前纯位运算实现按形状抠图的经典思路。这段原理建议多看两遍因为后面所有代码包括用MaskBlt、用TransparentBlt、甚至自己封装半透明混合本质上都是在跟这三步位运算打交道。3. 手写代码用BitBlt加掩码实现透明贴图原理清楚了代码反而是最不费劲的部分。下面给出一段完整的VC 6.0到VS2019都通用的实现。3.1 准备DC体系和位图资源透明贴图需要三个DC源图DC、掩码DC、目标DC。因为BitBlt的所有运算都发生在DC之间你没法跳过DC直接对HBITMAP做运算。void DrawTransparent(HDC hdcDest, int x, int y, HBITMAP hBitmap, HBITMAP hMask) { HDC hdcSrc CreateCompatibleDC(hdcDest); HDC hdcMask CreateCompatibleDC(hdcDest); HDC hdcTemp CreateCompatibleDC(hdcDest); BITMAP bm; GetObject(hBitmap, sizeof(bm), bm); HBITMAP pOldSrc (HBITMAP)SelectObject(hdcSrc, hBitmap); HBITMAP pOldMask (HBITMAP)SelectObject(hdcMask, hMask); HBITMAP hTempBmp CreateCompatibleBitmap(hdcDest, bm.bmWidth, bm.bmHeight); HBITMAP pOldTemp (HBITMAP)SelectObject(hdcTemp, hTempBmp); // 第一步目标区先用掩码清出洞 BitBlt(hdcTemp, 0, 0, bm.bmWidth, bm.bmHeight, hdcMask, 0, 0, SRCAND); // temp background mask // 第二步把源图用反掩码处理用hdcMask反色再AND // 先在temp里生成去掉透明部分的源图 HDC hdcTmpSrc CreateCompatibleDC(hdcDest); HBITMAP hSrcMasked CreateCompatibleBitmap(hdcDest, bm.bmWidth, bm.bmHeight); HBITMAP pOldTmpSrc (HBITMAP)SelectObject(hdcTmpSrc, hSrcMasked); // 把mask反相后与源图AND得到抠掉透明区的源图 BitBlt(hdcTmpSrc, 0, 0, bm.bmWidth, bm.bmHeight, hdcSrc, 0, 0, SRCAND); // 先复制源图实际要用src~mask // 注意这里需要先做一个反色掩码下面3.2小节详述 BitBlt(hdcDest, x, y, bm.bmWidth, bm.bmHeight, hdcTmpSrc, 0, 0, SRCPAINT); // 清理 SelectObject(hdcSrc, pOldSrc); SelectObject(hdcMask, pOldMask); SelectObject(hdcTemp, pOldTemp); SelectObject(hdcTmpSrc, pOldTmpSrc); DeleteObject(hSrcMasked); DeleteDC(hdcTmpSrc); DeleteDC(hdcTmpSrc); DeleteObject(hTempBmp); DeleteDC(hdcSrc); DeleteDC(hdcMask); DeleteDC(hdcTemp); }这段代码我故意留了一个不完整的地方就是第二步的反掩码你没看到这正是初学者最容易卡住的细节。3.2 反掩码的生成最容易被忽略的中间步骤在上面的代码注释里我提到第二步需要src ~mask。但BitBlt并没有直接的NOT源图再AND目标的ROP码。怎么办有两种常见做法做法一先把掩码反色到另一张临时位图上。// 把mask复制一份并反色 BitBlt(hdcInvMask, 0, 0, w, h, hdcMask, 0, 0, NOTSRCCOPY); // 此时hdcInvMask里的黑白与mask完全相反 // 然后再做 src AND invMask BitBlt(hdcTmpSrc, 0, 0, w, h, hdcSrc, 0, 0, SRCAND);NOTSRCCOPY这个ROP码会把源位图逐像素取反后拷贝到目标DC。设计初衷是用来做颜色反相但在掩码方案里它就是用来生成反掩码的利器。做法二用SRCINVERTXOR配合掩码实现更紧凑的运算链。三步运算可以用一次SRCINVERT替代部分步骤但那样代码可读性急剧下降。我在实际项目中见过老前辈用一行BitBlt加0x00AC0744这种魔法ROP值把三步压成两步。能用但可维护性极差。我自己给团队定的规矩是能三步写清楚就不用两步性能差异微乎其微但后人包括三个月后的自己读代码时少掉一把头发。3.3 完整实现把三次BitBlt串起来理顺反掩码问题后完整的透明贴图函数长这样void DrawTransparent(HDC hdcDest, int x, int y, HBITMAP hBitmap, HBITMAP hMask) { BITMAP bm; GetObject(hBitmap, sizeof(bm), bm); int w bm.bmWidth, h bm.bmHeight; HDC hdcSrc CreateCompatibleDC(hdcDest); HDC hdcMask CreateCompatibleDC(hdcDest); HDC hdcInvMask CreateCompatibleDC(hdcDest); HDC hdcResult CreateCompatibleDC(hdcDest); HBITMAP hInvMask CreateCompatibleBitmap(hdcDest, w, h); HBITMAP hResultBmp CreateCompatibleBitmap(hdcDest, w, h); HBITMAP pOldSrc (HBITMAP)SelectObject(hdcSrc, hBitmap); HBITMAP pOldMask (HBITMAP)SelectObject(hdcMask, hMask); HBITMAP pOldInvMask (HBITMAP)SelectObject(hdcInvMask, hInvMask); HBITMAP pOldResult (HBITMAP)SelectObject(hdcResult, hResultBmp); // 1. 生成反掩码 BitBlt(hdcInvMask, 0, 0, w, h, hdcMask, 0, 0, NOTSRCCOPY); // 2. 源图 AND 掩码 - 得到透明区为黑的源图 BitBlt(hdcResult, 0, 0, w, h, hdcSrc, 0, 0, SRCAND); // 注意这步其实是把源图先复制到result但顺序无意义 // 这里需要的是 hdcSrc AND hdcMask所以要再与掩码做AND BitBlt(hdcResult, 0, 0, w, h, hdcMask, 0, 0, SRCAND); // 此刻 hdcResult 是 源图抠掉透明部分 // 3. 目标区域先AND反掩码透明区保留背景非透明区清黑 // 此处直接操作目标DC但目标DC区域会先被改变所以通常 // 先用临时DC把目标区域拷出来再运算。为简化这里用result存放 // 第一步的洞。 HDC hdcTemp CreateCompatibleDC(hdcDest); HBITMAP hTempBmp CreateCompatibleBitmap(hdcDest, w, h); HBITMAP pOldTemp (HBITMAP)SelectObject(hdcTemp, hTempBmp); // 把目标区域先拷贝到temp BitBlt(hdcTemp, 0, 0, w, h, hdcDest, x, y, SRCCOPY); // 目标 AND 反掩码保留背景的透明区非透明区变黑 BitBlt(hdcTemp, 0, 0, w, h, hdcInvMask, 0, 0, SRCAND); // 现在temp是有洞的背景 // 4. 将抠好的源图OR到洞上 BitBlt(hdcTemp, 0, 0, w, h, hdcResult, 0, 0, SRCPAINT); // 5. 把最终结果贴回目标DC BitBlt(hdcDest, x, y, w, h, hdcTemp, 0, 0, SRCCOPY); // 清理所有DC和Bitmap SelectObject(hdcSrc, pOldSrc); SelectObject(hdcMask, pOldMask); SelectObject(hdcInvMask, pOldInvMask); SelectObject(hdcResult, pOldResult); SelectObject(hdcTemp, pOldTemp); DeleteObject(hInvMask); DeleteObject(hResultBmp); DeleteObject(hTempBmp); DeleteDC(hdcSrc); DeleteDC(hdcMask); DeleteDC(hdcInvMask); DeleteDC(hdcResult); DeleteDC(hdcTemp); }这段代码看着长但逻辑非常清晰反掩码 - 抠源图 - 掏洞 - 合洞 - 回贴。每一步对应一个中间产物调试时在每个BitBlt后用SaveDC或直接截图确认中间状态是排查问题最有效的手段。4. 掩码位图的制作资源文件里的黑白图与运行时动态生成写代码不是最难的难的是掩码位图从哪来。你不可能要求美术给每张图都单独出一张黑白掩码所以实际开发中通常有两种做法。4.1 静态掩码直接用位图编辑器绘制如果项目里贴图数量少、形状固定最直接的办法就是在Photoshop或位图编辑器里把源图里的透明区域涂成黑色、不透明区域涂成白色导出一张单色BMP作为Mask资源随程序发布。这里有个关键坑掩码位图必须是1bpp单色位图。在VC资源编辑器里插入Bitmap资源时如果选择的不是单色位图BitBlt的SRCAND运算就会出现诡异结果。原因很简单单色位图只有黑白两色像素值非0即1和源图做AND时能干净地把一边归零、一边保留。如果你用了8位或24位彩色的黑白图那些肉眼看着是白色的像素比如0xF0F0F0和源图AND后会产生微小的颜色偏移透明区清不干净贴出来有残影。判断一个位图是不是1bpp也很简单GetObject拿到的BITMAP结构里bmBitsPixel字段是1就对了。如果是8或24重新导出。4.2 动态掩码按透明色自动生成静态绘制适合美术素材固定的场景但如果你有一堆图片素材要批量处理手动画掩码能累到怀疑人生。更实际的做法是运行时根据源图的某个角点颜色作为透明色动态生成掩码。思路是先加载24位BMP源图读取左上角或任意指定点像素RGB值作为透明色然后遍历整张位图像素凡是颜色与透明色相近的像素设为黑色其余设为白色生成1bpp掩码。HBITMAP CreateMaskFromBitmap(HBITMAP hBitmap, COLORREF crTransparent) { BITMAP bm; GetObject(hBitmap, sizeof(bm), bm); // 创建与源图同尺寸的32位DIB来读取像素 BITMAPINFO bi {0}; bi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bi.bmiHeader.biWidth bm.bmWidth; bi.bmiHeader.biHeight -bm.bmHeight; // 负数表示自顶向下 bi.bmiHeader.biPlanes 1; bi.bmiHeader.biBitCount 32; bi.bmiHeader.biCompression BI_RGB; BYTE* pBits new BYTE[bm.bmWidth * bm.bmHeight * 4]; HDC hdc GetDC(NULL); GetDIBits(hdc, hBitmap, 0, bm.bmHeight, pBits, bi, DIB_RGB_COLORS); // 解析透明色RGB int tr GetRValue(crTransparent); int tg GetGValue(crTransparent); int tb GetBValue(crTransparent); // 创建单色位图 BYTE* pMaskBits new BYTE[bm.bmWidth * bm.bmHeight]; memset(pMaskBits, 0xFF, bm.bmWidth * bm.bmHeight); // 默认全白不透 for (int y 0; y bm.bmHeight; y) { for (int x 0; x bm.bmWidth; x) { int idx (y * bm.bmWidth x) * 4; BYTE b pBits[idx]; BYTE g pBits[idx 1]; BYTE r pBits[idx 2]; // 加点容差防止边缘颜色抖动导致黑边 if (abs(r - tr) 8 abs(g - tg) 8 abs(b - tb) 8) { // 该像素设为透明 - 掩码中为黑色 // 单色位图中每个字节8个像素位0表示黑色 int byteIdx y * ((bm.bmWidth 7) / 8) x / 8; pMaskBits[byteIdx] ~(0x80 (x % 8)); } } } HBITMAP hMask CreateBitmap(bm.bmWidth, bm.bmHeight, 1, 1, pMaskBits); delete[] pBits; delete[] pMaskBits; ReleaseDC(NULL, hdc); return hMask; }动态生成掩码有几个细节直接决定成败容差值一定不能设成0。源图里透明区域边缘往往有半透明像素或压缩噪声颜色和纯背景色有细微偏差。我一般给8~16的容差既能过滤噪声又不会误杀深色物体边界。如果你处理的是人物边缘带阴影的图容差太大会把阴影也抠透明出现缺胳膊少腿的效果。单色位图的字节对齐问题。每一行位数必须是4字节对齐(bm.bmWidth 31) / 32 * 4才是真正的stride。上面代码用((bm.bmWidth 7) / 8)只保证字节对齐没保证DWORD对齐。如果宽度不是4的倍数CreateBitmap会按内部规则对齐导致行尾几个像素的掩码错位。稳妥做法是先算好stride再逐行填。int stride ((bm.bmWidth 31) / 32) * 4; for (int y 0; y bm.bmHeight; y) { for (int x 0; x bm.bmWidth; x) { int byteIdx y * stride x / 8; pMaskBits[byteIdx] ~(0x80 (x % 8)); } }自顶向下和自底向上的问题。GetDIBits默认按自底向上扫描也就是内存里第一行是图像最后一行。如果直接按这个缓冲逐行处理生成的掩码会上下颠倒。解决办法就是上面代码里的biHeight -bm.bmHeight负高度强制自顶向下跟人的直觉一致减少出错概率。5. MaskBlt API把三次BitBlt压缩成一次如果觉得手动三步位运算太啰嗦GDI其实还提供了一个专门的APIMaskBlt。它把一个掩码位图和一个自定义ROP4射码组合在一起一行代码完成透明贴图。BOOL MaskBlt( HDC hdcDest, // 目标DC int xDest, // 目标X int yDest, // 目标Y int w, int h, // 宽高 HDC hdcSrc, // 源DC int xSrc, int ySrc, // 源起点 HBITMAP hbmMask, // 掩码位图 int xMask, int yMask, // 掩码起点 DWORD rop // 低字前景ROP高字背景ROP );用MaskBlt替代手写三步后核心调用变成// 前景ROP用SRCCOPY掩码为白色的地方显示源图 // 背景ROP用SRCCOPY不对这里要精细控制 // 正确用法前景ROP为SRCCOPY背景ROP为SRCAND或直接SRCCOPY MaskBlt(hdcDest, x, y, w, h, hdcSrc, 0, 0, hMask, 0, 0, MAKEROP4(SRCCOPY, SRCCOPY));MAKEROP4里的低16位定义掩码位图中白色区域1对应的ROP码高16位定义掩码位图中黑色区域0对应的ROP码。想要白色显示源图、黑色保留目标理论上应该是MAKEROP4(SRCCOPY, SRCCOPY)但很多人这样写完后发现目标区域整块被源图盖住掩码失效。原因是MaskBlt对掩码的处理有个隐藏的反色依赖它要求掩码位图中1代表源图、0代表背景但具体跟ROP怎么配合不同Windows版本行为存在差异。我在Win7和Win10上实测过同样的参数结果不同。所以我的建议是优先用MAKEROP4(SRCCOPY, SRCPAINT)黑色区域做OR保留背景。这个组合在绝大多数Windows版本上表现稳定。但即便如此MaskBlt在处理掩码颜色深度、以及源图和目标色深不一致时依然可能出现边缘毛刺。所以我的真实态度是MaskBlt适合快速原型和简单场景生产环境里我仍然用手写三步BitBlt。性能上三步BitBlt和一次MaskBlt差异非常小——MaskBlt内部本来就是按类似位运算实现的而可控性前者强得多。6. 实测中的坑颜色深度、DC兼容性与边缘残影掩码位图方案看着简单但实际工程里翻车点非常密集。我把这些年踩过的坑集中列出来按严重程度排序。6.1 32位色与16位色下的掩码失真这是最隐蔽的坑。如果你的源位图是24位真彩而目标窗口所在屏幕是16位色65535色BitBlt做AND和OR运算时GDI会在颜色转换中丢掉低位颜色信息。表现就是原本纯黑的掩码区域转换后变成0x0000或接近黑色原本纯白区域变成0xFFFF或接近白色。这本身不致命因为位运算只需要极端的黑和白。真正致命的是中间色。如果掩码图不是严格的1bpp而是8位灰度图在16位色下做AND运算时中间灰度如0x808080与目标像素AND后会让目标像素产生不可预测的颜色偏移。这就是为什么我前面反复强调掩码位图必须是1bpp单色位图。这不是强迫症是颜色深度转换带来的必然要求。6.2 DC不兼容导致的黑框CreateCompatibleDC创建的是与当前屏幕DC兼容的内存DC。如果你在一个屏幕色深为32位的机器上创建了DC然后用它去加载一张8位调色板位图SelectObject虽然不报错但后续的BitBlt在颜色转换时会丢失调色板信息透明边缘容易出现一圈黑边或白边。解决方案是源图DC用CreateCompatibleDC(NULL)即屏幕DC兼容然后把位图转成与目标DC兼容的格式再处理。我在封装透明贴图函数时会在函数入口统一把源图和掩码都转换成兼容位图宁可在初始化时多一点开销也不愿在逐帧贴图时出现肉眼可见的残影。6.3 边缘残影与半透明像素问题掩码位图是二值化的只有透和不透两个状态没有中间过渡。这意味着原图中所有带Alpha渐变或半透明的边缘像素在掩码方案里会被硬生生切成两个阵营要么全透明要么全不透明。结果就是斜线边缘、圆弧边缘出现明显的锯齿Aliasing。这是掩码位图方案的物理极限不是调参能解决的。如果你做的是游戏UI锯齿边缘在静态贴图时还勉强能看一旦有位移或旋转辣眼睛程度会直线上升。改善办法有两个一个是在美术环节就规避做图时把边缘像素处理成与背景色一致的纯色不要留半透明渐变另一个是在贴图时对边缘一两个像素做额外的颜色混合Alpha Blend但这已经超出纯BitBlt的范畴要引入AlphaBlend或自己写像素混合循环。后者性能开销大通常只在静态界面过渡时用。6.4 资源加载中的掩码匹配用VC的资源编辑器加载位图时很多人会踩一个坑源图和掩码在.rc文件里的ID不匹配或者掩码位图被资源编辑器自动压缩成了8位。LoadBitmap加载后GetObject检查bmBitsPixel如果是8恭喜你踩坑了。我在项目里习惯用LoadImage替代LoadBitmap显式指定加载为LR_CREATEDIBSECTION这样拿到的HBITMAP自带DIB节颜色格式可控后续GetDIBits和位运算的兼容性都更有保障。hBitmap (HBITMAP)LoadImage(hInst, MAKEINTRESOURCE(IDB_SRC), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);7. 性能优化与使用边界什么时候还值得用掩码位图聊聊性能。掩码位图方案在GDI时代被大量用于游戏和UI渲染因为BitBlt是硬件加速的在GDI的加速能力范围内三步BitBlt的开销主要集中在每帧多次DC创建/销毁中间临时位图的分配与释放优化手法也很直接把所有DC和临时位图在初始化时创建好运行时只做BitBlt。我封装过一个CMaskBltEngine类构造函数里创建好SrcDC、MaskDC、InvMaskDC和两张临时位图析构时统一释放。这样每帧贴图的开销就只剩三次BitBlt调用性能非常可观。内存占用方面一张1920x1080的24位源图加1bpp掩码临时位图需要两张32位ARGB每张约8MB总额外开销约16MB。如果是游戏界面里的全屏背景这个数字还能接受但如果要在嵌入式或低配设备上大量贴图就要考虑用MaskBlt减少临时位图数量了。使用边界问题哪些场景不该用掩码位图需要真正的半透明效果如玻璃、阴影、渐变掩码位图做不了直接上AlphaBlend。需要旋转缩放BitBlt系列函数完全无能为力得靠StretchBlt有质量损失或Direct2D/OpenGL。需要动态改变透明区域比如魔法效果的技能图标每次变都要重新生成掩码性能堪忧不如直接用带Alpha通道的32位BMP配合像素混合。也就是说掩码位图真正的主场是静态图片、形状固定、要求低依赖和高性能、素材格式老。这类场景在工控上位机、老旧系统的维护项目里依然大量存在。我在2020年还接过一个Windows XP工控项目系统不支持GDI所有图标和指示灯全靠这套掩码方案稳定运行两年多没出过问题。8. 扩展思路从掩码位图过渡到带Alpha通道的现代方案最后聊点进阶的。掩码位图的二值化局限性本质上是只有透明和不透明的二元思维。现代图像处理的正确思路是引入Alpha通道让每个像素拥有0~255的透明度值。但GDI原生的BitBlt不支持Alpha通道计算所以需要另想办法。方案一AlphaBlend函数。GDI提供AlphaBlendmsimg32.dll支持对整个位图统一施加一个透明度值也支持源图带Alpha通道时逐像素混合。调用方式BLENDFUNCTION bf; bf.BlendOp AC_SRC_OVER; bf.BlendFlags 0; bf.SourceConstantAlpha 255; // 全局透明度255为不透明 bf.AlphaFormat AC_SRC_ALPHA; // 使用源图的Alpha通道 AlphaBlend(hdcDest, x, y, w, h, hdcSrc, 0, 0, w, h, bf);使用前提是源图是32位带Alpha通道的DIB节就是PNG转过来的那种每个像素的B、G、R、A四个字节都有效。我通常用LoadImage加LR_CREATEDIBSECTION加载32位BMP或者在运行时用GDI把PNG解码成32位HBITMAP。方案二自己写像素循环做预乘Alpha混合。AlphaBlend要求源像素是预乘Alpha格式颜色分量已乘以Alpha值否则边缘会有黑边。一劳永逸的办法是加载图片后自己做一遍预乘处理再交给AlphaBlend。代码模式for (int y 0; y h; y) { BYTE* p pBits y * stride; for (int x 0; x w; x) { BYTE alpha p[3]; if (alpha 0) { p[0] p[1] p[2] 0; } else { p[0] (BYTE)(p[0] * alpha / 255); p[1] (BYTE)(p[1] * alpha / 255); p[2] (BYTE)(p[2] * alpha / 255); } p 4; } }这套预乘处理在游戏开发里是标配能解决边缘色边问题。缺点是C循环处理大图速度一般但只在加载时执行一次运行时无压力。回到掩码位图本身。我个人的看法是技术的迭代不是简单的旧方案被新方案淘汰而是旧方案退到更合适的位置。在需要极致可控、低依赖、老平台兼容的场景掩码位图依然是可靠的选择。理解它的位运算本质不仅让你能读懂老代码更能帮你建立对GDI底层渲染模型的直觉——这种直觉在遇到BitBlt系函数的各种诡异ROP码时会帮你少走很多弯路。如果你正在维护旧项目或需要兼容老旧系统把上面这套掩码方案收好如果你做的是新项目建议直接用32位带Alpha的位图配合AlphaBlend同时把预乘像素的细节处理好视觉效果和开发效率都会好很多。两种思路的核心最后都落在如何精确控制每个像素去留这个问题上想通了再多花哨的API也万变不离其宗。本文还有配套的精品资源点击获取