简介:面向仍在使用 VC6.0 维护 C++ 项目的开发者与图像处理初学者,这份资料聚焦老式开发环境下 PNG 图片的加载与透明化实现,借助 GDI+ 弥补 VC6.0 对 PNG 和 Alpha 通道支持不足的问题,适合需要给旧项目补充界面贴图、异形窗口或半透明效果的技术人员参考。压缩包共 70 个文件,约 4.73MB,以 34 个 GDI+ 头文件、3 个 cpp 源文件、8 张 png 素材为主,同时带有 dsp/dsw 工程配置、rc 资源脚本、GdiPlus.lib 静态库和可执行文件,构成一套可直接打开研究的完整示例工程。已有 717 人学习下载,说明其在 VC6.0 图形开发场景中仍有参考需求。资料价值在于把 GDI+ 初始化与关闭、Bitmap 读取、ColorMatrix 调整透明度、DrawImage 绘制以及资源释放串联到同一工程中,并保留钟表界面所需的 PNG 素材与资源文件,便于读者对照理解 PNG 透明合成和窗口透明处理的实现脉络,也可作为旧项目迁移前的兼容性样例。
1. VC6.0 加载 PNG 并透明化:先确认 GDI+ 这条路能走通
在 VC6.0 老工程里给按钮贴一张带透明背景的 PNG,是很多人做 VC++ PNG透明处理 时绕不开的场景:用 LoadImage 读不进来,用 IPicture 又经常把 alpha 通道吃掉,最后透明边缘发黑。这个拆解围绕 VC6.0 加载 PNG 并透明化处理,走的是 GDI+ 路线,重点解决三件事:把 PNG 读进内存、保留 alpha 通道、在对话框或控件上绘制时不闪不黑边。适合还在维护 VC6 工程、需要给界面贴异形图或半透明图标的从业者,也适合刚接手老代码、想先跑通一个最小可复现实例的人。
2. 给 VC6 接上 GDI+:环境、头文件与初始化顺序
VC6.0 自带的环境偏老,直接#include <gdiplus.h>很可能找不到头文件,或者链接时缺gdiplus.lib。常见做法是装一套较新的 Platform SDK,把 Include 和 Lib 目录配进 VC6 的搜索路径,并且放在自带 SDK 路径前面。不要只拷一个gdiplus.h就完事,GDI+ 的头文件和 lib 版本要对得上,否则会出现结构体定义不一致、链接符号找不到这类玄学问题。
2.1 为什么先排除 IPicture 和 LoadImage
LoadImage对 BMP、ICO 这些老格式很稳,但它不认 PNG。IPicture看起来能加载图片,实际在不同系统上对 PNG 的 alpha 支持不一致,有的环境能显示,有的环境直接把透明区画成黑色或白色。老工程里常见的CImage也不是 VC6 原生就有的东西,硬搬过来会带进一批新头文件和运行时依赖。
GDI+ 的优势是 PNG 解码和 alpha 混合都在系统 DLL 里完成,VC6 只需要拿到头文件和 lib,编译出来的程序在目标系统上调用gdiplus.dll。代价是要管好GdiplusStartup和GdiplusShutdown的生命周期,还要注意绘制时的双缓冲。对 VC6 这种老编译器来说,这条路线改动最小,也最容易回退。
我一般会先做一个只加载、只绘制、不掺业务逻辑的 Demo,确认 PNG 的透明区能正确露出背景,再往正式工程里搬。这样出了问题能快速判断是环境问题、加载问题还是重绘问题,不会一上来就在大工程里翻车。
2.2 把 gdiplus.h 和 gdiplus.lib 接进 VC6 工程
VC6 的目录配置在Tools > Options > Directories里,Include files 和 Library files 分别加新 SDK 的路径。配置完最好关掉 IDE 重开一次,避免缓存没刷新。工程里加这两行:
#include <windows.h> #include <gdiplus.h> #pragma comment(lib, "gdiplus.lib")gdiplus.h依赖windows.h里的一些基础类型,所以先包含windows.h更稳。#pragma comment(lib, "gdiplus.lib")让链接器自动找库,省得在 Project Settings 里手动加。若编译报GdiplusStartupInput未定义,先查 Include 路径是不是指到了老 SDK;若链接报GdiplusStartup未解析,查 Lib 路径和gdiplus.lib是否匹配。
VC6 下还有一个常见卡点:ULONG_PTR可能未定义。这个类型在较新的 SDK 里有,老环境没有。不要随手typedef unsigned long ULONG_PTR;掩盖问题,先确认 SDK 版本;确实要兼容时,再在包含gdiplus.h之前做条件定义,并且保证和 lib 的调用约定一致。
2.3 初始化与卸载:token 放在 App 层最稳
GDI+ 的初始化和卸载要成对出现。MFC 程序放在CWinApp::InitInstance和ExitInstance里最稳,因为对话框和控件都在 App 之后创建。代码骨架如下:
// 全局或 App 类成员 ULONG_PTR g_gdiplusToken = 0; BOOL CMyApp::InitInstance() { Gdiplus::GdiplusStartupInput startupInput; if (Gdiplus::GdiplusStartup(&g_gdiplusToken, &startupInput, NULL) != Gdiplus::Ok) { AfxMessageBox(_T("GDI+ 初始化失败")); return FALSE; } // 原有初始化代码放在后面 CWinApp::InitInstance(); return TRUE; } int CMyApp::ExitInstance() { if (g_gdiplusToken != 0) { Gdiplus::GdiplusShutdown(g_gdiplusToken); g_gdiplusToken = 0; } return CWinApp::ExitInstance(); }GdiplusStartupInput用默认构造即可,GdiplusStartup的第三个参数传NULL。g_gdiplusToken必须活到程序退出,不能放在某个对话框的局部变量里。曾经见过在OnInitDialog里初始化、在OnDestroy里卸载的写法,结果主窗口销毁后还有控件在绘制,直接崩在gdiplus.dll里。token 的生命周期宁长勿短。
2.4 一个最小可编译的透明绘制骨架
先不碰资源,直接从文件加载一张带 alpha 的 PNG,画到对话框客户区。代码:
// 对话框类成员:Gdiplus::Bitmap* m_pPng; BOOL CMyDialog::OnInitDialog() { CDialog::OnInitDialog(); // 路径按实际工程改,先用绝对路径排除工作目录问题 m_pPng = Gdiplus::Bitmap::FromFile(L"d:\\test\\logo.png"); if (m_pPng == NULL || m_pPng->GetLastStatus() != Gdiplus::Ok) { AfxMessageBox(_T("PNG 加载失败")); if (m_pPng != NULL) { delete m_pPng; m_pPng = NULL; } } return TRUE; } void CMyDialog::OnPaint() { CPaintDC dc(this); Gdiplus::Graphics graphics(dc.GetSafeHdc()); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); if (m_pPng != NULL) { graphics.DrawImage(m_pPng, 20, 20, m_pPng->GetWidth(), m_pPng->GetHeight()); } }Graphics直接用窗口 DC 构造,DrawImage的重载参数依次是图片、左上角 x/y、宽高。SmoothingModeHighQuality让缩放时的边缘更顺,但会增加一点绘制开销;如果只是原尺寸贴图,可以关掉。这个骨架只验证“能加载、能画出来”,透明区是否正确要看 PNG 本身有没有 alpha,以及背景有没有先画好。下一步再处理资源加载和闪烁。
3. 从文件与资源加载 PNG:两种入口和 alpha 保留细节
VC6 工程里图片放法一般两种:放磁盘文件,或者编进资源。文件方式调试方便,资源方式发布干净。两条路都要注意 alpha 不被破坏,尤其是从资源读进内存流时,所有权和释放顺序搞错会泄漏或崩溃。下面把两条入口拆开写,最后再补一个没有 alpha 通道时的抠色方案。
3.1 从文件加载:路径转宽字符与文件锁
Bitmap::FromFile在 GDI+ 里接收宽字符路径,VC6 默认 ANSI 工程直接传char*编不过。常见做法是用MultiByteToWideChar转一次。代码:
Gdiplus::Bitmap* LoadPngFromFile(const char* pszPath) { if (pszPath == NULL || pszPath[0] == '\0') return NULL; int nLen = MultiByteToWideChar(CP_ACP, 0, pszPath, -1, NULL, 0); if (nLen <= 0) return NULL; WCHAR* pwszPath = new WCHAR[nLen]; MultiByteToWideChar(CP_ACP, 0, pszPath, -1, pwszPath, nLen); Gdiplus::Bitmap* pBitmap = Gdiplus::Bitmap::FromFile(pwszPath); delete[] pwszPath; if (pBitmap != NULL && pBitmap->GetLastStatus() != Gdiplus::Ok) { delete pBitmap; pBitmap = NULL; } return pBitmap; }CP_ACP用当前代码页,路径里有中文时也能转。FromFile返回的Bitmap会持有文件句柄,直到delete pBitmap才释放。所以如果图片需要热更新,不能一直用FromFile锁着文件;常见做法是把文件读进内存,再用Bitmap::FromStream,或者每次重载前先释放旧Bitmap。调试阶段先用绝对路径,别一上来就相对路径,工作目录不对时加载失败很难查。
3.2 从资源加载:RCDATA 到 IStream 的所有权
资源方式先在.rc里加一行:
IDR_PNG_LOGO RCDATA "res\\logo.png"IDR_PNG_LOGO在resource.h里定义。VC6 资源编辑器不一定认得 RCDATA,直接手改.rc更稳。加载代码:
Gdiplus::Bitmap* LoadPngFromResource(HINSTANCE hInst, LPCTSTR lpName, LPCTSTR lpType) { HRSRC hRes = FindResource(hInst, lpName, lpType); if (hRes == NULL) return NULL; DWORD dwSize = SizeofResource(hInst, hRes); HGLOBAL hResData = LoadResource(hInst, hRes); if (hResData == NULL) return NULL; const void* pData = LockResource(hResData); if (pData == NULL) return NULL; HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, dwSize); if (hMem == NULL) return NULL; void* pMem = GlobalLock(hMem); memcpy(pMem, pData, dwSize); GlobalUnlock(hMem); IStream* pStream = NULL; if (CreateStreamOnHGlobal(hMem, TRUE, &pStream) != S_OK) { GlobalFree(hMem); return NULL; } Gdiplus::Bitmap* pBitmap = Gdiplus::Bitmap::FromStream(pStream); pStream->Release(); // TRUE 表示流释放时一并释放 hMem if (pBitmap != NULL && pBitmap->GetLastStatus() != Gdiplus::Ok) { delete pBitmap; pBitmap = NULL; } return pBitmap; }调用时lpType传RT_RCDATA,lpName传MAKEINTRESOURCE(IDR_PNG_LOGO)。GlobalAlloc必须用GMEM_MOVEABLE,CreateStreamOnHGlobal要求可移动内存。第二个参数TRUE表示流对象释放时自动释放hMem,所以pStream->Release()之后不要再GlobalFree(hMem),否则就是双重释放。这个坑很常见,尤其是从网上抄代码只改一半的时候。
3.3 DrawImage 与 AlphaBlend 的取舍
VC6 时代很多人习惯AlphaBlend,但它要求位图是预乘 alpha 的 DIB,PNG 解出来的直通 alpha 直接丢进去会颜色发灰或边缘不对。GDI+ 的DrawImage直接处理直通 alpha,代码少,边缘质量也更好。取舍可以看这张表:
| 方式 | 预乘 alpha | 边缘质量 | 适用场景 | VC6 集成成本 |
|---|---|---|---|---|
| GDI+ DrawImage | 不需要 | 高,支持平滑 | 带 alpha 的 PNG 贴图 | 中,需要 SDK |
| AlphaBlend | 需要 | 取决于 DIB 质量 | 已有 DIB 的旧绘制代码 | 低,但转换麻烦 |
| SetColorKey | 不需要 | 硬边,容易锯齿 | 纯色背景抠图 | 低 |
如果工程里已经有大量 DIB 绘制逻辑,不想全换 GDI+,可以只在 PNG 贴图这一层用DrawImage,画完再回到原 DC。不要为了统一而把整条绘制链都改成 GDI+,老工程里 CDC 和 GDI 对象混用容易出兼容问题。
3.4 没有 alpha 通道时用 SetColorKey 抠色
有些 PNG 是索引色或者没有 alpha,只有白底。这时候可以用SetColorKey把指定颜色抠掉:
void DrawPngWithColorKey(Gdiplus::Graphics& g, Gdiplus::Bitmap* pBitmap, int x, int y) { if (pBitmap == NULL) return; Gdiplus::Color keyColor(255, 255, 255); // 抠掉纯白 Gdiplus::ImageAttributes attr; attr.SetColorKey(keyColor, keyColor, Gdiplus::ColorAdjustTypeBitmap); g.DrawImage(pBitmap, Gdiplus::Rect(x, y, pBitmap->GetWidth(), pBitmap->GetHeight()), 0, 0, pBitmap->GetWidth(), pBitmap->GetHeight(), Gdiplus::UnitPixel, &attr); }SetColorKey的两个颜色参数是范围上下限,这里都传纯白,表示只抠纯白。边缘抗锯齿的半透明像素不会被完全抠掉,所以会出现一圈白边。这种方案适合图标边缘本来就很硬的图,不适合发丝、阴影、渐变边缘。能用带 alpha 的 PNG 就别用抠色,后者是权宜之计。
4. 透明绘制放进对话框和控件:重绘、闪烁与命中测试
透明 PNG 画到窗口上只是第一步,放进对话框后马上会遇到闪烁、背景擦除、点击穿透判断这些问题。VC6 的对话框默认会擦背景,OnPaint里再画图,两次绘制一叠加就闪。解决思路是双缓冲加抑制背景擦除,再把命中测试从矩形改成按 alpha 判断。
4.1 WM_ERASEBKGND 与双缓冲的配合
先让OnEraseBkgnd返回TRUE,告诉系统背景不用擦,全部在OnPaint里画。然后OnPaint用内存 DC 双缓冲:
BOOL CMyDialog::OnEraseBkgnd(CDC* pDC) { return TRUE; // 背景统一在 OnPaint 里画 } void CMyDialog::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(&rcClient); CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap memBmp; memBmp.CreateCompatibleBitmap(&dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp = memDC.SelectObject(&memBmp); // 先画背景,再画 PNG,顺序不能反 memDC.FillSolidRect(&rcClient, RGB(240, 240, 240)); Gdiplus::Graphics graphics(memDC.GetSafeHdc()); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); if (m_pPng != NULL) { graphics.DrawImage(m_pPng, 20, 20, m_pPng->GetWidth(), m_pPng->GetHeight()); } dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); }CreateCompatibleBitmap用窗口 DC 创建,尺寸取客户区。FillSolidRect是背景色,实际工程里可能是位图或渐变,替换这一行即可。BitBlt一次性拷到屏幕,闪烁基本就压住了。注意memDC.SelectObject(pOldBmp)要放回旧位图,否则memBmp析构时可能选不出去。
4.2 异形按钮的命中测试:按 alpha 判断
透明 PNG 做按钮时,不能再用矩形区域判断点击,否则点到透明区也会响应。常见做法是点击时取对应像素的 alpha,大于阈值才算命中:
bool HitTestPng(Gdiplus::Bitmap* pBitmap, int nX, int nY, BYTE byThreshold) { if (pBitmap == NULL) return false; if (nX < 0 || nY < 0 || nX >= (int)pBitmap->GetWidth() || nY >= (int)pBitmap->GetHeight()) { return false; } Gdiplus::Color color; if (pBitmap->GetPixel(nX, nY, &color) != Gdiplus::Ok) return false; return color.GetA() > byThreshold; }GetPixel每次调用有一定开销,放在OnLButtonDown里点一次没问题,别放在OnMouseMove里高频刷。如果要做悬停高亮,提前用LockBits把 alpha 读成一个BYTE数组缓存起来,命中时直接查表。阈值一般取 16 到 32,太低会把几乎透明的边缘也算命中,太高会漏掉半透明按钮的边缘。
4.3 背景图与控件叠加的绘制顺序
对话框里既有背景图又有透明 PNG 控件时,绘制顺序决定透明区露出什么。正确顺序是:先画对话框背景,再画背景图,最后画 PNG 控件。如果 PNG 控件是子窗口,子窗口的OnPaint只负责自己那块,父窗口背景要先画好。常见翻车是父窗口OnEraseBkgnd返回TRUE但OnPaint没画背景,子窗口透明区后面就是一片黑。
我一般会把背景绘制收在一个DrawBackground(CDC& dc)函数里,父窗口OnPaint和子控件需要背景时都调它。子控件透明区不要用SetBkMode(TRANSPARENT)硬扛,GDI+ 的 alpha 混合需要目标 DC 上已经有背景像素,背景是空的,混出来就是黑。
4.4 用 ColorMatrix 做整体透明和禁用态
PNG 自带的 alpha 控制每个像素的透明,整体透明度要用ColorMatrix。比如按钮禁用时整图变淡:
void DrawPngWithAlpha(Gdiplus::Graphics& g, Gdiplus::Bitmap* pBitmap, int x, int y, BYTE byAlpha) { if (pBitmap == NULL) return; float fAlpha = byAlpha / 255.0f; Gdiplus::ColorMatrix matrix = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, fAlpha, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; Gdiplus::ImageAttributes attr; attr.SetColorMatrix(&matrix, Gdiplus::ColorMatrixFlagsDefault, Gdiplus::ColorAdjustTypeBitmap); g.DrawImage(pBitmap, Gdiplus::Rect(x, y, pBitmap->GetWidth(), pBitmap->GetHeight()), 0, 0, pBitmap->GetWidth(), pBitmap->GetHeight(), Gdiplus::UnitPixel, &attr); }矩阵第四行第四列是 alpha 缩放系数,0全透明,1保持原样,0.5半透明。ColorAdjustTypeBitmap表示作用于位图。做淡入动画时按定时器改byAlpha重绘即可,但要注意每帧都双缓冲,不然动画会闪。
5. 避坑与排查:VC6 + PNG 透明最常见的五个翻车点
这一章按“现象 → 原因 → 解决”写五个实际踩过的坑。每个坑都尽量落到可操作的检查点,不绕概念。
5.1 编译报 gdiplus.h 找不到或链接失败
现象:fatal error C1083: Cannot open include file: 'gdiplus.h',或者编译过了但链接报GdiplusStartup未解析。
原因:VC6 的 Include/Lib 路径还指向老 SDK,或者只加了头文件路径没加库路径。也有工程把gdiplus.lib加进了 Project Settings,但文件名拼错。
解决:先确认Tools > Options > Directories里新 SDK 路径在自带路径前面,关掉 IDE 重开。再检查#pragma comment(lib, "gdiplus.lib")有没有被条件编译包住。链接报未解析时,用dumpbin /symbols查 lib 里有没有符号,别靠猜。VC6 的工程设置里如果有旧版 lib 冲突,把旧路径临时移走再编。
5.2 图片加载成功但背景全黑或全白
现象:PNG 能画出来,但透明区是黑块或白块,换一张图又正常。
原因:目标 DC 上没有先画背景,GDI+ 的 alpha 混合把透明区和未初始化的显存混在一起;或者 PNG 本身没有 alpha,程序按带 alpha 的方式绘制。
解决:先确认 PNG 的 alpha 通道。用画图工具打开看透明区是不是棋盘格,或者代码里读GetPixel看 alpha 值。目标 DC 上先FillSolidRect或画背景图,再DrawImage。如果图片本来没有 alpha,改用SetColorKey,别硬走透明混合。
5.3 透明边缘出现黑边或白边
现象:图标边缘有一圈黑边,或者白底图抠完后有一圈白边。
原因:黑边通常是预乘 alpha 和直通 alpha 混用,或者用AlphaBlend画了直通 alpha 的位图;白边是SetColorKey抠纯白时,抗锯齿的半透明像素没被完全抠掉。
解决:统一走 GDI+DrawImage,不要在同一张图上混用AlphaBlend。如果是SetColorKey产生的白边,换带 alpha 的 PNG,或者把抠色范围放宽,但放宽会误伤浅色像素。边缘质量要求高时,让设计重新导出带 alpha 的 PNG,比在代码里修边省事。
5.4 拖动对话框就花屏
现象:静态显示正常,一拖动窗口或遮挡后恢复,PNG 周围出现残影、黑块。
原因:OnEraseBkgnd返回TRUE但OnPaint没画全背景,或者双缓冲的BitBlt区域小于无效区域。也有子控件自己擦背景,和父窗口绘制打架。
解决:OnPaint里先算GetClientRect,内存位图按客户区大小创建,BitBlt覆盖整个客户区。子控件如果透明,父窗口要负责画它背后的背景,子控件不要重复擦。拖动时用WM_ERASEBKGND返回TRUE抑制闪烁,再用双缓冲一次性贴。
5.5 退出时崩溃或内存泄漏
现象:关闭对话框时崩在gdiplus.dll,或者任务管理器里句柄数一直涨。
原因:GdiplusShutdown在还有Bitmap、Graphics对象活着时被调用;或者CreateStreamOnHGlobal的第二个参数传了TRUE,代码里又GlobalFree了一次。
解决:GdiplusShutdown放到ExitInstance,确保所有 GDI+ 对象先析构。Bitmap用delete释放,不要用Release。资源加载的hMem所有权只归IStream,pStream->Release()后不要再碰。调试时在ExitInstance前加断点,看还有没有窗口在OnPaint。
6. 把透明 PNG 用到工程里:一套可复用的封装与验收方法
封装一个轻量类,把加载、释放和绘制收在一起,代码不用多,关键是所有权清楚:
class CPngImage { public: CPngImage() : m_pBitmap(NULL) {} ~CPngImage() { Release(); } bool LoadFromResource(HINSTANCE hInst, LPCTSTR lpName, LPCTSTR lpType) { Release(); m_pBitmap = LoadPngFromResource(hInst, lpName, lpType); return m_pBitmap != NULL; } void Release() { if (m_pBitmap != NULL) { delete m_pBitmap; m_pBitmap = NULL; } } Gdiplus::Bitmap* GetBitmap() { return m_pBitmap; } private: CPngImage(const CPngImage&); CPngImage& operator=(const CPngImage&); Gdiplus::Bitmap* m_pBitmap; };拷贝构造和赋值声明成私有,防止浅拷贝导致双重delete。LoadFromResource里先Release再加载,支持重复加载。绘制时只拿GetBitmap(),不要在外面delete。
验收透明效果别用纯色背景,用棋盘格背景最直观:透明区应该露出棋盘格,半透明边缘应该能看到渐变过渡。再拖动窗口、遮挡恢复、切换禁用态各看一遍,确认不闪不黑边。我现在的习惯是每次接 PNG 到 VC6 工程,都先把 GDI+ token 的生命周期、资源内存所有权和背景重绘顺序走一遍,再谈效果。希望帮到你。
本文还有配套的精品资源,点击获取