Windows Composition номер 10

Volume 30 Number 11

Графика и анимация - Windows Composition номер 10

By Кенни Керр | Windows 2015

В этой статье рассматривается предварительная версия программного обеспечения. Любая изложенная здесь информация может быть изменена.

Продукты и технологии:
Windows Composition, Windows Runtime, Desktop Window Manager, DirectComposition, Direct2D В статье рассматриваются:

  • Desktop Window Manager;
  • эволюция визуальных элементов (visuals) и поверхностей;
  • переход от DirectComposition к Windows Composition;
  • введение в типичный Windows Runtime API;
  • применение Direct2D для рендеринга визуальных элементов.

Механизм композиции Windows (composition engine), иначе известный как Desktop Window Manager (DWM), получил новый API для Windows 10. Ранее основным интерфейсом композиции был DirectComposition, но, как классический COM API, он был по большей части недоступен среднему разработчику приложений. Новый API композиции Windows опирается на Windows Runtime (WinRT) и предоставляет основы для высокопроизводительного рендеринга, сочетая в себе графику прямого режима (immediate-mode graphics), поддерживаемую Direct2D и Direct3D, с сохраненным деревом визуальных элементов, которое теперь обеспечивает намного улучшенные анимацию и эффекты.

Впервые я писал о DWM в 2006 году, когда Windows Vista еще была на стадии бета-версии (goo.gl/19jCyR). Он позволял вам управлять степенью эффекта размывания для данного окна и создавать пользовательских «хром», который красиво смешивался с рабочим столом. Рис. 1 иллюстрирует высоту этого достижения в Windows 7. Благодаря аппаратно ускоряемому рендерингу с помощью Direct3D и Direct2D можно было создавать сногсшибательные визуальные элементы для вашего приложения (goo.gl/IufcN1). Вы могли даже смешивать старый мир GDI и USER-элементов управления с DWM (goo.gl/9ITISE). Тем не менее, любой внимательный наблюдатель мог бы сказать, что DWM способен на гораздо большее. Функциональность Windows Flip 3D в Windows 7 была убедительным доказательством этого.

Windows Aero
Рис. 1. Windows Aero

В Windows 8 ввели новый API для DWM — DirectComposition, имя которого отдает должное DirectX-семейству классических COM API, которые вдохновили проектировщиков на эту архитектуру. DirectComposition начал давать разработчикам более четкую картину того, на что был способен DWM. Кроме того, он предложил улучшенную терминологию. DWM на самом деле является механизмом композиции Windows, и он позволял создавать сногсшибательные эффекты в Windows Vista и Windows 7, так как он фундаментальным образом изменил то, как выполнялся рендеринг окон рабочего стола. По умолчанию механизм композиции создавал поверхность переадресации (redirection surface) для каждого окна верхнего уровня. Я подробно описывал это в своей рубрике за июнь 2014 года (goo.gl/oMlVa4). Эти поверхности образовывали часть дерева визуальных элементов (visual tree), и DirectComposition позволял приложениям пользоваться той же технологией, предоставляя облегченный API режима сохранения (retained-mode API) для высокопроизводительной графики. DirectComposition предлагал управление деревом визуальных элементов и поверхностями, что давало возможность приложению перекладывать создание эффектов и анимаций на механизм композиции. Я описывал эти возможности в своей рубрике за август (goo.gl/CNwnWR) и сентябрь 2014 года (goo.gl/y7ZMLL). Я даже подготовил учебный курс для Pluralsight по высокопроизводительному рендерингу с помощью DirectComposition (goo.gl/fgg0XN).

Windows 8 дебютировала с DirectComposition наряду с впечатляющими усовершенствованиями в остальной части DirectX-семейства API, а также провозгласила новую эру для Windows API, который навсегда изменит то, как разработчики рассматривают операционную систему (ОС). Введение Windows Runtime затмило все остальное. Microsoft обещала совершенно новый способ создания приложений и доступа к сервисам ОС, который знаменовал постепенный уход так называемого Win32 API, столь давно доминировавшего в создании приложений и взаимодействии с ОС. Windows 8 резво взяла старт с большими огрехами, но в Windows 8.1 исправили массу проблем, и теперь Windows 10 предоставляет куда более обширный API, который удовлетворит намного больше разработчиков, заинтересованных в создании первоклассных, я бы даже сказал, серьезных приложений для Windows.

