使用 GDI+ 加载 JPG & PNG 资源






4.84/5 (70投票s)
一个使用 GDI+ 从资源加载 JPG 和 PNG 文件的类。
引言
最近,我需要显示一些 JPG 和 PNG 文件。我有一个 LeadTools 的旧副本以及这两种格式的开源库,但我希望我的可执行文件尽可能小。所以我决定尝试 GDI+。我很快发现 GDI+ 的设计很差而且非常古怪,但它在我发现 GDI+ 无法加载资源中存储的 JPG 或 PNG 图像之前,对我的用途来说效果很好!
就像我确定面临此问题的其他开发人员一样,我不相信文档,并尝试了 Bitmap::FromResource
,但徒劳无功。在浏览可用的 Bitmap 方法时,我偶然发现了 Bitmap::FromStream
。
经过一些测试和几次错误(主要由于糟糕的 GDI+ 文档),我编写了工作代码。休息一夜后,我决定将代码封装在一个简单的类中,以确保内存被释放。结果是两个类:CGdiPlusBitmap
和 CGdiPlusBitmapResource
。
陷阱
在讨论代码本身之前,GDI+ 有一个必须解决的注意事项。对于 JPG、某些 TIFF 和其他格式,必须始终提供原始图像信息。换句话说,如果你使用 Bitmap::FromFile
打开一个位图,你在图像打开时不能删除或更改该文件。此限制同样适用于 CGdiPlusBitmapResource
。(我的测试发现 PNG 和 BMP 文件似乎不遵循此泛化,尽管我不知道这是标准行为还是仅仅是我的文件集中的一个特例。)
GDI+ 初始化
在进行任何 GDI+ 调用之前,都需要初始化 GDI+。我建议将以下数据成员添加到从 CWinApp
派生的类中
ULONG_PTR m_gdiplusToken;
在 InitInstance()
中,添加以下调用
Gdiplus::GdiplusStartupInput gdiplusStartupInput; Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
在 ExitInstance()
中,添加以下内容
Gdiplus::GdiplusShutdown(m_gdiplusToken);
类
我创建了两个类,基类是一个非常简单的 Bitmap
封装,派生类封装了全局内存。我想,如果我有足够的耐心和愿望,我可以扩展那个类,但我没有必要这样做。(如果你想知道我为什么没有简单地从 ATL 类 CImage
派生,那是因为代码在一个不使用 MFC 或 ATL 的程序中使用。但是,代码非常简单,很容易修改为使用 CImage
作为基类。)
我将不浪费时间去介绍 CGdiPlusBitmap
类,只说它有一个公共数据成员 Bitmap* m_pBitmap
。(在我将 GDI+ 对象放在类名前面时,我使用了 Gdiplus
命名空间,以防开发人员不想声明 using namespace Gdiplus;
。)
CGdiPlusBitmapResource
类有几个构造函数和几个重载的 Load
函数。重载函数只是允许像我一样的懒惰程序员不必输入 MAKEINTRESOURCE
。主要的 Load
函数接收字符串形式的资源名称和类型,并且是该类的关键。代码如下
inline bool CGdiPlusBitmapResource::Load(LPCTSTR pName, LPCTSTR pType, HMODULE hInst) { Empty(); HRSRC hResource = ::FindResource(hInst, pName, pType); if (!hResource) return false; DWORD imageSize = ::SizeofResource(hInst, hResource); if (!imageSize) return false; const void* pResourceData = ::LockResource(::LoadResource(hInst, hResource)); if (!pResourceData) return false; m_hBuffer = ::GlobalAlloc(GMEM_MOVEABLE, imageSize); if (m_hBuffer) { void* pBuffer = ::GlobalLock(m_hBuffer); if (pBuffer) { CopyMemory(pBuffer, pResourceData, imageSize); IStream* pStream = NULL; if (::CreateStreamOnHGlobal(m_hBuffer, FALSE, &pStream) == S_OK) { m_pBitmap = Gdiplus::Bitmap::FromStream(pStream); pStream->Release(); if (m_pBitmap) { if (m_pBitmap->GetLastStatus() == Gdiplus::Ok) return true; delete m_pBitmap; m_pBitmap = NULL; } } m_pBitmap = NULL; ::GlobalUnlock(m_hBuffer); } ::GlobalFree(m_hBuffer); m_hBuffer = NULL; } return false; }
我认为代码非常自明,尽管那些知道 ::LoadResource
的返回值是 HGLOBAL
的人可能会对使用 CopyMemory
进行的明显双重复制感到困惑。简而言之,CreateStreamOnHGlobal
需要一个使用 GMEM_MOVEABLE
标志由 GlobalAlloc
分配的 HGLOBAL
句柄。
演示
演示是一个 Visual Studio .NET 2003 项目,支持 ANSI 和 UNICODE 构建。它允许您加载重采样后的 JPG 或 PNG 资源(对于好奇的人来说,我在夏威夷欧胡岛拍摄了这两张照片,用于制作多媒体产品。一张是拉耶湾,另一张是威基基拍摄的日落。)
GDI+ 免责声明
我不是 GDI+ 专家,也不是它的忠实粉丝,即使我觉得 GDI+ 有时非常有用。请不要问我关于它的问题。
为什么不用 IPicture?
有人问我为什么不使用 IPicture
。答案是三方面的;第一,IPicture
不支持 PNG 图像。第二,IPicture
基本上只是一个图像加载器,其功能比标准的 GDI 位图调用多不了多少。第三,IPicture
会立即解码图像数据。JPG 和 GIF 图像会比这个类占用更多内存。
更新
2004 年 4 月 22 日
CreateStreamOnHGlobal
现在第二个参数使用FALSE
,因为Bitmap
要求至少保留 JPG 图像的内存,我决定采取谨慎的做法。有趣的是,我的测试表明此标志经常被忽略,但我收到报告说并非总是如此,并且在技术上是错误的。此外,在修复此 bug 时,我意识到我在图像加载失败时没有清理全局内存。这导致代码按现在的方式重新排列。
2004 年 6 月 15 日
如果
Gdiplus::Bitmap::FromStream
失败,我添加了我认为多余但更正确的处理m_pBitmap
的代码。(不幸的是,文档对此主题保持沉默,未能成功时是否总是返回 NULL。)
2004 年 9 月 3 日
添加了关于 GDI+ 初始化的一节。
修复了示例应用程序中图像加载失败时可能出现的内存泄漏。