本文的方法可以加载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,以流的方式去加载。// 加载磁盘图片文件(传入文件的完整路径,返回加载到图片的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加载工程资源中的图片
(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类需要注意的地方
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加载的比较Gdiplus::Graphics graphics( hDstDC );
graphics.DrawImage( m_pImage, 0, 0, nWidth, nHeight ); // m_pImage为已经加载图片的Image对象指针
CImage中提供了多个绘制接口,比如AlphaBlend、BitBlt、Draw等,其中常用的Draw提供了多种参数的重载函数,方便大家使用。但是CImage在处理带透明色的png缩放绘制时,是有缺陷的。
调用CImage的Draw接口,将这张png图片按原始尺寸绘制出来的效果如下:
图片周边的透明区域是能完全透掉的。但是将图片进行缩小后,图片会出现较明显的失真,如下:
于是尝试使用带绘制质量设置的那个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就没问题了),效果如下:
缩放效果要好很多了,但是图片周边的透明区域却变成了黑色。所以CImage在处理带透明区域的png图片缩放时,是有问题的,此时应该选用GDI+中的Image类或Bitmap类来处理。使用GDI+ Image类的绘制效果如下,没有了CImage的缩放失真和透明区域变黑问题:
本文只体现了GDI+在处理带透明区域的png图片时优势,其实这只是GDI+的一小部分功能。比如利用GDI+,我们可以实现各种图片格式之间的相互转换(TL中将聊天框中的截图保存成多个格式的图片),可以利用GDI+中的转换矩阵,实现图片的缩放、旋转与平移操作(电子白板项目中就使用了GDI+的转换矩阵,很好的解决了图片缩放和旋转的问题)。
与GDI相比,GDI+要强大很多,能实现很多GDI所不能实现的酷炫UI效果。可能会有人说,GDI+会占用很多资源,绘制效率相对GDI要低一些。但就当前电脑的通用配置而言,硬件上的强大已经足以应付GDI+的占用需求,效率已不再是个问题。