Поставки Windows 10 начались в июле 2015 года с предварительной версией нового API композиции, который еще не был готов к полноценному использованию. Он по-прежнему может быть изменен, а значит, его нельзя применять в универсальных Windows-приложениях, передаваемых в Windows Store. Это только к лучшему, потому что API композиции, который теперь доступен для производственной эксплуатации, значительно изменился, причем в лучшую сторону. Это обновление Windows 10 также является первым случаем, когда один и тот же API композиции доступен на устройствах всех форм-факторов, укрепляя уверенность в универсальности платформы Universal Windows Platform (UWP). Композиция работает одинаково независимо от того, ориентируетесь вы на настольный компьютер с несколькими мониторами или на маленький смартфон.

Естественно, в Windows Runtime всем нравится то, что она наконец предоставляет, как обещали, общеязыковую исполняющую среду (common language runtime) для Windows. Если вы предпочитаете кодировать на C#, то можете использовать Windows Runtime напрямую через поддержку, встроенную в Microsoft .NET Framework. Если же вы, как и я, выбираете C++, то можете задействовать Windows Runtime без промежуточных и дорогостоящих абстракций. Windows Runtime опирается на COM, а не на .NET и, как таковая, идеально подходит для C++. Я буду использовать Modern C++ для Windows Runtime (moderncpp.com), языковую проекцию стандартного C++, но вы можете следовать за мной, применяя свой любимый язык, так как API один и тот же. Я даже предложу некоторые примеры на C#, чтобы проиллюстрировать, насколько бесшовно Windows Runtime может поддерживать разные языки.

API композиции Windows дистанцируется от своих корней в DirectX. Хотя DirectComposition предоставлял объект device, смоделированный по образцу Direct3D- и Direct2D-устройств, новый API композиции Windows начинает с компоновщика (compositor). Однако он служит той же цели, действуя как фабрика для ресурсов композиции. В остальном API композиции Windows очень похож на DirectComposition. Есть мишень композиции (composition target), которая представляет связь между окном и его деревом визуальных элементов. Разница становится более явной, когда вы внимательнее смотрите на визуальные элементы (визуалы). Визуал в DirectComposition имел свойство content, которое предоставляло ту или иную битовую карту. Битовая карта выступала в одной из трех ролей: поверхности композиции (composition surface), DXGI-цепочки обмена (swap chain) или поверхности переадресации другого окна. Типичное DirectComposition-приложение состояло из визуалов и поверхностей, причем поверхности работали как контент или битовые карты для различных визуалов. Как показано на рис. 2, дерево визуальных элементов композиции — штука несколько другая. Новый объект visual не имеет свойства content, и его рендеринг выполняется кистью композиции. Как оказалось, это более гибкая абстракция. Хотя кисть может просто визуализировать битовую карту, как и раньше, простые кисти сплошного цвета можно создавать очень эффективно; кроме того, возможно определение более сложных кистей, по крайней мере концептуально, в почти таком же стиле, как Direct2D предоставляет эффекты, которые можно обрабатывать как изображения. Вся разница в подходящей абстракции.

Дерево визуальных элементов композиции Windows
Рис. 2. Дерево визуальных элементов композиции Windows

Target Мишень
Visual Визуальный элемент
Brush Кисть

Давайте рассмотрим некоторые практические примеры, чтобы увидеть, как все это работает, и дать вам представление о том, что возможно. И вновь вы можете выбрать свою любимую языковую проекцию WinRT. На современном C++ компоновщик создается так:

using namespace Windows::UI::Composition;
Compositor compositor;

Аналогично то же самое можно сделать на C#:

using Windows.UI.Composition;
Compositor compositor = new Compositor();

Вы даже можете использовать более колоритный синтаксис, предлагаемый C++/CX:

using namespace Windows::UI::Composition;
Compositor ^ compositor = ref new Compositor();

