65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 GDI+ 加载 JPG & PNG 资源

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (70投票s)

2003 年 1 月 21 日

CPOL

4分钟阅读

viewsIcon

905231

downloadIcon

20393

一个使用 GDI+ 从资源加载 JPG 和 PNG 文件的类。

引言

最近,我需要显示一些 JPG 和 PNG 文件。我有一个 LeadTools 的旧副本以及这两种格式的开源库,但我希望我的可执行文件尽可能小。所以我决定尝试 GDI+。我很快发现 GDI+ 的设计很差而且非常古怪,但它在我发现 GDI+ 无法加载资源中存储的 JPG 或 PNG 图像之前,对我的用途来说效果很好!

就像我确定面临此问题的其他开发人员一样,我不相信文档,并尝试了 Bitmap::FromResource,但徒劳无功。在浏览可用的 Bitmap 方法时,我偶然发现了 Bitmap::FromStream

经过一些测试和几次错误(主要由于糟糕的 GDI+ 文档),我编写了工作代码。休息一夜后,我决定将代码封装在一个简单的类中,以确保内存被释放。结果是两个类:CGdiPlusBitmapCGdiPlusBitmapResource

陷阱

在讨论代码本身之前,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+ 初始化的一节。

修复了示例应用程序中图像加载失败时可能出现的内存泄漏。

© . All rights reserved.