一、位圖顯示新方法
用於操作DIB圖像的應用類有許多,筆者在"電腦編程與技巧"雜誌99年第10期, 介紹了一個封裝的通用圖像基類(CImage),可完成DIB圖像數據的管理和一些基本處理功能。其中大多的DIB類都採用直接分配內存的方式,然後用DIB操作函數來實現圖像的顯示。這種方式在Win98中,DIB操作函數將DIB內存直接寫入顯示內存中,但是在NT中,操作系統首先將DIB拷貝到服務器端創建一個DDB,然後再將DDB內存寫入顯存中,因此圖像顯示速度就會變慢。具體的原理有興趣的讀者可以參閱MSDN種的技術文獻中關於GDI操作的文章"Win32 動畫原理"。
在Win98/NT中提供了一個新的函數CreateDIBSection(),可以在客戶與服務器之間創建一個存儲DIB位圖的公共內存區給GDI。可以在該內存上執行各種GDI操作(包括利用BitBlt()函數直接輸出到顯存);另外可以直接訪問該內存。這樣就可以提高DIB位圖的顯示速度。但是可以看到極少數介紹這種方法的書籍上採用的方法是:先分配DIB內存讀入位圖,然後用獲得位圖信息再利用CreateDIBSection()函數分配內存,將數據拷貝到其中,最後將先分配DIB內存刪除。這種方法過程繁瑣就不說了,如果遇到調入的位圖有幾十兆,那速度將慢的驚人。
本文采用的顯示方法是:分配DIB內存就用CreateDIBSection()函數,然後用該函數返回的HBITMAP結構變量將位圖連接到一個CBitmap變量中。這樣在圖像顯示的時候,建立一個與當前DC兼容的DC,然後將位圖選入用BitBlt()或StretchBlt()函數來顯示。這種方法不僅顯示圖像的速度快,而且可以直接獲得圖像數據,對於圖像處理應用程序,可以提高圖像顯示和處理的性能。該方法是基於通用圖像基類(CImage)來實現的,
關於該基類的介紹,可參見“一個通用圖像基類”一文。具體的實現代碼如下:
在類的定義中加入下面的變量
HBITMAP hBitmap; // 位圖句柄
CBitmap m_Bitmap; // DDB位圖變量
CDC *BMP_DC; // 兼容的DC
CBitmap *m_lpOldBmp; // 存放舊圖像的指針
在類的構造函數中加入如下代碼,建立兼容的DC
BMP_DC = new CDC;
BMP_DC->CreateCompatibleDC( NULL );
在分配內存的函數中加入下面的代碼,位圖信息頭m_lpDibInfo要首先獲得。
// 用CreateDIBSection()分配圖像數據內存
hBitmap = CreateDIBSection( BMP_DC->m_hDC,// 兼容DC句柄
m_lpDibInfo,// 位圖信息頭
DIB_RGB_COLORS, // 色彩類型
(void **)&m_lpDibArray, // 數據內存指針
NULL, 0 );
// 如果內存分配成功,將它連接到一個CBitmap變量中
if( hBitmap != NULL )
{
m_Bitmap.Attach( hBitmap );
BMP_DC->SelectObject( m_Bitmap );
}
// 如果內存分配失敗,用new來分配內存
else
{
// 分配圖像數據內存
m_lpDibArray = new BYTE[ m_ImageSize ];
// 初始化圖像數據內存
memset( m_lpDibArray, 0, m_ImageSize );
// 如果分配失敗,報錯
if( m_lpDibArray == NULL ) AfxThrowMemoryException();
}
其中CreateDIBSection()函數第三個參數指明色彩類型,一般有兩種DIB_RGB_COLORS和DIB_PAL_COLORS。在Win98操作系統下,對於壓縮格式爲BI_BITFIELDS類型的BMP位圖(一般爲16位或32位有色彩掩碼位圖)應用參數DIB_PAL_COLORS,而對於壓縮格式爲BI_RGB類型的BMP位圖用的參數應爲DIB_RGB_COLORS。但是在NT或Win2000操作系統下,該參數只能爲DIB_RGB_COLORS,否者在爲16位或32位有色彩掩碼位圖分配內存時會出錯。由於筆者開發程序基於Win2000操作系統,因此該參數就直接用DIB_RGB_COLORS。
獲取圖像的數據可用指針m_lpDibArray,顯示圖像可用BitBlt()或StretchBlt()函數來完成。
下面給出的顯示函數,可以實現圖像放大和大型位圖的顯示,在視類的OnDraw()函數中調用,參數僅爲當前DC指針和客戶區的大小,其餘計算滾動位置和源圖範圍的工作均由該函數完成。
BOOL CImage::BestBlt(CDC *pMpDc, CRect ClientRect, BOOL IsFull)
{
// 將設備座標轉換位邏輯座標
pMpDc->DPtoLP( &ClientRect );
pMpDc->SetStretchBltMode( COLORONCOLOR );
// 計算對應客戶區的圖像區大小
int ClientW = ClientRect.Width();
int ClientH = ClientRect.Height();
CPoint BMPLUP, BMPRBP;
if( IsFull )
{
BMPLUP.x = BMPLUP.y = 0;
BMPRBP.x = m_ImageWidth;
BMPRBP.y = m_ImageHeight;
}
else
{
BMPLUP.x = (int) (ClientRect.left / m_fScale + 0.5);
BMPLUP.y = (int) (ClientRect.top / m_fScale + 0.5);
BMPRBP.x = (int) (ClientRect.right / m_fScale + 0.5);
BMPRBP.y = (int) (ClientRect.bottom / m_fScale + 0.5);
if( BMPRBP.x > m_ImageWidth )
{
BMPRBP.x = m_ImageWidth;
}
if( BMPRBP.y > m_ImageHeight )
{
BMPRBP.y = m_ImageHeight;
}
}
// 顯示圖像
return pMpDc->StretchBlt(ClientRect.left,
ClientRect.top,
ClientRect.Width(),
ClientRect.Height(),
BMP_DC,
BMPLUP.x,
BMPLUP.y,
BMPRBP.x - BMPLUP.x,
BMPRBP.y - BMPLUP.y,
SRCCOPY);
}
上面的位圖顯示方法不僅可以直接獲得圖像數據,而且圖像又可以作爲一個CBitmap變量,CBitmap變量作爲GDI對象可以靈活地被各種GDI函數調用,因此次方法結合了DIB和DDB兩種位圖優點,實現了位圖靈活、快速的顯示。
在許多動畫程序中,精靈(sprite)的出現給程序增色許多。精靈的顯示就涉及到透明位圖的顯示問題,其實精靈實際上視一幅矩形位圖,只是背景是固定的顏色,類似於電影拍攝中的藍幕技術。下面就給出幾種能實現透明位圖的顯示方法。
1.直接修改位圖數據
許多程序是用編制的DIB圖像類來實現圖像的顯示,如果直接改變圖像內存的數據,如果源圖像上點的顏色等於背景色,就不修改目標圖像,否則用源圖像點替代目標圖像點。這樣就可以實現精靈圖像的顯示。
這種方法有兩個缺點:一是要求精靈圖像較小,二是對於源圖像和目標圖像格式不同的情況處理起來比較困難,而且要求程序員對程序進行必要的優化以加快圖像更新速度,因此這種方法一般不被採用。
2.用TransparentBlt()函數實現
對於Win98或NT5.0操作系統,提供了一個函數TransparentBlt(),可以實現透明位圖的顯示。該函數類似於StretchBlt()函數,支持縮放操作,但不支持鏡像操作。具體用法如下:
TransparentBlt( pDC->m_hDC, // 目標設備環境句柄
10, // 目標矩形區域的左上角x座標
70, // 目標矩形區域的左上角y座標
w, // 目標矩形區域寬度
h, // 目標矩形區域高度
BackDC.m_hDC, // 源圖像的設備環境句柄
0, // 源圖像的左上角x座標
0, // 源圖像的左上角y座標
w, // 源圖像的寬度
h, // 源圖像的高度
BackColor );// 透明區域的顏色值
注意要使用該函數必須要將MSIMG32.LIB庫連接到工程中,可以通過在相應CPP文件中加入下面的代碼來將該庫連接進工程:
#pragma comment( lib, "MSIMG32.LIB")
3.用MaskBlt()函數實現
MaskBlt()函數通過一個單色模板位圖和相應的光柵操作碼來實現透明位圖得顯示。對於模板圖像上顏色值爲“1”的點,採用前景光柵碼來操作,而對於顏色值爲“0”的點,採用背景光柵碼來操作。如果前景光柵操作碼光柵操作碼爲0x00AA0029(顯示目標圖像點,具體含義可參見MSDN),只要製作好單色模板位圖就可以用MaskBlt()函數來實現透明位圖的顯示。實現的代碼如下:
// 設置位圖兼容DC的背景顏色爲透明色
BackDC.SetBkColor( BackColor );
// 生成模板位圖
MaskDC.BitBlt( 0, 0, w, h, &BackDC, 0, 0, NOTSRCCOPY );
// 顯示模板位圖
pDC->BitBlt(70, 10, 48, 48, &MaskDC, 0, 0, SRCCOPY);
// 生成MaskBlt透明顯示所需的光柵操作碼
DWORD dwFore = SRCCOPY; // 前景光柵碼
DWORD dwBack = 0x00AA0029; // 背景光柵碼
DWORD dwRop4 = MAKEROP4( dwFore, dwBack );
// 顯示透明位圖
pDC->MaskBlt(10, 70, w, h, &BackDC, 0, 0, MaskBmp, 0, 0, dwRop4);
生成單色模板位圖的方法比較簡單,首先將兼容DC的背景色設爲透明色,然後用BitBlt()函數將透明圖像寫入模板圖像兼容DC中,就可以得到所需的單色模板位圖。注意選用不同的光柵操作碼得到的結果是不同的。如果選用SRCCOPY如果源圖像的顏色與目標的背景色相同,則相應單色圖的輸出爲白色1,否則爲黑色0,這裏要選用NOTSRCCOPY光柵操作碼才能得到所需的模板。
4.用CImageList類實現
CImageList類是用來管理相同大小的圖標和位圖的類,可以實現圖標和位圖的透明顯示,因此可以用它來實現位圖的透明顯示,方法如下:
CImageList m_ImageList; // 定義CImageList類的對象
// 將位圖裝入,設置透明色
m_ImageList.Create( IDB_BITMAP_TRANS, 48, 1, BackColor );
// 顯示透明位圖
m_ImageList.Draw( pDC, 0, CPoint(10, 70), ILD_TRANSPARENT );
5.用光柵操作碼來實現
用於圖像顯示的三元光柵碼有兩百多個,三元光柵碼指明瞭源圖像、畫刷和目標圖像的顏色的組合操作方式,如果巧妙地利用光柵操作碼可以組合出許多的顯示透明位圖的方法,這裏給出一種。
// 生成模板位圖
MaskDC.BitBlt( 0, 0, w, h, &BackDC, 0, 0, SRCCOPY );
// 顯示模板位圖
pDC->BitBlt(70, 10, 48, 48, &MaskDC, 0, 0, SRCCOPY);
// 將源圖與目標圖像進行"異或(xor)"運算
// d xor s = d|s
pDC->BitBlt(10, 70, 48, 48, &BackDC, 0, 0, SRCINVERT);
// 將模板與目標圖像進行 "and" 運算
// 透明的部分保持不變,不透明的部分爲 0
pDC->BitBlt(10, 70, 48, 48, &MaskDC, 0, 0, SRCAND);
// 將源圖與目標圖像進行"異或(xor)"運算
// 0 xor G = G, 因此不透明的地方被源圖覆蓋
// d|s xor s = d, 因此透明的地方恢復
pDC->BitBlt(10, 70, 48, 48, &BackDC, 0, 0, SRCINVERT);
6.用PlgBlt()函數實現
PlgBlt()函數可以將源DC中的圖像傳送到目標DC上的一個平行四邊形區域中,而且可以根據傳入的單色模板位圖來實現透明位圖的顯示。該函數的第一個參數是三個點結構的指針,第四個點的計算由函數內部實現,該點的位置如下圖示意。只要計算好平行四邊形區域,也可以實現位圖的旋轉顯示,本文將在下面討論。
實現代碼如下:
┏━━━━━━━━━┓
┃ Pic01.jpg 文件 ┃
┗━━━━━━━━━┛
圖1 平行四邊形構成示意圖
// 生成模板位圖
MaskDC.BitBlt(0, 0, w, h, &BackDC, 0, 0, NOTSRCCOPY);
// 顯示模板位圖
pDC->BitBlt(70, 10, 48, 48, &MaskDC, 0, 0, SRCCOPY);
// 計算顯示的位置
CPoint Pt[3];
Pt[0].x = 10;
Pt[0].y = 70;
Pt[1].x = Pt[0].x + w;
Pt[1].y = Pt[0].y;
Pt[2].x = Pt[0].x;
Pt[2].y = Pt[0].y + h;
// 顯示透明位圖
pDC->PlgBlt(Pt, &BackDC, 0, 0, w, h, MaskBmp, 0, 0);
三、圖像的幾何變換顯示
1. 用StretchBlt()函數實現圖像鏡像顯示
StretchBlt()函數支持圖像的鏡像顯示,如果將目標區域的高度或寬度取爲負值,就可以實現圖像的鏡像顯示。
// 顯示正常圖像
pDC->StretchBlt(100, 100, 48, 48, &BackDC, 0, 0, 48, 48, SRCCOPY);
// 顯示水平對稱圖像
pDC->StretchBlt(100, 100, -48, 48, &BackDC, 0, 0, 48, 48, SRCCOPY);
// 顯示垂直對稱圖像
pDC->StretchBlt(100, 100, 48, -48, &BackDC, 0, 0, 48, 48, SRCCOPY);
// 顯示中心對稱圖像
pDC->StretchBlt(100, 100, -48, -48, &BackDC, 0, 0, 48, 48, SRCCOPY);
2.用SetWorldTransform()函數實現幾何變換顯示 SetWorldTransform()函數設置世界座標與目標DC座標之間的二維座標變換,可以實現圖像的旋轉、鏡像、縮放、平移、剪切以及上述各種變換的組合變換。該函數的第二個參數爲XFORM結構變量,具體定義讀者可參見MSDN,它的變換方程如下:
x' = x * eM11 + y * eM21 + eDx,
y' = x * eM12 + y * eM22 + eDy,
可以用CombineTransform()函數將兩個變換組合爲一個變換。下面的例子實現了位圖的旋轉顯示,要實現其它的變換,只需給XFORM結構變量賦予不同的值就可以實現。另外注意一點:只有首先用SetGraphicsMode()函數將DC的屬性設爲GM_ADVANCED類型,SetWorldTransform()函數纔有效,並且注意文字顯示座標也要隨之變化。
// 計算旋轉的參數
double Angle = 40.0/ 180* 3.1415926;
float cosAngle = (float)cos( Angle );
float sinAngle = (float)sin( Angle );
XFORM xform;
xform.eM11 = cosAngle;
xform.eM12 =-sinAngle;
xform.eM21 = sinAngle;
xform.eM22 = cosAngle;
xform.eDx = 0;
xform.eDy = 0;
// 設置DC的屬性,使得SetWorldTransform()執行有效
SetGraphicsMode( pDC->m_hDC, GM_ADVANCED );
// 設置座標轉換方式
SetWorldTransform( pDC->m_hDC, &xform );
// 顯示位圖
pDC->StretchBlt(10, 100, 90, 90, &BackDC, 0, 0, 48, 48, SRCCOPY);
3.用PlgBlt()函數實現圖像旋轉、剪切顯示
在介紹圖像透明已經介紹了PlgBlt()函數的用法,只要按所需計算好顯示區域的A、B、C點的座標就可以實現圖像旋轉、剪切顯示。這裏要注意如果要顯示全部圖像可以將模板位圖像素值用下面的方法設爲“1”。
MaskDC.BitBlt(0, 0, w, h, &BackDC, 0, 0, WHITENESS);
4.直接修改位圖數據
在介紹圖像透明已經介紹了這種方法,只要設計好算法,這種方法可以實現更多的變換。比如類似於Photoshop中的各種變換濾鏡,限於篇幅,本文對這種方法就不進行討論了。
四、圖像的梯度填充效果實現
編過OpengGL程序的讀者一定知道,在對場景內的物體進行光滑明暗處理時,如果多邊形的各個頂點顏色不同,則多邊形內部的點就會用Gouraud方法來進行平滑,融合處理。以往用GDI函數來實現採用的方法一般爲:將區域分爲許多小份,用漸變顏色的畫刷進行填充。其實可以用GradientFill()函數實現三角形片、三角形扇和矩形區域的梯度填充,如果與SelectClipRgn()函數結合就可以實現特殊區域的梯度填充顯示。下面的例子實現了三角形片和矩形梯度填充。
// 定義三角形的邊長
int w = 100;
// 定義三角形片的頂點結構變量
TRIVERTEX vert[4];
// 給頂點賦座標和顏色值
vert[0].x = 10;
vert[0].y = 10;
vert[0].Red = 0x0000;
vert[0].Green = 0x0000;
vert[0].Blue = 0x0000;
vert[0].Alpha = 0x0000;
vert[1].x = vert[0].x + w;
vert[1].y = vert[0].y;
vert[1].Red = 0x0000;
vert[1].Green = 0x0000;
vert[1].Blue = 0xff00;
vert[1].Alpha = 0x0000;
vert[2].x = vert[0].x + w;
vert[2].y = vert[0].y + w;
vert[2].Red = 0x0000;
vert[2].Green = 0x0000;
vert[2].Blue = 0xff00;
vert[2].Alpha = 0x0000;
vert[3].x = vert[0].x;
vert[3].y = vert[0].y + w;
vert[3].Red = 0xff00;
vert[3].Green = 0xff00;
vert[3].Blue = 0xff00;
vert[3].Alpha = 0x0000;
// 指定三角形片的順序
GRADIENT_TRIANGLE gTRi[2];
gTRi[0].Vertex1 = 0;
gTRi[0].Vertex2 = 1;
gTRi[0].Vertex3 = 2;
gTRi[1].Vertex1 = 0;
gTRi[1].Vertex2 = 2;
gTRi[1].Vertex3 = 3;
// 填充三角形片
GradientFill( hdc, vert, 4, &gTRi, 2, GRADIENT_FILL_TRIANGLE );
// 定義矩形區域的左上和右下頂點結構變量
TRIVERTEX Rectvert[2];
// 給頂點賦座標和顏色值
Rectvert[0].x = vert[0].x + w + 10;
Rectvert[0].y = 10;
Rectvert[0].Red = 0x0000;
Rectvert[0].Green = 0x0000;
Rectvert[0].Blue = 0x0000;
Rectvert[0].Alpha = 0x0000;
Rectvert[1].x = Rectvert[0].x + w;
Rectvert[1].y = Rectvert[0].y + w;
Rectvert[1].Red = 0xFF00;
Rectvert[1].Green = 0x0000;
Rectvert[1].Blue = 0x0000;
Rectvert[1].Alpha = 0x0000;
// 指定矩形區域的左上和右下頂點順序
GRADIENT_RECT gRect;
gRect.UpperLeft = 0;
gRect.LowerRight = 1;
// 水平方向梯度填充矩形
GradientFill( hdc, Rectvert, 2, &gRect, 1, GRADIENT_FILL_RECT_H );
Rectvert[0].x += 10 + w;
Rectvert[1].x += 10 + w;
// 垂直方向梯度填充矩形
GradientFill( hdc, Rectvert, 2, &gRect, 1, GRADIENT_FILL_RECT_V );
五、獲取任何DC中圖像
從DC中獲取圖像有許多的用處,比如將DDB圖像轉化位DIB圖像保存,實現屏幕圖像抓取等。
爲了擴大CImage類的功能,添加了一個GetIMGFromDC()函數,可以將傳入的DC中指定區域的圖像保存到CImage類中。具體的代碼如下:
BOOL CImage::GetIMGFromDC( CDC *pDC, // 傳入的DC指針
int ox, // 區域的左上點x座標
int oy, // 區域的左上點y座標
int cw, // 區域的寬度
int ch) // 區域的高度
{
CBitmap bitmap; // 定義位圖
CDC memDC; // 定義兼容DC
// 創建定義兼容DC
memDC.CreateCompatibleDC(pDC);
// 創建DDB兼容位圖
bitmap.CreateCompatibleBitmap(pDC, m_ImageWidth, m_ImageHeight);
// 將兼容位圖選入兼容DC
CBitmap* pOldBitmap = memDC.SelectObject( &bitmap );
// 設置Blt模式
memDC.SetStretchBltMode( COLORONCOLOR );
// 將當前DC的圖像寫入兼容DC中
memDC.StretchBlt( 0, 0, m_ImageWidth, m_ImageHeight, pDC, ox, oy, cw, ch, SRCCOPY );
// 獲得色彩類型
if( m_lpDibInfo->bmiHeader.biCompression == BI_RGB )
m_uBltUsage = DIB_RGB_COLORS;
else if( m_lpDibInfo->bmiHeader.biCompression == BI_BITFIELDS )
m_uBltUsage = DIB_PAL_COLORS;
// 從位圖中獲得圖像數據
GetDIBits ( memDC.m_hDC, HBITMAP( bitmap ),
0, m_ImageHeight, m_lpDibArray,
m_lpDibInfo, m_uBltUsage );
// 恢復兼容DC
memDC.SelectObject( pOldBitmap );
// 刪除兼容DC
memDC.DeleteDC();
// 刪除位圖
bitmap.DeleteObject();
return true;
}
如果要將獲取當前屏幕的圖像,使用的方法爲:
// 獲得屏幕的尺寸
int ScreenX = ::GetSystemMetrics(SM_CXSCREEN);
int ScreenY = ::GetSystemMetrics(SM_CYSCREEN);
// 定義CImage對象
CImage lhw(ScreenX/ 2, ScreenY/ 2, 16, 0);
// 定義一個當前屏幕的DC
CClientDC dc( NULL );
// 抓取屏幕圖像
lhw.GetIMGFromDC(&dc, 0, 0, ScreenX, ScreenY);
創建的CImage圖像對象的尺寸爲屏幕尺寸的一半,該函數內部實現圖像的縮放。