Все это эквиваленты с точки зрения API и простое отражение различий в языковых проекциях. Сегодня вы можете писать UWP-приложения в основном двумя способами. По-видимому, самый распространенный подход — использование пространства имен Windows.UI.Xaml в ОС. Если XAML не особо важен в вашем приложении, вы также можете использовать нижележащую модель приложений напрямую безо всякой зависимости от XAML. Я описывал модель WinRT-приложений в своей рубрике за август 2013 года (goo.gl/GI3OKP). Применяя этот подход, вам просто нужна минимальная реализация интерфейсов IFrameworkView и IFrameworkViewSource, после чего вы готовы к дальнейшей работе. На рис. 3 дан базовый каркасный код на C#, который вы можете использовать для начала. Композиция Windows также предлагает глубокую интеграцию с XAML, но давайте начнем с простого приложения без XAML — так будет легче понять композицию. Я вернусь к XAML в этой статье несколько позже.

Рис. 3. Модель приложения Windows Runtime на C#

using Windows.ApplicationModel.Core;
using Windows.UI.Core;
class View : IFrameworkView, IFrameworkViewSource
{
  static void Main()
  {
    CoreApplication.Run(new View());
  }
  public IFrameworkView CreateView()
  {
     return this;
  }
  public void SetWindow(CoreWindow window)
  {
    // Здесь подготавливаем ресурсы композиции...
  }
  public void Run()
  {
    CoreWindow window = CoreWindow.GetForCurrentThread();
    window.Activate();
    window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
  }
  public void Initialize(CoreApplicationView applicationView) { }
  public void Load(string entryPoint) { }
  public void Uninitialize() { }
}

Компоновщик должен конструироваться в методе SetWindow объекта приложения (рис. 3). По сути, это самая ранняя точка в жизненном цикле приложения, где это возможно, так как компоновщик зависит от диспетчера окна, а это та самая точка, в которой окно и диспетчер начинают свое существование. Связь между компоновщиком и представлением приложения может быть после этого установлена созданием мишени композиции:

CompositionTarget m_target = nullptr;
// ...
m_target = compositor.CreateTargetForCurrentView();

Важно, чтобы приложение поддерживало существование мишени композиции, поэтому вы должны сделать ее переменной-членом вашей реализации IFrameworkView. Как упоминалось ранее, мишень композиции представляет связь между окном или представлением и его деревом визуальных элементов. Все, что вы можете сделать с мишенью композиции, — задать корневой визуальный элемент. Как правило, им будет визуал контейнера:

ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root(root);

Здесь я использую C++, в котором недостает языковой поддержки свойств, поэтому свойство Root проецируется как методы-аксессоры. Код на C# очень похож, но добавляется синтаксис свойств:

ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root = root;

DirectComposition предоставлял только один вид визуала, который поддерживал разные виды поверхностей для представления контента битовой карты. Windows-композиция предлагает небольшую иерархию классов, которая представляет разные виды визуалов, кистей и анимаций, тем не менее есть только один вид поверхности, и ее можно создать лишь на C++, потому что он является частью Interop API Windows-композиции, предназначенного для использования инфраструктурами вроде XAML и разработчиками более изощренных приложений.

Иерархия визуальных классов показана на рис. 4. CompositionObject — это ресурс, поддерживаемый компоновщиком (compositor). Все объекты композиции потенциально иметь анимируемые свойства. Visual предоставляет уйму свойств для управления многими аспектами относительной позиции, внешнего вида, параметров отсечения и рендеринга визуала. Он включает свойство матричного преобразования, а также сокращения для масштабирования и поворачивания. Это мощный базовый класс. ContainerVisual, напротив, является сравнительно простым классом, который лишь добавляет свойство Children. Хотя вы можете создавать визуалы контейнера напрямую, SpriteVisual добавляет возможность сопоставлять кисть, чтобы визуальный элемент мог сам выполнять рендеринг своих пикселей.

Визуалы композиции
Рис. 4. Визуалы композиции

Для корневого визуала контейнера можно создать любое количество дочерних визуалов:

VisualCollection children = root.Children();

Они тоже могут быть визуалами контейнера, но более вероятно, что они станут спрайтовыми визуалами (sprite visuals). Я мог бы добавить три визуальных элемента как дочерние корневого визуала, используя цикл for в C++:

using namespace Windows::Foundation::Numerics;
for (unsigned i = 0; i != 3; ++i)
{
  SpriteVisual visual = compositor.CreateSpriteVisual();
  visual.Size(Vector2{ 300.0f, 200.0f });
  visual.Offset(Vector3{ 50 + 20.0f * i, 50 + 20.0f * i });
  children.InsertAtTop(visual);
}

