使用GDI+和CImage类加载png图片

    本文的方法可以加载bmp、jpg、png等多种格式的图片,但由于大多软件都使用可带透明色的png图片,所以以加载png图片为研究切入点,找到对应的加载办法。本文结合TrueLink代码的实际使用情况,分别讲述使用GDI+和CImage来加载png图片的方法,并对使用过程中的一些细节和问题进行了总结。GDI+主要使用Image类;CImage则是微软在新版的VS中新增的MFC类,内部主要也是用GDI+来实现的。文中的内容是将原先的几篇博文整理而来。

1 图片加载的相关说明

    Windows提供的API和MFC中常用的CBitmap类,都不能用来加载png图片。比如常用的API函数LoadImage,只能用来加载位图、光标和图标图片;CBitmap则只能加载位图图片。对于大多数软件都用的、可带透明效果的png图片,则需要寻找其他的加载办法。下面就TrueLink代码的实际使用情况,详细介绍GDI+ Image和MFC类CImage加载png图片的实现方法,以及一些使用过程中应当注意的问题。

2 使用GDI+ Image加载png图片

    GDI+中用来加载图片的主要有Image和Bitmap两个类,其中Bitmap继承于Image,本文主要讨论使用Image类。Image类中提供两个用来加载图片的两个静态函数:Image::FromFile和Image::FromStream,如下所示:

   static Image* FromFile(
        IN const WCHAR* filename,
        IN BOOL useEmbeddedColorManagement = FALSE
    );

    static Image* FromStream(
        IN IStream* stream,
        IN BOOL useEmbeddedColorManagement = FALSE
    );
    图片加载一般主要有两种场景:一是加载磁盘上的图片文件(比如在TrueLink聊天框中插入图片,或者是插入表情文件);另一个则是通过资源id加载资源中的图片(TrueLink的UI贴图均是添加到资源中的,绘制前从资源中取得图片)。对于磁盘上的图片文件,似乎这两个方法都可以使用。Image::FromFile使用比较简单,直接通过图片文件的绝对路径去加载即可,非常方便。Image::FromStream使用流程则比较复杂。但是使用Image::FromFile有个问题,会将磁盘上的文件“锁住”,导致如果其他地方要同时加载该文件,则可能会出现加载失败的问题。所以,无论是磁盘上图片文件,还是资源中的图片,都调用Image::FromStream,以流的方式去加载。
2.1 Image加载磁盘图片文件
    加载磁盘图片文件的具体流程为:先将指定完整路径的图片文件数据读到由GlobalAlloc申请来的HGLOBAL内存中,然后调用CreateStreamOnHGlobal函数在HGLOBAL内存数据基础上创建流,最后调用Image::FromStream将图片数据加载到其内部new出来的Image对象中,代码如下:
// 加载磁盘图片文件(传入文件的完整路径,返回加载到图片的Image对象指针)
Image*  LoadFromFile( LPCTSTR pszFileName )  
{  
     Image* pImage = NULL;
	ASSERT( pszFileName != NULL );  

	CFile file;  
	DWORD dwSize;  

	// 打开文件
	if ( !file.Open( szFileName,  
		CFile::modeRead |   
		CFile::shareDenyWrite ) )  
	{  
		TRACE( _T( "Load (file): Error opening file %s\n" ), szFileName );  
		return FALSE;  
	};  

	// 根据文件内容大小分配HGLOBAL内存  
	dwSize = (DWORD)file.GetLength();  
	HGLOBAL hGlobal = GlobalAlloc( GMEM_MOVEABLE | GMEM_NODISCARD, dwSize );  
	if ( !hGlobal )  
	{  
		TRACE( _T( "Load (file): Error allocating memory\n" ) );  
		return FALSE;  
	};  

	char *pData = reinterpret_cast(GlobalLock(hGlobal));  
	if ( !pData )  
	{  
		TRACE( _T( "Load (file): Error locking memory\n" ) );  
		GlobalFree( hGlobal );  
		return FALSE;  
	};  

	// 将文件内容读到HGLOBAL内存中  
	TRY  
	{  
		file.Read( pData, dwSize );  
	}  
	CATCH( CFileException, e );                                            
	{  
		TRACE( _T( "Load (file): An exception occured while reading the file %s\n"),  
			szFileName );  
		GlobalFree( hGlobal );  
		e->Delete();  
		file.Close();  
		return FALSE;  
	}  
	END_CATCH  

	GlobalUnlock( hGlobal );  
	file.Close();  

	// 利用hGlobal内存中的数据创建stream  
	IStream *pStream = NULL;  
	if ( CreateStreamOnHGlobal( hGlobal, TRUE, &pStream ) != S_OK )  
	{  
		return FALSE;  
	}  

	// 将图片文件数据加载到Image对象中
	pImage = Image::FromStream( pStream );  
	ASSERT( pImage != NULL )  

	// 要加上这一句,否则由GlobalAlloc得来的hGlobal内存没有被释放,导致内存泄露,由于  
	// CreateStreamOnHGlobal第二个参数被设置为TRUE,所以调用pStream->Release()会自动  
	// 将hGlobal内存(参见msdn对CreateStreamOnHGlobal的说明),by zzx 2014/04/17   
	pStream->Release();  

	return pImage;
}  
2.2    Image加载工程资源中的图片
    加载资源图片的具体流程为:
    (1)从资源中读出图片资源数据:先调用FindResource,根据资源ID和资源类型(注意:这个资源类型就是在资源中添加文件时提示输入的资源类型标识串,比如下面代码中的“PNG”和“ZIP”),找到资源信息块句柄。然后将句柄传给LoadResource去加载资源,由SizeofResource得到资源数据的大小,调用GlobalAlloc分配HGLOBAL全局内存,再调用调用LockResource将资源数据锁住,然后将资源数据拷贝到HGLOBAL全局内存中。

    (2)创建流,通过流将图片资源数据加载到Image对象中:调用CreateStreamOnHGlobal函数在HGLOBAL内存数据基础上创建流,最后调用Image::FromStream将图片数据加载到其内部new出来的Image对象中。具体的代码如下所示:

// 加载图片资源(传入待加载图片的资源id和资源类型,返回加载到图片的Image对象指针)
Image* LoadFromRes( UINT nResID, LPCTSTR lpszResType, HINSTANCE hInstance )
{
	Image* pImage = NULL;

	ASSERT( lpszResType );

// 通过资源id和类型找到资源数据块,注意:这个资源类型就是在资源中添
	// 加文件时提示输入的资源类型标识串,比如下面代码中的“PNG”和“ZIP”
	HRSRC hPic = FindResource( hInstance, MAKEINTRESOURCE(nResID), lpszResType );
	HANDLE hResData = NULL;
	if ( !hPic || !( hResData = LoadResource( hInstance,hPic ) ) )
	{
		::OutputDebugString( _T( "Load (resource): Error loading resource: %d\n" ) );
		return NULL;
	}

	// 获取资源数据的大小,供GlobalAlloc使用
	DWORD dwSize = SizeofResource( hInstance, hPic );

    // 根据资源数据大小,分配HGLOBAL内存
	HGLOBAL hGlobal = GlobalAlloc( GMEM_MOVEABLE | GMEM_NODISCARD, dwSize );
	if ( !hGlobal )
	{
		::OutputDebugString( _T("Load (resource): Error allocating memory\n" ) );
		FreeResource( hResData );
		return NULL;
	}

	char *pDest = reinterpret_cast (GlobalLock(hGlobal));
	char *pSrc = reinterpret_cast (LockResource(hResData)); // 锁住资源
	if ( !pSrc || !pDest )
	{
		::OutputDebugString( _T( "Load (resource): Error locking memory\n" ) );
		GlobalFree( hGlobal );
		FreeResource( hResData );
		return NULL;
	};

	// 将资源数据拷贝到HGLOBAL内存中,用于创建流
	memcpy( pDest, pSrc, dwSize );
	FreeResource( hResData );
	GlobalUnlock( hGlobal );

	IStream *pStream = NULL;
	if ( CreateStreamOnHGlobal( hGlobal, TRUE, &pStream ) != S_OK )
	{
		return NULL;
	}

	pImage = Image::FromStream( pStream );

	// 要加上这一句,否则由GlobalAlloc得来的hGlobal内存没有被释放,导致内存泄露,由于
	// CreateStreamOnHGlobal第二个参数被设置为TRUE,所以调用pStream->Release()会自动
	// 将hGlobal内存(参见msdn对CreateStreamOnHGlobal的说明),by zzx 2014/04/17	pStream->Release();

	return pImage;
}
2.3 使用Image类需要注意的地方
   1、Image对象的释放
    对于FromFile和FromStream这两个函数,都是静态函数,MSDN对于返回值的文档化说明为:This method returns a pointer to the new Bitmap/Image object(在VS中GO到函数的定义出也是能看出来的,函数返回是new出来的对象),如下所示:
使用GDI+和CImage类加载png图片_第1张图片
    这意味着什么呢?因为返回的是静态函数内部new出来的对象,是需要我们使用者来负责销毁的,即对象使用完了后要我们手动将之delete掉。如果不delete掉,不仅会导致内存泄漏,也会导致GDI句柄泄漏。这点在我们的项目开发中是深有体会的。
    2、Image::FromFile和Image::FromStream选择
    在使用Image::FromFile时发现,在将指定的文件加载到Image对象中后,会将磁盘上对应的文件“锁住”,如果其他地方要同时加载该文件,则可能会出现加载失败问题,这也是我们在开发过程中遇到的。我们的处理办法是,不使用Image::FromFile,使用Image::FromStream。 对于Image::FromStream,我们先将文件读到内存中,然后再将内存中数据拷贝到创建的HGLOBAL内存中,然后调用Image::FromStream从流中将图片数据加载到Image对象中。
    3、内存资源释放问题
    在上面提供的函数中,结尾处必须要加上“pStream->Release();”这句,否则会导致内存泄漏,因为上面GlobalAlloc来的HGLOBAL内存没有释放。但是在上述代码中,使用完后并没有调用GlobalFree来释放内存,那自动释放内存是如何做到的呢?那我们就来看看MSDN中对 CreateStreamOnHGlobal函数的说明:
使用GDI+和CImage类加载png图片_第2张图片
    参数fDeleteOnRelease的说明:A value that indicates whether the underlying handle for this stream object should be automatically freed when the stream object is released.If set to FALSE, the caller must free the hGlobal after the final release. If set to TRUE, the final release will automatically free the hGlobal parameter. 也就是说,当将fDeleteOnRelease参数设置为FALSE时,调用pStream->Release();时就不会自动释放 GlobalAlloc来的内存,此时必须手动调用GlobalFree来释放;当将fDeleteOnRelease参数设置为TRUE时,在调用 pStream->Release();是会自动将GlobalAlloc来的内存释放掉。  

4、使用CImage加载png图片
    新版本VS的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函数支持对文件路径的加载的方式,其实最终调用的还是GDI+中Image::FromFile中的内部的代码,也有锁住图片文件的问题,所以也是不能用的,最终对于png图片的加载还是要使用CImage::Load流加载的方式,相关代码如下:(因为上面在讲述Image加载时已经提供了两个函数,处理方法类似,所以此处只给出一个函数实例-从资源中加载图片数据)

// 从资源中加载,返回加载图片数据的CImage对象指针
CImage* LoadCImage( UINT nID, LPCTSTR lpszType, HINSTANCE hInstance )  
{  
	CImage* pImage = NULL;  

	hInstance = ( NULL == hInstance ) ? ::AfxGetResourceHandle() : hInstance;  

	// 如果是位图,则直接调用CImage::LoadFromResource去加载 
	if( RT_BITMAP == lpszType )  
	{  
		pImage = new CImage();  
		pImage->LoadFromResource( hInstance, nID );  
		if ( !pImage->IsNull() )  
		{  
			return pImage;  
		}  
		else  
		{  
			delete pImage;  
			pImage = NULL;  
			return NULL;  
		}  
	}     

	// 如果是png图片,则使用流加载的方式
	CString strLog;  
	HRSRC hRsrc = ::FindResource( hInstance, MAKEINTRESOURCE(nID), lpszType );   
	ASSERT( hRsrc != NULL );  
	if ( NULL == hRsrc )   
	{  
		return NULL;  
	}  

	DWORD dwSize = ::SizeofResource( hInstance, hRsrc);  
	LPBYTE lpRsrc = (LPBYTE)::LoadResource( hInstance, hRsrc);  
	ASSERT( lpRsrc != NULL );  
	if ( NULL == hRsrc )   
	{  
		return NULL;  
	}  

	HGLOBAL hMem = ::GlobalAlloc( GMEM_FIXED, dwSize );  
	if ( NULL == hMem )   
	{  
		::FreeResource( lpRsrc );  
		return NULL;  
	}  

	LPBYTE pMem = (LPBYTE)::GlobalLock( hMem );  
	if ( NULL == pMem )   
	{  
		::GlobalUnlock( hMem );  
		::GlobalFree( hMem );  
		::FreeResource( lpRsrc );  
		return NULL;  
	}  

	// 将资源中的图片文件数据拷贝到流内存中
	memcpy( pMem, lpRsrc, dwSize );   
	IStream * pStream = NULL;  
	HRESULT hr = ::CreateStreamOnHGlobal( hMem, FALSE, &pStream );  
	if ( pStream != NULL && hr == S_OK )   
	{  
		pImage = new CImage();  
		HRESULT hrs = pImage->Load( pStream );  
		pStream->Release();  

		// 释放  
		::GlobalUnlock( hMem );  
		::GlobalFree( hMem );  
		::FreeResource( lpRsrc );  

		if ( hrs == S_OK )  
		{  
			// 对于CImage在处理png图片时,如果png图片中alpha通道,则加载完后要对透明色做一个处理 
			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(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;     
					}     
				}  
			}  

			return pImage;  
		}  
		else  
		{  
			delete pImage;  
			pImage = NULL;  
			return NULL;  
		}  
	}  
	else  
	{  
		::GlobalUnlock( hMem );  
		::GlobalFree( hMem );  
		::FreeResource( lpRsrc );  
		return NULL;  
	}  
}  

    使用CImage时,如果png图片包含透明区域,需要对透明区域做一个特殊处理,具体代码见上面的函数中。

5、GDI+加载和CImage加载的比较
    使用GDI+的Image相对比较复杂,使用之前要初始化GDI+库,绘制到界面时要借助GDI+中的Graphics类。比如要将Image类对象中的图片绘制到目标DC上:
Gdiplus::Graphics graphics( hDstDC );
graphics.DrawImage( m_pImage, 0, 0, nWidth, nHeight ); // m_pImage为已经加载图片的Image对象指针
    CImage中提供了多个绘制接口,比如AlphaBlend、BitBlt、Draw等,其中常用的Draw提供了多种参数的重载函数,方便大家使用。但是CImage在处理带透明色的png缩放绘制时,是有缺陷的。
    为了测试png图片的绘制效果,用截图软件截得了一张360窗口的图片,然后使用PhotoShop等工具在图片的周边加上了透明的区域,然后保存成png图片文件,即图片中包含了透明区域,加了透明区域的图片在Photoshop中的效果如下(在photoshop中生成带透明区域的图片的具体方法:先创建一个比360图片稍大的画板,设置背景色为透明,然后把360图片粘帖到上面即可):

 使用GDI+和CImage类加载png图片_第3张图片
调用CImage的Draw接口,将这张png图片按原始尺寸绘制出来的效果如下:

使用GDI+和CImage类加载png图片_第4张图片 
图片周边的透明区域是能完全透掉的。但是将图片进行缩小后,图片会出现较明显的失真,如下:
使用GDI+和CImage类加载png图片_第5张图片
于是尝试使用带绘制质量设置的那个Draw接口,如下:

第3个参数是绘制质量设置参数,参数说明如下:

//--------------------------------------------------------------------------
// Quality mode constants
//--------------------------------------------------------------------------
enum QualityMode
{
    QualityModeInvalid   = -1,
    QualityModeDefault   = 0,
    QualityModeLow       = 1, // Best performance
    QualityModeHigh      = 2  // Best rendering quality
};

//--------------------------------------------------------------------------
// Alpha Compositing quality constants
//--------------------------------------------------------------------------
enum CompositingQuality
{
    CompositingQualityInvalid          = QualityModeInvalid,
    CompositingQualityDefault          = QualityModeDefault,
    CompositingQualityHighSpeed        = QualityModeLow,
    CompositingQualityHighQuality      = QualityModeHigh,
    CompositingQualityGammaCorrected,
    CompositingQualityAssumeLinear
};

选择InterpolationModeHighQuality(通过CompositingQuality枚举体的注释得知,这个参数是用来处理alpha透明通道混合的)高质量参数来绘制(最近同事在编写代码的过程中,要将图片绘制到指定的窗口上,由于图片没有透明区域,就直接使用CImage来加载并绘制,结果发现对图片进行缩小时,有明显的失真,图片中的线条重叠、纹理变重,后来选用带绘制质量参数的那个CImage::Draw接口,设置高质量绘制类型InterpolationModeHighQuality就没问题了,效果如下:
使用GDI+和CImage类加载png图片_第6张图片
    缩放效果要好很多了,但是图片周边的透明区域却变成了黑色。所以CImage在处理带透明区域的png图片缩放时,是有问题的,此时应该选用GDI+中的Image类或Bitmap类来处理。使用GDI+ Image类的绘制效果如下,没有了CImage的缩放失真和透明区域变黑问题:
使用GDI+和CImage类加载png图片_第7张图片
    本文只体现了GDI+在处理带透明区域的png图片时优势,其实这只是GDI+的一小部分功能。比如利用GDI+,我们可以实现各种图片格式之间的相互转换(TL中将聊天框中的截图保存成多个格式的图片),可以利用GDI+中的转换矩阵,实现图片的缩放、旋转与平移操作(电子白板项目中就使用了GDI+的转换矩阵,很好的解决了图片缩放和旋转的问题)。
    与GDI相比,GDI+要强大很多,能实现很多GDI所不能实现的酷炫UI效果。可能会有人说,GDI+会占用很多资源,绘制效率相对GDI要低一些。但就当前电脑的通用配置而言,硬件上的强大已经足以应付GDI+的占用需求,效率已不再是个问题。

你可能感兴趣的:(GDI+,UI界面)