`

【技巧】分层窗口Layered Windows和Direct2D技术

阅读更多
作者: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来了。

  • 大小: 35.2 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics