此文章由机器翻译。
借助 C++ 进行 Windows 开发
Direct2D 与桌面应用程序中的呈现
在我最后一列,我向您展示如何其实是容易与 c + + 创建桌面应用程序,无需任何库或框架。事实上,如果你感到特别是自虐,您可以编写从整个桌面应用程序在您的 WinMain 函数内所做的图 1。当然,这种做法只是不能缩放。
图 1 狂的窗口
int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)
{
WNDCLASS wc = {};
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hInstance = module;
wc.lpszClassName = L"window";
wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM
wparam, LPARAM lparam) -> LRESULT
{
if (WM_DESTROY == message)
{
PostQuitMessage(0);
return 0;
}
return DefWindowProc(window, message, wparam, lparam);
};
RegisterClass(&wc);
CreateWindow(wc.lpszClassName, L"Awesome?!",
WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);
MSG message;
BOOL result;
while (result = GetMessage(&message, 0, 0, 0))
{
if (-1 != result) DispatchMessage(&message);
}
}
此外演示了如何活动模板库 (ATL) 提供好的 c + + 抽象隐藏很多这种机制和 Windows 模板库 (WTL) 如何考虑这更进一步,主要是为应用程序大量投资于用户和 GDI 应用程序发展办法 (见我 2 月列在 msdn.microsoft.com/magazine/jj891018)。
在 Windows 应用程序呈现的未来是硬件-加速 Direct3D,但这真的就是不切实际来直接处理如果所有你想要做是呈现一个二维的应用程序或游戏。 这就是 Direct2D 发挥作用的地方。 我简要介绍了 Direct2D,当它首次宣布在几年以前,但我要花,未来几个月考虑很多仔细看看 Direct2D 发展。 签出我 2009 年 6 月的专栏文章,"引入 Direct2D"(msdn.microsoft.com/magazine/dd861344),有关的体系结构和基本原则的 Direct2D 的介绍。
Direct2D 设计的关键支柱之一是它呈现的重点和其他 Windows 应用程序开发方面留给您或其他您可能使用的库。 虽然 Direct2D 为了在桌面窗口中呈现,它是您实际上提供此窗口并优化它的 Direct2D 呈现。 所以这个月,我会注重独特的关系 Direct2D 和桌面应用程序窗口之间。 你可以做许多事情,以优化处理和呈现过程的窗口。 您想要减少不必要的绘画和避免闪烁,只是为用户提供最佳的体验。 当然,你也会想要提供一个易于管理的框架,开发应用程序。 我会解决这些问题在这里。
桌面窗口
在 ATL 示例中我上个月,给从 ATL 所类模板派生的窗口类的示例。 一切都很好地包含在应用程序的窗口类。 然而,什么最终会发生的情况很多的窗口和呈现水暖告终穿插窗口的特定于应用程序的呈现和事件处理。 若要解决此问题,我倾向推此样板化的代码放入基类,切实可行的尽量使用编译时多态性,达到应用程序的窗口类时此基类需要提请其注意。 由 ATL 和 WTL 有很多使用这种方法,那么为什么不把它扩展为您自己的类吗?
图 2 说明了这种分离。 基类是桌面窗口类模板。 模板参数使基类能够打电话到混凝土不使用虚函数的类。 在这种情况下,它使用这种技术隐藏一堆呈现特定的预处理和后处理同时到应用程序的窗口的调用来执行实际的绘图操作。 一会儿,我就会展开 DesktopWindow 类模板上,但其窗口类注册,首先需要有点工作。
图 2 桌面窗口
template <typename T>
class DesktopWindow :
public CWindowImpl<DesktopWindow<T>, CWindow,
CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
BEGIN_MSG_MAP(DesktopWindow)
MESSAGE_HANDLER(WM_PAINT, PaintHandler)
MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
END_MSG_MAP()
LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
{
PostQuitMessage(0);
return 0;
}
LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
{
PAINTSTRUCT ps;
VERIFY(BeginPaint(&ps));
Render();
EndPaint(&ps);
return 0;
}
void Render()
{
...
static_cast<T *>(this)->Draw();
...
}
...
};
struct SampleWindow : DesktopWindow<SampleWindow>
{
void Draw()
{
...
}
};
优化窗口类
用于桌面应用程序的 Windows API 的现实之一是它旨在简化与传统的用户和 GDI 资源呈现。 这些"方便"的一些需要禁用,以允许 Direct2D 接管,以避免不必要画导致难看的闪烁。 其他这些默认值也必须予以调整,以更好地适合 Direct2D 呈现的方式开展工作。 这可以通过更改窗口类信息之前已注册,但您可能已经注意到 ATL 隐藏这从程序员来实现。 幸运的是,仍有方式来实现这一目标。
上个月,我表明 Windows API 期望基于其规范创建窗口之前注册窗口类结构如何。 窗口类的特性之一是其背景的画笔。 Windows 使用此 GDI 画笔窗口开始绘画之前清除窗口的工作区。 这是方便用户和 GDI 的日子,但是是不必要的和一些闪烁的 Direct2D 应用程序的原因。 一个简单的方法来避免这种情况是通过设置背景刷柄 nullptr 窗口类结构中。 如果您要开发相对较快的 Windows 7 或 Windows 8 的计算机上,您可能认为这是不必要的因为你没有注意到任何闪烁。 这是只是因为现代的 Windows 桌面如此有效地组成的图形处理单元 (GPU) 很难把它捡起来。 然而,它容易足够应变呈现管道,夸大效果,您可能会遇到较慢的计算机上。 如果窗口的背景画笔为白色,然后添加一点睡眠延迟来绘制窗口的客户端区域形成了鲜明对比的黑色画笔,— — 10 ms 和 100 ms 之间的任何地方 — — 你得捡撞击闪烁没有麻烦。 那么如何避免它?
如我所述,如果您的窗口类注册缺少背景画笔,然后 Windows 不会有任何画笔,以清除您的窗口。 但是,您可能已经注意到在窗口类注册完全隐藏的 ATL 示例中的详情。 常见的解决方案是以处理默认窗口处理句柄的 WM_ERASEBKGND 消息 — — 提供的 DefWindowProc 函数 — — 按绘画窗口类背景画笔与窗口的工作区。 如果您处理此消息,返回 true,则会出现没有绘画。 这是一个原因能够解决办法,为此消息发送到窗口无论是否该窗口类有一个有效的背景画笔或不。 另一个解决方案是只是避免此不操作处理程序,放在第一位从窗口类中删除背景画笔。 幸运的是,ATL 使相对简单的重写创建窗口的这一部分。 在创建期间,ATL 窗口来获取此窗口类信息上调用 GetWndClassInfo 方法。 您可以提供您自己的实现,这种方法,但 ATL 提供了一个方便的宏实现该接口为您:
DECLARE_WND_CLASS_EX(nullptr, CS_HREDRAW | CS_VREDRAW, -1);
为此宏的最后一个参数要刷的常数,但-1 值诱骗它清除此属性的窗口类结构。 稳赚不赔的方法来确定是否已经消除了窗口的背景是检查由 WM_PAINT 处理程序内的 BeginPaint 函数填充的 PAINTSTRUCT。 如果其转硫酶成员是虚假的然后你知道 Windows 已清除你窗口的背景,或至少一些代码 WM_ERASEBKGND 消息作出回应和清除它的本意是。 如果 WM_ERASEBKGND 消息处理程序不会或不能清除背景,然后它由 WM_PAINT 消息处理程序,若要这样做。 不过,在这里我们可以采用 Direct2D 完全接管是窗口客户区的呈现,并避免这双幅画。 只是一定要调用 EndPaint 函数,确保 Windows 您确实画你的窗口,否则 Windows 将继续纠缠你不必要流的 WM_PAINT 消息。 当然,这将会损害您的应用程序性能并增加总体电力消费。
窗口类信息值得我们注意的其他方面是窗口类样式。 这是,事实上,以前的宏的第二个参数是什么。 CS_HREDRAW 和 CS_VREDRAW 样式使窗口每次在垂直方向和水平方向调整窗口大小使其无效。 当然,这没有必要。 可以,例如,处理 WM_SIZE 消息并使无效的窗口,但我总是高兴时 Windows 将保存我写几个多余的代码行。 不管怎样,如果您忽略无效的窗口,然后 Windows 不会发送您的窗口任何 WM_PAINT 消息时窗口的大小减小。 这可能是细的如果你是快乐的窗口的内容将被剪切,但它很常见这些天来绘制各种窗口资产相对于窗口的大小。 无论你喜欢什么,这是一项明确的决定,您需要为您的应用程序窗口进行。
虽然我对窗口背景的主题,它通常是可取明确宣布无效的窗口。 这使您可以保留您的窗口呈现植根于 WM_PAINT 消息,而不是不得不处理画在不同的地方和不同的代码路径通过您的应用程序。 你可能想要画一些东西以响应鼠标单击。 当然,你可以,呈现右有消息处理程序中。 或者,您可以只是无效的窗口,让 WM_PAINT 处理程序使应用程序的当前状态。 这是 InvalidateRect 函数的作用。 ATL 提供的只是换了此函数的 Invalidate 方法。 什么往往混淆了有关此功能的开发人员是如何处理"擦除"参数。 常规的智慧似乎是说"yes"要擦除会立即重新绘制窗口和说"no"将推迟这某种程度上。 这不是真的和尽可能多的文件说。 无效的窗口,将导致它迅速进行重新绘制。 而不是 DefWindowProc 函数,它通常将清除窗口背景擦除选项。 如果擦除为 true,然后对 BeginPaint 的后续调用将清除窗口背景。 在这里,然后,是完全避免窗口类背景画笔,而不是依赖于 WM_ERASEBKGND 消息处理程序的另一个原因。 无背景刷,BeginPaint 再次没有什么用,绘画因此擦除选项不起作用。 如果你让 ATL 设置背景画笔为您的窗口类,那么你需要作废您的窗口,因为这将再次介绍闪烁时要小心。 为此目的向 DesktopWindow 类模板添加此受保护的成员:
void Invalidate()
{
VERIFY(InvalidateRect(nullptr, false));
}
它也是一个好主意,若要处理 WM_DISPLAYCHANGE 消息,要使之无效的窗口。 这可以确保适当地重新绘制窗口应显示某些东西变化影响窗口的外观。
运行应用程序
我想使我的应用程序的 WinMain 函数相对简单。 为了实现这一目标,我添加到要隐藏整个窗口和 Direct2D 工厂创建,以及消息循环的 DesktopWindow 类模板中运行的公共方法。 DesktopWindow 的运行方法所示图 3。 这让我很简单地写我的应用程序的 WinMain 函数:
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
SampleWindow window;
return window.Run();
}
图 3 DesktopWindow Run 方法
int Run()
{
D2D1_FACTORY_OPTIONS fo = {};
#ifdef DEBUG
fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif
HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
fo,
m_factory.GetAddressOf()));
static_cast<T *>(this)->CreateDeviceIndependentResources();
VERIFY(__super::Create(nullptr, nullptr, L"Direct2D"));
MSG message;
BOOL result;
while (result = GetMessage(&message, 0, 0, 0))
{
if (-1 != result)
{
DispatchMessage(&message);
}
}
return static_cast<int>(message.wParam);
}
在创建窗口之前, 我准备 Direct2D 厂选项通过启用调试层为调试版本。 我强烈建议你不要是相同的因为它允许 Direct2D 出各种有用的诊断跟踪您开发应用程序。 D2D1CreateFactory 函数返回工厂接口指针,我交给 Windows 运行时库优秀 ComPtr 智能指针,DesktopWindow 类的受保护成员。 我然后调用 CreateDeviceIndependentResources 方法来创建任何独立于设备的资源 — — 几何图形和可重用的整个应用程序生命周期的描边样式的东西。 虽然我允许派生的类重写此方法,提供一个空的存根,DesktopWindow 类模板中如果这不需要了。 Run 方法最后通过阻止同一个简单的消息循环。 签出解释消息循环的最后一个月的列。
呈现目标
Direct2D 呈现目标应创建作为 WM_PAINT 消息处理程序的一部分进行调用的 Render 方法内部的需求。 有别于某些其他 Direct2D 呈现目标,它是完全有可能的设备 — — 在大多数情况下 GPU — — 提供硬件加速呈现为一个桌面窗口可以消失或更改某些方式使呈现目标所分配的任何资源无效。 由于在 Direct2D 即时模式呈现的性质,应用程序负责跟踪资源的是特定于设备的和可能需要不时重新创建。 幸运的是,这很容易管理。 图 4 提供了完整的 DesktopWindow 渲染方法。
图 4 DesktopWindow 渲染方法
void Render()
{
if (!m_target)
{
RECT rect;
VERIFY(GetClientRect(&rect));
auto size = SizeU(rect.right, rect.bottom);
HR(m_factory->CreateHwndRenderTarget(RenderTargetProperties(),
HwndRenderTargetProperties(m_hWnd, size),
m_target.GetAddressOf()));
static_cast<T *>(this)->CreateDeviceResources();
}
if (!(D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))
{
m_target->BeginDraw();
static_cast<T *>(this)->Draw();
if (D2DERR_RECREATE_TARGET == m_target->EndDraw())
{
m_target.Reset();
}
}
}
Render 方法首先检查是否有效的管理呈现目标 COM 接口的 ComPtr。 这种方式,它只将重新创建呈现目标在必要时。 这会发生至少一次呈现在窗口第一次。 如果有事情发生到基础设备,或不论何种原因,呈现目标需要重新创建,那么最后的 Render 方法中的 EndDraw 方法图 4 将返回 D2DERR_RECREATE_TARGET 常量。 然后使用 ComPtr 重置方法只是释放该呈现器目标。 该窗口要求绘制自身,在下一次的渲染方法会创建新的 Direct2D 的议案通过呈现目标。
它开始通过获取物理像素中的窗口的工作区。 Direct2D 大部分使用逻辑像素以独占方式,使它能够支持高 DPI 显示自然。 这一点,它将启动物理显示和其逻辑坐标系统之间的关系。 然后,它调用 Direct2D 工厂创建呈现目标对象。 此时它将调用到派生的应用程序窗口类创建任何特定于设备的资源 — — 画笔和所依赖的基本呈现目标设备上的位图的事情。 再次,空的存根 (stub) 是由 DesktopWindow 类提供如果这不需要。
在绘图中之前, 的 Render 方法检查窗口是实际可见的和不完全阻塞。 这样可以避免任何不必要的呈现。 通常情况下,这仅发生在底层的 DirectX 交换链是看不见的例如,当用户锁定或切换桌面时。 BeginDraw 和 EndDraw 方法然后跨越对应用程序窗口绘制方法的调用。 Direct2D 愿借此机会批处理顶点缓冲区中的几何图形、 合并绘制命令,以在 GPU 上提供的最大吞吐量和性能。
最后的关键一步以正确结合一个桌面窗口的 Direct2D 是时调整窗口的大小调整呈现目标。 我已经讲到如何在窗口是自动失效,以确保它迅速重新绘制,但呈现目标本身已经没有窗口的大小已经改变了的想法。 幸运的是,这很容易做,作为图 5 所示。
图 5 调整呈现目标
MESSAGE_HANDLER(WM_SIZE, SizeHandler)
LRESULT SizeHandler(UINT, WPARAM, LPARAM lparam, BOOL &)
{
if (m_target)
{
if (S_OK != m_target->Resize(SizeU(LOWORD(lparam),
HIWORD(lparam))))
{
m_target.Reset();
}
}
return 0;
}
假设 ComPtr 目前拥有一个有效的呈现目标 COM 接口指针,呈现目标调整大小方法称为新的大小,与提供的窗口消息需要 LPARAM 的。 如果然后呈现目标不能调整其内部资源的所有由于某种原因,ComPtr 只复位,迫使要重新创建的下一个时间呈现的呈现目标要求。
而这正是我已在本月的专栏中的余地。 您现在可以创建和管理一个桌面窗口以及使用 GPU 来呈现您的应用程序窗口中您所需要的一切。 随着探索 Direct2D 加入我下个月。
Kenny Kerr 是设在加拿大的 Pluralsight 和 Microsoft MVP 的作者一个计算机程序员。在他的博客 kennykerr.ca 你可以跟随他在 Twitter 上和 twitter.com/kennykerr。
衷心感谢以下技术专家对本文的审阅:Worachai Chaoweeraprasit