[轉貼] Windows編程和麪向對象技術 chap11

第十一講 多媒體編程

 

  隨着多媒體技術的迅猛發展和PC性能的大幅度提高,在PC機上運行的應用程序越來越多地採用了多媒體技術.如果你編寫的應用程序能夠發出美妙的聲音,播放有趣的動畫,無疑將會給人留下深刻的映象.

  Windows 95提供了對多媒體編程的良好支持,本章將幫助讀者迅速掌握一些實用的多媒體編程技術,主要的內容包括:

調色板

位圖

依賴於設備的位圖(DDB)

與設備無關的位圖(DIB)

動畫控件

媒體控制接口(MCI).

小結 

 

11.1調色板

 

11.1.1 調色板的原理

  PC機上顯示的圖象是由一個個像素組成的,每個像素都有自己的顏色屬性。在PC的顯示系統中,像素的顏色是基於RGB模型的,每一個像素的顏色由紅(B)、綠(G)、藍(B)三原色組合而成。每種原色用8位表示,這樣一個的顏色就是24位的。以此推算,PC的SVGA適配器可以同時顯示224約一千六百多萬種顏色。24位的顏色通常被稱作真彩色,用真彩色顯示的圖象可達到十分逼真的效果。

  但是,真彩色的顯示需要大量的視頻內存,一幅640×480的真彩色圖象需要約1MB的視頻內存。由於數據量大增,顯示真彩色會使系統的整體性能迅速下降。爲了解決這個問題,計算機使用調色板來限制顏色的數目。調色板實際上是一個有256個表項的RGB顏色表,顏色表的每項是一個24位的RGB顏色值。使用調色板時,在視頻內存中存儲的不是的24位顏色值,而是調色板的4位或8位的索引。這樣一來,顯示器可同時顯示的顏色被限制在256色以內,對系統資源的耗費大大降低了。

  顯示器可以被設置成16、256、64K、真彩色等顯示模式,前兩種模式需要調色板。在16或256色模式下,程序必須將想要顯示的顏色正確地設置到調色板中,這樣才能顯示出預期的顏色。圖11.1顯示了調色板的工作原理。使用調色板的一個好處是不必改變視頻內存中的值,只需改變調色板的顏色項就可快速地改變一幅圖象的顏色或灰度。

  在DOS中,調色板的使用不會有什麼問題。由於DOS是一個單任務操作系統,一次只能運行一個程序,因此程序可以獨佔調色板。在Windows環境下,情況就不那麼簡單了。Windows是一個多任務操作系統,可以同時運行多個程序。如果有幾個程序都要設置調色板,就有可能產生衝突。爲了避免這種衝突,Windows使用邏輯調色板來作爲使用顏色的應用程序和系統調色板(物理調色板)之間的緩衝。

T11_1.tif (194188 bytes)

圖11.1 調色板工作原理

 

  在Windows中,應用程序是通過一個或多個邏輯調色板來使用系統調色板(物理調色板)。在256色系統調色板中,Windows保留了20種顏色作爲靜態顏色,這些顏色用作顯示Windows界面,應用程序一般不能改變。缺省的系統調色板只包含這20種靜態顏色,調色板的其它項爲空。應用程序要想使用新的顏色,必須將包含有所需顏色的邏輯調色板實現到系統調色板中。在實現過程中,Windows首先將邏輯調色板中的項與系統調色板中的項作完全匹配,對於邏輯調色板中不能完全匹配的項,Windows將其加入到系統調色板的空白項中,系統調色板總共有236個空白項可供使用,若系統調色板已滿,則Windows將邏輯調色板的剩餘項匹配到系統調色板中儘可能接近的顏色上。

  每個設備上下文都擁有一個邏輯調色板,缺省的邏輯調色板只有20種保留顏色,如果要使用新的顏色,則應該創建一個新的邏輯調色板並將其選入到設備上下文中。但光這樣還不能使用新顏色,程序只有把設備上下文中的邏輯調色板實現到系統調色板中,新的顏色才能實現。在邏輯調色板被實現到系統調色板時,Windows會建立一個調色板映射表。當設備上下文用邏輯調色板中的顏色繪圖時,GDI繪圖函數會查詢調色板映射表以把像素值從邏輯調色板的索引轉換成系統調色板的索引,這樣當像素被輸出到視頻內存中時就具有了正確的顏色值。圖11.2說明了這種映射關係,從圖中讀者可以體會到邏輯調色板的緩衝作用。在該圖中,GDI繪圖函數使用邏輯調色板的索引1中的顏色來繪圖,通過查詢調色板映射表,得知系統調色板中的第23號索引與其完全匹配,這樣實際輸出到視頻內存中的像素值是23。注意圖中還演示了顏色的不完全匹配,即邏輯調色板中的索引15和系統調色板中的索引46。

  每個要使用額外顏色的窗口都會實現自己的邏輯調色板,邏輯調色板中的每種顏色在系統調色板中都有相同或相近的匹配。調色板的實現優先權越高,匹配的精度也就越高。Windows規定,活動窗口的邏輯調色板(如果有的話)具有最高的實現優先權。這是因爲活動窗口是當前與用戶交互的窗口,應該保證其有最佳的顏色顯示。非活動窗口的優先權是按Z順序自上到下確定的(Z順序就是重疊窗口的重疊順序)。活動窗口有權將其邏輯調色板作爲前景調色板實現,非活動窗口則只能實現背景調色板。

提示:術語活動窗口(Active window)或前臺窗口(Foreground window)是指當前與用戶交互的窗口,活動窗口的頂端的標題條呈高亮顯示,而非活動窗口的標題條則是灰色的。活動窗口肯定是一個頂層窗口(Top-level window),頂層窗口是指沒有父窗口或父窗口是桌面窗口的窗口,這種窗口一般都有標題和邊框,主要包括框架窗口和對話框。術語重疊窗口是指作爲應用程序主窗口的窗口,我們可以把對話框看成是一種特殊的重疊式窗口。

 

T11_2.tif (192888 bytes)

圖11.2 調色板的映射關係

 

11.1.2 調色板的創建和實現

MFC的CPalette類對邏輯調色板進行了封裝。該類的成員函數CreatePalette負責創建邏輯調色板,該函數的聲明爲:

BOOL CreatePalette( LPLOGPALETTE lpLogPalette ); //成功則返回TRUE。

參數lpLogPalette是一個指向LPLOGPALETTE結構的指針,LPLOGPALETTE結構描述了邏輯調色板的內容,該結構的定義爲:

typedef struct tagLOGPALETTE {

WORD palVersion; //Windows版本號,一般是0x300

WORD palNumEntries; //調色板中顏色表項的數目

PALETTEENTRY palPalEntry[1]; //每個表項的顏色和使用方法

} LOGPALETTE;

  結構中最重要的成員是PALETTEENTRY數組,數組項的數目由palNumEntries成員指定。PALETTEENTRY結構對調色板的某一個顏色表項進行了描述,該結構的定義爲:

typedef struct tagPALETTEENTRY {

BYTE peRed; //紅色的強度(0~255,下同)

BYTE peGreen; //綠色的強度

BYTE peBlue; //藍色的強度

BYTE peFlags;

} PALETTEENTRY;

  成員peFlags說明了顏色表項的使用方法,在一般應用時爲NULL,若讀者對peFlags的詳細說明感興趣,可以查看Visual C++的聯機幫助。

  可以看出,創建調色板的關鍵是在PALETTEENTRY數組中指定要使用的顏色。這些顏色可以是程序自己指定的特殊顏色,也可以從DIB位圖中載入。邏輯調色板的大小可根據用戶使用的顏色數來定,一般不能超過256個顏色表項。

  CreatePalette只是創建了邏輯調色板,此時調色板只是一張孤立的顏色表,還不能對系統產生影響。程序必需調用CDC::SelectPalette把邏輯調色板選入到要使用它的設備上下文中,然後調用CDC::RealizePalette把邏輯調色板實現到系統調色板中。函數的聲明爲:

CPalette* SelectPalette( CPalette* pPalette, BOOL bForceBackground );
該函數把指定的調色板選擇到設備上下文中。參數pPalette指向一個CPalette對象。參數bForceBackground如果是TRUE,那麼被選擇的調色板總是作爲背景調色板使用,如果bForceBackground是FALSE並且設備上下文是附屬於某個窗口的,那麼當窗口是活動窗口或活動窗口的子窗口時,被選擇的調色板將作爲前景調色板實現,否則作爲背景調色板實現。如果使用調色板的是一個內存設備上下文,則該參數被忽略。函數返回設備上下文原來使用的調色板,若出錯則返回NULL。