Можно легко вообразить окно приложения на рис. 5, тем не менее этот код не приведет к рендерингу чего бы то ни было, поскольку с этими визуалами не сопоставлена кисть. Иерархия классов кистей представлена на рис. 6. CompositionBrush — это просто базовый класс для кистей, он не предоставляет никакой собственной функциональности. CompositionColorBrush — простейший производный класс, предлагающий только свойство color для рендеринга визуалов сплошным цветом. Возможно, это звучит не слишком захватывающе, но не забудьте, что к этому свойству можно подключать анимации. Классы CompositionEffectBrush и CompositionSurfaceBrush взаимосвязаны, но это более сложные кисти, так как они поддерживаются другими ресурсами. CompositionSurfaceBrush будет выполнять рендеринг поверхности композиции для любых подключенных визуалов. У него множество свойств, управляющих рисованием битовой карты, например интерполяцией, выравниванием, растягиванием, не говоря уже об операциях с самой поверхностью. CompositionEffectBrush принимает ряд кистей поверхности, создавая разнообразные эффекты.

Дочерние визуалы в окне
Рис. 5. Дочерние визуалы в окне

Кисти композиции
Рис. 6. Кисти композиции

Создание и применение цветной кисти — достаточно прямолинейная процедура. Вот пример на C#:

using Windows.UI;
CompositionColorBrush brush = compositor.CreateColorBrush();
brush.Color = Color.FromArgb(0xDC, 0x5B, 0x9B, 0xD5);
visual.Brush = brush;

Создать объект анимации и потом применить эту анимацию к конкретному объекту композиции на удивление легко.

Структура Color предоставляется пространством имен Windows.UI и поддерживает альфа-канал, красный, зеленый и синий как 8-битовые значения цветов, что является отходом от предпочтения значений с плавающей точкой в DirectComposition и Direct2D. Удобство этого подхода к визуалам и кистям в том, что свойство цвета можно изменить в любой момент и любые визуалы, ссылающиеся на ту же кисть, будут автоматически обновлены. Кроме того, как я уже намекал, свойство Color можно даже анимировать. Как же это работает? Ответ на этот вопрос приводит нас к классам анимации.

Иерархия классов анимации показана на рис. 7. Базовый класс CompositionAnimation предоставляет возможность сохранять именованные значения для использования в выражениях. Подробнее о выражениях я расскажу чуть позже. KeyFrameAnimation предоставляет типичные свойства анимации по ключевым кадрам (keyframe-based) вроде длительности, итерации и поведения окончания (stop behavior). Разные классы анимации по ключевым кадрам предлагают специфичные для типа методы вставки ключевых кадров, а также специфичные для типа свойства анимации. Например, ColorKeyFrameAnimation позволяет вставлять ключевые кадры с цветовыми значениями и управлять цветовым пространством для интерполяции между ключевыми кадрами.

Анимации композиции
Рис. 7. Анимации композиции

Создать объект анимации и потом применить эту анимацию к конкретному объекту композиции на удивление легко. Допустим, я хочу анимировать прозрачность визуального элемента. Я мог бы задать прозрачность этого визуала равной 50% прямо в скалярном значении на C++:

visual.Opacity(0.5f);

В качестве альтернативы можно создать объект скалярной анимации с ключевыми кадрами для получения переменной animation со значениями от 0.0 до 1.0, представляющими 0–100% прозрачности:

ScalarKeyFrameAnimation animation =
  compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0.0f, 0.0f); // Optional
animation.InsertKeyFrame(1.0f, 1.0f);

Первый параметр InsertKeyFrame — относительное смещение от начала анимации (0.0) к ее концу (1.0). Второй параметр — это значение переменной animation в этой точке анимационной последовательности. Поэтому данная анимация будет плавно изменять значение от 0.0 до 1.0 в течение длительности анимации. Затем можно указать общую длительность этой анимации:

using namespace Windows::Foundation;
animation.Duration(TimeSpan::FromSeconds(1));

Подготовив анимацию, достаточно подключить ее к объекту композиции и свойству на выбор:

