Windows Vista for Developers——第三部分补充:控件和桌面窗口管理器
作者: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开发技术的过程中助上一臂之力!