UINT RealizePalette( );
該函數把設備上下文中的邏輯調色板實現到系統調色板中。函數的返回值表明調色板映射表中有多少項被改變了。

 

  如果某一個窗口要顯示特殊的顏色,那麼一般應該在處理WM_PAINT消息時實現自己的邏輯調色板。也就是說,在OnPaint或OnDraw函數中重繪以前,要調用SelectPalette和RealizePalette。如果窗口顯示的顏色比較重要,則在調用SelectPalette時應該指定bForceBackground參數爲FALSE。

  前景調色板具有使用顏色的最高優先級,它有無條件佔用系統調色板(20種保留顏色除外)的權力,也就是說,如果需要,前景調色板將覆蓋系統調色板的236個表項,而不管這些表項是否正被別的窗口使用。背景調色板則無權破壞系統調色板中的已使用項。

  請讀者注意,前景調色板應該是唯一。如果一個活動窗口同時要實現幾個邏輯調色板,那麼只能有一個調色板作爲前景調色板實現,也即在調用CDC::SelectPalette時只能有一個bForceBackground被指定爲FALSE,其它的bForceBackground必需爲TRUE。通常是把具有輸入焦點的窗口的調色板作爲前景調色板實現,其它窗口只能使用背景調色板。如果活動窗口的子窗口全都使用前景調色板,則會導致程序的死循環。

提示:請讀者注意區分活動窗口和有輸入焦點的窗口。有輸入焦點的窗口要麼是活動窗口本身,要麼是活動窗口的子窗口。也就是說,活動窗口不一定具有輸入焦點,當活動窗口的子窗口獲得輸入焦點時,活動窗口就會失去輸入焦點。

 

11.1.3 使用顏色的三種方法

  在調用GDI函數繪圖時,可以用不同的方法來選擇顏色。Windows用COLORREF數據類型來表示顏色,COLORREF型值的長度是4字節,其中最高位字節可以取三種不同的值,分別對應三種使用顏色的方法。表11.1列出了這些不同的取值及其含義。

 

表11.1 COLORREF型值的最高位字節的含義

取值

含義

0x00

指定RGB引用。此時三個低位字節含有紅、綠、藍色的強度,Windows將抖動20種保留的顏色來匹配指定的顏色,而不管程序是否實現了自己的調色板。

0x01

指定調色板索引引用。此時最低位字節含有邏輯調色板的索引,Windows根據該索引在邏輯調色板中找到所需的顏色。

0x02

指定調色板RGB引用。此時三個低位字節含有紅、綠、藍色的強度,Windows會在邏輯調色板中找到最匹配的顏色。

爲了方便用戶的使用,Windows提供了三個宏來構建三種不同的COLORREF數據,它們是:

COLORREF RGB(BYTE bRed,BYTE bGreen,BYTE bBlue); //RGB引用

COLORREF PALETTEINDEX(WORD wPaletteIndex); //調色板索引引用

COLORREF PALETTERGB(BYTE bRed,BYTE bGreen, //調色板RGB引用
BYTE bBlue);

例如,我們可以用上述三種方法來指定刷子的顏色。下面的代碼用系統調色板中的紅色建立一個刷子:

CBrush brush;

brush.CreateSolidBrush(RGB(255,0,0));

pDC->SelectObject(&brush);

下面的代碼用邏輯調色板的索引2中的顏色來創建一個刷子:

pDC->SelectPalette(&m_Palette,FALSE);

pDC->RealizePalette( );

CBrush brush;

brush.CreateSolidBrush(PALETTEINDEX(2));

pDC->SelectObject(&brush);

下面的代碼用邏輯調色板中最匹配的深灰色來創建一個刷子:

pDC->SelectPalette(&m_Palette,FALSE);

pDC->RealizePalette( );

CBrush brush;

brush.CreateSolidBrush(PALETTERGB(20,20,20));

pDC->SelectObject(&brush);

11.1.4 與系統調色板有關的消息

  爲了協調各個窗口對系統調色板的使用,Windows在必要的時侯會向頂層窗口和重疊窗口發送消息WM_QUERYNEWPALETTE和WM_PALETTECHANGED。

  當某一頂層或重疊窗口(如主框架窗口)被激活時,會收到WM_QUERYNEWPALETTE消息,在窗口創建之初也會收到該消息,該消息先於WM_PAINT消息到達窗口。如果活動窗口要使用特殊的顏色,則在收到該消息時應該實現自己的邏輯調色板並重繪窗口。如果窗口實現了邏輯調色板,那麼WM_QUERYNEWPALETTE消息的處理函數應返回TRUE。通常窗口在收到該消息後應該爲有輸入焦點的窗口(如視圖)實現前景調色板,但如果程序覺得它顯示的顏色並不重要,那麼在收到該消息後可以把邏輯調色板作爲背景調色板實現(指定CDC::SelectPalette函數的bForceBackground參數爲TRUE),這樣程序就失去了使用系統調色板的最高優先權。

  當活動窗口實現其前景調色板並改變了系統調色板時,Windows會向包括活動窗口在內的所有的頂層窗口和重疊窗口發送WM_PALETTECHANGED消息,在該消息的wParam參數中包含了改變系統調色板的窗口的句柄。其它窗口如果使用了自己的邏輯調色板,那麼應該重新實現其邏輯調色板,並重繪窗口。這是因爲系統調色板已經被改變了,必需重新建立調色板映射表並重繪,否則可能會顯示錯誤的顏色。當然,非活動窗口只能使用背景調色板,所以顯示的顏色肯定沒有在前臺的時侯好。要注意只有在活動窗口實現了前景調色板且改變了系統調色板時,纔會產生WM_PALETTECHANGED消息。也就是說,如果窗口在調用CDC::SelectPalette時指定bForceBackground參數爲TRUE,那麼是不會產生WM_PALETTECHANGED消息。

  總之,WM_QUERYNEWPALETTE消息爲活動窗口提供了實現前景調色板的機會,而WM_PALETTECHANGED消息爲窗口提供了適應系統調色板變化的機會。

  需要指出的是,子窗口是收不到與調色板有關的消息的。因此,如果子窗口(如視圖)要使用自己的邏輯調色板,那麼頂層窗口或重疊窗口應該及時通知子窗口與調色板有關的消息。

11.1.5 具體實例

  現在讓我們來看一個使用調色板的演示程序。該程序名爲TestPal,如圖11.3所示,該程序顯示了兩組紅色方塊,每組方塊都是16×16共256個。左邊的這組方塊是用邏輯調色板畫的,紅色的強度從0到255遞增,作爲對比,在右邊用RGB引用畫出了256個遞增的紅色方塊。讀者可以對比這兩組方塊的顏色質量,以體會調色板索引引用和RGB引用的區別。該程序也着重向讀者演示了處理調色板消息的方法。

T11_3.tif (237628 bytes)

圖11.3 TestPal程序

 

  首先,請讀者用AppWizard建立一個名爲TestPal的MFC單文擋應用程序。然後,用ClassWizard爲CMainFrame類加入WM_QUERYNEWPALETTE和WM_PALETTECHANGED消息的處理函數,使用缺省的函數名。接着,在TestPal.h文件中類CTestPalApp的定義前加入下面一行:

#define WM_DOREALIZE WM_USER+200

當收到調色板消息時,主框架窗口會發送用戶定義的WM_DOREALIZE消息通知視圖。

最後,請讀者按清單11.1和11.2修改程序。

 

清單11.1 CMainFrame類的部分代碼

BOOL CMainFrame::OnQueryNewPalette()

{

// TODO: Add your message handler code here and/or call default

 

GetActiveView()->SendMessage(WM_DOREALIZE);

return TRUE; //返回TRUE表明實現了邏輯調色板

}

 

void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd)

{

CFrameWnd::OnPaletteChanged(pFocusWnd);

// TODO: Add your message handler code here

 

if(GetActiveView()!=pFocusWnd)

GetActiveView()->SendMessage(WM_DOREALIZE);

}

 

清單11.2 CTestPalView類的部分代碼

// TestPalView.h : interface of the CTestPalView class

class CTestPalView : public CView

{

. . .

protected:

 

CPalette m_Palette;

. . .

afx_msg LRESULT OnDoRealize(WPARAM wParam, LPARAM lParam);

DECLARE_MESSAGE_MAP()

};

 

// TestPalView.cpp : implementation of the CTestPalView class

 

BEGIN_MESSAGE_MAP(CTestPalView, CView)

 

. . .

ON_MESSAGE(WM_DOREALIZE, OnDoRealize)

END_MESSAGE_MAP()

 

CTestPalView::CTestPalView()

{

// TODO: add construction code here

 

LPLOGPALETTE pLogPal;

pLogPal=(LPLOGPALETTE)malloc(sizeof(LOGPALETTE)+

sizeof(PALETTEENTRY)*256);

pLogPal->palVersion=0x300;

pLogPal->palNumEntries=256;

for(int i=0;i<256;i++)

{

pLogPal->palPalEntry[i].peRed=i; //初始化爲紅色

pLogPal->palPalEntry[i].peGreen=0;

pLogPal->palPalEntry[i].peBlue=0;

pLogPal->palPalEntry[i].peFlags=0;

}

if(!m_Palette.CreatePalette(pLogPal))

AfxMessageBox("Can't create palette!");

}

 