visual.StartAnimation(L"Opacity", animation);

Метод StartAnimation на самом деле наследуется от базового класса CompositionObject, а значит, вы можете анимировать свойства множества различных классов. Это еще один отход от DirectComposition, где каждое анимируемое свойство предоставляло перегрузки для скалярных значений, а также объекты анимации. Windows-композиция предлагает гораздо более богатую систему свойств, которая открывает некоторые очень интересные возможности. В частности, она поддерживает возможность написания текстовых выражений, сокращающих количество кода, необходимого для более интересных анимаций и эффектов. Эти выражения разбираются в период выполнения, компилируются, а затем эффективно исполняются механизмом композиции в Windows.

Вообразите, что вам нужно поворачивать визуальный элемент по оси Y и придать глубину его внешнему виду. Свойства RotationAngle визуального элемента, измеряемого в радианах, недостаточно, потому что оно не создаст преобразование, которое включает перспективу. По мере поворачивания визуала край, ближайший к человеческому глазу, должен выглядеть крупнее, а противоположный край — меньшим. На рис. 8 показан ряд вращающихся визуальных элементов, иллюстрирующих это поведение.

Вращение визуалов
Рис. 8. Вращение визуалов

Как вы могли бы реализовать такой анимированный эффект? Что ж, давайте начнем со скалярной анимации по ключевым кадрам для угла поворота:

ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(1.0f, 2.0f * Math::Pi,
  compositor.CreateLinearEasingFunction());
animation.Duration(TimeSpan::FromSeconds(2));
animation.IterationBehavior(AnimationIterationBehavior::Forever);

Функция линейной плавности (linear easing function) переопределяет исходную функцию ускорения/замедления для создания непрерывного вращения. Затем нужно определить пользовательский объект со свойством, на которое можно ссылаться из выражения. Компоновщик предоставляет набор свойств как раз для этой цели:

CompositionPropertySet rotation = compositor.CreatePropertySet();
rotation.InsertScalar(L"Angle", 0.0f);

Набор свойств также является объектом композиции, поэтому можно использовать метод StartAnimation, чтобы анимировать пользовательское свойство так же легко, как любое встроенное:

rotation.StartAnimation(L"Angle", animation);

WinRT-классы могут реализовать дополнительные COM-интерфейсы, не видимые напрямую, если у вас есть лишь метаданные компонента в Windows.

Теперь у меня есть объект, чье свойство Angle «находится в движении». Далее нужно определить матрицу преобразования для создания требуемого эффекта, в то же время делегируя его этому анимированному свойству для самого угла поворота. Знакомьтесь с выражениями (expressions):

ExpressionAnimation expression =
  compositor.CreateExpressionAnimation(
    L"pre * Matrix4x4.CreateFromAxisAngle(axis, rotation.Angle) * post");

Анимация на основе выражения (expression animation) не является объектом анимации по ключевым кадрам, поэтому здесь нет относительных смещений по ключевым кадрам, на которые могли бы изменяться переменные, управляющие анимацией (с применением какой-либо функции интерполяции). Вместо этого выражения просто ссылаются на параметры, которые сами могут анимироваться в более традиционном смысле. Тем не менее, обязанность определения того, что такое «pre», «axis», «rotation» и «post», лежит на мне. Начнем с параметра axis:

expression.SetVector3Parameter(L"axis", Vector3{ 0.0f, 1.0f, 0.0f });

Метод CreateFromAxisAngle внутри выражения ожидает поворачивания оси, и это определяет ось вокруг оси Y. Он также ожидает передачи угла поворота и в этом мы можем положиться на набор свойств rotation с его анимируемым свойством Angle:

expression.SetReferenceParameter(L"rotation", rotation);

Чтобы гарантировать поворачивание по центру визуального элемента, а не по левому краю, нужно заранее перемножить матрицу вращения, созданную CreateFromAxisAngle, с матрицей трансляции, которая логически смещает ось в точку вращения:

expression.SetMatrix4x4Parameter(
  L"pre", Matrix4x4::Translation(-width / 2.0f, -height / 2.0f, 0.0f));

Вспомните, что перемножение матриц не является перестановочным (commutative), поэтому матрицы pre и post действительно таковы, как есть. Наконец, после матрицы вращения можно добавить некоторую перспективу, а затем восстановить визуальный элемент в его исходном местоположении:

expression.SetMatrix4x4Parameter(
  L"post", Matrix4x4::PerspectiveProjection(width * 2.0f) *
    Matrix4x4::Translation(width / 2.0f, height / 2.0f, 0.0f));

Это удовлетворяет все параметры, на которые ссылается выражение, и теперь можно просто использовать анимацию на основе выражения, чтобы анимировать визуал, используя его свойство TransformMatrix:

visual.StartAnimation(L"TransformMatrix", expression);

На данный момент я исследовал разнообразные способы создания, заполнения и анимации визуальных элементов, но как быть, если мне потребуется выполнять рендеринг визуалов напрямую? DirectComposition предлагал как предварительно выделенные поверхности (preallocated surfaces), так и разреженные битовые карты (sparsely allocated bitmaps), называемые виртуальными поверхностями, которые выделялись по запросу и были масштабируемыми. Windows-композиция, на первый взгляд, вообще не позволяет создавать поверхности. Есть класс CompositionDrawingSurface, но нет способа создать его без помощи извне. Ответ кроется в Interop API механизма композиции Windows. WinRT-классы могут реализовать дополнительные COM-интерфейсы, не видимые напрямую, если у вас есть лишь метаданные компонента в Windows. Зная об этих замаскированных интерфейсах, можно легко запросить их на C++. Естественно, это потребует несколько больше работы, когда вы шагнете за границы изящных абстракций, предоставляемых рядовым разработчикам через API композиции Windows. Так что первым делом мне нужно создать устройство рендеринга и использовать Direct3D 11, поскольку Windows-композиция пока не поддерживает Direct3D 12:

ComPtr<ID3D11Device> direct3dDevice;

Затем я установлю флаги создания устройства:

unsigned flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT |
                 D3D11_CREATE_DEVICE_SINGLETHREADED;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

Поддержка BGRA позволяет использовать более доступный Direct2D API для рендеринга с помощью этого устройства, а затем функция D3D11CreateDevice создает само аппаратное устройство:

check(D3D11CreateDevice(nullptr, // адаптер
                        D3D_DRIVER_TYPE_HARDWARE,
                        nullptr, // модуль
                        flags,
                        nullptr, 0, // наивысший уровень
                                    // возможностей
                        D3D11_SDK_VERSION,
                        set(direct3dDevice),
                        nullptr, // реальный уровень
                                 // возможностей
                        nullptr)); // контекст устройства

Далее нужно запросить DXGI-интерфейс устройства, так как это требуется для создания Direct2D-устройства:

ComPtr<IDXGIDevice3> dxgiDevice = direct3dDevice.As<IDXGIDevice3>();

Теперь пора создать само Direct2D-устройство:

ComPtr<ID2D1Device> direct2dDevice;

Здесь я вновь разрешу применение отладочного уровня для дополнительной диагностики:

D2D1_CREATION_PROPERTIES properties = {};
#ifdef _DEBUG
properties.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

Я мог бы сначала создать Direct2D-фабрику для создания устройства. Это было бы полезно, если бы я хотел создать аппаратно-независимые ресурсы. Здесь я просто использую сокращение, предоставляемое функцией D2D1CreateDevice:

check(D2D1CreateDevice(get(dxgiDevice), properties, set(direct2dDevice)));

Поскольку CompositionGraphicsDevice является WinRT-типом, вновь можно использовать современный C++.

Устройство рендеринга готово. У меня имеется Direct2D-устройство, которое можно использовать для рендеринга чего угодно. Теперь я должен сообщить механизму композиции Windows об этом устройстве. И здесь в игру вступают те самые замаскированные интерфейсы. Располагая компоновщиком, который я постоянно применял, я могу запросить интерфейс ICompositorInterop:

namespace abi = ABI::Windows::UI::Composition;
ComPtr<abi::ICompositorInterop> compositorInterop;
check(compositor->QueryInterface(set(compositorInterop)));

