简介:对仍在使用VC6.0的C++开发者而言,加载PNG并实现透明化处理是常见且棘手的需求,PNG格式自带Alpha通道,但VC6.0默认不直接支持解析,需要借助GDI+扩展库完成。压缩包内置完整GDI+运行环境与可运行示例工程,共70个文件;其中34个GDI+头文件覆盖图像绘制、颜色矩阵与图像属性定义,3个C++源文件写出加载PNG、读取Alpha通道、应用ColorMatrix调整透明度并绘制到设备上下文的实现,另有8张PNG素材、编译好的EXE和LIB库,整体4.73MB。示例工程GDIPClock以时钟程序为载体,完整演示从GDI+初始化、Bitmap创建,到透明混合绘制、窗口半透明设置的全过程,也包含SetLayeredWindowAttributes的用法。已有715人学习下载,对于维护老项目或学习VC6.0图形编程的读者,这份资料既提供可验证的成品,也能作为提取头文件、库与代码片段的依据,显著降低GDI+配置门槛。
1. 为什么在 VC6.0 里做 PNG 透明化,绕不开 GDI+
VC6.0 的 MFC 和 Win32 API 对 BMP、JPEG 的支持还算顺手,但提到 PNG 就卡住了——官方 CImage 在 VC6.0 里只是一个残缺的存根类,Load出来的是 24 位无 Alpha 通道的位图,透明像素直接变成黑色底。想要让 PNG 的透明背景真正“透”下去,我试过解析 IHDR/IDAT 手写 zlib 解压,试过找第三方库,最后都绕回同一个结论:用 GDI+ 加载,再配合 GDI 的AlphaBlend把预乘 Alpha 的 32 位位图合成上去。这套组合是 VC6.0 下做 PNG 透明处理成本最低、最不容易翻车的路径。这篇笔记把我拆过的完整流程、参数含义和踩过的坑都写透,照着做能少走一个月的弯路。
2. 搭建 VC6.0 + GDI+ 工作环境:头文件顺序与初始化是第一个分水岭
2.1 VC6.0 引入 GDI+ 的四个前置动作
GDI+ 不是 VC6.0 安装包自带的,它随 SDK 分发。但光有gdiplus.h不够,编译器版本太老,有一堆类型和宏需要提前声明和定义。常见做法是新建一个 Win32 控制台或 MFC 对话框工程后,做四件事:
第一,在stdafx.h或主文件顶部按顺序引入 Windows 头文件和 GDI+ 头文件。顺序不能颠倒:必须#include <windows.h>在先,#include <gdiplus.h>在后,因为 GDI+ 头文件依赖 Windows 的GdiplusFlatAPI 声明,而且 VC6.0 的windows.h和gdiplus.h在min/max宏上有冲突,先 Windows 后 GDI+ 可避开。
第二,VC6.0 没有ULONG_PTR这个类型,GDI+ 的头文件和库签名里大量使用它。解决方式是在包含gdiplus.h之前定义:
// 放在所有 #include 之前 #ifndef ULONG_PTR #define ULONG_PTR unsigned long #endif #include <windows.h> #include <gdiplus.h>第三,链接库。最省事的是用#pragma comment(lib, "gdiplus.lib")在代码里声明,避免去项目设置里手配路径。
第四,GDI+ 使用前必须调用GdiplusStartup初始化,程序退出前调用GdiplusShutdown释放。这个初始化是全局性的,不做的后果是后续所有 GDI+ 对象创建失败,Bitmap::FromFile返回的类型是Image但GetLastStatus()永远是OutOfMemory——而且不报任何弹窗,排查起来特别迷惑。
// 全局变量,用于持有 GDI+ 的启动令牌 ULONG_PTR g_gdiplusToken = 0; // 初始化:建议在 WinMain 或 InitInstance 的最前面调用 BOOL InitGdiPlus() { GdiplusStartupInput gdiplusStartupInput; // 第 3 个参数为 NULL,表示不注册后台线程回调 if (GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, NULL) != Gdiplus::Ok) { return FALSE; } return TRUE; } // 释放:在程序退出路径的最后调用 void ShutdownGdiPlus() { if (g_gdiplusToken != 0) { GdiplusShutdown(g_gdiplusToken); g_gdiplusToken = 0; } }这里GdiplusStartup第一个参数是输出令牌,第二个参数是GdiplusStartupInput结构体,里面可以指定GdiplusVersion、DebugEventCallback等。默认构造的GdiplusStartupInput把GdiplusVersion设为 1,对 VC6.0 配 GDI+ 1.1 版本也够用。第三个参数传 NULL 表示不需要 GDI+ 的线程池回调,这个在线程环境里曾出现过内存访问冲突,我一般直接 NULL,不碰它。
2.2 MFC 工程里初始化位置的取舍
MFC 对话框程序里有InitInstance,如果项目里同时有 Win32 代码,WinMain也会过早调用 GDI+。实际项目里我见过两种典型翻车:
一种是把GdiplusStartup放在OnInitDialog里,但有些全局对象(比如静态Bitmap指针)在InitInstance阶段已经创建,此时 GDI+ 还没启动,加载全部失败。另一种是 DLL 工程,在DllMain里调GdiplusStartup,结果在DLL_PROCESS_DETACH时线程状态不可靠,偶发死锁。
我的习惯是:对话框程序就放在InitInstance的最前边,CWinApp::InitInstance调用之前;DLL 程序不做全局初始化,而是提供一个InitGdiPlus()接口,由宿主进程在合适的时机调用,并把GdiplusShutdown的触发放在所有 GDI+ 对象销毁之后。
2.3 验证环境是否就绪
环境搭好后先跑一段最简单的加载测试,确认 GDI+ 可用,再做透明化逻辑。测试代码不复杂:加载一个带 Alpha 的 PNG 并读取某个像素的颜色值,如果 GDI+ 没初始化成功,这里会返回Status::OutOfMemory(错误码很误导人,实际上是未初始化)。
Gdiplus::Bitmap* pTestBmp = Gdiplus::Bitmap::FromFile(L"D:\\test.png"); if (pTestBmp == NULL || pTestBmp->GetLastStatus() != Gdiplus::Ok) { // GDI+ 未初始化或文件路径错误,这里容易被误导 }FromFile使用宽字符路径,VC6.0 里用L""字面量没问题。如果项目是 MBCS 编码,路径含中文时记得用MultiByteToWideChar转换,否则FromFile返回的文件打开失败错误。这一章把这些做好,后面的透明化流程就不会在最基础的地方卡壳。
3. 加载 PNG 并做预乘 Alpha:把透明的“底片”洗出来
3.1 为什么不能直接 GetHBITMAP 拿去 AlphaBlend
GDI+ 的Bitmap::GetHBITMAP可以把 PNG 转成 HBITMAP,但默认出来的位图格式视颜色深度而定。对于 32 位 PNG,GetHBITMAP返回的是 32 位 DIB,但 Alpha 通道保留方式有个关键陷阱:GDI+ 内部是直通 Alpha(straight alpha),也就是说像素的 RGB 分量没有和 Alpha 相乘。而 GDI 的AlphaBlend函数默认要求预乘 Alpha(premultiplied alpha)。直接把GetHBITMAP的结果交给AlphaBlend,透明边缘会出现一种“脏边”——半透明像素的 RGB 值偏大,结果就是彩色光晕,红、绿、蓝在最外边沿形成一圈暗色杂色,特别明显。
我拆过好几个纯 GDI 项目,发现多数人不是不知道AlphaBlend,而是不知道这中间还差一个预乘步骤。前端同事给的 PNG 在浏览器里好看,到了 MFC 界面里边缘全是“油污”,其实就是这个原因。
3.2 手动读取像素并完成预乘
绕开GetHBITMAP,更稳定的是用LockBits拿到像素缓冲自行预乘,再构建 DIB 交给 GDI。步骤固定:打开位图 → 锁定 32 位 ARGB 缓冲 → 遍历像素执行预乘公式 → 解锁 → 用CreateDIBSection生成 HBITMAP。
// 加载 PNG,并返回预乘处理后的 32 位 ARGB 像素缓冲 BYTE* LoadPNGToPremultipliedBuffer(LPCWSTR lpszFilePath, int& nWidth, int& nHeight) { Gdiplus::Bitmap* pBitmap = Gdiplus::Bitmap::FromFile(lpszFilePath); if (pBitmap == NULL || pBitmap->GetLastStatus() != Gdiplus::Ok) { return NULL; } nWidth = pBitmap->GetWidth(); nHeight = pBitmap->GetHeight(); // 锁定整张图到内存缓冲 Gdiplus::Rect rect(0, 0, nWidth, nHeight); Gdiplus::BitmapData bmpData; Gdiplus::Status status = pBitmap->LockBits( &rect, Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeWrite, PixelFormat32bppARGB, &bmpData); if (status != Gdiplus::Ok) { delete pBitmap; return NULL; } BYTE* pPixels = new BYTE[bmpData.Stride * nHeight]; memcpy(pPixels, bmpData.Scan0, bmpData.Stride * nHeight); // 逐像素预乘:RGB = RGB * Alpha / 255 for (int y = 0; y < nHeight; y++) { BYTE* pRow = pPixels + y * bmpData.Stride; for (int x = 0; x < nWidth; x++) { BYTE* pPixel = pRow + x * 4; BYTE alpha = pPixel[3]; if (alpha == 0) { pPixel[0] = pPixel[1] = pPixel[2] = 0; // 全透明像素 RGB 归零 } else { pPixel[0] = (BYTE)((UINT)pPixel[0] * alpha / 255); pPixel[1] = (BYTE)((UINT)pPixel[1] * alpha / 255); pPixel[2] = (BYTE)((UINT)pPixel[2] * alpha / 255); } } } pBitmap->UnlockBits(&bmpData); delete pBitmap; return pPixels; }LockBits的参数里,PixelFormat32bppARGB是关键——它强制 GDI+ 把像素格式统一为 BGRA 内存布局(注意内存里实际顺序是 B、G、R、A),后续 AlphaBlend 才能识别 alpha 通道。Stride是每行字节数,有可能是负值(自底向上位图),这里统一按正 Stride 处理,实际使用时最好判断一下符号,否则遍历时行指针方向会错,图像直接上下颠倒。
预乘公式本身很简单,但有个细节:alpha == 0时 RGB 直接清零,是为了避免黑边残留。因为直通 Alpha 的位图里,全透明像素的 RGB 可能还残留原色,预乘后 RGB 乘 0 变成 0,正好消除边缘杂色。
3.3 从预乘缓冲构建 HBITMAP
拿到预乘后的像素缓冲后,把它填入BITMAPINFO结构,用CreateDIBSection生成 HBITMAP。这部分是纯 GDI 操作,后续AlphaBlend才能正确解析。
HBITMAP CreateBitmapFromPremultipliedBuffer(BYTE* pPixels, int nWidth, int nHeight) { BITMAPINFO bmi; ZeroMemory(&bmi, sizeof(BITMAPINFO)); bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = nWidth; bmi.bmiHeader.biHeight = -nHeight; // 负值表示自顶向下,方便坐标处理 bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; BYTE* pDibBits = NULL; HBITMAP hBitmap = ::CreateDIBSection( NULL, // 无依赖 DC,用内存 DC 也可 &bmi, DIB_RGB_COLORS, (void**)&pDibBits, NULL, 0); if (hBitmap == NULL || pDibBits == NULL) { return NULL; } // 拷贝预乘数据到 DIB 缓冲 int nStride = nWidth * 4; memcpy(pDibBits, pPixels, nStride * nHeight); return hBitmap; }biHeight用负值非常关键。AlphaBlend的处理跟 BITMAP 的扫描行顺序相关,负高度声明了自顶向下的 DIB,这样坐标系的 Y 轴方向和屏幕坐标一致,后续在窗口里计算位置时不会有“上下倒置”的意外。CreateDIBSection的第一个参数传 NULL 是可以的,因为BITMAPINFO已经完整描述了格式。如果传入某个 DC,DIB 的颜色表会按那台设备的格式对齐,反而可能引入格式漂移,所以我常驻用 NULL。
构建完 HBITMAP,后续的绘制就完全交给 GDI 了,GDI+ 的 Bitmap 对象可以尽早删除。这里有个内存管理的教训:很多人用完LockBits不调UnlockBits,导致同一个Bitmap对象后续无法再次锁定,甚至delete时崩溃;而CreateDIBSection分配的缓冲区由 HBITMAP 持有,删除 HBITMAP 时自动释放,不需要手动delete[],之前new[]的临时缓冲pPixels却必须手动释放——这两个是不同生命周期的对象,别搞混。
4. AlphaBlend 合成渲染:参数、流程和 DC 的配合
4.1 BLENDFUNCTION 与 SourceConstantAlpha 的细粒度控制
AlphaBlend是 32 位 GDI 里把源位图合成到目标 DC 的函数,核心参数是BLENDFUNCTION结构体。很多初用者只关心AlphaFormat = AC_SRC_ALPHA,却把SourceConstantAlpha理解错了,导致整体透明度不如预期。
BLENDFUNCTION bf; bf.BlendOp = AC_SRC_OVER; // 标准 alpha 混合 bf.BlendFlags = 0; // 保留值,必须为 0 bf.SourceConstantAlpha = 0xFF; // 全局透明度,255 表示完全使用像素自身 alpha bf.AlphaFormat = AC_SRC_ALPHA; // 源位图包含每像素 alpha 通道参数表:
| 成员 | 值 | 含义与注意点 |
|---|---|---|
BlendOp | AC_SRC_OVER | 源叠加到目标上,不能用AC_SRC_OVER以外的值 |
BlendFlags | 0 | 必须为 0,微软文档没解释用途,填其他值会表现异常 |
SourceConstantAlpha | 0~255 | 0 全透明,255 完全使用像素 alpha。中间值会和像素 alpha 相乘 |
AlphaFormat | AC_SRC_ALPHA | 源位图有独立 alpha 通道。省略此标志时,只按全局透明度混合 |
这里的坑在于、如果SourceConstantAlpha设为 128,最终透明度不是 50%,而是 128/255 × 像素alpha,且 RGB 值在预乘基础上会被 GDI 再给一次混合运算,结果偏暗。想要“整体淡出”效果,可以在第 3 章预乘时把每个像素的 RGB 和 Alpha 一起按比例缩放,而不是依赖SourceConstantAlpha的半透明功能。这个区别在动态透明度动画里特别容易踩雷。
4.2 完整绘制流程:内存 DC → AlphaBlend → BitBlt
直接AlphaBlend到窗口 DC 是能显示,但反复刷新时会产生闪烁,而且半透明合成容易因为 DC 的残留状态出现残影。标准做法是先把背景画到内存 DC,再把 PNG 合成到内存 DC 上,最后一次性BitBlt到窗口 DC。具体流程:
// 统一绘制函数:把预乘后的 HBITMAP 绘制到 hWnd 的 (destX, destY) 处 BOOL DrawPremultipliedPNG(HWND hWnd, HBITMAP hSrcBitmap, int nWidth, int nHeight, int destX, int destY) { HDC hdcWin = ::GetDC(hWnd); HDC hdcMem = ::CreateCompatibleDC(hdcWin); HDC hdcSrc = ::CreateCompatibleDC(hdcWin); // 选入源位图 HBITMAP hOldSrc = (HBITMAP)::SelectObject(hdcSrc, hSrcBitmap); // 创建与窗口兼容的内存位图,用于承载合成结果 HBITMAP hMemBmp = ::CreateCompatibleBitmap(hdcWin, destX + nWidth, destY + nHeight); HBITMAP hOldMem = (HBITMAP)::SelectObject(hdcMem, hMemBmp); // 1. 先填充背景(示例为白色,实际可换成界面底图) RECT rcMem = {0, 0, destX + nWidth, destY + nHeight}; HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 255, 255)); ::FillRect(hdcMem, &rcMem, hBrush); ::DeleteObject(hBrush); // 2. 把 PNG 合成到内存 DC BLENDFUNCTION bf; bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.SourceConstantAlpha = 0xFF; bf.AlphaFormat = AC_SRC_ALPHA; BOOL bOK = ::AlphaBlend( hdcMem, destX, destY, nWidth, nHeight, // 目标 DC 与绘制位置 hdcSrc, 0, 0, nWidth, nHeight, // 源 DC 与位图区域 bf); // 3. 一次性拷到窗口 if (bOK) { ::BitBlt(hdcWin, 0, 0, destX + nWidth, destY + nHeight, hdcMem, 0, 0, SRCCOPY); } // 还原并释放对象 ::SelectObject(hdcSrc, hOldSrc); ::SelectObject(hdcMem, hOldMem); ::DeleteObject(hMemBmp); ::DeleteDC(hdcSrc); ::DeleteDC(hdcMem); ::ReleaseDC(hWnd, hdcWin); return bOK; }AlphaBlend的目标和源都要求 32 位色深。源 DC 里选入的是CreateDIBSection创建的 32 位 DIB,没问题;目标 DC 这里用的是CreateCompatibleBitmap创建的 DDB,它的位深跟随窗口 DC——如果窗口位于 16 位色深的显示模式下,AlphaBlend可能回退到简单混合甚至失败。测试时如果发现透明效果时好时坏,先查显示设置。更稳的目标位图是另一个CreateDIBSection32 位 DIB,代价是最后BitBlt时要多一次格式转换,但在现代环境里这反而是开销更低且稳定度更高的路径。
4.3 坐标是最后一个易错点
AlphaBlend的目标坐标是内存 DC 的坐标,不是窗口客户区的坐标。上面的destX/destY是相对内存 DC 的偏移,如果内存 DC 的大小正好等于窗口客户区,那这两个值就等于窗口客户区里的坐标。如果内存 DC 尺寸比客户区大(例如做滚动界面),就得先分清AlphaBlend的坐标是否已经包含滚动偏移。我见过有人把窗口坐标直接传给内存 DC,导致图片位置垂直偏移一段距离,调了几天才意识到是两个坐标系。
5. 避坑:VC6.0 + PNG 透明处理的六个血泪教训
5.1 现象:GDI+ 加载 PNG 总返回 OutOfMemory,文件明明存在
跑Bitmap::FromFile后,GetLastStatus()拿到OutOfMemory。但这个错误根本不是内存不够,而是 GDI+ 未初始化或者路径有问题。VC6.0 项目里最容易出现它的是GdiplusStartup调用被放在InitInstance之后,而某些全局对象在之前已经尝试加载位图。
解决:确认GdiplusStartup全局唯一且提前调用;确认路径是宽字符,用MultiByteToWideChar或L""。另外,FromFile接受文件路径但 VC6.0 的默认当前目录是工程目录而不是可执行文件目录,调试时路径写相对路径很容易找不到文件。
5.2 现象:透明区域变成黑色或纯色矩形
AlphaBlend结果中 PNG 的透明部分不是透出背景,而是显示为黑色背景或深灰色。原因多半是位图格式没有 alpha 通道:PNG 被GetHBITMAP转成了 24 位 DDB,透明信息被丢弃。
解决:不要用GetHBITMAP,用LockBits+PixelFormat32bppARGB把像素完整取出来,再构建 32 位 DIB。验证方式很简单:在调试器里检查 DIB 的BITMAPINFOHEADER.biBitCount,如果小于 32,基本就是这里丢了 alpha。
5.3 现象:透明边缘出现一圈彩色“脏边”或者暗晕
这是最典型的预乘失误。PNG 的半透明边缘像素保存的是“颜色 + 独立 alpha”,颜色值没有跟 alpha 相乘。AlphaBlend期望的是预乘数据,于是半透明区域的颜色值参与混合时会偏大,形成红/绿/蓝的色晕。
解决:在第 3 章的预乘循环里,对每个非零 alpha 像素执行RGB = RGB * alpha / 255。注意这个操作必须在LockBits修改像素后进行,不能等到CreateDIBSection之后再单独做一次运算——那样你已经把原始像素拷贝到 DIB,再改就只能再读一遍 DIB 内存,效率差一倍。
5.4 现象:VC6.0 编译报错——“gdiplus.h”C2065: 'ULONG_PTR' undeclared
GDI+ 头文件大量引用ULONG_PTR,VC6.0 的标准库没有定义它。网上有人建议直接typedef unsigned long ULONG_PTR,但放在windows.h之后会因为类型冲突编译失败。
解决:在包含gdiplus.h之前加#define ULONG_PTR unsigned long和#ifndef保护。同时注意gdiplus.h和windows.h的包含顺序,先windows.h后gdiplus.h,能避免多组min/max宏冲突。
5.5 现象:内存泄漏,一段时间后渲染越来越慢
LockBits后忘记UnlockBits,Bitmap对象持有了像素缓冲的锁,缓冲无法释放,反复加载不同 PNG 会让 GDI+ 的堆占用持续增长。另一个常见泄漏是把CreateDIBSection返回的pDibBits当成普通new[]分配的内存去delete[],造成非法释放。
解决:给LockBits和UnlockBits配对使用,建议做一个 RAII 封装;CreateDIBSection的内存随DeleteObject(hBitmap)自动释放,不要再手动 free。绘制循环里要确保每次SelectObject后恢复旧对象并删除新建对象,这些 GDI 对象的生命周期不管,最终会导致绘图区域花屏。
5.6 现象:动态切换图片时界面闪烁,或出现上一帧的残影
原因是直接在窗口 DC 上做AlphaBlend,又没先擦背景,残留的上帧像素会混进合成结果。VC6.0 对比现代框架没有自动双缓冲,窗口重绘时 GDI 也没有保留模式的帮助。
解决:严格走第 4 章的流程,所有合成都在内存 DC 完成,最后BitBlt一次上屏。如果WM_PAINT里触发了多次AlphaBlend,确保先FillRect清掉整个目标区域。还有个小细节:内存 DC 的尺寸和客户区不一致时,先GetClientRect再创建,别用GetWindowRect拿到的包含边框的尺寸。
6. 进阶技巧:透明 PNG 动画的验证与性能边界
6.1 用“黑底检查法”快速验证预乘是否正确
做透明处理时,我习惯在测试窗口里放两个开关,一个把背景涂成纯黑,一个涂成纯白。如果预乘做错了,黑色背景下透明边缘会出现一圈亮边,白色背景下会出现暗边,颜色会像“光晕”一样。这个验证法比肉眼观察原图快得多,也能自动判断是“预乘错误”还是“alpha 丢失”。如果黑底和白底下边缘都正常,说明像素的 RGB 和 alpha 都输入正确。这个检查我建议写成一个测试按钮,一键切换背景色,别每次手动改代码——除非你想把时间耗在反复编译上。
// 测试用:将窗口背景色在纯黑和纯白之间切换,观察 PNG 边缘 BOOL g_bBlackBackground = TRUE; void TestBackgroundSwitch(HWND hWnd) { g_bBlackBackground = !g_bBlackBackground; COLORREF bgColor = g_bBlackBackground ? RGB(0, 0, 0) : RGB(255, 255, 255); // ... 创建内存 DC,FillRect 用 bgColor,再 AlphaBlend 合成 PNG }合成效果在黑白两种背景下都干净,预乘基本没问题。假如只在黑色背景下看到亮边,问题一定出在 RGB 没有乘 alpha——未预乘的像素在黑色背景上会显示为偏色,因为黑底上残留颜色分外显眼。这个方法也适用于判断位图格式是否 32 位:背景色根本没有透过来的话,说明 alpha 通道都没有被识别。
6.2 性能优化:不要把预乘放在绘制循环里
LockBits加逐像素遍历的开销不小。一张 512×512 的 32 位 PNG,纯 C++ 循环遍历 26 万个像素,在老机器上也要几十毫秒。如果界面每帧都调LoadPNGToPremultipliedBuffer,帧率和 CPU 占用会很难看。我的做法是只在图片加载时做一次预乘,生成 32 位 DIB 后长期缓存,后续绘制只做AlphaBlend。某些特殊的动态效果(比如透明度由 0 渐变成 255)需要动态修改 alpha,那也只在 alpha 变化的帧里重新预乘,不变化时直接绘制。
如果图片特别大,还可以考虑用内存换时间:一次性加载整张 PNG 的 GDI+ Bitmap 存全局,需要某个矩形区域时就LockBits只锁定那个矩形,而不是整张图。但 VC6.0 下的 GDI+LockBits对矩形锁定的默认实现是把整个缓冲锁住,性能提升有限。实测下来,最有效的优化反而是把代码从遍历指针改为按Stride跳跃访问,减少 cache miss。
6.3 透明动画的“脏矩形”刷新法
做移动的小图标动画时,没必要整个窗口重绘。GDI+ 的 PNG 合成到内存 DC 后,只把图片变化范围内的区域BitBlt到窗口,也就是GetUpdateRect加上一个膨胀边界的“脏矩形”。可以降低 AlphaBlend 的调用面积,特别是半透明动画在慢速设备上能有效减少残影——残影通常是因为上一次的图片残留没被覆盖,脏矩形刷新法天然避免这个问题。
还有一点与绘图无关但常被忽略:VC6.0 的 GDI+ 底层依赖系统版本对msimg32.dll的AlphaBlend导出。如果跑在老旧精简版系统上,AlphaBlend可能不存在,调用直接失败。遇到这种情况,程序启动时检查GetProcAddress(GetModuleHandle("msimg32.dll"), "AlphaBlend")是否可用,不可用就降级为BitBlt绘制不透明版本,避免运行时崩溃。这算是我吃了好几次亏后才加的防护逻辑,从那以后我每次做 GDI 透明合成都会强制走一遍“黑底白底切换验证 + 脏矩形刷新 + 预处理缓存”这套流程,希望帮到你。
本文还有配套的精品资源,点击获取