作者:Kenny Kerr 翻译:Ray Linn
在关于Direct2D技术的第三讲里,我将要展示其在互操作性上无与伦比的能力。我不打算遍历关于互操作性的所有细节,我想给你演示一个实际应用:分层窗口。分层窗口是那些已经久已存在且未被改进的Windows诸多特性之一,因此特别需要利用现代图形技术来提高它的使用效率。
这儿,我假定你有些Direct2D编程的基本知识。 如果没有,我建议你读下6月(我以前的文章:msdn.microsoft.com/magazine/dd861344 )和9月( msdn.microsoft.com/magazine/ee413543 )的MSDN有关文章,它们介绍了Direct2D的基础编程和绘图的有关内容。
一般来说,使用分层窗口总是为了些个不同的目标。通常,他们可以用来轻松,高效地制作的视觉效果和无闪烁的渲染。在过去GDI大行其道的日子里,使用分层窗口可算是个高招。但在普遍使用硬件加速的今天,分层窗口就有点落伍了,因为分层窗口仍然属于User32/GDI世界,且没有任何有效手段来支持高性能,高品质的微软图形平台-- DirectX。
分层窗口的确提供了某种独特的能力,它可以创建每像素不同透明度的窗口,目前Windows SDK尚未提供其他方式能实现同一目标。
应该指出的是,有两种分层窗口。这取决于是否需要控制每像素透明度或只需要控制整个窗口透明度。 本文的分层窗口是指前者,但如果你只需要控制整个窗口透明度,那么你可以在创建窗口之后,简单地调用之设置alpha值的SetLayeredWindowAttributes函数。
Verify(SetLayeredWindowAttributes(
windowHandle,
0, // no color key
180, // alpha value
LWA_ALPHA));
这儿假设你在采用了WS_EX_LAYERED扩展样式来创建窗口或在创建之后调用SetWindowLong来设置样式。这种方式的好处是显而易见的,你不需要对应用程序的重画窗口的方式做出任何改变,因为桌面窗口管理器(DWM)窗口会自动融合(Blend) 任何适当的窗口。如图:
在另一种情况下,你就需要自己处理一切。当然,如果使用了诸如Direct2D这样全新的渲染技术,这不是一个问题!
我们该如何进行? 从原理上来讲是相当直接的。首先,您需要填充一个UPDATELAYEREDWINDOWINFO结构,它提供了分层窗口的位置和大小,以及一个GDI设备上下文(DC),DC定义了窗口的表面,也潜藏了问题。DC是属于GDI这个旧世界的,且离硬件加速的DirectX的新世界甚远。
除了需要自己分配UPDATELAYEREDWINDOWINFO结构里的所有指针这些麻烦外,还有就是UPDATELAYEREDWINDOWINFO在Windows SDK中并未被详细说明,使用起来有些混乱。 笼统地讲,你需要为五个结构分配内存:通过DC复制的位图的所在位置,更新时窗口在桌面的位置,要复制的位图大小也就是窗口的大小:
POINT sourcePosition = {};
POINT windowPosition = {};
SIZE size = { 600, 400 };
然后是BLENDFUNCTION结构,它定义分层窗口将如何与桌面融合。这是一个令人惊讶的多功能结构,往往被忽视,但也可以非常有用。通常你可以如下填充它:
BLENDFUNCTION blend = {};
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;
AC_SRC_ALPHA常量表明源位图有一个alpha通道,这是最常见的场景。
有意思的是,你可以象在SetLayeredWindowAttributes函数中一样使用SourceConstantAlpha来控制整个窗口的透明度。当它设为255时, 分层窗口将只使用每个像素的alpha值,你可以将其持续调整到零,即完全透明,来产生诸如渐入渐出的效果而不需要额外的重绘成本。这也是为什么它被称为BLENDFUNCTION结构的原因:这个结构的值生成了α-融合窗口。
最后我们如下填充LUPDATELAYEREDWINDOWINFO结构:
UPDATELAYEREDWINDOWINFO info = {};
info.cbSize = sizeof(UPDATELAYEREDWINDOWINFO);
info.pptSrc = &sourcePosition;
info.pptDst = &windowPosition;
info.psize = &size;
info.pblend = &blend;
info.dwFlags = ULW_ALPHA;
上面的代码是相当清晰的,唯一未被提及的是dwFlags成员变量。如果你使用过UpdateLayeredWindow函数,那么ULW_ALPHA常量,看起来就相当熟悉,它表明应该使用融合功能。
最后,您需要提供源DC的句柄,并调用UpdateLayeredWindowIndirect函数来更新窗口:
info.hdcSrc = sourceDC;
Verify(UpdateLayeredWindowIndirect(
windowHandle, &info));
总的就是这样。该窗口将不会收到任何WM_PAINT消息。任何时候你需要显示或更新窗口,只需调用UpdateLayeredWindowIndirect功能。 我用一个LayeredWindowInfo包装类来未上面的代码做个模板以方便后续使用:
class LayeredWindowInfo {
const POINT m_sourcePosition;
POINT m_windowPosition;
CSize m_size;
BLENDFUNCTION m_blend;
UPDATELAYEREDWINDOWINFO m_info;
public:
LayeredWindowInfo(
__in UINT width,
__in UINT height) :
m_sourcePosition(),
m_windowPosition(),
m_size(width, height),
m_blend(),
m_info() {
m_info.cbSize = sizeof(UPDATELAYEREDWINDOWINFO);
m_info.pptSrc = &m_sourcePosition;
m_info.pptDst = &m_windowPosition;
m_info.psize = &m_size;
m_info.pblend = &m_blend;
m_info.dwFlags = ULW_ALPHA;
m_blend.SourceConstantAlpha = 255;
m_blend.AlphaFormat = AC_SRC_ALPHA;
}
void Update(
__in HWND window,
__in HDC source) {
m_info.hdcSrc = source;
Verify(UpdateLayeredWindowIndirect(window, &m_info));
}
UINT GetWidth() const { return m_size.cx; }
UINT GetHeight() const { return m_size.cy; }
};
下面的代码演示了使用ATL/WTL的分层窗口和LayeredWindowInfo包装类的一个基本骨架.这首先要注意的是,代码没有必要调用UpdateWindow,因为此代码不使用WM_PAINT消息。 相反,它将立即调用Render方法,依次执行绘图,并提供DC给LayeredWindowInfo的Update方法。 绘图是如何发生、DC又从何而来,这是最有趣的地方。
class LayeredWindow :
public CWindowImpl<LayeredWindow,
CWindow, CWinTraits<WS_POPUP, WS_EX_LAYERED>> {
LayeredWindowInfo m_info;
public:
BEGIN_MSG_MAP(LayeredWindow)
MSG_WM_DESTROY(OnDestroy)
END_MSG_MAP()
LayeredWindow() :
m_info(600, 400) {
Verify(0 != __super::Create(0)); // parent
ShowWindow(SW_SHOW);
Render();
}
void Render() {
// Do some drawing here
m_info.Update(m_hWnd,
/* source DC goes here */);
}
void OnDestroy() {
PostQuitMessage(1);
}
};
GDI/GDI+的方法
我会先告诉您是在GDI/GDI+中是如何进行的。首先你需要创建一个乘以32字节/像素的位图,它采用蓝-绿-红-alpha(BGRA)颜色通道字节顺序。预先乘以32意味着颜色通道的值已经和alpha值相乘。这会给alpha融合图形带来更好的性能,但它也意味着你需要将颜色值除以alpha值以得到真正的颜色值。在GDI的术语中,这称为32BPP的设备无关位图(DIB),它通过填充BITMAPINFO结构,并传递给CreateDIBSection函数来创建。
BITMAPINFO bitmapInfo = {};
bitmapInfo.bmiHeader.biSize =
sizeof(bitmapInfo.bmiHeader);
bitmapInfo.bmiHeader.biWidth =
m_info.GetWidth();
bitmapInfo.bmiHeader.biHeight =
0 – m_info.GetHeight();
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression =
BI_RGB;
void* bits = 0;
CBitmap bitmap(CreateDIBSection(
0, // no DC palette
&bitmapInfo,
DIB_RGB_COLORS,
&bits,
0, // no file mapping object
0)); // no file offset
这儿有很多细节我们尚未涉及。这个API函数还有很长的路要走。应该注意的是,我指定为位图的负高度。BITMAPINFOHEADER结构定义位图是自上而下还是自下而上。如果高度为正,你得到一个自下而上的位图,反之,如果高度为负,则得到自下而上的位图。自上而下的位图,它们的原点在左上角,而自下而上的位图,原点则在左下角。
虽然不是严格要求,但我还是倾向使用自下而上的位图,因为这是现在的Windows图形处理元件的最常见格式,这样可以提高互操作性。我们可以通过以下方法计算出一个为正值的stride:
UINT stride = (width * 32 + 31) / 32 * 4;
现在,您有足够的信息来通过位指针绘制位图。除非您完全疯了,要使用绘图函数来实现,不幸地是,GDI提供的大部分函数并不支持alpah通道。这是GDI+的用武之地。
虽然你可以把位图的数据直接传递给GDI+,不过我们只要为它创建一个DC,反正我们传递给UpdateLayeredWindowIndirect函数的就只是它.要创建DC,我们需要调用恰当地命名为CreateCompatibleDC的函数,它会创建一个与桌面兼容的内存DC。然后,可以调用SelectObject函数来将位图选入DC中。下面的GdiBitmap包装类包含了这些有的没的功能:
class GdiBitmap {
const UINT m_width;
const UINT m_height;
const UINT m_stride;
void* m_bits;
HBITMAP m_oldBitmap;
CDC m_dc;
CBitmap m_bitmap;
public:
GdiBitmap(__in UINT width,
__in UINT height) :
m_width(width),
m_height(height),
m_stride((width * 32 + 31) / 32 * 4),
m_bits(0),
m_oldBitmap(0) {
BITMAPINFO bitmapInfo = { };
bitmapInfo.bmiHeader.biSize =
sizeof(bitmapInfo.bmiHeader);
bitmapInfo.bmiHeader.biWidth =
width;
bitmapInfo.bmiHeader.biHeight =
0 - height;
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression =
BI_RGB;
m_bitmap.Attach(CreateDIBSection(
0, // device context
&bitmapInfo,
DIB_RGB_COLORS,
&m_bits,
0, // file mapping object
0)); // file offset
if (0 == m_bits) {
throw bad_alloc();
}
if (0 == m_dc.CreateCompatibleDC()) {
throw bad_alloc();
}
m_oldBitmap = m_dc.SelectBitmap(m_bitmap);
}
~GdiBitmap() {
m_dc.SelectBitmap(m_oldBitmap);
}
UINT GetWidth() const {
return m_width;
}
UINT GetHeight() const {
return m_height;
}
UINT GetStride() const {
return m_stride;
}
void* GetBits() const {
return m_bits;
}
HDC GetDC() const {
return m_dc;
}
};
GDI+的图形类,提供了在设备上绘图的一些方法,可以用来构造位图的DC。下面代码显示了如何让上面的LayeredWindow类用GDI+进行渲染。一旦我们把它和代码样板相结合,就相当直接:窗口的大小被传递给GdiBitmap的构造函数,位图的DC被传给Graphics的构造函数和Update方法。虽然简单,但GDI或GDI+的大部分函数都不支持硬件加速,也没有提供特别强大的渲染功能。
class LayeredWindow :
public CWindowImpl< ... {
LayeredWindowInfo m_info;
GdiBitmap m_bitmap;
Graphics m_graphics;
public:
LayeredWindow() :
m_info(600, 400),
m_bitmap(m_info.GetWidth(), m_info.GetHeight()),
m_graphics(m_bitmap.GetDC()) {
...
}
void Render() {
// Do some drawing with m_graphics object
m_info.Update(m_hWnd,
m_bitmap.GetDC());
}
...
架构问题
相反的,在WPF里创建一个分层窗口只需要以下几步:
class LayeredWindow : Window {
public LayeredWindow() {
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
// Do some drawing here
}
}
虽然非常简单,但它掩盖了其中的复杂性和使用分层窗口的架构限制。不管你如何优化它,分层窗口仍然必须遵循的在这篇文章里阐述的架构原则。虽然WPF中可以用硬件加速来进行渲染,但得到的结果仍然需要被复制到预乘的BGRA位图中,在调用UpdateLayeredWindowIndirect更新显示之前,它仍需被选入一个兼容的DC。WPF暴露出来的东西并不比一个bool变量多,它不得不为你无法控制的行为做些适当的选择。但为什么这是个问题?因为这涉及到硬件。
图形处理单元(GPU)提供了专用内存以达到最佳的性能。这意味着,如果你需要处理现有的位图,它就会从系统内存(RAM)复制到GPU内存,这往往要比在系统内存的两点间复制要慢得多。 反过来也是如此:如果你使用GPU创建和渲染位图,然后将它复制到系统内存,这也是一个昂贵的复制操作。
通常,这种情况不该发生的,GPU所渲染的位图应该直接被送往显示设备。但在分层窗中中,位图则必须被传送回系统内存,因为User32/GDI调用了需要访问位图的内核态和用户态的资源。例如User32里鼠标点击分层窗口的情况,在分层窗口上的点击与位图的alpha值是相关的,如果点击的地方是透明的,那么鼠标的消息会穿透该分层窗口。因此,系统内存里需要一个位图的副本来应付这种情况。一旦用UpdateLayeredWindowIndirect来拷贝位图,它又被直接送回GPU让DWM能够组合出桌面。
除了往返内存复制的开销,GPU和CPU之间的同步也是昂贵的。不象典型的CPU操作,GPU的操作往往都是异步的,当批量执行一系列渲染指令时,这提供了很好的性能。每次我们需要跨越到CPU时,GPU不的不强制清空批处理命令,而CPU不得不等待GPU完成操作,而达不到最佳性能。
这意味着我们需要对这些往返操作的开销和频率提高警惕。如果正在呈现的场景足够复杂,那么硬件加速性能可以轻松超过复制位图的开销。反之,如果渲染的开销不大,可以由CPU来进行,你可能会发现,没有硬件加速反而提供了更好的性能能。做出选择并不容易。有些图形处理器甚至没有专用的内存,而使用的系统内存,从而降低了部分复制开销。
但是不管是WPI还是GDI都不会给你选择的机会。在GDI的情况下,你只能是用CPU,而在
WPF里,你总被迫是用WPF的渲染方式,通常是硬件加速的Direct3D。
这时Direct2D来了。