Механизм композиции в Windows (Windows composition engine) отражает отход от концепции, согласно которой каждое DirectX-приложение требует создания собственной цепочки обмена (swap chain), и движение в сторону той, где даже столь фундаментальная конструкция не обязательна. Разумеется, вы можете по-прежнему писать Direct3D- и Direct2D-приложения, используя цепочку обмена для презентации, но больше не обязаны делать это. Механизм композиции значительно приближает нас к «железу» — GPU, позволяя приложениям напрямую создавать поверхности композиции.
Единственное предназначение механизма композиции — составлять воедино различные битовые карты. Вы можете запрашивать выполнение разнообразных эффектов, преобразований и даже анимаций, но в конечном счете все сводится к композиции битовых карт. Сам механизм не обладает никакими средствами рендеринга графики, например теми, которые предоставляются Direct2D или Direct3D, и не определяет векторы или текст. Все, что его волнует, — композиция. Передайте ему набор битовых карт, и он будет делать удивительные вещи, комбинируя и составляя их вместе.
Эти битовые карты могут принимать любые формы. Битовая карта на самом деле может быть видеопамятью. Она может быть даже цепочкой обмена, как я проиллюстрировал в своей рубрике на июнь (msdn.microsoft.com/magazine/dn745861). Но если вы действительно хотите начать использовать механизм композиции, вам нужно изучить поверхности композиции. Поверхность композиции (composition surface) — это битовая карта, предоставляемая непосредственно механизмом композиции, и, как таковая, она обеспечивает определенные оптимизации, которых сложно добиться при работе с другими формами битовых карт.
На этот раз я намерен взять окно со смешиванием по альфа-каналу из своей предыдущей статьи и показать, как его можно воспроизвести с помощью поверхности композиции вместо цепочки обмена. Это дает некоторые интересные преимущества и в то же время ставит уникальные и трудные задачи — особенно то, как в этом случае нужно обрабатывать потерю устройства и масштабирование DPI индивидуально для каждого монитора (per-monitor DPI scaling). Но сначала мне придется вернуться к вопросу управления окнами.
В предыдущих выпусках своей рубрики я использовал либо ATL для управления окнами, либо демонстрировал, как регистрировать, создавать и «прокачивать» (pump) оконные сообщения непосредственно в Windows API. У обоих подходов есть свои плюсы и минусы. ATL по-прежнему прекрасно подходит для управления окнами, но постепенно теряет интерес со стороны разработчиков, поскольку Microsoft явно уже давно прекратила как-либо ее развивать. С другой стороны, создание окна напрямую с помощью RegisterClass и CreateWindow потенциально проблематично, так как вам не удастся легко сопоставить C++-объект с описателем окна (window handle). Если вы когда-либо подумывали о подготовке к такому союзу, то, наверное, не удержались от соблазна подглядеть исходный код ATL, чтобы понять, как этого добиться. И наверняка лишь убедились, что в нем хватает «черной магии», которая творится с некими сущностями — переходными шлюзами (thunks) и даже с языком ассемблера.
Хорошая новость в том, что не обязательно идти по столь трудному пути. Хотя ATL определенно обеспечивает очень эффективную диспетчеризацию сообщений, простое решение, включающее только стандартный C++, позволяет добиться ровным счетом того же. Я не хочу слишком сильно отвлекаться на механику оконных процедур, поэтому просто отсылаю вас к рис. 1, где показан несложный шаблон класса, который выполняет необходимую подготовительную работу для сопоставления указателя this с окном. Этот шаблон использует сообщение WM_NCCREATE для получения указателя и сохраняет его вместе с описателем окна. Впоследствии указатель извлекается, и сообщения посылаются самому крайнему в цепочке наследования (most derived) обработчику сообщений.
Рис. 1. Простой шаблон оконного класса
template <typename T>
struct Window
{
HWND m_window = nullptr;
static T * GetThisFromHandle(HWND window)
{
return reinterpret_cast<T *>(GetWindowLongPtr(window,
GWLP_USERDATA));
}
static LRESULT __stdcall WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam)
{
ASSERT(window);
if (WM_NCCREATE == message)
{
CREATESTRUCT * cs = reinterpret_cast<CREATESTRUCT *>(lparam);
T * that = static_cast<T *>(cs->lpCreateParams);
ASSERT(that);
ASSERT(!that->m_window);
that->m_window = window;
SetWindowLongPtr(window,
GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(that));
}
else if (T * that = GetThisFromHandle(window))
{
return that->MessageHandler(message,
wparam,
lparam);
}
return DefWindowProc(window,
message,
wparam,
lparam);
}
LRESULT MessageHandler(UINT const message,
WPARAM const wparam,
LPARAM const lparam)
{
if (WM_DESTROY == message)
{
PostQuitMessage(0);
return 0;
}
return DefWindowProc(m_window,
message,
wparam,
lparam);
}
};
Предположение заключается в том, что некий производный класс будет создавать окно и передавать указатель this как последний параметр при вызове функции CreateWindow или CreateWindowEx. Производный класс может просто зарегистрировать и создать окно, а затем реагировать на оконные сообщения с помощью переопределенной версии MessageHandler. Это переопределение полагается на полиморфизм этапа компиляции, поэтому нет нужды в виртуальных функциях. Однако эффект это дает тот же, и вам по-прежнему нужно позаботиться о реентерабельности. На рис. 2 показан конкретный оконный класс, который опирается на шаблон класса Window. Этот класс создает и регистрирует окно в своем конструкторе, но полагается на оконную процедуру, предоставляемую его базовым классом.
Рис. 2. Конкретный оконный класс
struct SampleWindow : Window<SampleWindow>
{
SampleWindow()
{
WNDCLASS wc = {};
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hInstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
wc.lpszClassName = L"SampleWindow";
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
RegisterClass(&wc);
ASSERT(!m_window);
VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP,
wc.lpszClassName,
L"Window Title",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
nullptr,
nullptr,
wc.hInstance,
this));
ASSERT(m_window);
}
LRESULT MessageHandler(UINT message,
WPARAM const wparam,
LPARAM const lparam)
{
if (WM_PAINT == message)
{
PaintHandler();
return 0;
}
return __super::MessageHandler(message,
wparam,
lparam);
}
void PaintHandler()
{
// Рендер...
}
};
Заметьте, что в конструкторе на рис. 2 наследуемый член m_window не инициализирован (nullptr) до вызова CreateWindow, но становится инициализированным после возврата управления этой функцией. Это может показаться чем-то вроде шаманства, но все дело в оконной процедуре, которая подключается, когда начинают поступать сообщения задолго до возврата функции CreateWindow. Почему важно это учитывать? Используя код наподобие этого, вы можете воспроизвести тот же опасный эффект, что и при вызове виртуальных функций из конструктора. Если вы намерены продолжить наследование, обязательно вынесите создание окна из конструктора, чтобы эта форма реентерабельности не подставила вам подножку. Вот простая функция WinMain, которая может создать окно и прокачивать оконные сообщения:
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
SampleWindow window;
MSG message;
while (GetMessage(&message, nullptr, 0, 0))
{
DispatchMessage(&message);
}
}
Хорошо, вернемся к основной теме. Теперь, когда у меня есть простая абстракция оконного класса, я могу упростить управление набором ресурсов, необходимых для создания DirectX-приложения. Кроме того, я покажу, как правильно обрабатывать масштабирование DPI. Хотя я подробно осветил эту тему в своей рубрике за февраль 2014 года (msdn.microsoft.com/magazine/dn574798), при комбинировании масштабирования DPI с DirectComposition API возникают некоторые уникальные трудные задачи. Я начну с верхнего уровня. Мне нужно включить API масштабирования оболочки:
#include <ShellScalingAPI.h>
#pragma comment(lib, "shcore")
Теперь я могу приступить к сборке ресурсов, необходимых для воплощения в жизнь моего окна. Учитывая, что у меня есть оконный класс, можно просто сделать их членами класса. Сначала Direct3D-устройство:
ComPtr<ID3D11Device> m_device3d;
Потом устройство DirectComposition:
ComPtr<IDCompositionDesktopDevice> m_device;
В прошлой статье я использовал интерфейс IDCompositionDevice для представления устройства композиции. Это интерфейс, появившийся в Windows 8, но в Windows 8.1 замененный интерфейсом IDCompositionDesktopDevice, который наследует от другого нового интерфейса, IDCompositionDevice2. Они никак не связаны с оригиналом. Интерфейс IDCompositionDevice2 служит для создания большинства ресурсов композиции и, кроме того, управляет транзакционной композицией (transactional composition). Интерфейс IDCompositionDesktopDevice добавляет возможность создавать некоторые специфичные для окна ресурсы композиции.
Мне также нужна мишень композиции (composition target), визуал (visual) и поверхность:
ComPtr<IDCompositionTarget> m_target;
ComPtr<IDCompositionVisual2> m_visual;
ComPtr<IDCompositionSurface> m_surface;
Мишень композиции представляет привязку между окном рабочего стола и визуальным деревом (visual tree). На самом деле я могу сопоставить с одним окном два визуальных дерева, но подробнее об этом мы поговорим в одной из будущих статей. Визуал представляет узел в визуальном дереве. Я собираюсь исследовать визуалы в следующей статье, поэтому на данный момент у меня будет только один корневой визуал. Здесь я просто использую интерфейс IDCompositionVisual2, производный от интерфейса IDCompositionVisual, рассмотренного в предыдущей статье. Наконец, поверхность представляет контент или битовую карту, сопоставленную с визуалом. В прошлой статье я использовал цепочку обмена как контент визуала, но вскоре покажу, как создать вместо этого поверхность композиции.
Чтобы проиллюстрировать, как выполнять рендеринг чего-либо и управлять ресурсами рендеринга, мне нужно еще несколько переменных-членов:
ComPtr<ID2D1SolidColorBrush> m_brush;
D2D_SIZE_F m_size;
D2D_POINT_2F m_dpi;
SampleWindow() :
m_size(),
m_dpi()
{
// RegisterClass / CreateWindowEx, как и раньше
}
Сплошная цветная Direct2D-кисть обходится весьма недорого в создании, но многие другие ресурсы рендеринга гораздо тяжеловеснее. Я буду использовать эту кисть для иллюстрации того, как создавать ресурсы рендеринга вне цикла рендеринга. DirectComposition API при необходимости также берет на себя создание Direct2D-мишени рендеринга. Это позволяет вам использовать поверхность композиции через Direct2D, но также означает, что вы теряете часть контекстно-зависимой информации. Конкретнее, вы больше не можете кешировать применимый множитель масштабирования DPI в мишени рендера, так как DirectComposition создает его за вас по запросу. Кроме того, вы больше не можете полагаться на метод GetSize мишени рендера, чтобы узнать размер окна. Но не волнуйтесь: вскоре я покажу, как компенсировать все эти недостатки.
Как и в случае любого приложения, которое опирается на Direct3D-устройство, мне нужно соблюдать осторожность в управлении ресурсами, которые находятся на физическом устройстве, исходя из предположения, что устройство может быть потеряно когда угодно. GPU может быть удален, может зависнуть, перезапуститься или просто рухнуть. Вдобавок нужно быть аккуратным, чтобы не допустить ошибочной реакции на оконные сообщения, которые могут поступать до создания стека устройства. Я буду использовать указатель на Direct3D-устройство, чтобы указывать, создано ли устройство:
bool IsDeviceCreated() const
{
return m_device3d;
}
Это просто помогает сделать запрос явным. Я буду также использовать этот указатель для инициации сброса стека устройства, чтобы принудительно вызвать создание всех аппаратно-зависимых ресурсов заново:
void ReleaseDeviceResources()
{
m_device3d.Reset();
}
И вновь это просто помогает сделать данную операцию явной. Здесь я мог бы освободить все аппаратно-зависимые ресурсы, но это не является жестким требованием и может быстро превратиться в головную боль при сопровождении по мере добавления или удаления различных ресурсов. Сердцевина процесса создания устройства находится в другом вспомогательном методе:
void CreateDeviceResources()
{
if (IsDeviceCreated()) return;
// Создаем устройства и ресурсы...
}
Именно в методе CreateDeviceResources я могу создавать (в том числе, заново) стек устройства, аппаратное устройство и различные ресурсы, необходимые окну. Сначала я создаю Direct3D-устройство, на котором основано все остальное:
HR(D3D11CreateDevice(nullptr, // адаптер
D3D_DRIVER_TYPE_HARDWARE,
nullptr, // модуль
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
nullptr, 0, // максимально доступный уровень возможностей
D3D11_SDK_VERSION,
m_device3d.GetAddressOf(),
nullptr, // реально доступный уровень возможностей
nullptr)); // контекст устройства
Обратите внимание на то, как полученный указатель на интерфейс захватывается членом m_device3d. Теперь нужно запросить интерфейс DXGI устройства:
ComPtr<IDXGIDevice> devicex;
HR(m_device3d.As(&devicex));
В прошлой статье именно в этот момент я создал фабрику DXGI и цепочку обмена для использования в композиции. Создав цепочку обмена, я обернул ее в битовую карту Direct2D, битовую карту связал с контекстом устройства и т. д. В данном случае я буду действовать по-другому. Создав Direct3D-устройство, я создам Direct2D-устройство, указывающее на первое, а затем создам устройство DirectComposition, указывающее на Direct2D-устройство. Вот Direct2D-устройство:
ComPtr<ID2D1Device> device2d;
HR(D2D1CreateDevice(devicex.Get(),
nullptr, // свойства по умолчанию
device2d.GetAddressOf()));
Я использую вспомогательную функцию, предоставляемую Direct2D API, вместо более привычного объекта фабрики Direct2D. Полученное Direct2D-устройство просто наследует модель потоков от DXGI-устройства, но вы можете переопределить это, а также включить отладочную трассировку. Вот устройство DirectComposition:
HR(DCompositionCreateDevice2(
device2d.Get(),
__uuidof(m_device),
reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));
Для предосторожности я использую метод ReleaseAndGetAddressOf члена m_device, чтобы поддерживать повторное создание стека устройства после потери этого устройства. Располагая устройством композиции, теперь можно создать мишень композиции, как это делалось в предыдущей статье:
HR(m_device->CreateTargetForHwnd(m_window,
true, // самое верхнее
m_target.ReleaseAndGetAddressOf()));
И корневой визуал:
HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));
Теперь пора сосредоточиться на поверхности композиции, которая заменяет цепочку обмена. Как и фабрика DXGI, которая не имела никакого представления о том, насколько велики должны быть буферы цепочки обмена при вызове мной метода CreateSwapChainForComposition, устройство DirectComposition не имеет ни малейшего понятия, насколько велика должна быть нижележащая поверхность. Мне нужно запросить размер клиентской области окна и сообщить эту информацию процессу создания поверхности:
RECT rect = {};
VERIFY(GetClientRect(m_window,
&rect));
В структуре RECT есть члены left, top, right и bottom, с помощью которых можно определить нужный размер создаваемой поверхности в физических пикселях:
HR(m_device->CreateSurface(rect.right - rect.left,
rect.bottom - rect.top,
DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_ALPHA_MODE_PREMULTIPLIED,
m_surface.ReleaseAndGetAddressOf()));
Учитывайте, что реальная поверхность может оказаться больше запрошенного размера. Дело в том, что механизм композиции может группировать операции выделения памяти (allocations) для большей эффективности. Это не проблема, но она может повлиять на полученный контекст устройства, поскольку вам не удастся положиться на его метод GetSize; об этом мы поговорим чуть позже.
Параметры метода CreateSurface, к счастью, являются упрощением структуры DXGI_SWAP_CHAIN_DESC1, состоящей из множества полей. Вслед за размером я задаю формат пикселей и альфа-режим, и устройство композиции возвращает указатель на только что созданную поверхность композиции. Затем эту поверхность можно просто установить как контент визуального объекта и задать этот визуал в качестве корня мишени композиции:
HR(m_visual->SetContent(m_surface.Get()));
HR(m_target->SetRoot(m_visual.Get()));
Однако на этом этапе мне не нужно вызывать метод Commit устройства композиции. Я буду обновлять поверхность композиции в своем цикле рендеринга, но эти изменения вступят в силу, только когда будет вызван метод Commit. К этому моменту механизм композиции готов начать рендеринг, но я еще не связал все концы. Они не имеют отношения к композиции, тем не менее важны для корректного и эффективного использования Direct2D при рендеринге. Прежде всего вне цикла рендеринга следует создать любые ресурсы, специфичные для мишени рендера, такие как битовые карты и кисти. Это может оказаться несколько затруднительным, потому что мишень рендера создается DirectComposition. К счастью, единственное требование заключается в том, что эти ресурсы должны создаваться в том же адресном пространстве, что и мишень рендера, поэтому здесь я просто использую временный контекст устройства, чтобы создать такие ресурсы:
ComPtr<ID2D1DeviceContext> dc;
HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
dc.GetAddressOf()));
Затем с помощью этой мишени рендера я создаю единственную в приложении кисть:
D2D_COLOR_F const color = ColorF(0.26f,
0.56f,
0.87f,
0.5f);
HR(dc->CreateSolidColorBrush(color,
m_brush.ReleaseAndGetAddressOf()));
Далее этот контекст устройства отбрасывается, а кисть повторно используется в цикле рендеринга. Хотя это не слишком интуитивно понятно, но вскоре все прояснится. Последнее, что мне остается сделать перед рендерингом, — заполнить переменные-члены m_size и m_dpi. Обычно метод GetSize мишени рендера Direct2D предоставляет размер этой мишени в логических пикселях, также известных как аппаратно-независимые пиксели. В этом логическом размере уже учитывается действующий DPI, поэтому сначала займемся им. Как было показано в моей статье за февраль 2014 года, посвященной приложениям с поддержкой высокого DPI, я могу запросить реальный DPI для конкретного окна, сначала определив монитор, где находится основная часть данного окна, а затем получив действующий DPI на этом мониторе. Вот как это выглядит:
HMONITOR const monitor = MonitorFromWindow(m_window,
MONITOR_DEFAULTTONEAREST);
unsigned x = 0;
unsigned y = 0;
HR(GetDpiForMonitor(monitor,
MDT_EFFECTIVE_DPI,
&x,
&y));
Эти значения можно потом кешировать в члене m_dpi, чтобы иметь возможность легко обновлять контекст устройства, предоставляемый DirectComposition API внутри цикла рендеринга:
m_dpi.x = static_cast<float>(x);
m_dpi.y = static_cast<float>(y);
Теперь вычисление логического размера клиентской области в логических пикселях заключается в простом получении структуры RECT, где уже хранится размер в физических пикселях, и масштабировании под имеющиеся значения действующего DPI:
m_size.width = (rect.right - rect.left) * 96 / m_dpi.x;
m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y;
И на этом работа метода CreateDeviceResources заканчивается. Как все это сводится воедино, показано на рис. 3, где метод CreateDeviceResources приведен полностью.
Рис. 3. Создание стека устройства
void CreateDeviceResources()
{
if (IsDeviceCreated()) return;
HR(D3D11CreateDevice(nullptr, // адаптер
D3D_DRIVER_TYPE_HARDWARE,
nullptr, // модуль
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
nullptr, 0, // максимально доступный уровень возможностей
D3D11_SDK_VERSION,
m_device3d.GetAddressOf(),
nullptr, // реально доступный уровень возможностей
nullptr)); // контекст устройства
ComPtr<IDXGIDevice> devicex;
HR(m_device3d.As(&devicex));
ComPtr<ID2D1Device> device2d;
HR(D2D1CreateDevice(devicex.Get(),
nullptr, // свойства по умолчанию
device2d.GetAddressOf()));
HR(DCompositionCreateDevice2(
device2d.Get(),
__uuidof(m_device),
reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));
HR(m_device->CreateTargetForHwnd(m_window,
true, // самое верхнее
m_target.ReleaseAndGetAddressOf()));
HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));
RECT rect = {};
VERIFY(GetClientRect(m_window,
&rect));
HR(m_device->CreateSurface(rect.right - rect.left,
rect.bottom - rect.top,
DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_ALPHA_MODE_PREMULTIPLIED,
m_surface.ReleaseAndGetAddressOf()));
HR(m_visual->SetContent(m_surface.Get()));
HR(m_target->SetRoot(m_visual.Get()));
ComPtr<ID2D1DeviceContext> dc;
HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
dc.GetAddressOf()));
D2D_COLOR_F const color = ColorF(0.26f,
0.56f,
0.87f,
0.5f);
HR(dc->CreateSolidColorBrush(color,
m_brush.ReleaseAndGetAddressOf()));
HMONITOR const monitor = MonitorFromWindow(m_window,
MONITOR_DEFAULTTONEAREST);
unsigned x = 0;
unsigned y = 0;
HR(GetDpiForMonitor(monitor,
MDT_EFFECTIVE_DPI,
&x,
&y));
m_dpi.x = static_cast<float>(x);
m_dpi.y = static_cast<float>(y);
m_size.width = (rect.right - rect.left) * 96 / m_dpi.x;
m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y;
}
Прежде чем реализовать обработчики сообщений, нужно переопределить MessageHandler шаблона класса Window, чтобы указать, какие сообщения я хочу обрабатывать. Как минимум, нужно обрабатывать сообщения WM_PAINT, где я буду предоставлять команды рисования, WM_SIZE, где я буду подстраивать размер поверхности, и WM_DPICHANGED, где я буду обновлять действующий DPI и размер окна. На рис. 4 показан этот MessageHandler; как и следовало ожидать, он просто пересылает сообщения соответствующим обработчикам.
Рис. 4. Диспетчеризация сообщений
LRESULT MessageHandler(UINT message,
WPARAM const wparam,
LPARAM const lparam)
{
if (WM_PAINT == message)
{
PaintHandler();
return 0;
}
if (WM_SIZE == message)
{
SizeHandler(wparam, lparam);
return 0;
}
if (WM_DPICHANGED == message)
{
DpiHandler(wparam, lparam);
return 0;
}
return __super::MessageHandler(message,
wparam,
lparam);
}
Обработчик WM_PAINT — то место, где я создаю аппаратные ресурсы по запросу перед вхождением в последовательность рисования. Помните, что CreateDeviceResources ничего не делает, если устройство уже существует:
void PaintHandler()
{
try
{
CreateDeviceResources();
// Команды рисования ...
}
Тем самым я могу отреагировать на потерю устройства простым освобождением указателя на Direct3D-устройство через метод ReleaseDeviceResources, а в следующий раз обработчик WM_PAINT создаст все заново. Этот процесс полностью заключен в блок try, чтобы надежно обрабатывать любые проблемы с устройством. Теперь начинаем рисовать на поверхности композиции и для этого вызываем ее метод BeginDraw:
ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
HR(m_surface->BeginDraw(nullptr, // вся поверхность
__uuidof(dc),
reinterpret_cast<void **>(dc.GetAddressOf()),
&offset));
BeginDraw возвращает контекст устройства — Direct2D-мишень рендера. И я буду использовать его для группирования команд рисования. DirectComposition API задействует Direct2D-устройство, изначально предоставленное мной, при создании устройства композиции, которое здесь создает и возвращает контекст устройства. Я могу дополнительно предоставить структуру RECT (со значениями в физических пикселях), чтобы обрезать поверхность, или указать nullptr, чтобы разрешить неограниченный доступ к поверхности рисования. Метод BeginDraw возвращает и смещение (вновь в физических пикселях), указывая начало координат на поверхности рисования. Оно не обязательно должно быть в ее верхнем левом углу, поэтому следует позаботиться о подстройке или преобразовании любых команд рисования, чтобы они были должным образом смещены.
Поверхность композиции также предоставляет метод EndDraw, и эти два метода заменяют одноименные методы из Direct2D. Вы не должны вызывать соответствующие методы контекста устройства, так как DirectComposition API берет это на себя. Очевидно, DirectComposition API также обеспечивает, чтобы в контексте устройства была поверхность композиции, выбранная как его мишень. Более того, важно, чтобы вы не удерживали контекст устройства, а освобождали его сразу по завершении рисования. Учтите: нет никаких гарантий, что поверхность сохранит контент любого предыдущего кадра, который мог быть нарисован, поэтому нужно позаботиться либо об очистке мишени, либо о перерисовке каждого пикселя перед окончанием работы.
Полученный контекст устройства готов к работе, но к нему пока не применен масштабный множитель действующего для окна DPI. Я могу использовать значения DPI, ранее вычисленные в моем методе CreateDeviceResources, чтобы сейчас обновить контекст устройства:
dc->SetDpi(m_dpi.x,
m_dpi.y);
Я также буду использовать матрицу преобразования с трансляцией (translation transformation matrix) для подстройки команд рисования с учетом смещения, требуемого DirectComposition API. Мне просто нужно быть внимательным и транслировать смещение в логические пиксели, потому что именно этого ожидает Direct2D:
dc->SetTransform(Matrix3x2F::Translation(offset.x * 96 / m_dpi.x,
offset.y * 96 / m_dpi.y));
Теперь можно очистить мишень и нарисовать что-то специфичное для приложения. Здесь я рисую простой прямоугольник аппаратно-зависимой кистью, созданной ранее в моем методе CreateDeviceResources:
dc->Clear();
D2D_RECT_F const rect = RectF(100.0f,
100.0f,
m_size.width - 100.0f,
m_size.height - 100.0f);
dc->DrawRectangle(rect,
m_brush.Get(),
50.0f);
Я полагаюсь на кешированный член m_size, а не на размер, сообщаемый методом GetSize, так как тот выдает размер нижележащей поверхности вместо размера клиентской области.
Завершение последовательности рисования включает несколько этапов. Сначала нужно вызвать метод EndDraw поверхности. Это сообщает Direct2D выполнить любые сгруппированные команды и записать их в поверхность композиции. После этого поверхность будет готова к композиции, но не ранее чем будет вызван метод Commit устройства композиции. В этот момент любые изменения в визуальном дереве, в том числе любые обновленные поверхности, объединяются в пакет и становятся доступными механизму композиции в виде одной транзакционной единицы. Это завершает процесс рендеринга. Остается лишь один вопрос: было ли потеряно Direct3D-устройство. Метод Commit сообщит о любой неудаче, и блок catch освободит устройство. Если все прошло удачно, можно сообщить Windows, что я успешно нарисовал окно, проверив всю клиентскую область окна с помощью функции ValidateRect. В ином случае я должен освободить устройство. Вот как это может выглядеть:
// Команды рисования ...
HR(m_surface->EndDraw());
HR(m_device->Commit());
VERIFY(ValidateRect(m_window, nullptr));
}
catch (ComException const & e)
{
ReleaseDeviceResources();
}
Мне не нужно ничего перерисовывать явным образом, поскольку Windows просто продолжит посылать сообщения WM_PAINT, если я не отреагирую проверкой клиентской области. Обработчик WM_SIZE отвечает за подстройку размера поверхности композиции, а также за обновление кешированного размера мишени рендера. От меня не будет реакции, если устройство не создано или если окно свернуто:
void SizeHandler(WPARAM const wparam,
LPARAM const lparam)
{
try
{
if (!IsDeviceCreated()) return;
if (SIZE_MINIMIZED == wparam) return;
// ...
}
Окно обычно принимает сообщение WM_SIZE до того, как у него появляется возможность создать стек устройства. Когда это происходит, я просто игнорирую сообщение. Я также игнорирую его, если сообщение WM_SIZE является результатом свертывания окна. Я не хочу в этом случае выполнять излишние операции подстройки размера поверхности. Как и обработчик WM_PAINT, обработчик WM_SIZE заключает свои операции в блок try. Изменение размера или создание заново поверхности, как в этом случае, вполне может оказаться неудачным из-за потери устройства и это должно приводить к повторному созданию стека устройства. Но сначала я могу получить новый размер клиентской области:
unsigned const width = LOWORD(lparam);
unsigned const height = HIWORD(lparam);
И обновить кешированный размер в логических пикселях:
m_size.width = width * 96 / m_dpi.x;
m_size.height = height * 96 / m_dpi.y;
Размер поверхности композиции изменять нельзя. Я использую то, что можно было бы назвать не виртуальной поверхностью (non-virtual surface). Механизм композиции также предлагает виртуальные поверхности, размер которых можно изменять, но об этом мы поговорим в следующей статье. В данном случае я просто освобождаю текущую поверхность, а затем создаю ее заново. Поскольку изменения в визуальном дереве не отражаются, пока эти изменения не будут переданы и зафиксированы, пользователь не увидит никаких миганий в процессе удаления поверхности и ее повторного создания. Вот как это может выглядеть:
HR(m_device->CreateSurface(width,
height,
DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_ALPHA_MODE_PREMULTIPLIED,
m_surface.ReleaseAndGetAddressOf()));
HR(m_visual->SetContent(m_surface.Get()));
После этого я могу реагировать на любые проблемы, освобождая ресурсы устройства, чтобы следующее сообщение WM_PAINT вызвало их повторное создание:
// ...
}
catch (ComException const & e)
{
ReleaseDeviceResources();
}
Вот и все, что касается обработчика WM_SIZE. Обязательный заключительный этап — реализация обработчика WM_DPICHANGED для обновления эффективного DPI и размера окна. WPARAM сообщения предоставляет новые значения DPI, а LPARAM — новый размер. Я просто обновляю переменную-член m_dpi окна, а затем вызываю метод SetWindowPos, чтобы обновить размер окна. Тогда окно получит другое сообщение WM_SIZE, которое мой обработчик WM_SIZE задействует для подстройки значения члена m_size и повторного создания поверхности. На рис. 5 приведен пример того, как обрабатывать эти сообщения WM_DPICHANGED.
Рис. 5. Обработка обновлений DPI
void DpiHandler(WPARAM const wparam,
LPARAM const lparam)
{
m_dpi.x = LOWORD(wparam);
m_dpi.y = HIWORD(wparam);
RECT const & rect = *reinterpret_cast<RECT const *>(lparam);
VERIFY(SetWindowPos(m_window,
0, // родственного окна нет
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOACTIVATE | SWP_NOZORDER));
}
Я рад, что члены семейства DirectX теснее сближаются, их взаимодействие и производительность повышаются — благодаря глубокой интеграции Direct2D и DirectComposition. Надеюсь, что и вы рады не меньше меня открывающимся возможностям в создании функциональных неуправляемых приложений, использующим DirectX.