作者:Kenny Kerr
翻譯:Dflying Chen
原文:http://weblogs.asp.net/kennykerr/archive/2007/01/23/controls-and-the-desktop-window-manager.aspx
請同時參考《Windows Vista for Developers》系列。
在所有《Windows Vista for Developers》系列文章中,《Windows Vista for Developers——第三部分:桌面視窗管理員》是最受歡迎的(通過Blog的流量統計、Email問題的主題等得出)。
目前為止,我所聽到的最常見的問題就是如何在啟用半透明效果時也能正確地呈現出控制項。回憶一下,我寫DMW文章的時候Windows Vista還沒有RTM。在這些較早版本的Vista中,我們可以使用那個透明像素的hack來輕鬆地在半透明效果上繪出需要的控制項。在那篇文章中我也示範了這個hack的實際應用。不幸的是,當微軟公司正式發布Vista時,這個hack已經沒用了,只留下了滿腹狐疑的開發人員……應該怎麼辦呢?
不得不說我每天的工作非常繁忙,還有很多其他的委託事項需要處理,以至於沒有時間發布替代的解決方案。不過為了拯救我的email信箱,我還是決定再給出一個解決方案來談談這個最常見的問題。
如何才能在半透明效果上顯示一個文字框?
解決這個問題的辦法有很多種。更明確一些地說,有很多種覆寫預設的標準/常用控制項繪圖方式的辦法。
你可以接受WM_PAINT訊息並自行繪製控制項。這樣做的工作量似乎不少,所以大多數開發人員都不喜歡,但這種方法確實管用,讓我們能夠用必需的Alpha通道進行繪製,進而顯示出正確的半透明效果。我的DMW執行個體程式就示範了這種方法,雖然其中用的不是某個控制項。
另一種方法是owner draw控制項。這樣做的工作量也不少,不過卻比接受WM_PAINT訊息簡單多了,作業系統卻為你做了不少。owner draw方法是個很不錯的主意,適合大多數但不是所有的控制項。值得一提的是對於文字框來說,owner draw就不管用。
還有一種更簡單的方法,就是custom draw,但它所適用的控制項更少。
另外,對於少數幾個控制項,你也可以處理WM_CTLCOLORxxx訊息,並設定其文本和背景顏色。
看看目前我們列出的這幾個選項,只有最後一個支援文字框控制項且相對比較簡單。不過這種方法與半透明效果配合的卻並不怎麼好,因為它需要較為原始的GDI支援,而GDI卻並不支援Alpha混合。
再重複一遍:如何才能在半透明效果上顯示一個文字框?
有時候(比如現在)我就在想為什麼我不在微軟公司工作呢?微軟公司也不會因為我的這些Blog上的文章給我任何報酬……
在昨天又收到一封Email詢問如何在半透明效果上顯示出文字框控制項之後,我終於決定查看一下Windows SDK,看看有沒有什麼新的辦法。順便說一句,若你不經常查閱Windows SDK的話,我強烈見你養成這個好習慣。憑著直覺,我開始在SDK的Themes和Visual Styles節中查看。不管怎樣,這部分內容是負責提供控制項的樣式的。
讓我注意到的第一個東西就是Windows Vista 的UxTheme.dll中心添加的一系列函數,用來支援緩衝繪圖。一開始這看起來似乎並不是那麼吸引人,因為DMW已經提供了一定程度上的雙緩衝。但若是有了緩衝繪圖,那就意味著我們可以捕獲、修改記憶體中的位元影像之後,再將其顯示出來。當然,這並不是什麼新玩意,我們也可以手工實現同樣的功能。但Visual Styles中提供的這些新功能卻簡化了我們的工作,並可以很漂亮地解決這個火燒眉毛的問題。
緩衝繪圖API
緩衝繪圖API提供了一系列的函數,用來將映像繪至裝置上下文(DC)。因為映像將被繪至DC,所以你前面學的GDI還有用武之地。嘿,兄弟,確實如此,現在還沒必要將你整個的程式遷移到Windows Presentation Foundation(WPF)上!
開始之前,你應該在每個線程中都至少調用一次BufferedPaintInit 函數,用來初始化這一系列的API。注意,每次調用BufferedPaintInit 都必須對應著一個同一線程上的BufferedPaintUnInit 調用。
若想開始緩衝繪圖操作,只要簡單地調用BeginBufferedPaint 函數即可。該函數接受一個目的DC,以及一個目的矩形地區,用來指定最終的緩衝將要繪製的位置。還有一些額外的參數,可以用來控制某些緩衝相關的特性。其中一個就是緩衝的類型——謝天謝地它支援裝置無關位元影像(Device Independent Bitmap,DIB)類型,這就足夠我們進行Alpha混合操作了。BeginBufferedPaint 函數然後返回一個控制代碼,我們可以將其傳遞到其他緩衝繪圖API,或是某個將要繪製的DC中。
能夠接受該緩衝繪圖控制代碼的其中一個函數就是BufferedPaintSetAlpha。該函數可以讓我們簡單地更新整個緩衝的Alpha通道,並將其設定為一個單一的值,以期實現各種不同層級的透明/半透明效果。需要注意的是緩衝內的所有像素都將被更新為同一個的Alpha值。
最後,我們即可將該緩衝拷貝到目標DC上了,並調用EndBufferedPaint 函數釋放由BeginBufferedPaint 分配的相關資源。
目前為止,你差不多也能想象到接下來要怎麼做了。首先用緩衝繪圖API建立一個緩衝映像,然後在該緩衝映像上繪出我們需要的文字框,接下來更新緩衝映像的Alpha通道,最後將緩衝繪製到表單的DC上。讓我們看一個執行個體程式。
BufferedPaint類
下面這個類將緩衝繪圖API用C++封裝起來,以簡化其使用。
class BufferedPaint
{
public:
BufferedPaint() :
m_handle(0)
{
COM_VERIFY(::BufferedPaintInit());
}
~BufferedPaint()
{
COM_VERIFY(::BufferedPaintUnInit());
}
HRESULT Begin(HDC targetDC,
const RECT& targetRect,
BP_BUFFERFORMAT format,
__in_opt BP_PAINTPARAMS* options,
__out CDCHandle& bufferedDC)
{
ASSERT(0 == m_handle);
m_handle = ::BeginBufferedPaint(targetDC,
&targetRect,
format,
options,
&bufferedDC.m_hDC);
HRESULT result = S_OK;
if (0 == m_handle)
{
result = HRESULT_FROM_WIN32(::GetLastError());
}
return result;
}
HRESULT End(bool updateTargetDC)
{
ASSERT(0 != m_handle);
HRESULT result = ::EndBufferedPaint(m_handle,
updateTargetDC);
m_handle = 0;
return result;
}
HRESULT SetAlpha(__in_opt const RECT* rect,
BYTE alpha)
{
ASSERT(0 != m_handle);
return ::BufferedPaintSetAlpha(m_handle,
rect,
alpha);
}
private:
HPAINTBUFFER m_handle;
};
OpaqueEdit類
下面這個C++類繼承於系統的文字框類,這樣我們即可方便地重寫其繪圖相關的方法。
class OpaqueEdit :
public CWindowImpl<OpaqueEdit, CEdit>
{
public:
BEGIN_MSG_MAP_EX(OpaqueEdit)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
REFLECTED_COMMAND_CODE_HANDLER_EX(EN_CHANGE, OnChange)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
private:
LRESULT OnPaint(UINT /*message*/,
WPARAM /*wParam*/,
LPARAM /*lParam*/,
BOOL& /*handled*/)
{
CPaintDC targetDC(m_hWnd);
CDCHandle bufferedDC;
if (SUCCEEDED(m_bufferedPaint.Begin(targetDC,
targetDC.m_ps.rcPaint,
BPBF_TOPDOWNDIB,
0, // options
bufferedDC)))
{
SendMessage(WM_PRINTCLIENT,
reinterpret_cast<WPARAM>(bufferedDC.m_hDC),
PRF_CLIENT);
COM_VERIFY(m_bufferedPaint.SetAlpha(0, // entire buffer
255)); // 255 = opaque
// Copy buffered DC to target DC
COM_VERIFY(m_bufferedPaint.End(true));
}
return 0;
}
void OnChange(UINT /*notifyCode*/,
int /*control*/,
HWND /*window*/)
{
VERIFY(InvalidateRect(0, // entire window
FALSE)); // don't erase background
}
BufferedPaint m_bufferedPaint;
};
可以看到,在WM_PAINT訊息的處理函數中,我們將WM_PRINTCLIENT訊息發送給了該文字框,讓其繪製到經過緩衝的DC上。然後將該緩衝的Alpha通道值設定為255(完全不透明)並更新了目標DC。EN_CHANGE的處理函數可能會讓你有些吃驚。因為文字框的繪製發生在WM_PAINT訊息之外,當控制項中的常值內容發生變化時,我們需要進行再次重繪。在這個樣本中,我僅僅是讓該控制項失效,這樣它會再次接收到一個新的WM_PAINT訊息。這種實現方式還有一定的最佳化空間,但目前為止對於這個樣本程式來說已經足夠用了。需要提到的是因為DMW自動提供了雙緩衝,所以重複地進行繪製並不會造成介面閃爍。
SampleDialog類
下面的這個類保證了該表單作為一塊無縫的“玻璃”顯示出來,並在其中添加一個前面定義的OpaqueEdit 類作為文字框。
class SampleDialog :
public CDialogImpl<SampleDialog>
{
public:
enum { IDD = IDD_SAMPLE };
BEGIN_MSG_MAP(MainWindow)
MSG_WM_INITDIALOG(OnInitDialog)
MSG_WM_ERASEBKGND(OnEraseBackground)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
private:
bool OnInitDialog(HWND /*control*/,
LPARAM /*lParam*/)
{
const MARGINS margins = { -1 };
COM_VERIFY(::DwmExtendFrameIntoClientArea(m_hWnd,
&margins));
VERIFY(m_edit.SubclassWindow(GetDlgItem(IDC_CONTROL)));
return true; // Yes, go ahead and set the keyboard focus.
}
bool OnEraseBackground(CDCHandle dc)
{
CRect rect;
VERIFY(GetClientRect(&rect));
dc.FillSolidRect(&rect,
#000000);
return true; // Yes, I erased the background.
}
LRESULT OnCancel(WORD /*notifyCode*/,
WORD identifier,
HWND /*window*/,
BOOL& /*handled*/)
{
VERIFY(EndDialog(identifier));
return 0;
}
OpaqueEdit m_edit;
};
大功告成!希望本文中提到的技術能在你掌握Windows Vista開發技術的過程中助上一臂之力!