void CTestPalView::OnDraw(CDC* pDC)

{

CTestPalDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

 

// TODO: add draw code for native data here

 

CBrush brush,*pOldBrush;

int x,y,i;

 

pDC->SelectPalette(&m_Palette,FALSE);

pDC->RealizePalette();

pDC->SelectStockObject(BLACK_PEN);

for(i=0;i<256;i++)

{

x=(i%16)*16;

y=(i/16)*16;

brush.CreateSolidBrush(PALETTEINDEX(i)); //調色板索引引用

pOldBrush=pDC->SelectObject(&brush);

pDC->Rectangle(x,y,x+16,y+16);

pDC->SelectObject(pOldBrush);

brush.DeleteObject();

}

for(i=0;i<256;i++)

{

x=(i%16)*16+300;

y=(i/16)*16;

brush.CreateSolidBrush(RGB(i,0,0)); //RGB引用

pOldBrush=pDC->SelectObject(&brush);

pDC->Rectangle(x,y,x+16,y+16);

pDC->SelectObject(pOldBrush);

brush.DeleteObject();

}

}

 

 

LRESULT CTestPalView::OnDoRealize(WPARAM wParam, LPARAM)

{

CClientDC dc(this);

dc.SelectPalette(&m_Palette,FALSE);

if(dc.RealizePalette()) //若調色板映射被改變則刷新視圖

GetDocument()->UpdateAllViews(NULL);

return 0L;

}

  在CTestPalView的構造函數中創建了一個含有256個遞增紅色的邏輯調色板。

  當變爲活動窗口以及窗口創建時,TestPal程序的主框架窗口都會收到WM_QUERYNEWPALETTE消息,該消息的處理函數OnQueryNewPalette負責發送WM_DOREALIZE消息通知視圖, 並返回TRUE以表明活動窗口實現了邏輯調色板。WM_DOREALIZE消息的處理函數CTestPalView::OnDoRealize爲視圖實現一個前景調色板,該函數中有一個判斷語句可提高程序運行的效率:如果CDC::RealizePalette返回值大於零,則說明調色板映射表發生了變化,此時必須刷新視圖,否則製圖中的顏色將失真。如果RealizePalette返回零則說明調色板映射沒有變化,這時就沒有必要刷新視圖。

  無論是TestPal還是別的應用程序在實現前景調色板並改變了系統調色板時,TestPal程序的主框架窗口都會收到WM_PALETTECHANGED消息。請注意該消息的處理函數CMainFrame::OnPaletteChanged有一個pFocusWnd參數,該參數表明是哪一個窗口改變了系統調色板。函數用pFocusWnd來判斷,如果是別的應用程序實現了前景調色板,則通知視圖調用OnDoRealize實現其邏輯調色板,注意雖然CDC::SelectPalette的bForceBackground參數是FALSE,但這時視圖的邏輯調色板是作爲背景調色板實現的。如果是TestPal自己的視圖實現了前景調色板,則沒有必要調用OnDoRealize。

  請讀者將Windows當前的顯示模式設置爲256色,然後編譯並運行TestPal,對比一下RGB引用與調色板索引引用的效果,讀者不難發現左邊用調色板索引引用輸出的顏色比右邊好的多。通過該程序我們可以看出,即使在系統調色板中已實現了豐富的紅色的情況下,RGB引用得到的紅色仍然是20種保留顏色的抖動色。

  讀者可以打開Windows的畫筆程序,並在該程序中打開一幅256色的位圖(如Windows目錄下的Forest.bmp)。在畫筆和TestPal程序之間來回切換,讀者可以看到,由於兩個應用程序都正確的處理了調色板消息,在前臺的應用程序總是具有最好的顏色顯示,而後臺程序的顏色雖然有些失真,但還比較令人滿意。

  需要指出的是,TestPal程序只使用了一個邏輯調色板,所以它處理調色板消息的方法比較簡單。如果程序要用到多個邏輯調色板,那麼就需要採取一些新措施來保證只有一個邏輯調色板作爲前景調色板使用。在11.4節讀者可以看到使用多個邏輯調色板時的處理方法。

11.2 位圖

  Windows用位圖(Bitmap)來顯示和保存圖像,從單色的到24位真彩色圖像都可以存儲到位圖中。

  位圖實際上是一個像素值陣列,像素陣列存儲在一個字節數組中,每一個像素的位數可以是1、4、8或24。單色位圖的字節數組中的每一位代表一個像素,16色位圖的字節數組中每一個字節存儲兩個像素,256色的位圖每一個字節存儲一個像素,而真彩色位圖中每個像素用三個字節來表示。在256色以下的位圖中存儲的像素值實際上是調色板索引,在真彩色位圖中存儲的則是像素的RGB顏色值。

  位圖分爲依賴於設備的位圖(DDB)和與設備無關的位圖(DIB),二者有不同的用途。 

11.3 依賴於設備的位圖(DDB)

DDB(Device-dependent bitmap)依賴於具體設備,這主要體現在以下兩個方面:

  • DDB的顏色模式必需與輸出設備相一致。例如,如果當前的顯示設備是256色模式,那麼DDB必然也是256色的,即一個像素用一個字節表示。

  • 在256色以下的位圖中存儲的像素值是系統調色板的索引,其顏色依賴於系統調色板。

 

由於DDB高度依賴輸出設備,所以DDB只能存在於內存中,它要麼在視頻內存中,要麼在系統內存中。

 

11.3.1 DDB的創建

MFC的CBitmap類封裝了DDB。該類提供了幾個函數用來創建DDB:

BOOL LoadBitmap( LPCTSTR lpszResourceName );
BOOL LoadBitmap( UINT nIDResource );
該函數從資源中載入一幅位圖,若載入成功則返回TRUE。資源位圖實際上是一個DIB,該函數在載入時把它轉換成了DDB。

BOOL CreateBitmap( int nWidth, int nHeight, UINT nPlanes, UINT nBitcount, const void* lpBits );
該函數用來創建一幅空白的DDB。參數nWidth和nHeight以像素爲單位說明了位圖的寬度和高度。nPlanes是DDB的色平面數,nBitcount是每個色平面的顏色位數。一般來說,nPlanes爲1,而nBitcount代表DDB中每個像素值所佔的位數,但在創建16色DDB時,nPlanes爲4,而nBitcount爲1。參數lpBits指向存儲像素陣列的數組,該數組應該逐行存儲位圖的每個像素值。注意,數組中每行像素的數目必需是偶數個字節,如果是奇數,則應該用0補足。若創建成功函數返回TRUE。

BOOL CreateCompatibleBitmap( CDC* pDC, int nWidth, int nHeight );
該函數創建一個與指定設備上下文兼容的DDB。參數pDC指向一個設備上下文,nWidth和nHeight是DDB的尺寸。若創建成功函數返回TRUE。

 

可以調用CBitmap的成員函數GetBitmap來查詢DDB的各種屬性(如尺寸):

int GetBitmap( BITMAP* pBitMap );
該函數用來獲得與DDB有關的信息,參數pBitMap指向一個BITMAP結構。BITMAP結構的定義爲:

typedef struct tagBITMAP {

LONG bmType; //必需爲0

LONG bmWidth; //位圖的寬度(以像素爲單位)

LONG bmHeight; //位圖的高度(以像素爲單位)

LONG bmWidthBytes; //每一掃描行所需的字節數,應是偶數

WORD bmPlanes; //色平面數

WORD bmBitsPixel; //色平面的顏色位數

LPVOID bmBits; //指向存儲像素陣列的數組

} BITMAP;

 

11.3.2 DDB的用途

  DDB的主要用途是保存位圖。要保存的位圖可以來自資源位圖,也可以是一個繪圖的結果。

  前面說過,在256色以下的顯示模式中,DDB中的像素值是系統調色板的索引。一般在系統調色板中除了保留的20種靜態顏色外,其它表項都有可能被應用程序改變。如果DDB中有一些像素值是指向20種靜態顏色以外的顏色,那麼該位圖的顏色將是不穩定的。因此,DDB不能用來長期存儲色彩豐富的位圖。如果位圖使用的大部分顏色都是20種保留色,則該位圖可以用CBitmap對象保存在內存中。例如,用CDC::LoadBitmap載入的資源位圖一般都是顏色較簡單的位圖,對於那些顏色比較豐富的位圖,只有使用下面將要介紹的DIB才能長期保存。

在窗口中顯示DDB的方法有些特別,其過程分以下幾步:

構建一個CDC對象,然後調用CDC::CreateCompatibleDC創建一個兼容的內存設備上下文。

調用CDC::SelectObject將DDB選入內存設備上下文中。

調用CDC::BitBlt或CDC::StretchBlt將DDB從內存設備上下文中輸出到窗口的設備上下文中。

調用CDC::SelectObject把原來的DDB選入到內存設備上下文中並使新DDB脫離出來。

 

下面這段代碼在視圖中顯示了一個DDB:

void CMyView::OnDraw( CDC* pDC)

{

. . .

CDC MemDC;

CBitmap *oldBmp;

BITMAP bmpInfo;

int bmWidth,bmHeight;

MemDC.CreateCompatibleDC(pDC);

oldBmp=MemDC.SelectObject(&m_Bitmap); //m_Bitmap是一個CBitmap對象

m_Bitmap.GetBitmap(&bmpInfo); //獲取位圖的尺寸

bmWidth=bmpInfo.bmWidth;

bmHeight=bmpInfo.bmHeight;

pDC->BitBlt(0,0,bmWidth,bmHeight,&MemDC,0,0,SRCCOPY);

MemDC.SelectObject(oldBmp); //使位圖m_Bitmap脫離設備上下文

. . .

}

函數CDC::BitBlt的聲明爲:

BOOL BitBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop );

  該函數把源設備上下文中的位圖複製到本身的設備上下文中,兩個設備上下文可以是內存設備上下文,也可以是同一個設備上下文。參數x和y是目的矩形的邏輯座標,參數nWidth和nHeight說明了目的矩形及源位圖的寬和高。pSrcDC指向源設備上下文,xSrc和ySrc說明了源矩形相對於源位圖左上角的偏移。參數dwRop指定了光柵操作(ROP)代碼,一些常用的ROP代碼如表11.2所示。

 

表11.2 常用的ROP代碼

ROP碼

含義

BLACKNESS

輸出黑色

DSTINVERT

反轉目的位圖

MERGECOPY

用與操作把圖案(Pattern)與源位圖融合起來

MERGEPAINT

用或操作把反轉的源位圖與目的位圖融合起來

NOTSRCCOPY

把源位圖反轉然後拷貝到目的地

NOTSRCERASE

用或操作融合源和目的位圖,然後再反轉

PATCOPY

把圖案拷貝到目的位圖中

PATINVERT

用異或操作把圖案與目的位圖相融合

PATPAINT

用或操作融合圖案和反轉的源位圖,然後用或操作把結果與目的位圖融合

SRCAND

用與操作融合源位圖和目的位圖

SRCCOPY

把源位圖拷貝到目的位圖

SRCERASE

先反轉目的位圖,再用與操作將其與源位圖融合

SRCINVERT

用異或操作融合源位圖和目的位圖

SRCPAINT

用或操作融合源位圖和目的位圖

WHITENESS

輸出白色

 

 

函數CDC::StretchBlt的聲明爲:

BOOL StretchBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop );

 

  該函數把位圖從源矩形拷貝到目的矩形中,如果源和目的矩形尺寸不同,那麼將縮放位圖的功能以適應目的矩形的大小。函數的大部分參數與BitBlt的相同,但多了兩個參數nSrcWidth和nSrcHeight用來指定源矩形的寬和高。

  DDB的一個重要用途是用作設備上下文的顯示錶面。每一個設備上下文都包含有一個DDB,該位圖實際上是在顯示設備的緩衝區中(如視頻內存),我們可以把它看做設備上下文的顯示錶面,設備上下文用GDI函數繪圖實際上就是修改它所包含的DDB(顯示錶面)的過程。

  普通的設備上下文都是在屏幕上繪圖的,而使用內存設備上下文則可以在系統內存中繪製圖形。內存設備上下文是一種特殊的設備上下文,它將系統內存用作顯示錶面。程序可以使用內存設備上下文預先在系統內存中繪製複雜的圖形,然後再快速地將其複製到實際的設備上下文的顯示錶面上,而繪製圖形的結果仍保存在內存設備上下文的DDB中。

提示:有人可能會想到用BitBlt函數把繪圖結果從顯示設備拷貝到內存設備上下文中,這種方法可以工作,但有時會出錯。當源矩形被別的窗口遮住時,BitBlt會把別的窗口中的像素拷貝下來。

  內存設備上下文缺省的DDB是一個1×1的單色位圖,如此小的顯示錶面顯然是沒有用的,因此程序一般要爲內存設備對象選擇一個合適大小的彩色DDB。

下面這段代碼創建了一個內存設備上下文,並在其包含的DDB中畫了一個灰色實心矩形,然後再把DDB輸出到屏幕上。

void CMyView::OnDraw(CDC* pDC)

{

. . .

CDC MemDC;

CBitmap bm,*oldBmp;

MemDC.CreateCompatibleDC(pDC); //創建一個兼容的內存設備上下文

bm.CreateCompatibleBitmap(pDC,100,50); //創建一個兼容的DDB

oldBmp=MemDC.SelectObject(&bm);

MemDC.SelectStockObject(BLACK_PEN);

MemDC.SelectStockObject(GRAY_BRUSH);

MemDC.Rectangle(0,0,50,50); //在DDB中畫一個矩形

pDC->BitBlt(0,0,100,50,&MemDC,0,0,SRCCOPY);

MemDC.SelectObject(oldBmp); //使位圖bm對象脫離設備上下文

. . .

}

  在上面的代碼中,繪圖的結果保存在位圖bm中,一旦調用MemDC.SelectObject(oldBmp)使位圖bm脫離設備上下文,該位圖就可以被其它對象使用。

11.4 與設備無關的位圖(DIB)

 

DIB(Device-indepentent bitmap)的與設備無關性主要體現在以下兩個方面:

  • DIB的顏色模式與設備無關。例如,一個256色的DIB即可以在真彩色顯示模式下使用,也可以在16色模式下使用。

  • 256色以下(包括256色)的DIB擁有自己的顏色表,像素的顏色獨立於系統調色板。

 

  由於DIB不依賴於具體設備,因此可以用來永久性地保存圖象。DIB一般是以*.BMP文件的形式保存在磁盤中的,有時也會保存在*.DIB文件中。運行在不同輸出設備下的應用程序可以通過DIB來交換圖象。

DIB還可以用一種RLE算法來壓縮圖像數據,但一般來說DIB是不壓縮的。

11.4.1 DIB的結構

  與Borland C++下的框架類庫OWL不同,MFC未提供現成的類來封裝DIB。儘管Microsoft列出了一些理由,但沒有DIB類確實給MFC用戶帶來很多不便。用戶要想使用DIB,首先應該瞭解DIB的結構。

  在內存中,一個完整的DIB由兩部分組成:一個BITMAPINFO結構和一個存儲像素陣列的數組。BITMAPINFO描述了位圖的大小,顏色模式和調色板等各種屬性,其定義爲

typedef struct tagBITMAPINFO {

BITMAPINFOHEADER bmiHeader;

RGBQUAD bmiColors[1]; //顏色表

} BITMAPINFO;

RGBQUAD結構用來描述顏色,其定義爲

typedef struct tagRGBQUAD {

BYTE rgbBlue; //藍色的強度

BYTE rgbGreen; //綠色的強度

BYTE rgbRed; //紅色的強度

BYTE rgbReserved; //保留字節,爲0

} RGBQUAD;

注意,RGBQUAD結構中的顏色順序是BGR,而不是平常的RGB。

BITMAPINFOHEADER結構包含了DIB的各種信息,其定義爲

typedef struct tagBITMAPINFOHEADER{

DWORD biSize; //該結構的大小

LONG biWidth; //位圖的寬度(以像素爲單位)

LONG biHeight; //位圖的高度(以像素爲單位)

WORD biPlanes; //必須爲1

WORD biBitCount //每個像素的位數(1、4、8、16、24或32)

DWORD biCompression; //壓縮方式,一般爲0或BI_RGB (未壓縮)

DWORD biSizeImage; //以字節爲單位的圖象大小(僅用於壓縮位圖)

LONG biXPelsPerMeter; //以目標設備每米的像素數來說明位圖的水平分辨率

LONG biYPelsPerMeter; //以目標設備每米的像素數來說明位圖的垂直分辨率

DWORD biClrUsed; /*顏色表的顏色數,若爲0則位圖使用由biBitCount指定的最大顏色數*/

DWORD biClrImportant; //重要顏色的數目,若該值爲0則所有顏色都重要

} BITMAPINFOHEADER;

  與DDB不同,DIB的字節數組是從圖象的最下面一行開始的逐行向上存儲的,也即等於把圖象倒過來然後在逐行掃描。另外,字節數組中每個掃描行的字節數必需是4的倍數,如果不足要用0補齊。

DIB可以存儲在*.BMP或*.DIB文件中。DIB文件是以BITMAPFILEHEADER結構開頭的,該結構的定義爲

