1. 程式人生 > >使用GDI+和CImage類載入png圖片

使用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<char*>(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<char *> (GlobalLock(hGlobal));
	char *pSrc = reinterpret_cast<char *> (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出來的物件),如下所示:

    這意味著什麼呢?因為返回的是靜態函式內部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函式的說明:

    引數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<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;     
					}     
				}  
			}  

			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圖片粘帖到上面即可):

 
呼叫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+的佔用需求,效率已不再是個問題。