0


VC++几种加载图片方法的讨论(附源码)

VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585 在UI软件中一般都需要加载bmp、jpg、png等多种格式的图片,针对不同的场合使用了不同的方法。本文将分别讲述使用LoadBitmap、CImage和GDI+ Image类来加载图片的方法。

   LoadBitmap是API函数,GDI+主要讲使用Image类(也可以用Bitmap类),CImage则是微软在新版的VS中新增的MFC类,内部主要也是用GDI+来实现的。文中对这几种方法进行了一定的延伸,进行了优劣分析,对适用场景进行了总结和概括。

1、图片加载的相关说明

   Windows提供的API函数LoadBitmap只能加载位图资源,不能用来加载png等其他格式的图片。加载png等其他格式的图片,则要使用新版VS自带的CImage类或者GDI+中的Image类。

2、使用LoadBitmap来加载位图图片

   API函数LoadBitmap最简单,功能最为单一,只能加载位图图片。之所以要讲解这个函数,是因为从这个函数能引申出一点有意思的东西。LoadBitmap的函数原型如下所示:
HBITMAP LoadBitmap(
    HINSTANCE hInstance,  // handle to application instance
    LPCTSTR lpBitmapName  // name of bitmap resource
    );

第一个参数为资源所在模块的实例,第二个参数为bitmap资源的名称字符串,或者资源id。如果资源id,则要使用MAKEINTRESOURCE处理一下,该宏的实现如下:

#define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
#define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))
#ifdef UNICODE
#define MAKEINTRESOURCE  MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE  MAKEINTRESOURCEA
#endif // !UNICODE

从宏的实现可以看出,资源id就是强转成字符串地址的。我们可以进一步想一下,LoadBitmap既支持出入资源名称字符串,也支持传入资源id,那LoadBitmap内部是如何识别出来这两种情况的呢?

   无意中看到了IS_INTRESOURCE宏,其实现如下:
#define IS_INTRESOURCE(_r) ((((ULONG_PTR)(_r)) >> 16) == 0)
   对于32位的ULONG_PTR,如果高16位为0,则表示是资源id,不是名称字符串地址,这是为什么呢?看过Windows核心编程的或者有软件异常处理经验的朋友,肯定知道鼎鼎大名的64KB的“NULL指针内存区”的概念。查看Windows核心编程一书中的“内存管理”部分的“虚拟地址空间的分区”那一节,进程的地址分区如下所示:

从图中开出,NULL指针内存区是从0开始的64KB的地址空间。保留该分区的目的是为了帮助程序员捕获对空指针的赋值,如果进程中的线程试图读取或写入位于这一分区内的内存地址,就会引发内存访问违例并导致进程被终止。在分析程序异常问题时,经常会遇到空指针或者是访问很小的内存地址(64KB范围内的地址)的异常,会导致进程被异常终止,出现闪退。

    所以,如果传入的是名称字符串地址,地址值肯定是大于64KB的;而资源id是16位无符号整数,整数值肯定小于等于2^16(64K)的,如果转化为32位ULONG_PTR类型,高16位肯定为0,所以可以使用IS_INTRESOURCE宏将字符串地址值和资源id值区分开来。

   对于非位图资源或图片,则要使用CImage类,或者GDI+类了。当然,除了LoadBitmap,还有一个API是LoadImage,不仅能加载bitmap,还能光标和图标等,此处就不一一介绍,都比较简单,用LoadBitmap只是想引出上面的比较有意思的问题。

3、使用CImage加载图片

   新版本VS2010的MFC库中提供了可以加载bmp、jpg、gif、png等多种格式的CImage类,给我们带来了很大的便利。CImage类中提供了多个方法,比如Load、LoadFromResource,都可以加载图片。Load支持文件路径加载和流加载两种方式,LoadFromResource则支持直接从资源中加载。
    但是经调试跟踪发现,跟到LoadFromResource的函数实现中,发现该函数内部调用的就是windows API函数LoadImage,只能用于加载bitmap、cursor和icon图片,代码如下:
void CImage::LoadFromResource(
    _In_opt_ HINSTANCE hInstance,
    _In_z_ LPCTSTR pszResourceName) throw()
{
    HBITMAP hBitmap;

    hBitmap = HBITMAP( ::LoadImage( hInstance, pszResourceName, IMAGE_BITMAP, 0,
        0, LR_CREATEDIBSECTION ) );

    Attach( hBitmap );
}

即png图片是不能使用该函数的。

     Load函数支持对文件路径加载和流加载的两种方式,函数声明如下:
