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


    目录

    1、图片加载的相关说明

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

    3、使用CImage加载图片

    4、使用GDI+ Image加载图片

    5、CImage和Image的比较

    6、总结


    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的函数原型如下所示:

    1. HBITMAP LoadBitmap(
    2.     HINSTANCE hInstance,  // handle to application instance
    3.     LPCTSTR lpBitmapName  // name of bitmap resource
    4.     );

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

    1. #define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
    2. #define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))
    3. #ifdef UNICODE
    4. #define MAKEINTRESOURCE  MAKEINTRESOURCEW
    5. #else
    6. #define MAKEINTRESOURCE  MAKEINTRESOURCEA
    7. #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图片,代码如下:

    1. void CImage::LoadFromResource(
    2.     _In_opt_ HINSTANCE hInstance,
    3.     _In_z_ LPCTSTR pszResourceName) throw()
    4. {
    5.     HBITMAP hBitmap;
    6.     hBitmap = HBITMAP( ::LoadImage( hInstance, pszResourceName, IMAGE_BITMAP, 0,
    7.         0, LR_CREATEDIBSECTION ) );
    8.     Attach( hBitmap );
    9. }

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

             Load函数支持对文件路径加载和流加载的两种方式,函数声明如下:

    1. HRESULT Load(_In_z_ LPCTSTR pszFileName) throw();
    2. 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函数,如下:

    1. BOOL CImage::Draw(
    2.     _In_ HDC hDestDC,
    3.     _In_ int xDest,
    4.     _In_ int yDest,
    5.     _In_ int nDestWidth,
    6.     _In_ int nDestHeight,
    7.     _In_ int xSrc,
    8.     _In_ int ySrc,
    9.     _In_ int nSrcWidth,
    10.     _In_ int nSrcHeight) const throw()
    11. {
    12.     BOOL bResult;
    13.     ATLASSUME( m_hBitmap != NULL );
    14.     ATLENSURE_RETURN_VAL( hDestDC != NULL, FALSE );
    15.     ATLASSERT( nDestWidth > 0 );
    16.     ATLASSERT( nDestHeight > 0 );
    17.     ATLASSERT( nSrcWidth > 0 );
    18.     ATLASSERT( nSrcHeight > 0 );
    19.     GetDC();
    20. #if WINVER >= 0x0500
    21.     if( ((m_iTransparentColor != -1) || (m_clrTransparentColor != (COLORREF)-1)) && IsTransparencySupported() )
    22.     {
    23.         bResult = ::TransparentBlt( hDestDC, xDest, yDest, nDestWidth, nDestHeight,
    24.             m_hDC, xSrc, ySrc, nSrcWidth, nSrcHeight, GetTransparentRGB() );
    25.     }
    26.     else if( m_bHasAlphaChannel && IsTransparencySupported() )
    27.     {
    28.         BLENDFUNCTION bf;
    29.         bf.BlendOp = AC_SRC_OVER;
    30.         bf.BlendFlags = 0;
    31.         bf.SourceConstantAlpha = 0xff;
    32.         bf.AlphaFormat = AC_SRC_ALPHA;
    33.         bResult = ::AlphaBlend( hDestDC, xDest, yDest, nDestWidth, nDestHeight,
    34.             m_hDC, xSrc, ySrc, nSrcWidth, nSrcHeight, bf );
    35.     }
    36.     else
    37. #endif  // WINVER >= 0x0500
    38.     {
    39.         bResult = ::StretchBlt( hDestDC, xDest, yDest, nDestWidth, nDestHeight,
    40.             m_hDC, xSrc, ySrc, nSrcWidth, nSrcHeight, SRCCOPY );
    41.     }
    42.     ReleaseDC();
    43.     return( bResult );
    44. }

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

    1.     CImage* pImage = NULL;
    2.     ......// 中间new CImage和加载图片的代码省略
    3.     if ( pImage->GetBPP() == 32 )
    4.     {
    5.         for(int i = 0; i < pImage->GetWidth(); i++)   
    6.         {   
    7.             for(int j = 0; j < pImage->GetHeight(); j++)   
    8.             {   
    9.                 unsigned char* pucColor = reinterpret_cast<unsigned char *>(pImage->GetPixelAddress(i , j));   
    10.                 pucColor[0] = pucColor[0] * pucColor[3] / 255;   
    11.                 pucColor[1] = pucColor[1] * pucColor[3] / 255;   
    12.                 pucColor[2] = pucColor[2] * pucColor[3] / 255;   
    13.             }   
    14.         }
    15.     }

           对于想了解AlphaBlend绘制渲染算法(公式)的,可以查看MSDN上对该函数的说明,上面有详细的算法说明,如下:

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

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

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

    1. BOOL Draw(
    2.      _In_ HDC hDestDC,
    3.      _In_ const RECT& rectDest,
    4.      _In_ Gdiplus::InterpolationMode interpolationMode) const throw();

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

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

    4、使用GDI+ Image加载图片

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

           Image类也提供了两个加载图片的接口:FromFile和FromStream,如下:

    1.     static Image* FromFile(
    2.         IN const WCHAR* filename,
    3.         IN BOOL useEmbeddedColorManagement = FALSE
    4.     );
    5.     static Image* FromStream(
    6.         IN IStream* stream,
    7.         IN BOOL useEmbeddedColorManagement = FALSE
    8.     );

    这个两个函数和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不仅能给我们答疑,还可能会给我们提供解决问题的方法。

  • 相关阅读:
    【httpd】 Apache http服务器目录显示不全解决
    2023年CSP-J真题详解+分析数据(题目篇)
    opencv入门建议
    【知识点】图与图论入门
    Django 入门学习总结8-管理页面的生成
    SpringCloud-Docker原理解析
    网络安全-防火墙安全加固
    2023年考PMP证书有什么意义?一定要清楚!
    Django 请求与相应实例及解决表单返回403问题方法
    vue input防抖
  • 原文地址:https://blog.csdn.net/chenlycly/article/details/126235752