本文配套源碼
有沒有方法創建一個半透明的窗口,並將該窗口上發生的所有鼠標事件都傳遞到桌面或另一個應用窗口處理?
當然可以,並且相當容易。你只要創建一個“分層窗口”即可。我寫了一個 小程序叫 lwtest 來示范如何做。你可以下載源代碼。為了創建分層窗口,你需要擴展式樣 WS_EX_LAYERED,此外,為了能在透明窗口上進行鼠標點擊,你還需要 WS_EX_TRANSPARENT 擴展式樣。 在窗口創建之後,你可以同時設置兩個式樣,MFC代碼如下:
int CMainFrame::OnCreate (...)
{
...
ModifyStyleEx(0, WS_EX_LAYERED|WS_EX_TRANSPARENT);
}
ModifyStyle 和 ModifyStyleEx 是專用的 MFC CWnd 方法,其作用顧名思義。如果你用 C 語言編寫,那麼得調用 GetWindowLong(GWL_EXSTYLE) 來獲取擴展式樣,然後必須調用 SetWindowLong(GWL_EXSTYLE)來設置式樣。其效果與 ModifyStyle(Ex)一樣。當然,你也可以在創建窗 口的時候使用此式樣。
一旦創建了分層窗0口,你便可以調用 SetLayeredWindowAttributes 來 設置透明度。可用的分層窗口屬性之一是 LWA_ALPHA,它就是用來調整透明度的,取值范圍從 0(完全 透明)到 255(不透明)。要得到半透明的效果,可以這樣調用 SetLayeredWindowAttributes:
// in CMainFrame::OnCreate
SetLayeredWindowAttributes(0, 255 * 0.50, LWA_ALPHA);
這裡我用乘法來表示一般公式;你可以僅用 128,因為那是 255 的一半(四捨 五入)。你還可以用專門的顏色作為透明色。此時,你得用 LWA_COLORKEY 作為屬性,在第一個參數中 指定 COLORREF。Windows 會讓所有像素顏色都呈透明。注意前面的代碼段假設你是從 CWnd 派生對象中 調用。如果用 C 語言,你得使用 ::SetLayeredWindowAttributes,它帶有一個額外的參數 HWND。
你可以用分層窗口來進行動畫和其它轉換效果的處理;詳細細節請參考文檔中的“分層窗 口”部分。
我正在寫一個幻燈顯示程序,該程序要顯示JPEG圖像序列。我使用了 2002年三 月刊專欄文章中的 CPicture 類來繪制圖像(參見:C++ Q&A: Do You Have a License for that GIF? PreSubclassWindow, EOF in MFC, and More)。那個程序運行得很好。但我現在想添加從某一張 圖像到下一張圖像的漸變特性。我在網頁中用轉換效果可以做到。那麼是否有辦法從程序代碼中實現圖 像漸變特性?
借助 COM 確實可以在 IE 中實現轉換效果。這些效果包括——漸變、 擦除,框入、框出、棒狀等等——在DirectX 中都支持。具體細節已經超出了本文的討論范 圍,所以我只能讓你去看相關文檔,其內容參見“Internet Development SDK”中的 “Using Transforms in C++”。你需要熟悉 COM 以及一些基本的 DirectX 知識,如:表層 (surfaces)和轉換(transforms)(DXSurface 和 DXTransform)。
如果你僅僅是想實現圖像 到圖像的漸變,我可以給你示范如何用 GDI+ 函數 AlphaBlend 來實現,微軟的老大們在 MFC 中已經對 之進行了足夠友好的包裝,CDC::AlphaBlend。AlphaBlend 中的 alpha 是一個圖形學術語。它表示位圖 使用3個字節來說明一個像素:每個字節分別表示 紅、綠、藍的值。由於 32位的 DWORD 有4個字節,多 余的這個字節常被用作“alpha channel”,用於指定像素的透明度。這個 alpha 值按照如 下的公式來合並像素:
[R,G,B]blended = ?[R,G,B]image + (1-?? [R,G,B] background
當 alpha 為 0 時,你得到的是背景(圖像完全透明);當 alpha 為 1 時,你 得到非透明圖像(完全不透明)。實際有透明效果的 alpha 值是一個 8 位的字節表示的值,范圍從0- 255,0 和 1 只是表示透明和非透明兩個極端。它們都是可用的 alpha 值,但大多數應用程序不需要; 多數應用程序使用一個常量 alpha 值來處理整個對象,如一幅圖像。例如,你可能想讓一幅特定的圖像 以25%的透明度顯示。
AlphaBlend 函數類似老的 BitBlt 和 StretchBlt,但它僅僅實現漸變。 發音為“blit”,這個術語是從古老的 PDP-10 BLT (塊轉移)指令派生而來,這個指令用 於將大塊內存從一個位置轉移到另一個位置。AlphaBlend 的細節如 Figure 1 所示,參數簡單明了,但 是用 AlphaBlend 來實現漸變很繁瑣,因為只調用一次是不行的,必須重復調用來產生漸變效果,用一 個定時器和 0-255 之間不同的 alpha 值來控制。
為了展示 AlphaBlend 實際的工作過程,我編 寫了一個程序 BlendView,該程序基於我的一個圖像查看程序,參見 2002 年 3 月刊的專欄文章。 BlendView 可以查看各種圖像文件(BMP、JPG、GIF 以及其它任何 GDI+ 支持的格式),但是當你打開 一幅新圖像時,原來的圖像會漸變成新圖像,如 Figure 2 所示。
Figure 2 原圖像
Figure 2 漸變的圖像
Figure 2 最終的 圖像
為了將一幅圖像漸變為另一幅,你需要兩幅圖像,當用戶打開一個新文檔時,MFC 要做的第 一件事情是銷毀舊的那個對象。所以你考慮在 MFC 加載新圖像前將舊圖像保存在某個地方。因為漸變效 果概念上屬於視圖處理(繪制圖像范疇),所以我把處理過程放在在視圖(View)中。也就是說在視圖 中保存舊圖像。但視圖如何知道何時要保存圖像呢?你當然得告訴它。幸運的是,CDocument 具備一個 方法,你可以用它來隨時通知視圖發生了什麼。這個方法就是 CDocument::UpdateAllViews:
// in Doc.cpp:
BOOL CPictureDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
UpdateAllViews(NULL, PREOPENDOC, this);
return m_pict.Load (lpszPathName);
}
PREOPENDOC 是我自己的枚舉代碼,在 doc.h 中定義。當你調用 UpdateAllViews 時,將自己的“提示代碼”(一個32位整數)隨一個指針傳遞到“提 示對象”,該對象可以是任何 CObject 派生的 MFC 類。這裡我傳的是文檔本身。注意我是在加載 新圖像之前調用 UpdateAllViews,而舊圖像仍然有效。視圖處理通知消息保存該圖像:
void CPictureView::OnUpdate(CView* pSender,
LPARAM lHint, CObject* pHint)
{
if (lHint==CPictureDoc::PREOPENDOC) {
SaveDocImage((CPictureDoc*)pHint);
}
}
相同的 OnUpdate 函數處理所有文檔的通知消息,所以你得檢查發送了哪個通 知消息。一般情況下,提示代碼和提示對象背後的工作原理是文檔以提示方式提供信息,告訴視圖它需 要更新屏幕的哪一部份。對於 CPictureView 來說,如果提示代碼是 PREOPENDOC,那麼 CPictureView 則調用一個輔助函數 SaveDocImage 來保存當前圖像。Figure 3 是 SaveDocImage 的代碼,它創建一個 位圖和內存設備上下文(DC),然後在內存設備上下文中呈現圖像,在文檔摧毀原來圖像後有效地進行漸 變拷貝。
現在,當用戶打開一個新文件,文檔通知視圖以及 OnUpdate 處理例程以位圖形式保存 圖像。漸變是怎麼做出來的呢?它需要重復調用 AlphaBlend 從老圖像漸變成新圖像。最顯而易見的方 法是設置一個定時器。假設你想用三秒來漸變。為了用 100 步來實現漸變,你可以將定時期設置成 3000/100=30毫秒。但問題是 AlphaBlend實際上花了大量的時間來處理漸變。而且,所花的時間依賴於 圖像的大小。較大的圖像漸變的時間較長。如果你使用定時器來做,最後得到的幻燈效果是較小的圖像 更快,較大的圖像更慢——可能不是你想要的結果。
保持漸變時間為常量的方法是固 定持續時間——假設為 3,000 毫秒——然後根據實際逝去的時間計算 alpha 值 ,假設第一次迭代發生在 t+20 毫秒。那麼你可以用的 alpha 值為 20*255/3000 = 1 (取最近似的一 個整數)。然後根據當前時間計算的 alpha 值立即進行另一次漸變。如果逝去的時間超過一半,你最終 的 alpha 值是 .5。通過用實際逝去的時間計算 alpha 值,你可以保證漸變總是按時完成,但缺點是較 大的圖像無法平滑地完成漸變,因為它們的迭代過程更少,在 AlphaBlend 中花的時間更多。
所 有這些實現難易程度不一。Figure 3 和 Figure 4 是詳細代碼。當 CPictureView::OnUpdate 獲得 PREOPENDOC 通知時,保存舊圖像之後,它將數據成員 m_iStartTime 置為當前時鐘時間。時鐘時間是 自該進程啟動後的“時鐘嘀嗒”數。每秒嘀嗒數為 CLOCKS_PER_SEC(通常為 1,000)。當 OnUpdate 返回時,控制傳回到文檔和 MFC,它調用視圖的 OnInitialUpdate 函數,該函數調用 OnUpdate,它重畫窗口。最後,Windows 向你的視圖發送 MW_PAINT 消息,MFC 通過調用視圖的虛擬 OnDraw 方法處理該消息。這是 MFC 的基本常識:在某個視圖中,繪圖在 OnDraw 進行,而不是 OnPaint 中。CPictureView 是這樣繪制的:
void CPictureView::OnDraw(CDC* pDC)
{
CPicture* ppic = // get current picture
if (m_iStartTime) {
// do blend
} else {
// render as normal
ppic- >Render(pDC,rc);
}
}
我省略了漸變的細節,主要突出 CPictureView 如何用 m_iStartTime 作為標志來確定是否漸變。以 下是實現漸變需要的基本步驟。
創建一個內存 DC;
在該內存 DC 中繪制位圖;
計算漸變的 alpha 值;
在該內存 DC 中 AlphaBlend 位圖;
將結果拷貝到屏幕(BitBlt);
在畫面以外的內存 DC 中進行漸變然後拷貝到屏幕這一步是很重要的;否則用戶將會看到一閃而過的 中間圖像。因為 AlphaBlend 需要設備上下文,而不是 CPicture 對象,首先繪制新圖像(所以我調用 CPicture::Render ),然後在其上漸變舊圖像要方便一些。所以我用的 alpha 值與先從舊的圖像開始 顯示所用的 alpha 值相反轉(1-alpha) ,換句話說,不是先從舊圖像開始,然後在上面以越來越多的 效果漸變新圖像。我是先從新圖像開始,然後在上面以越來越少的效果漸變舊圖像。很聰明,不是嗎? 網格效果處理方法一樣。以下是計算 AlpahBlend alpha 值的關鍵代碼行:
int alpha = ((clock() - m_iStartTime) * 255) / BLEND_DURATION;
alpha = max(255-alpha,0);
漸變之後,如果計算的 alpha 值大於 0,那麼就需要處理更多的漸變效果。所以 OnDraw 調用 Invalidate(FALSE) 在不擦除背景的情況下而重畫窗口。Windows 發送另一個 WM_PAINT 消息 ——只是要等到當前消息處理完成。這樣一來(使 WM_PAINT 為有效消息),沒有阻塞。在漸 變期間,用戶仍然能使用應用程序。你可以在漸變期間改變窗口大小來證明這一點。CPictureView 在新 的窗口尺寸下保持漸變。
如果算出的 alpha 值為 0,漸變完成。計時器停止。這時,OnDraw 調 用輔助函數 StopBlending,該函數刪除舊圖像並將 m_iStartTime 設置為 0,暗示 OnDraw 停止漸變。 現在當視圖需要繪制時,OnDraw 通過調用 CPicture::Render 進行常規繪制,直接呈現新圖像,不發生 漸變。
如果你使用活動模板庫(ATL)CImage 類來保存圖像(而不是用 CPicture,這是我很久 以前實現的一個類,當時 CImage 還未出現),你可以用 CImage::AlphaBlend,不過用它來進行漸變會 產生一些開銷。
如果你使用微軟的 .NET 框架,你可以用 Graphics.DrawImage 重載方法函數之 一來進行 alpha 漸變,該重載有一個 ImageAttributes 對象參數。ImageAttributes 中的一個方法是 SetColorMatrix。顏色矩陣為一個5x5 矩陣,定義紅、綠、藍顏色映射以及 alpha 加第五個 w 通道, 對角線上必須是 1,其它地方必須為 0(學過數學的的人都知道,第五通道被用於實現非線性轉換)。 為了完成半透明漸變,你得用單位矩陣(對角線上為 1,其余都為 0),然後將 alpha 值 (ColorMatrix.Matrix33)置為 .5f 並用它繪制圖像。