HRESULT Load(_In_z_ LPCTSTR pszFileName) throw();
HRESULT Load(_Inout_ IStream* pStream) throw();

如果是要加载磁盘上的图片文件,则使用支持文件路径的Load函数。如果要从工程资源中通过资源id去加载资源中的图片,则要使用支持流加载的Load函数,其具体方法是,先从工程资源中将图片数据读出来,然后使用该数据创建流,然后通过流的方式来将图片加载进CImage对象中,具体代码可以参见本人的另一篇案例“使用GDI+和CImage加载png图片”。

   其实,从磁盘上加载图片文件,也可以使用支持刘加载的Load函数,只是稍微复杂一点,即将图片文件内容读到buffer中,然后用该buffer创建流,在流上将图片加载进来。

   使用CImage加载图片要注意一点,如果加载的是带透明通道的png图片,则要在调用Draw接口之前对RGB值进行alpha通道值预乘。这是为什么呢?可以查看CImage的多个重载的Draw函数的实现,最终都是调用带9个参数的那个Draw函数,如下:
BOOL CImage::Draw(
    _In_ HDC hDestDC,
    _In_ int xDest,
    _In_ int yDest,
    _In_ int nDestWidth,
    _In_ int nDestHeight,
    _In_ int xSrc,
    _In_ int ySrc,
    _In_ int nSrcWidth,
    _In_ int nSrcHeight) const throw()
{
    BOOL bResult;

    ATLASSUME( m_hBitmap != NULL );
    ATLENSURE_RETURN_VAL( hDestDC != NULL, FALSE );
    ATLASSERT( nDestWidth > 0 );
    ATLASSERT( nDestHeight > 0 );
    ATLASSERT( nSrcWidth > 0 );
    ATLASSERT( nSrcHeight > 0 );

    GetDC();

#if WINVER >= 0x0500
    if( ((m_iTransparentColor != -1) || (m_clrTransparentColor != (COLORREF)-1)) && IsTransparencySupported() )
    {
        bResult = ::TransparentBlt( hDestDC, xDest, yDest, nDestWidth, nDestHeight,
            m_hDC, xSrc, ySrc, nSrcWidth, nSrcHeight, GetTransparentRGB() );
    }
    else if( m_bHasAlphaChannel && IsTransparencySupported() )
    {
        BLENDFUNCTION bf;

        bf.BlendOp = AC_SRC_OVER;
        bf.BlendFlags = 0;
        bf.SourceConstantAlpha = 0xff;
        bf.AlphaFormat = AC_SRC_ALPHA;
        bResult = ::AlphaBlend( hDestDC, xDest, yDest, nDestWidth, nDestHeight,
            m_hDC, xSrc, ySrc, nSrcWidth, nSrcHeight, bf );
    }
    else
#endif  // WINVER >= 0x0500
    {
        bResult = ::StretchBlt( hDestDC, xDest, yDest, nDestWidth, nDestHeight,
            m_hDC, xSrc, ySrc, nSrcWidth, nSrcHeight, SRCCOPY );
    }

    ReleaseDC();

    return( bResult );
}

由上图可以看出,如果加载的图片中包含有alpha通道,则CImage内部会调用AlphaBlend来绘制渲染,而AlphaBlend在调用时要求事先要对RGB值进行alpha通道值的预乘,所以使用CImage加载图片后需要对对RGB值进行alpha通道值的预乘,预乘的代码如下:

    CImage* pImage = NULL;

    ......// 中间new CImage和加载图片的代码省略

    if ( pImage->GetBPP() == 32 )
    {
        for(int i = 0; i < pImage->GetWidth(); i++)   
        {   
            for(int j = 0; j < pImage->GetHeight(); j++)   
            {   
                unsigned char* pucColor = reinterpret_cast<unsigned char *>(pImage->GetPixelAddress(i , j));   
                pucColor[0] = pucColor[0] * pucColor[3] / 255;   
                pucColor[1] = pucColor[1] * pucColor[3] / 255;   
                pucColor[2] = pucColor[2] * pucColor[3] / 255;   
            }   
        }
    }
   对于想了解AlphaBlend绘制渲染算法(公式)的,可以查看MSDN上对该函数的说明,上面有详细的算法说明,如下:

所以,作为Windows开发人员,要养成查看MSDN的好习惯。在我们遇到问题或者想深入了解时,MSDN可以为我们提供很大的帮助和指引。

   但是CImage有个缺陷,如果加载的是带透明通道的png图片,在绘制时需要进行缩放,如果不使用带绘制质量参数的Draw接口,则会出现图片中的文字失真,如下所示:

于是使用带绘制质量参数的接口,传入高质量绘制InterpolationModeHighQuality参数:

BOOL Draw(
     _In_ HDC hDestDC,
     _In_ const RECT& rectDest,
     _In_ Gdiplus::InterpolationMode interpolationMode) const throw();

但是绘制出来后,透明区域变成了黑色,如下:

所以,使用CImage绘制带透明通道的png图片时,如果要进行缩放,则透明区域会变黑,所以此时就不能使用CImage了,就要使用下面的GDI+ Image图片了。

4、使用GDI+ Image加载图片

   GDI+中有两个类来加载图片,分别是Bitmap类和Image类。其实上面说到的CImage类,其内部就是使用GDI+ Bitmap类来加载图片的。下面主要来说明使用Image类来加载。

   Image类也提供了两个加载图片的接口:FromFile和FromStream,如下:
    static Image* FromFile(
        IN const WCHAR* filename,
        IN BOOL useEmbeddedColorManagement = FALSE
    );

    static Image* FromStream(
        IN IStream* stream,
        IN BOOL useEmbeddedColorManagement = FALSE
    );

这个两个函数和CImage的重载的两个Load函数使用是类似的。

   如果是要加载磁盘上的图片文件,则使用支持文件路径的FromFile函数。如果要从工程资源中通过资源id去加载资源中的图片,则要使用支持流加载的FromStream函数,其具体方法是,先从工程资源中将图片数据读出来,然后使用该数据buffer来创建流,然后通过流的方式来将图片加载进Image对象中。

   使用上述两个函数时,要注意一下,这两个函数都是静态的,返回的Image对象,都是函数内部new出来的,所以在Image对象使用完成后需要我们去delete掉。

   使用Image也有个问题,即使用Image::FromFile直接从磁盘上加载图片文件,会将文件锁住,文件将不能重命名或者删除,比如使用下面的代码:
Image* pImgTest = Image::FromFile( L"E:\\TEST_PIC_2.png" );

则对应的文件不能删除,如果删除则提示:

所以尽量不要使用Image::FromFile函数,都使用Image::FromStream函数。那对于磁盘的文件,该如何使用Image::FromStream函数呢?其实和从资源中加载一样,都是将图片数据读出来,然后在上局buffer上创建流,然后调用Image::FromFile从流上将图片加载进来。

   我们在写代码的过程中,需要多想一想,需要一定的扩散思维。既然Image::FromFile有锁文件的问题,那支持从磁盘上加载文件的CImage::Load是不是也有锁文件的问题呢?有时我们可能图方便,直接从磁盘上加载的Load比从流上加载Load要方便的多,只要传入文件路径即可。经过测试,支持从磁盘上加载的CImage::Load是没有锁文件的问题的。CImage内部使用的是GDI+的Bitmap类,最开始没仔细看,觉得Bitmap类和Image类在调用GDI+内部的接口是差不多的,应该也会有锁文件的问题,其实不然,仔细一看是有差别的:

   Bitmap类中调用的DllExports::GdipCreateBitmapFromFileICM接口,而Image类内部调用的是DllExports::GdipLoadImageFromFileICM,如下:

即Bitmap类和Image类内部在加载磁盘文件时实现时不同的,所以Image::FromFile会锁文件,CImage::Draw不会锁文件就不难理解了。

    另外,Image类不需要外部进行alpha通道预乘,Image类内部会自己处理。

5、CImage和Image的比较

   CImage用着可能要比Image类要简单一点,直接调用Draw接口绘制即可,而Image要借助GDI+的Graphics类才能完成绘制。

   CImage在处理带透明通道的png图片的缩放时会导致透明区域变黑,所以此场景下就不能使用CImage了,要改用GDI+中的Image类。对于不需要缩放的场合,可以直接只用CImage来加载,CImage类更加简单方便。

   Image类同样也有自己的问题,Image::FromFile会锁住文件,所以在需要使用Image类的时候使用Image::FromStream接口。

6、总结

   在遇到问题时,要多想一想,多问自己一些问题,不确定的东西,需要自己写测试代码,亲自去测试去跟踪,大家要养成这样的习惯。此外,对待网上拷贝的代码也要谨慎,网上很多时候可以给我们提供一个思路,但是代码很多地方都可能有问题,需要我们进行测试和改进后才能使用。

   再就是,作为一个合格的Windows开发人员,要养成查看MSDN的好习惯,MSDN不仅能给我们答疑,还可能会给我们提供解决问题的方法。

本文转载自: https://blog.csdn.net/chenlycly/article/details/126235752
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。

“VC++几种加载图片方法的讨论(附源码)”的评论:

还没有评论