typedef struct tagBITMAPFILEHEADER {

WORD bfType; //文件類型,必須爲“BM”

DWORD bfSize; //文件的大小

WORD bfReserved1; //爲0

WORD bfReserved2; //爲0

DWORD bfOffBits; //存儲的像素陣列相對於文件頭的偏移量

} BITMAPFILEHEADER;

  緊隨該結構的是一個BITMAPINFOHEADER結構,然後是RGBQUAD結構組成的顏色表(如果有的話),文件最後存儲的是DIB的像素陣列。

  DIB的顏色信息儲存在自己的顏色表中,程序一般要根據顏色表爲DIB創建邏輯調色板。在輸出一幅DIB之前,程序應該將其邏輯調色板選入到相關的設備上下文中並實現到系統調色板中,然後再調用相關的GDI函數(如::SetDIBitsToDevice或::StretchDIBits)輸出DIB。在輸出過程中,GDI函數會把DIB轉換成DDB,這項工作主要包括以下兩步:

將DIB的顏色格式轉換成與輸出設備相同的顏色格式。例如,在真彩色的顯示模式下要顯示一個256色的DIB,則應該將其轉換成24位的顏色格式。

將DIB像素的邏輯顏色索引轉換成系統調色板索引。

 

11.4.2 編寫DIB類

  由於MFC未提供DIB類,用戶在使用DIB時將面臨繁重的Windows API編程任務。幸運的是,Visual C++提供了一個較高層次的API,簡化了DIB的使用。這些API函數實際上是由MFC的DibLook例程提供的,它們位於DibLook目錄下的dibapi.cpp、myfile.cpp和dibapi.h文件中,主要包括:

ReadDIBFile //把DIB文件讀入內存

SaveDIB //把DIB保存到文件中

CreateDIBPalette //從DIB中創建一個邏輯調色板

PaintDIB //顯示DIB

DIBWidth //返回DIB的寬度

DIBHeight //返回DIB的高度

 

如果讀者對這些函數的內部細節感興趣,那麼可以研究一下dibapi.cpp和myfile.cpp文件,但要做好喫苦的準備。

  即使利用上述API,編寫使用DIB的程序仍然不是很輕鬆。爲了滿足讀者的要求,筆者編寫了一個名爲CDib的較簡單的DIB類,該類是基於上述API的,它的主要成員函數包括:

BOOL Load(LPCTSTR lpszFileName);
該函數從文件中載入DIB,參數lpszFileName說明了文件名。若成功載入則函數返回TRUE,否則返回FALSE。

BOOL LoadFromResource(UINT nID);
該函數從資源中載入位圖,參數nID是資源位圖的ID。若成功載入則函數返回TRUE,否則返回FALSE。

CPalette* GetPalette()
返回DIB的邏輯調色板。

BOOL Draw(CDC *pDC, int x, int y, int cx=0, int cy=0);
該函數在指定的矩形區域內顯示DIB,它具有縮放位圖的功能。參數pDC指向用於繪圖的設備上下文,參數x和y說明了目的矩形的左上角座標,cx和cy說明了目的矩形的尺寸,cx和cy若有一個爲0則該函數按DIB的實際大小繪製位圖,cx和cy的缺省值是0。若成功則函數返回TRUE,否則返回FALSE。

int Width(); //以像素爲單位返回DIB的寬度

int Height(); //以像素爲單位返回DIB的高度

  CDib類的源代碼在清單11.3和11.4列出,CDib類的定義位於CDib.h中,CDib類的成員函數代碼位於CDib.cpp中。對於CDib類的代碼這裏就不作具體解釋了,讀者只要會用就行。

清單11.3 CDib.h

#if !defined MYDIB

#define MYDIB

 

#include "dibapi.h"

 

class CDib

{

public:

CDib();

~CDib();

protected:

HDIB m_hDIB;

CPalette* m_palDIB;

public:

BOOL Load(LPCTSTR lpszFileName);

BOOL LoadFromResource(UINT nID);

CPalette* GetPalette() const

{ return m_palDIB; }

BOOL Draw(CDC *pDC, int x, int y, int cx=0, int cy=0);

int Width();

int Height();

void DeleteDIB();

};

 

#endif

 

 

清單11.4 Cdib.cpp

#include <stdafx.h>

#include "CDib.h"

 

#ifdef _DEBUG

#undef THIS_FILE

static char BASED_CODE THIS_FILE[] = __FILE__;

#endif

 

CDib::CDib()

{

m_palDIB=NULL;

m_hDIB=NULL;

}

 

CDib::~CDib()

{

DeleteDIB();

}

 

void CDib::DeleteDIB()

{

if (m_hDIB != NULL)

::GlobalFree((HGLOBAL) m_hDIB);

if (m_palDIB != NULL)

delete m_palDIB;

}

 

//從文件中載入DIB

BOOL CDib::Load(LPCTSTR lpszFileName)

{

HDIB hDIB;

CFile file;

CFileException fe;

if (!file.Open(lpszFileName, CFile::modeRead|CFile::shareDenyWrite, &fe))

{

AfxMessageBox(fe.m_cause);

return FALSE;

}

TRY

{

hDIB = ::ReadDIBFile(file);

}

CATCH (CFileException, eLoad)

{

file.Abort();

return FALSE;

}

END_CATCH

 

DeleteDIB(); //清除舊位圖

m_hDIB=hDIB;

 

m_palDIB = new CPalette;

if (::CreateDIBPalette(m_hDIB, m_palDIB) == NULL)

{

// DIB有可能沒有調色板

delete m_palDIB;

m_palDIB = NULL;

}

return TRUE;

}

 

//從資源中載入DIB

BOOL CDib::LoadFromResource(UINT nID)

{

HINSTANCE hResInst = AfxGetResourceHandle();

HRSRC hFindRes;

HDIB hDIB;

LPSTR pDIB;

LPSTR pRes;

HGLOBAL hRes;

//搜尋指定的資源

hFindRes = ::FindResource(hResInst, MAKEINTRESOURCE(nID), RT_BITMAP);

if (hFindRes == NULL) return FALSE;

hRes = ::LoadResource(hResInst, hFindRes); //載入位圖資源

if (hRes == NULL) return FALSE;

DWORD dwSize=::SizeofResource(hResInst,hFindRes);

 

hDIB = (HDIB) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dwSize);

if (hDIB == NULL) return FALSE;

pDIB = (LPSTR)::GlobalLock((HGLOBAL)hDIB);

pRes = (LPSTR) ::LockResource(hRes);

memcpy(pDIB, pRes, dwSize); //把hRes中的內容複製hDIB中

::GlobalUnlock((HGLOBAL) hDIB);

 

DeleteDIB();

m_hDIB=hDIB;

m_palDIB = new CPalette;

if (::CreateDIBPalette(m_hDIB, m_palDIB) == NULL)

{

// DIB有可能沒有調色板

delete m_palDIB;

m_palDIB = NULL;

}

return TRUE;

}

 

int CDib::Width()

{

if(m_hDIB==NULL) return 0;

LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) m_hDIB);

int cxDIB = (int) ::DIBWidth(lpDIB); // Size of DIB - x

::GlobalUnlock((HGLOBAL) m_hDIB);

return cxDIB;

}

 

int CDib::Height()

{

if(m_hDIB==NULL) return 0;

LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) m_hDIB);

int cyDIB = (int) ::DIBHeight(lpDIB); // Size of DIB - y

::GlobalUnlock((HGLOBAL) m_hDIB);

return cyDIB;

}

 

//顯示DIB,該函數具有縮放功能

//參數x和y說明了目的矩形的左上角座標,cx和cy說明了目的矩形的尺寸

//cx和cy若有一個爲0則該函數按DIB的實際大小繪製,cx和cy的缺省值是0

BOOL CDib::Draw(CDC *pDC, int x, int y, int cx, int cy)

{

if(m_hDIB==NULL) return FALSE;

CRect rDIB,rDest;

rDest.left=x;

rDest.top=x;

if(cx==0||cy==0)

{

cx=Width();

cy=Height();

}

rDest.right=rDest.left+cx;

rDest.bottom=rDest.top+cy;

rDIB.left=rDIB.top=0;

rDIB.right=Width();

rDIB.bottom=Height();

return ::PaintDIB(pDC->GetSafeHdc(),&rDest,m_hDIB,&rDIB,m_palDIB);

}

 

11.4.3 使用CDib類的例子

  現在讓我們來看一個使用CDib類的例子。如圖11.4所示,程序名爲ShowDib,是一個多文檔應用程序,它的功能與VC的DibLook例程有些類似,可同時打開和顯示多個位圖。

T11_4.jpg (24220 bytes)

圖11.4 用ShowDib來顯示位圖

 

  請讀者用AppWizard建立一個名爲ShowDib的MFC工程。程序應該用滾動視圖來顯示較大的位圖,所以在MFC AppWizard的第6步應把CShowDibView的基類改爲CScrollView。

  由於ShowDib程序要用到CDib類,所以應該把dibapi.cpp、myfile.cpp、dibapi.h、CDib.cpp和CDib.h文件拷貝到ShowDib目錄下,並選擇Project->Add to Project->Files命令把這些文件加到ShowDib工程中。

  在ShowDib.h文件中CShowDibApp類的定義之前加入下面一行:

#define WM_DOREALIZE WM_USER+200

  當收到調色板消息時,主框架窗口會發送用戶定義的WM_DOREALIZE消息通知視圖。

  接下來,需要用ClassWizard爲CMainFrame加入WM_QUERYNEWPALETTE和WM_PALETTECHANGED消息的處理函數,爲CShowDibDoc類加入OnOpenDocument函數。

  最後,請讀者按清單11.5、11.6和11.7修改程序。

清單11.5 CMainFrame類的部分代碼

// MainFrm.cpp : implementation of the CMainFrame class

void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd)

{

CMDIFrameWnd::OnPaletteChanged(pFocusWnd);

// TODO: Add your message handler code here

 

SendMessageToDescendants(WM_DOREALIZE, 1); //通知所有的子窗口

}

 

BOOL CMainFrame::OnQueryNewPalette()

{

// TODO: Add your message handler code here and/or call default

 

CMDIChildWnd* pMDIChildWnd = MDIGetActive();

if (pMDIChildWnd == NULL)

return FALSE; // 沒有活動的MDI子框架窗口

CView* pView = pMDIChildWnd->GetActiveView();

pView->SendMessage(WM_DOREALIZE,0); //只通知活動視圖

return TRUE; //返回TRUE表明實現了邏輯調色板

}

 

 

清單11.6 CShowDibDoc類的部分代碼

// ShowDibDoc.h : interface of the CShowDibDoc class

 

#include "CDib.h"

 

class CShowDibDoc : public CDocument

{

. . .

// Attributes

public:

 

CDib m_Dib;

. . .

};

 

// ShowDibDoc.cpp : implementation of the CShowDibDoc class

 

BOOL CShowDibDoc::OnOpenDocument(LPCTSTR lpszPathName)

{

if (!CDocument::OnOpenDocument(lpszPathName))

return FALSE;

// TODO: Add your specialized creation code here

 

BeginWaitCursor();

BOOL bSuccess=m_Dib.Load(lpszPathName); //載入DIB

EndWaitCursor();

return bSuccess;

}

 

 

 

清單11.7 CShowDibView類的部分代碼

// ShowDibView.h : interface of the CShowDibView class

class CShowDibView : public CScrollView

{

. . .

afx_msg LRESULT OnDoRealize(WPARAM wParam, LPARAM lParam);

DECLARE_MESSAGE_MAP()

};

 

// ShowDibView.cpp : implementation of the CShowDibView class

 

BEGIN_MESSAGE_MAP(CShowDibView, CScrollView)

. . .

ON_MESSAGE(WM_DOREALIZE, OnDoRealize)

END_MESSAGE_MAP()

 

void CShowDibView::OnInitialUpdate()

{

CScrollView::OnInitialUpdate();

CSize sizeTotal;

// TODO: calculate the total size of this view

 

CShowDibDoc* pDoc = GetDocument();

sizeTotal.cx = pDoc->m_Dib.Width();

sizeTotal.cy = pDoc->m_Dib.Height();

SetScrollSizes(MM_TEXT, sizeTotal); //設置視圖的滾動範圍

}

 

void CShowDibView::OnActivateView(BOOL bActivate, CView* pActivateView, CView* pDeactiveView)

{

// TODO: Add your specialized code here and/or call the base class

 

if(bActivate)

OnDoRealize(0,0); //刷新視圖

CScrollView::OnActivateView(bActivate, pActivateView, pDeactiveView);

}

 

 

LRESULT CShowDibView::OnDoRealize(WPARAM wParam, LPARAM)

{

CClientDC dc(this);

//wParam參數決定了該視圖是否實現前景調色板

dc.SelectPalette(GetDocument()->m_Dib.GetPalette(),wParam);

if(dc.RealizePalette())

GetDocument()->UpdateAllViews(NULL);

return 0L;

}

 

void CShowDibView::OnDraw(CDC* pDC)

{

CShowDibDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

// TODO: add draw code for native data here

 

pDoc->m_Dib.Draw(pDC,0,0); //輸出DIB

}

  在程序中使用CDib對象的代碼很簡單。當用戶在ShowDib程序中選擇File->Open命令並從打開文件對話框中選擇了一個BMP文件後,CShowDibDoc::OnOpenDocument函數被調用,該函數調用CDib::Load載入位圖。在CShowDibView::OnDraw中,調用CDib::Draw輸出位圖。在CShowDibView::OnInitialUpdate中,根據DIB的尺寸來確定視圖的滾動範圍。

  需要重點研究的是ShowDib如何處理調色板問題的。ShowDib是一個多文檔應用程序,可以同時顯示多幅位圖。由於每個位圖一般都有不同的調色板,這樣就產生了共享系統調色板的問題。程序必須採取措施來保證只有一個視圖的邏輯調色板作爲前景調色板使用。

  當主框架窗口收到WM_QUERYNEWPALETTE消息時,主框架窗口向具有輸入焦點的視圖發送wParam參數爲0的WM_DOREALIZE消息,該視圖的消息處理函數CShowDibView::OnDoRealize爲視圖實現前景調色板並在必要時重繪視圖,這樣活動視圖中的位圖就具有最佳顏色顯示。

  如果活動視圖在實現其前景調色板時改變了系統調色板,或是別的應用程序的前景調色板改變了系統調色板,那麼Windows會向所有頂層窗口和重疊窗口發送WM_PALETTECHANGED消息,DibLook的主框架窗口也會收到該消息。主框架窗口對該消息的處理是向所有的視圖發送wParam參數爲1的WM_DOREALIZE消息,通知它們實現各自的背景調色板並在必要時重繪,這樣所有的位圖都能顯示令人滿意的顏色。

  當某一視圖被激活時,需要調用OnDoRealize來實現其前景調色板,這一任務由CShowDibView:: OnActivateView函數來完成。

11.5 動畫控件

  Windows 95支持一種動畫控件(Animate control),動畫控件可以播放AVI格式的動畫片(AVI Clip),動畫片可以來自一個AVI文件,也可以來自資源中。合理地使用動畫控件,可以使程序的界面更加形象生動。

11.5.1 動畫控件的使用

MFC的CAnimateCtrl類封裝了動畫控件,該類的Create成員函數負責創建動畫控件,其聲明爲:

BOOL Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );

  參數dwStyle是如表11.3所示的控件風格的組合,參數rect指定了控件的尺寸,pParentWnd指向父窗口,nID是控件的ID。若創建成功則函數返回TRUE。

表11.3 動畫控件的風格

風格

含義

ACS_CENTER

使動畫片居於控件中央,並使動畫片打開後控件窗口的尺寸和位置保持不變。如果不指定該風格,則控件的尺寸會自動調整來適應動畫片的大小。

ACS_TRANSPARENT

使動畫片的背景透明(不輸出動畫片的背景色)。

ACS_AUTOPLAY

一旦打開動畫片後就一直重複播放。

  除表中的風格外,一般還要爲動畫控件指定WS_CHILD、WS_VISIBLE和WS_BORDER窗口風格。例如,要創建一個能自動播放的動畫控件,應該指定其風格爲WS_CHILD|WS_VISIBLE|WS_BORDER|ACS_AUTOPLAY。

  用戶可以向對話框模板中加入動畫控件,在模板編輯器的控件面板上,動畫控件是用一個電影膠片的圖形來表示的。在動畫控件的屬性對話框中可以指定上表列出的風格。只要不指定ACS_CENTER風格,用戶就不必關心動畫控件的尺寸,因爲在打開動畫片時控件的尺寸會被自動調整成動畫片的幅面大小。

CAnimateCtrl類主要的成員函數包括:

BOOL Open( LPCTSTR lpszFileName );
BOOL Open( UINT nID );
Open函數從AVI文件或資源中打開動畫片,如果參數lpszFileName或nID爲NULL,則系統將關閉以前打開的動畫片。若成功則函數返回TRUE。

BOOL Play( UINT nFrom, UINT nTo, UINT nRep );
該函數用來播放動畫片。參數nFrom指定了播放的開始幀的索引,索引值必須小於65536,若爲0則從頭開始播放。nTo指定了結束幀的索引,它的值必須小於65536,若爲-1則表示播放到動畫片的末尾。nRep是播放的重複次數,若爲-1則無限重複播放。若成功則函數返回TRUE。

BOOL Seek( UINT nTo );
該函數用來靜態地顯示動畫片的某一幀。參數nTo是幀的索引,其值必須小於65536,若爲0則顯示第一幀,若爲-1則顯示最後一幀。若成功則函數返回TRUE。

BOOL Stop( );
停止動畫片的播放。若成功則函數返回TRUE。