ICompositorInterop предоставляет методы для создания поверхности композиции из DXGI-поверхности, что определенно было бы удобно, если бы я хотел включить существующую цепочку обмена в дерево визуальных элементов композиции. Но он содержит кое-что еще, куда более интересное. Его метод CreateGraphicsDevice создаст объект CompositionGraphicsDevice для данного устройства рендеринга. CompositionGraphicsDevice является обычным классом в API композиции Windows, а не замаскированным интерфейсом, но у него нет конструктора, поэтому придется создать его с помощью C++ и интерфейса ICompositorInterop:

CompositionGraphicsDevice device = nullptr;
check(compositorInterop->CreateGraphicsDevice(get(direct2dDevice), set(device)));

Поскольку CompositionGraphicsDevice является WinRT-типом, вновь можно использовать современный C++, а не указатели и ручную обработку ошибок. И именно CompositionGraphicsDevice наконец-то позволяет мне создать поверхность композиции:

using namespace Windows::Graphics::DirectX;
CompositionDrawingSurface surface =
  compositionDevice.CreateDrawingSurface(Size{ 100, 100 },
    DirectXPixelFormat::B8G8R8A8UIntNormalized,
    CompositionAlphaMode::Premultiplied);

Здесь я создаю поверхность композиции размером 100 × 100 пикселей. Заметьте, что это физические пиксели, а не логические координаты с поддержкой DPI, предполагаемые и предоставляемые в остальной части Windows-композиции. Поверхность также обеспечивает рендеринг со смешиванием по 32-разрядному альфа-каналу, поддерживаемый Direct2D. Конечно, Direct3D и Direct2D пока не предоставляются через Windows Runtime, поэтому для рисования на этой поверхности придется вернуться к замаскированным интерфейсам:

ComPtr<abi::ICompositionDrawingSurfaceInterop> surfaceInterop;
check(surface->QueryInterface(set(surfaceInterop)));

Во многом аналогично DirectComposition механизм композиции Windows поддерживает методы BeginDraw и EndDraw в интерфейсе ICompositionDrawingSurfaceInterop, которые заменяют типичные вызовы Direct2D-методов под теми же именами:

ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
check(surfaceInterop->BeginDraw(nullptr, // обновляем
                                         // прямоугольник
                                  __uuidof(dc),
                                reinterpret_cast<void **>(set(dc)),
                                &offset));

Windows-композиция принимает исходное устройство рендеринга, переданное в момент создания устройства композиции, и на его основе создает контекст устройства или мишень рендеринга. При желании я могу передать отсекающий прямоугольник с размерами в физических пикселях, но здесь я просто выбираю неограниченный доступ к поверхности рендеринга. BeginDraw также возвращает смещение (вновь в физических пикселях), в том числе начало координат предполагаемой поверхности рисования. Оно не обязательно находится в левом верхнем углу мишени рендеринга, поэтому нужно позаботиться о подстройке или преобразовании любых команд рисования для корректного использования этого смещения. И вновь не вызывайте BeginDraw применительно к мишени рендеринга, поскольку механизм композиции Windows уже сделал это за вас. Эта мишень логически принадлежит API композиции, и следует быть осторожным, чтобы не удерживать ее после вызова EndDraw. Теперь мишень рендеринга готова, но не осведомлена о логическом или эффективном DPI для представления. Я могу использовать пространство имен Windows::Graphics::Display, чтобы получить логический DPI для текущего представления и задать DPI, который будет применяться Direct2D при рендеринге:

using namespace Windows::Graphics::Display;
DisplayInformation display = DisplayInformation::GetForCurrentView();
float const dpi = display.LogicalDpi();
dc->SetDpi(dpi, dpi);

Заключительный этап перед тем, как может начаться рендеринг, — та или иная обработка смещения в композиции. Одно из простых решений в том, чтобы использовать это смещение для создания матрицы преобразования. Вспомните, что Direct2D оперирует логическими пикселями, а значит, я должен использовать не только смещение, но и недавно заданное значение DPI:

dc->SetTransform(D2D1::Matrix3x2F::Translation(offset.x * 96.0f / dpi,
                                               offset.y * 96.0f / dpi));

К этому моменту вы можете рисовать что угодно до вызова метода EndDraw применительно к interop-интерфейсу поверхности, чтобы быть уверенным в том, что любые пакеты Direct2D-команд рисования обрабатываются и изменения на поверхности отражаются в дереве визуальных элементов композиции:

check(surfaceInterop->EndDraw());

Конечно, я пока не сопоставил эту поверхность с визуальным элементом и, как упоминал ранее, у визуалов больше нет свойства content, а потому их рендеринг должен выполняться с помощью кисти. К счастью, компоновщик создаст кисть, представляющую уже подготовленную поверхность:

CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);

Затем можно создать обычную спрайтовую кисть и использовать ее для того, что вытащить визуальный элемент на свет божий:

SpriteVisual visual = compositor.CreateSpriteVisual();
visual.Brush(brush);
visual.Size(Vector2{ ... });

Если вам по-прежнему недостаточно возможностей взаимодействия, вы можете даже взять XAML-элемент и получить нижележащий визуальный элемент композиции. Вот пример на C#:

using Windows.UI.Xaml.Hosting;
Visual visual = ElementCompositionPreview.GetElementVisual(button);

Несмотря на кажущийся временный статус, ElementCompositionPreview на самом деле готов к коммерческому использованию и может быть задействован в приложениях, передаваемых в Windows Store. Для любого UI-элемента статический метод GetElementVisual вернет визуальный элемент из нижележащего дерева визуальных элементов композиции. Заметьте, что он возвращает Visual, а не ContainerVisual или SpriteVisual, поэтому работать напрямую с дочерними визуальными элементами или применять кисть нельзя, но можно настраивать многие свойства визуальных элементов, предлагаемые Windows-композицией. Вспомогательный класс ElementCompositionPreview содержит некоторые дополнительные статические методы для добавления дочерних визуальных элементов под вашим контролем. Вы можете изменять смещение визуала и операции вроде проверки на попадание курсора в те или иные части UI (hit testing) по-прежнему будут работать на уровне XAML. Вы можете даже применять анимацию напрямую в Windows-композиции, не разрушая инфраструктуру XAML, которая опирается на нее. Давайте создадим простую скалярную анимацию для поворота кнопки. Мне нужно получить компоновщик от визуального элемента, а затем создать объект анимации, как и раньше:

Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();

Сформируем простую анимацию для медленного вечного вращения кнопки с применением функции линейной плавности:

animation.InsertKeyFrame(1.0f, (float) (2 * Math.PI),
  compositor.CreateLinearEasingFunction());

Затем я указываю, что один полный поворот должен занимать три секунды, а вращение — продолжаться вечно:

animation.Duration = TimeSpan.FromSeconds(3);
animation.IterationBehavior = AnimationIterationBehavior.Forever;

Наконец, я просто соединяя анимацию с визуальным элементом, предоставленным XAML, указывая механизму композиции анимировать его свойство RotationAngle:

visual.StartAnimation("RotationAngle", animation);

Хотя вы, возможно, сумеете вытянуть это из одного XAML, механизм композиции Windows предоставляет гораздо большую мощь и гибкость, учитывая, что он располагается на куда более низком уровне абстракции и, несомненно, обеспечивает более высокую производительность. Как еще один пример, Windows-композиция предоставляет кватернионные анимации (quaternion animations), в настоящее время не поддерживаемые XAML.

Говорить о механизме композиции Windows можно и нужно гораздо дольше и подробнее. По моему скромному мнению, на сегодняшний день это самый прорывной WinRT API.

Говорить о механизме композиции Windows можно и нужно гораздо дольше и подробнее. По моему скромному мнению, на сегодняшний день это самый прорывной WinRT API. Уровень его мощи просто ошеломляет, и в отличие от столь многих крупных API для UI и графики он не требует жертвовать производительностью и даже чрезмерно длительного освоения. Во многих отношениях Windows-композиция является наиболее репрезентативной среди всего того, что есть хорошего и интересного на платформе Windows.

Следите за заметками группы Windows Composition в Twitter: @WinComposition.


Кенни Керр (Kenny Kerr)— высококвалифицированный программист. Живет в Канаде. Автор учебных курсов для Pluralsight, обладатель звания Microsoft MVP. Ведет блог kennykerr.ca. Кроме того, читайте его заметки в twitter.com/kennykerr.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Марку Элдэму (Mark Aldham), Джеймсу Кларку (James Clarke), Джону Серна (John Serna), Джеффри Столлу (Jeffrey Stall) и Нику Уоггонеру (Nick Waggoner).