BOOL Close( );
關閉並從內存中清除動畫片。若成功則函數返回TRUE。

  一般來說,應該把動畫片放在資源裏,而不是單獨的AVI文件中。這樣做可以使應用程序更容易管理,否則,如果應用程序要附帶一大堆BMP或AVI文件,會給人一種凌亂和不專業的感覺。Visual C++不直接支持AVI資源,但用戶可以創建一種新的資源類型來包含AVI。在VC的一個名爲cmnctrls的MFC例子中提供了幾個AVI文件(如dillo.avi),如果用戶要把象dillo.avi這樣的AVI文件包含到程序的資源中,則應按以下幾步去做:

在程序的資源視圖中單擊鼠標右鍵,並在彈出菜單中選擇Import...命令。

在文件選擇對話框中選擇dillo.avi文件,按Import按鈕退出。

按Import按鈕退出後,會出現一個Custom Resource Type對話框,如圖11.5所示。如果是第一次向資源中加入AVI文件,那麼應該在Resource type編輯框中爲動畫片類資源起一個名字(如AVI),若以前已創建過AVI型資源,則可以在直接在列表框中選擇AVI型。按OK後,dillo.avi就被加入到資源中。

按Alt+Enter鍵後,可以在屬性對話框中修改資源的ID。

T11_5.tif (104882 bytes)

圖11.5 Custom Resource Type對話框

 

  創建動畫控件的方法與創建普通控件相比並沒有什麼不同,用戶可以用ClassWizard把動畫控件和CAnimateCtrl對象聯繫起來。動畫控件的使用很簡單,下面的這段代碼打開並不斷重複播放一個資源動畫,它們通常是位於OnInitDialog函數中:

m_AnimateCtrl.Open(IDR_AVI1);

m_AnimateCtrl.Play(0,-1,-1);

  如果爲動畫控件指定了ACS_AUTOPLAY風格,則在調用Open後就會自動重複播放,不必調用Play。程序一般不需要調用Close來關閉動畫片,因爲這個任務在控件被刪除時會自動完成。但如果在控件已包含一個動畫片的情況下,需要打開一個新的動畫片,則程序應先調用Close刪除原來的動畫片。

11.5.2 動畫控件的侷限

動畫控件並不能播放所有的AVI文件,只有滿足下列條件的AVI文件才能被播放:

  • AVI文件必須是無聲的,不能有聲道。

  • AVI文件必須是未壓縮的,或是用RLE算法壓縮的。

  • AVI的調色板必須保持不變。

  動畫控件最大的侷限性在於它只能顯示系統調色板中缺省的顏色,因此如果用動畫控件來播放一個256色的AVI文件,那麼播放效果看起來就象一個16色的動畫一樣,很不理想。

  總之,動畫控件只能播放一些簡單的,顏色數較少的AVI動畫。如果要較滿意地播放256色的AVI文件,就要利用下面介紹的MCI接口。

11.6 Win 32的多媒體服務

  Windows 95/NT提供了豐富的多媒體服務功能,包括大量從低級到高級的多媒體API函數。利用這些功能強大的API,用戶可以在不同層次上編寫多媒體應用程序。有關多媒體服務的內容完全可以寫一本書,本節只是向讀者簡要地介紹一些最常用的多媒體服務。

  在用Visual C++開發多媒體應用時,用戶必須在所有要用到多媒體函數的源程序中包含MMSYSTEM.H頭文件,並且該文件位置應在WINDOWS.H頭文件的後面。另外,在連接程序時要用到WINMM.LIB引入庫,所以用戶應該在Project Settings對話框的Link頁的Object/library modules欄中加入WINMM.LIB,或者在源程序中加入下面一行:

#pragma comment(lib, "winmm.lib")

11.6.1 高級音頻函數

  Windows提供了三個特殊的播放聲音的高級音頻函數:MessageBeep、PlaySound和sndPlaySound。這三個函數可以滿足播放波形聲音的一般需要,但它們播放的WAVE文件(波形聲音文件)的大小不能超過100KB,如果要播放較大的WAVE文件,則應該使用MCI服務。

  MessageBeep讀者已經用過了,該函數主要用來播放系統報警聲音。系統報警聲音是由用戶在控制面板中的聲音(Sounds)程序中定義的,或者在WIN.INI的[sounds]段中指定。該函數的聲明爲:

BOOL MessageBeep(UINT uType);

參數uType說明了告警級,如表11.4所示。若成功則函數返回TRUE。

 

表11.4 系統告警級

級別

描述

-1

從機器的揚聲器中發出蜂鳴聲。

MB_ICONASTERISK

播放由SystemAsterisk定義的聲音。

MB_ICONEXCLAMATION

播放由SystemExclamation定義的聲音。

MB_ICONHAND

播放由SystemHand定義的聲音。

MB_ICONQUESTION

播放由SystemQuestion定義的聲音。

MB_OK

播放由SystemDefault定義的聲音

  在開始播放後,MessageBeep函數立即返回。如果該函數不能播放指定的報警聲音,它就播放SystemDefault定義的系統缺省聲音,如果連繫統缺省聲音也播放不了,那麼它就會在計算機的揚聲器上發出嘟嘟聲。在缺省時上表的MB_系列聲音均未定義。

  MessageBeep只能用來播放少數定義的聲音,如果程序需要播放數字音頻文件(*.WAV文件)或音頻資源,就需要使用PlaySound或sndPlaySound函數。

  PlaySound函數的聲明爲:

BOOL PlaySound(LPCSTR pszSound, HMODULE hmod,DWORD fdwSound);

 

  參數pszSound是指定了要播放聲音的字符串,該參數可以是WAVE文件的名字,或是WAV資源的名字,或是內存中聲音數據的指針,或是在系統註冊表WIN.INI中定義的系統事件聲音。如果該參數爲NULL則停止正在播放的聲音。參數hmod是應用程序的實例句柄,當播放WAV資源時要用到該參數,否則它必須爲NULL。參數fdwSound是標誌的組合,如表11.5所示。若成功則函數返回TRUE,否則返回FALSE。

 

表11.5 播放標誌

標誌

含義

SND_APPLICATION

用應用程序指定的關聯來播放聲音。

SND_ALIAS

pszSound參數指定了註冊表或WIN.INI中的系統事件的別名。

SND_ALIAS_ID

pszSound參數指定了預定義的聲音標識符。

SND_ASYNC

用異步方式播放聲音,PlaySound函數在開始播放後立即返回。

SND_FILENAME

pszSound參數指定了WAVE文件名。

SND_LOOP

重複播放聲音,必須與SND_ASYNC標誌一塊使用。

SND_MEMORY

播放載入到內存中的聲音,此時pszSound是指向聲音數據的指針。

SND_NODEFAULT

不播放缺省聲音,若無此標誌,則PlaySound在沒找到聲音時會播放缺省聲音。

SND_NOSTOP

PlaySound不打斷原來的聲音播出並立即返回FALSE。

SND_NOWAIT

如果驅動程序正忙則函數就不播放聲音並立即返回。

SND_PURGE

停止所有與調用任務有關的聲音。若參數pszSound爲NULL,就停止所有的聲音,否則,停止pszSound指定的聲音。

SND_RESOURCE

pszSound參數是WAVE資源的標識符,這時要用到hmod參數。

SND_SYNC

同步播放聲音,在播放完後PlaySound函數才返回。

 

 

  在C:/WINDOWS/MEDIA目錄下有一個名爲The Microsoft Sound.wav的聲音文件,在Windows 95啓動時會播放這個聲音。下面我們用三種方法來調用PlaySound函數播出Windows 95的啓動聲音。

  第一種方法是直接播出聲音文件,相應的代碼爲:

PlaySound("c://win95//media//The Microsoft Sound.wav", NULL, SND_FILENAME | SND_ASYNC);

  注意參數中的路徑使用兩個連續的反斜槓轉義代表一個反斜槓。

  第二種方法是把聲音文件加入到資源中,然後從資源中播放聲音。Visual C++支持WAVE型資源,用戶在資源視圖中單擊鼠標右鍵並選擇Import命令,然後在文件選擇對話框中選擇The Microsoft Sound.wav文件,則該文件就會被加入到WAVE資源中。假定聲音資源的ID爲IDR_STARTWIN,則下面的調用同樣會輸出啓動聲音:

PlaySound((LPCTSTR)IDR_STARTWIN, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);

  第三種方法是用PlaySound播放系統聲音,Windows啓動的聲音是由SystemStart定義的系統聲音,因此可以用下面的方法播放啓動聲音:

PlaySound("SystemStart",NULL,SND_ALIAS|SND_ASYNC);

函數sndPlaySound的功能與PlaySound類似,但少了一個參數。函數的聲明爲:

BOOL sndPlaySound(LPCSTR lpszSound, UINT fuSound);

 

  除了不能指定資源名字外,參數lpszSound與PlaySound的是一樣的。參數fuSound是如何播放聲音的標誌,可以是SND_ASYNC、SND_LOOP、SND_MEMORY、SND_NODEFAULT、SND_NOSTOP和SND_SYNC的組合,這些標誌的含義與PlaySound的一樣。

可以看出,sndPlaySound不能直接播放聲音資源。要用該函數播放WAVE文件,可按下面的方式調用:

sndPlaySound(“MYSOUND.WAV”,SND_ASYNC);

11.6.2 MCI

  MCI(Media Control Interface,媒體控制接口)向Windows程序提供了在高層次上控制媒體設備接口的能力。程序不必關心具體設備,就可以對激光唱機(CD)、視盤機、波形音頻設備、視頻播放設備和MIDI設備等媒體設備進行控制。對於程序員來說,可以把MCI理解爲設備面板上的一排按鍵,通過選擇不同的按鍵(發送不同的MCI命令)可以讓設備完成各種功能,而不必關心設備內部實現。比如,對於play,視盤機和CD機有不同的反應(一個是播放視頻,一個播放音頻),而對用戶來說卻只需要按同一按鈕。

  應用程序通過向MCI發送命令來控制媒體設備。MCI命令接口分命令字符串和命令消息兩種,兩者具有相同的功能。命令字符串具有使用簡單的特點,但是它的執行效率不如命令消息。

  所有的MCI命令字符串都是通過多媒體API函數mciSendString傳遞給MCI的,該函數的聲明爲:

MCIERROR mciSendString(

LPCTSTR lpszCommand, //MCI命令字符串

LPTSTR lpszReturnString, //存放反饋信息的緩衝區

UINT cchReturn, //緩衝區的長度

HANDLE hwndCallback //回調窗口的句柄,一般爲NULL

); //若成功則返回0,否則返回錯誤碼。

 

  該函數返回的錯誤碼可以用mciGetErrorString函數進行分析,該函數的聲明爲:

BOOL mciGetErrorString(

DWORD fdwError, //函數mciSendString或mciSendCommand返回的錯誤碼

LPTSTR lpszErrorText, //接收描述錯誤的字符串的緩衝區

UINT cchErrorText //緩衝區的長度

);

  下面是使用mciSendString函數的一個簡單例子:

char buf[50];

MCIERROR mciError;

mciError=mciSendString(“open cdaudio”,buf,strlen(buf),NULL);

if(mciError)

{

mciGetErrorString(mciError,buf,strlen(buf));

AfxMessageBox(buf);

return;

}

  open cdaudio命令打開CD播放器,如果出錯(如驅動器內沒有CD)則返回錯誤碼,此時可以用mciGetErrorString函數取得錯誤信息字符串。open是MCI打開設備的命令,cdaudio是MCI設備名。MCI的設備類型在表11.6列出。

 

表11.6 MCI設備類型

設備類型

描述

animation

動畫設備

cdaudio

CD播放器

dat

數字音頻磁帶機

digitalvideo

某一窗口中的數字視頻(不基於GDI)

other

未定義的MCI設備

overlay

重疊設備(窗口中的模擬視頻)

scanner

圖象掃描儀

sequencer

MIDI序列器

videodisc

視盤機

waveaudio

播放數字波形文件的音頻設備

  請讀者注意,設備類型和設備名是不同的概念。設備類型是指響應一組共用命令的一類MCI設備,而設備名則是某一個MCI設備的名字。系統需要用不同的設備名來區分屬於同一設備類型的不同設備。

  設備名是在註冊表或SYSTEM.INI的[mci]部分定義的,典型的[mci]段如下所示:

[mci]

cdaudio=mcicda.drv

sequencer=mciseq.drv

waveaudio=mciwave.drv

avivideo=mciavi.drv

videodisc=mcipionr.drv

  等號的左邊是設備名,右邊是對應的MCI驅動程序。當安裝了新的MCI驅動程序時,系統要用不同的設備名來區分。設備名通常與驅動程序中的設備類型名相同,如cdaudio和waveaudio等,但也有例外,如avivideo設備是一個digitalvideo類型的設備。

  使用MCI設備一般包括打開、使用和關閉三個過程。MCI的大部分命令可以控制不同的媒體設備。例如,可以用play命令來播放WAVE文件、視頻文件或CD。表11.7列出常用的MCI命令字符串,表中大部分命令都具有通用性。在MCI命令的後面一般要跟一個設備名以指定操作的對象。

 

表11.7 常用的MCI命令

命令

描述

capacility

查詢設備能力

close

關閉設備

info

查詢設備的信息

open

打開設備

pause

暫停設備的播放或記錄

play

開始設備播放

record

開始

resume

恢復暫停播放或記錄的設備

seek

改變媒體的當前位置

set

改變設置

status

查詢設備狀態信息

stop

停止設備的播放或記錄

 

 

  例如,上面的例子打開了一個CD播放機後,可以發送常用的命令來控制CD機:

play cdaudio from <位置> to <位置>。若省略from則從當前磁道開始播放,若省略to則播放到結束。

pause cdaudio。暫停播放。

stop cdaudio。停止播放。

resume cdaudio。繼續被暫停的播放。

status cdaudio number of tracks。查詢CD的磁道數。status cdaudio current track可以查詢當前磁道。

seek cdaudio to <位置>。移動到指定磁道。

set cdaudio door open/closed。彈出或縮進CD盤。

close cdaudio。關閉設備。

 

  MCI設備可以按簡單設備和複合設備進行分類。象cdaudio這樣的設備不使用文件,我們稱之爲簡單設備,而複合設備在播放時要用到數據文件,如數字視頻(digitalvideo)和波形音頻(waveaudio)設備,我們把這些數據文件叫做設備元素。

  在打開一個複合設備時要指定設備名和設備元素。例如,下面命令打開一個波形音頻設備:

open mysound.wav type waveaudio

  可以只爲複合設備指定設備元素,例如:

open mysound.wav

  如下面所示,系統通過查找註冊表或WIN.INI的[mci extensions]可以確定打開哪一個設備。

[mci extensions]

mid=Sequencer

rmi=Sequencer

wav=waveaudio

avi=AVIVideo

  有時,程序需要多次打開同一設備來播放不同的數據文件。例如,誰也不能否認在屏幕上同時播放兩個AVI文件的可能性,在這種情況下,需要爲每次打開的設備起一個不同的別名,這樣MCI才能區分兩個播放設備。例如,下面這段代碼打開並播放了兩個AVI文件:

char buf[50];

mciSendString("open dillo.avi type avivideo alias dillo",buf,strlen(buf),NULL);

mciSendString("play dillo repeat",buf,strlen(buf),NULL); //重複播放

mciSendString("open search.avi type avivideo alias search",buf,strlen(buf),NULL);

mciSendString("play search",buf,strlen(buf),NULL);

在用open命令打開設備時,如果指定了別名,則以後對該設備的操作都要使用別名。

  到目前爲止,我們使用的都是MCI命令字符串。讀者可能己經有了這樣的體會,命令字符串具有簡單易學的優點,但這種接口與C/C++的風格相去甚遠,如果程序要查詢和設置大量數據,那麼用字符串的形式將很不方便。

  MCI的命令消息接口提供了C語言接口,它速度更快,並且更能符合C/C++程序員的需要。所有MCI命令消息都是通過mciSendCommand函數發送的,該函數的聲明爲:

MCIERROR mciSendCommand(

MCIDEVICEID IDDevice, //設備的ID,在打開設備時不用該參數

UINT uMsg, //命令消息

DWORD fdwCommand, //命令消息的標誌

DWORD dwParam //指向包含命令消息參數的結構

); //若成功則返回0,否則返回錯誤碼

 

清單11.8的代碼演示了用MCI命令消息來打開和重複播放一個AVI文件:

 

清單11.8

MCI_DGV_OPEN_PARMS mciOpen;

UINT wDeviceID;

MCIERROR mciError;

 

mciOpen.lpstrDeviceType = "avivideo"; //設備名

mciOpen.lpstrElementName = "dillo.avi"; //設備元素

 

mciError=mciSendCommand(0, MCI_OPEN,

MCI_OPEN_TYPE|MCI_OPEN_ELEMENT, //使用了設備元素

(DWORD)&mciOpen);

if(mciError)

{

char s[80];

mciGetErrorString(mciError,s,80);

AfxMessageBox(s);

return ;

}

wDeviceID=mciOpen.wDeviceID; //保存設備ID

MCI_DGV_PLAY_PARMS mciPlay;

mciError=mciSendCommand(wDeviceID, MCI_PLAY, MCI_DGV_PLAY_REPEAT,

(DWORD)&mciPlay);

. . .

  可以看出,用命令消息比用命令字符串要複雜的多。命令消息與命令字符串是對應的,例如,open與MCI_OPEN完成的是一樣的功能。變量wDeviceID用來保存設備的ID,系統用ID來標識不同的設備,以保證命令發給正確的對象。

  限於篇幅,對MCI的命令消息就不作詳細介紹了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章