Вступительное слово.

В данной статье рассмотрю начало работы с графикой, а именно что нужно сделать, чтобы вывести геометрию любой сложности (в рамках 2D) и любого цвета. Всё остальное рассмотрим дальше, в следующих статьях.

Из прошлых статей известно, что есть клиентская область окна, где у нас полная власть над содержимым, и неклиентская, где тоже власть, но меньше. А отсюда можно легко прийти к тому, что есть некие массивы, которые содержат информацию о пикселях клиентской области, и их нам надо менять. По сути, так оно и работает, только я очень упростил.

Интересующий нас массив называется буфером. Есть их два типа - Back и Front. Первый - туда непосредственно идёт запись, а уже итоговый результат помещается в Front и отображается на экране. Но это тоже упрощение.

Сами буферы и различные функции относятся к интерфейсу IDXGISwapChain. Но это так, к слову, мы пока особо ничего такого затрагивать не будем. (DXGI - это Microsoft DirectX Graphics Infrastructure (Графическая инфраструктура DirectX).)

В целом для лучшего понимания нужно учитывать, что есть такая цепочка:

  1. Физическая видеокарта (GPU)

  2. Драйвер видеокарты (Display Driver)

  3. DXGI Адаптер (IDXGIAdapter)

  4. DXGI Устройство (IDXGIDevice) -> D3D11 Устройство (ID3D11Device)

  5. D2D1 Устройство (ID2D1Device) -> D2D1 Фабрика (ID2D1Factory)

  6. D2D1 Контекст устройства (ID2D1DeviceContext)

  7. Рендер-таргет (ID2D1RenderTarget)

  8. Окно (HWND) → Desktop Window Manager (DWM)

  1. Физическая видеокарта это и есть видеокарта

  2. Драйвер видеокарты (Display Driver) - включает в себя WDDM 2.0+ (Windows Display Driver Model), а он уже на Kernel Mode и User Mode(nvldumd.dll если драйвер от нвидиа и atiumd64.dll если драйвер от амд), DXGI Runtime (dxgi.dll)

  3. DXGI Адаптер (IDXGIAdapter) - логическое представление видеокарта в DXGI, от него отходят:

    1. IDXGIObject, от него:

      1. IDXGIAdapter(Адаптер (видеокарта)), от него:

        1. IDXGIOutput1 (Расширенные возможности)

        2. IDXGIOutput6 (HDR поддержка)

      2. IDXGIAdapter2 (Дополнительная информация)

    2. IDXGIDevice (Устройство (логическое))

    3. IDXGIFactory (Фабрика для создания объектов). Собственно этим будем очень часто пользоваться.

  4. Рендер-таргет (ID2D1RenderTarget) - собственно сегодня с ним познакомимся, от него наследуется разные варианты куда рисовать.

Вся работа с графикой состоит в первую очередь из создания IDXGIFactory, но в Direct2D это называется ID2D1Factory. Ещё важно то, что к подобным данным мы напрямую доступа не имеем (реализация скрыта), поэтому храним указатель на объект, и доступ к его функциям осуществляется через разыменование, а также любые действия, такие как создание, - через передачу указатель на указатель на ID2D1Factory. Собственно, где угодно пишем:

ID2D1Factory* pFactory = nullptr;

Теперь - создание фабрики. Вызываем функцию D2D1CreateFactory. Первый аргумент - это флаг: D2D1_FACTORY_TYPE_SINGLE_THREADED (однопоточное приложение) или D2D1_FACTORY_TYPE_MULTI_THREADED (многопоточное приложение), второй аргумент - это указатель на указатель на ID2D1Factory.

D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory);

Как я говорил, есть IDXGISwapChain, а в ней - буферы, но инициализирует их цель рендеринга (Render Target) (упрощение). Цель рендеринга, как можно догадаться из названия, определяет, куда будет происходить рисование. Всего их четыре вида, но нас интересует пока один - ID2D1HwndRenderTarget. Это оконный вывод, который привязан к конкретному окну. И тут, как с ID2D1Factory: создаём через ID2D1Factory, разыменовываем и вызываем функцию CreateHwndRenderTarget. Аргументы:

Аргументы функции CreateHwndRenderTarget
  1. ссылка на константу D2D1_RENDER_TARGET_PROPERTIES название renderTargetProperties - это настройки самой цели рендера. Получаем структуру требуемую, через другую функцию RenderTargetProperties во внутреннем пространстве(namespace) D2D1 (пишем D2D1::RenderTargetProperties), аргументы:

    1. флаг D2D1_RENDER_TARGET_TYPE название type. Объяснение: Тип рендеринга(CPU или GPU). Варианты флага:

      1. флаг D2D1_RENDER_TARGET_TYPE_SOFTWARE значение 0x1 - программный рендеринг (CPU).

      2. флаг D2D1_RENDER_TARGET_TYPE_HARDWARE значение 0x2 - аппаратное ускорение (GPU).

      3. флаг D2D1_RENDER_TARGET_TYPE_DEFAULT значение 0x0 - система сама определит(стоит по умолчанию(но лучше в ручную указать)).

    2. ссылка на константу D2D1_PIXEL_FORMAT название pixelFormat. Устанавливает значение, которое является результатом функции PixelFormat в namespace D2D1, аргументы:

      1. какой-либо флаг DXGI_FORMAT - задаёт кол-во байтов под канал и порядок цветовых каналов. Флаги:

        1. DXGI_FORMAT_UNKNOWN - авто-выбор (обычно B8G8R8A8_UNORM).

        2. DXGI_FORMAT_B8G8R8A8_UNORM - 8 бит на канал, первый канал Blue, после Green , Red и последние Aplha.

        3. DXGI_FORMAT_R8G8B8A8_UNORM - 8 бит на канал, первый канал Reg, после Green , Blue и последние Aplha.

        4. DXGI_FORMAT_R16G16B16A16_FLOAT - 16 бит на канал, первый канал Reg, после Green , Blue и последние Aplha.

        5. DXGI_FORMAT_R32G32B32A32_FLOAT - 32 бит на канал, первый канал Reg, после Green , Blue и последние Aplha.

          К слову если 8 бит на канал, то это значение нормализовано в шейдере как значения от 0 до 1, если 16 бит то от -65504.0, +65504.0, если 32 то от -3.4e38 до +3.4e38.

      2. Какой-либо флаг D2D1_ALPHA_MODE - задаёт режим прозрачности. Флаги:

        1. флаг D2D1_ALPHA_MODE_UNKNOWN значение 0 - Система выбирает автоматически.

        2. флаг D2D1_ALPHA_MODE_PREMULTIPLIED значение 1 - Предумноженный альфа-канал. Формула RGB = (R×A, G×A, B×A).

        3. флаг D2D1_ALPHA_MODE_STRAIGHT значение 2 - Прямой (не предумноженный) альфа-канал. Формула RGB = (R, G, B) независимо от альфы.

        4. флаг D2D1_ALPHA_MODE_IGNORE значение 3 - Альфа-канал игнорируется.

      3. тип FLOAT название dpiX - значение dpi по оси X.

      4. тип FLOAT название dpiY - значение dpi по оси Y.

      5. тип D2D1_RENDER_TARGET_USAGE название usage - определяет тип рендер-таргета, возможность выполнения в память, сжатие, отправка на сервер. Флаги:

        1. флаг D2D1_RENDER_TARGET_USAGE_NONE - Стандартный рендер-таргет без специальных возможностей.

        2. флаг D2D1_RENDER_TARGET_USAGE_FORCE_BITMAP_REMOTING - Принудительное использование битмап-рендеринга(по сути изображение в памяти). Схема такая: Локальный рендеринг → Битмап → Сжатие → Передача по сети → Отображение на клиенте.

        3. флаг D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE - Возможность взаимодействия со старым GDI API

      6. тип D2D1_FEATURE_LEVEL_DEFAULT название minLevel - минимальная поддержка DirectX. (9 или 10, при выборе 10 производительность будет в разы лучше). Флаг:

        1. флаг D2D1_FEATURE_LEVEL_9 - DirectX 9

        2. флаг D2D1_FEATURE_LEVEL_10 - DirectX 10

          Разницы там много, начиная что в DirectX 10 есть Shader Model 4.0 , возможность создания сложных эффектов ну и так далее.

  2. тип ссылка на константу D2D1_HWND_RENDER_TARGET_PROPERTIES название hwndRenderTargetProperties, получаем благодаря функции HwndRenderTargetProperties, по сути очередные настройки. Аргументы:

    Аргументы функции
    1. тип HWND название hwnd - ид(дескриптор) окна.

    2. тип D2D1::SizeU название pixelSize - D2D1::SizeU это структура где два поля тип UINT32(unsigned int) первый это ширина, второй высота. По умолчанию если аргумент содержит структуру SizeU содержащую нули, для быстрого получения структуры используйте функцию D2D1::Size(x,y);

    3. флаги D2D1_PRESENT_OPTIONS:

      1. флаг D2D1_PRESENT_OPTIONS_NONEзначение 0 - VSYNC включён.

      2. флаг D2D1_PRESENT_OPTIONS_IMMEDIATELY значение 2 - VSYNC отключён. Производительность выше.

      3. флаг D2D1_PRESENT_OPTIONS_RETAIN_CONTENTS значение 1 - Особый режим, при нём не нужна очистка(об этом дальше), вы просто каждый раз рисуете что-то новое, не убирая старое.

  3. тип ссылка на указатель на структуру ID2D1HwndRenderTarget.

 ID2D1HwndRenderTarget* pRenderTarget = nullptr;
pFactory->CreateHwndRenderTarget(
        D2D1::RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_HARDWARE,D2D1::PixelFormat(DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_UNKNOWN),0.0f,0.0f, D2D1_RENDER_TARGET_USAGE_NONE, D2D1_FEATURE_LEVEL_10),
        D2D1::HwndRenderTargetProperties(hMainHwnd, size),
        &pRenderTarget
    );

Теперь создав цель рендеринга, можно приступить к рисованию.

Рисование начинается с разыменование ID2D1HwndRenderTarget и вызова функции BeginDraw, она ничего не принимает и не возвращает, но выполняет ряд важных действий:

Список действий функции BeginDraw
  1. Захват мьютекса доступа к GPU контексту(это по сути различные данные нужны для графической работы - Ресурсы видеопамяти, Командным буферам GPU и т.п.). Если кто-то читает и ни разу не читал о многопоточности, то захват мьютекса это обеспечение гарантий того, что ничто не получит доступ к чему-то через различные для этого инструменты(вызов функции и получения копии и т.п. В общем многие способы).

  2. Подготовка командного буфера. Если по простому именно в нём копятся все команды для рисования.

  3. Валидация состояния устройства (видеокарты), система проверяет всё ли хорошо, если нет, то получается ошибки разных видов:

    1. GPU перестал отвечать (5+ секунд) - DXGI_ERROR_DEVICE_HUNG

    2. Видеокарта физически извлечена - DXGI_ERROR_DEVICE_REMOVED

    3. Драйвер выполнил сброс устройства - DXGI_ERROR_DEVICE_RESET

    4. Внутренняя ошибка драйвера - DXGI_ERROR_DRIVER_INTERNAL_ERROR

    5. Неверные параметры вызовов - DXGI_ERROR_INVALID_CALL

      Но в целом, об этом в другой статье.

  4. Блокировка back buffer лишь для записи, как с первым пунктом, но сугубо для других операций.

  5. Сброс счётчиков ошибок и метрик.

Теперь EndDraw:

Список действий функции EndDraw
  1. Финализация командного буфера. По сути выполняются проверки , сортировки и отправка GPU.

  2. Синхронизация CPU-GPU. По сути ожидание, пока GPU выполнит все команды.

  3. Презентация кадра (только для HwndRenderTarget). По сути отправка одного кадра и подготовка нового и обработка потери устройства.

  4. Проверка ошибок и состояния устройства. Если GPU не доступно, будут ошибки и нужно будет пересоздать устройство, но об этом в следующих статьях(и не много ниже, что бы понять что за ошибка, достаточно записать код полученный результатом от EndDraw и проверить). Но так же в целом проверяет буферы и т.п.

  5. Освобождение ресурсов и разблокировка.

  6. Сбор метрик и статистики.

  7. Обработка особых сценариев. По сути восстановление после ошибок.

  8. Оптимизации для следующего кадра.

Перед рисованием есть ещё одна важная тема: многим ресурсам, например, тем, что созданы через ID2D1HwndRenderTarget, нужно вручную освобождать память, как и самому ID2D1HwndRenderTarget. Они называются устройство-зависимые ресурсы (Device-Dependent). Ещё есть устройство-независимые ресурсы (Device-Independent). Немного о них и о том, что вообще надо делать:

  1. Устройство-зависимые ресурсы (Device-Dependent):

    Ресурсы, которые хранятся в видеопамяти GPU и привязаны к конкретному графическому устройству. Если что-то с устройством случится, ресурсы нужно будет пересоздать.

    Ситуации при которых надо освободить ресурсы и создать по новой.
    1. Код ошибка(результат функции)(HRESULT) D2DERR_RECREATE_TARGET из EndDraw(). Основные причины:

      Основные причины
      1. Изменения в системе графики: обновление драйвера видеокарты, переключение между дискретной и интегрированной графикой (в ноутбуках), физическое отключение монитора или видеокарты.

      2. Изменение режима отображения: смена разрешения экрана, выход из полноэкранного режима (например, по Alt+Tab) или блокировка экрана (Ctrl+Alt+Del).

      3. Проблемы с драйвером или памятью: аварийный сброс драйвера видеокарты (TDR - Timeout Detection and Recovery) или исчерпание видеопамяти (VRAM).

      4. Ошибки в коде приложения: некорректное использование API, например, рисование за границами битмапа, может вызывать сбой на некоторых видеокартах.

    2. Изменение разрешения экрана. В оконную процедуру придёт сообщение WM_DISPLAYCHANGE.

    3. Переключение полноэкранного режима. В оконную процедуру придёт сообщение WM_SIZE.

    4. Смена активного GPU.(Переключение между видеокартами или на горячую если вытащили). В оконную процедуру придёт сообщение WM_POWERBROADCAST(wParam должно равно быть PBT_APMRESUMEAUTOMATIC).

    5. TDR (Timeout Detection & Recovery). GPU завис и был сброшен системой. То есть EndDraw вернуло DXGI_ERROR_DEVICE_RESET или DXGI_ERROR_DEVICE_REMOVED или DXGI_ERROR_DEVICE_HUNG. Или когда драйвер видеокарты обновился.

    6. Изменение DPI/масштабирования. В оконную процедуру придёт сообщение WM_DPICHANGED.

    7. Потеря фокуса полноэкранного приложения. В оконную процедуру придёт сообщение WM_ACTIVATEAPP.

    8. Изменение формата пикселей/цветового пространства. В оконную процедуру придёт сообщение WM_DISPLAYCHANGE.

    9. Системные события питания. Спящий режим/гибернация. В оконную процедуру придёт сообщение WM_POWERBROADCAST. Если wParam равен PBT_APMSUSPEND, то это обозначение что устройство ушло в сон. Если PBT_APMRESUMESUSPEND выход из сна, если PBT_APMRESUMEAUTOMATIC - GPU мог измениться (ноутбук подключили к док-станции).

    10. Изменение конфигурации мониторов. Подключение/отключение мониторов. В оконную процедуру придёт сообщение WM_DEVICECHANGE, wParam должен быть равен DBT_DEVNODES_CHANGED.

    11. Ошибки выделения видеопамяти. Нехватка VRAM. Это будет в следующих статьях.

    12. Смена темы оформления Windows. В оконную процедуру придут такие сообщения как WM_THEMECHANGED или WM_SYSCOLORCHANGE.

    13. Обновление Windows. Критические системные обновления. Отследит сложно. Но если обновление задело графику, то это сломает скорей всего приложение.

    Собственно, что сделать. Вызывайте функцию Release из каждого ресурса, и по новой создаёте.

  2. Устройство-независимые ресурсы (Device-Independent):

    Собственно, что нужно сделать: вызовите метод Release у каждого такого ресурса, а затем создайте их заново.

Что бы начать рисовать геометрические объекты, нужно задавать их координаты, а делается это через различные структуры:

Структуры для задавания координат
  1. структура D2D1_POINT_2F - первое поле тип FLOAT и хранит координату по оси X, второе поле тип FLOAT и хранит координату по оси Y. Способ получения через функцию Point2F в пространстве имён D2D1, аргументы функции идентичные полям структуры.

  2. структура D2D1_RECT_F - первое поле тип FLOAT и хранит координату по оси X верхнего левого угла прямоугольника , второе поле тип FLOAT и хранит координату по оси Y верхнего левого угла прямоугольника, третье поле тип FLOAT и хранит координату X правого нижнего угла прямоугольника, четвёртое поле тип FLOAT и хранит координату Y правого нижнего угла прямоугольника. Способ получения через функцию RectF в пространстве имён D2D1, аргументы функции идентичные полям структуры.

  3. структура D2D1_ROUNDED_RECT - первое поле структура D2D1_RECT_F , второе поле тип FLOAT хранит радиус по оси X, третье поле тип FLOAT хранит радиус по оси Y. Способ получения через функцию RoundedRect в пространстве имён D2D1.

  4. cтруктура D2D1_ARC_SEGMENT - первое поле point типа D2D1_POINT_2F хранит конечную точку дуги, второе поле size типа D2D1_SIZE_F хранит размеры эллипса (радиусы), третье поле rotationAngle типа FLOAT хранит угол поворота эллипса в градусах, четвертое поле sweepDirection типа D2D1_SWEEP_DIRECTION задает направление обхода дуги(флаг D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE число 0 - рисуется против часовой. Флаг D2D1_SWEEP_DIRECTION_CLOCKWISE число 1 - рисуется по часовой), пятое поле arcSize типа D2D1_ARC_SIZE задает размер дуги (малая или большая). Способ получения через функцию ArcSegment в пространстве имён D2D1.

  5. cтруктура D2D1_BEZIER_SEGMENT - первое поле point1 типа D2D1_POINT_2F хранит первую контрольную точку, второе поле point2 типа D2D1_POINT_2F хранит вторую контрольную точку, третье поле point3 типа D2D1_POINT_2F хранит конечную точку кривой Безье. Способ получения через функцию BezierSegment в пространстве имён D2D1.

  6. cтруктура D2D1_QUADRATIC_BEZIER_SEGMENT - первое поле point1 типа D2D1_POINT_2F хранит контрольную точку, второе поле point2 типа D2D1_POINT_2F хранит конечную точку квадратичной кривой Безье. Способ получения через функцию QuadraticBezierSegment в пространстве имён D2D1.

  7. указатель на любую структуру наследующуюся от ID2D1Geometry - создаётся и заполняется через фабрику(разыменование ID2D1Factory и вызов функции CreatePathGeometry с передачей одного аргумента - указатель на указатель на структуру ID2D1PathGeometry. После создания, разыменовываем и вызываем функцию Open и передаём указатель на указатель на ID2D1GeometrySink это по сути набор геометрии. О заполнении позже.

А ещё, кроме координат, нужно указать цвет. И, хотя это может показаться неочевидным, мы не можем сделать это просто - для этого нам нужно передать указатель на указатель на структуру ID2D1SolidColorBrush. При этом это самый простой вид кисти (кисть хранит информацию о цвете, но о них тоже поговорим позже). Она, как и другие, наследуется от ID2D1Brush. Пишем:

ID2D1SolidColorBrush* brush = nullptr;

Чтобы создать кисть, разыменуйте указатель ID2D1HwndRenderTarget* и вызовите метод CreateSolidColorBrush. Первый аргумент - структура D2D1_COLOR_F, у которой первое поле имеет тип FLOAT и название r (red), а далее три аналогичных поля: g (green), b (blue) и a (alpha).

Получить структуру можно через перечисление ColorF в пространстве имён D2D1 (например, D2D1::ColorF::AliceBlue). Если нужно указать свой цвет, используйте конструктор:

D2D1::ColorF(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);

где вместо букв подставляются значения цветовых компонентов.

Второй аргумент - указатель на указатель на ID2D1SolidColorBrush. В результате получите кисть с нужным цветом.

ID2D1SolidColorBrush* brush = nullptr;
 pRenderTarget->CreateSolidColorBrush(D2D1::ColorF::ColorF(255.0f / 255.0f,255.0f / 255.0f,255.0f / 255.0f,255.0f / 255.0f), &brush);

Алгоритм определения устройство-зависимых ресурсов: по интерфейсу создания - если ресурс создаётся через рендер-таргет, он является устройство-зависимым.

Создав кисть, можно перейти к функциям рисования: квадратов, прямых, сложной геометрии и т.п.

Рисуем базовые примитивы - линии и фигуры. (Все функции находятся в рендер-таргете, вызываются через разыменование указателя):

Базовые примитивы - линии и фигуры
  1. Линии:

    1. функция DrawLine возвращает тип void - принимает 4 аргумента:

      1. структура D2D1_POINT_2F назначение - первая координата линии . Получить экземпляр можно в качестве результат функции D2D1::Point2F , первый аргумент которой это координата X (float тип), а второй тип координата Y(float тип) .

      2. структура D2D1_POINT_2F назначение - вторая координата линии.

      3. указатель на структуру ID2D1Brush (по сути любая кисть наследуется от этого класса) назначение - цвет линии.

      4. тип FLOAT назначение - толщина линии(в пикселях).

  2. Прямоугольник:

    1. функция DrawRectangle возвращает тип void - принимает 3 аргумента:

      1. структура D2D1_RECT_F назначение - прямоугольник для обводки. Получить экземпляр можно в качестве результат функции D2D1::RectF, аргументы которой это координаты left, top, right, bottom (все типа FLOAT).

      2. указатель на структуру ID2D1Brush назначение - кисть для обводки.

      3. тип FLOAT назначение - толщина обводки (в пикселях).

        Рисуется контур прямоугольника с заданной толщиной обводки.

    2. функция FillRectangle возвращает тип void - принимает 2 аргумента:

      1. структура D2D1_RECT_F назначение - прямоугольник для заливки.

      2. указатель на структуру ID2D1Brush назначение - кисть для заливки.

        Полностью закрашенный прямоугольник.

    3. функция DrawRoundedRectangle возвращает тип void - принимает 3 аргумента:

      1. структура D2D1_ROUNDED_RECT назначение - скругленный прямоугольник для обводки. Получить экземпляр можно в качестве результат функции D2D1::RoundedRect, первый аргумент которой это D2D1_RECT_F, второй и третий - радиусы скругления по X и Y (тип FLOAT).

      2. указатель на структуру ID2D1Brush назначение - кисть для обводки.

      3. тип FLOAT назначение - толщина обводки (в пикселях).

        Контур прямоугольника со скругленными углами.

    4. функция FillRoundedRectangle возвращает тип void - принимает 2 аргумента:

      1. структура D2D1_ROUNDED_RECT назначение - скругленный прямоугольник для заливки.

      2. указатель на структуру ID2D1Brush назначение - кисть для заливки.

        Полностью закрашенный прямоугольник со скругленными углами.

  3. Круг или Эллипс:

    1. функция DrawEllipse возвращает тип void - принимает 3 аргумента:

      1. структура D2D1_ELLIPSE назначение - эллипс для обводки. Получить экземпляр можно в качестве результат функции D2D1::Ellipse, первый аргумент которой это центр (D2D1_POINT_2F), второй и третий - радиусы по X и Y (тип FLOAT).

      2. указатель на структуру ID2D1Brush назначение - кисть для обводки.

      3. тип FLOAT назначение - толщина обводки (в пикселях).

        контур эллипса или круга.

    2. функция FillEllipse возвращает тип void - принимает 2 аргумента:

      1. структура D2D1_ELLIPSE назначение - эллипс для заливки.

      2. указатель на структуру ID2D1Brush назначение - кисть для заливки.

        полностью закрашенный эллипс или круг.

    3. функция DrawGeometry возвращает тип void - принимает 3 аргумента:

      1. указатель на интерфейс ID2D1Geometry назначение - геометрия для обводки.

      2. указатель на структуру ID2D1Brush назначение - кисть для обводки.

      3. тип FLOAT назначение - толщина обводки (в пикселях).

        контур произвольной фигуры (многоугольника или сложной формы).

    4. функция FillGeometry возвращает тип void - принимает 2 аргумента:

      1. указатель на интерфейс ID2D1Geometry назначение - геометрия для заливки.

      2. указатель на структуру ID2D1Brush назначение - кисть для заливки.

        полностью закрашенная произвольная фигура.

  4. Сложная геометрия:

    Как говорил ранее, надо заполнить ID2D1GeometrySink для этого через разыменование и вызываем различные функции:

    1. Всё начинается с BeginFigure(добавление новой модели геометрической), первый аргумент D2D1_POINT_2F - начальная точка, второй аргумент флаг D2D1_FIGURE_BEGIN(указывают, будет ли фигура заполняться цветом при использовании метода FillGeometry ):

      1. флаг D2D1_FIGURE_BEGIN_FILLED - фигура будет заполнена цветом. Возвращает реальные границы фигуры. То есть, по сути, пишем если наша геометрическая модель замкнутая.

      2. флаг D2D1_FIGURE_BEGIN_HOLLOW - фигура будет создает фигуру только для контура (обводки). Фигура не будет заполнена (останется пустой). Возвращает нулевые границы. То есть, по сути, пишем если наша геометрическая модель не замкнутая.

    2. Различные функции для добавления точек:

      1. функция addLine возвращает void и добавляет точку к предыдущей точки добавленной или к начальной. Принимает единственный аргумент, структура D2D1_POINT_2F.

      2. функция AddLines возвращает void и добавляет последовательность линий, соединяя точки из массива последовательно от текущей позиции. Принимает два аргумента: указатель на массив структур D2D1_POINT_2F и количество точек в массиве.

      3. функция AddArc возвращает void и добавляет дугу от текущей позиции до указанной конечной точки. Принимает единственный аргумент - указатель на структуру D2D1_ARC_SEGMENT.

      4. функция AddBezier возвращает void и добавляет кубическую кривую Безье от текущей позиции до конечной точки через две контрольные точки. Принимает единственный аргумент - указатель на структуру D2D1_BEZIER_SEGMENT.

      5. Функция AddQuadraticBezier возвращает void и добавляет квадратичную кривую Безье от текущей позиции до конечной точки через одну контрольную точку. Принимает единственный аргумент - указатель на структуру D2D1_QUADRATIC_BEZIER_SEGMENT.

      6. Функция AddBeziers возвращает void и добавляет последовательность кубических кривых Безье, соединяя их последовательно от текущей позиции. Принимает два аргумента: указатель на массив структур D2D1_BEZIER_SEGMENT и количество кривых в массиве.

      7. Функция AddQuadraticBeziers возвращает void и добавляет последовательность квадратичных кривых Безье, соединяя их последовательно от текущей позиции. Принимает два аргумента: указатель на массив структур D2D1_QUADRATIC_BEZIER_SEGMENT и количество кривых в массиве.

    3. Различные манипуляции:

      1. Функция SetFillMode возвращает void и устанавливает правило заливки для всей геометрии. Принимает один аргумент: значение D2D1_FILL_MODE, определяющее алгоритм заливки (ALTERNATE или WINDING). Флаги:

        1. D2D1_FILL_MODE_ALTERNATE (0) - правило четности (alternate fill mode). Луч, выпущенный из точки, пересекает контур фигуры. Если количество пересечений нечетное - точка заливается, если четное - не заливается.

        2. D2D1_FILL_MODE_WINDING (1) - правило ненулевого витка (winding fill mode). Учитывает направление обхода контура. Если суммарное число витков (с учетом направления) не равно нулю - точка заливается.

      2. Функция SetSegmentFlags возвращает void и устанавливает флаги для последующих сегментов пути. Принимает один аргумент: значение D2D1_PATH_SEGMENT, содержащее битовые флаги управления отображением сегментов. Про D2D1_PATH_SEGMENT:

        1. Перечисление D2D1_PATH_SEGMENT - устанавливает флаги для сегментов пути. Принимает значения (можно комбинировать через OR):

          1. D2D1_PATH_SEGMENT_NONE (0x00000000) - без специальных флагов.

          2. D2D1_PATH_SEGMENT_FORCE_UNSTROKED (0x00000001) - сегмент не будет обводиться при вызове DrawGeometry

          3. D2D1_PATH_SEGMENT_FORCE_ROUND_LINE_JOIN (0x00000002) - принудительно использовать скругленные соединения линий

    4. Заканчивать фигуру через вызов EndFigure возвращает void и завершает текущую фигуру , принимает флаг(должна быть фигура открыта или закрыта, если первое, то последняя точка не соединяется с первой, иначе соединяется):

      1. D2D1_FIGURE_END_OPEN значение 0 - фигура открыта.

      2. D2D1_FIGURE_END_CLOSED значение 1 - фигура закрыта.

    5. Когда закончили добавлять вызываете Close, это фиксирует геометрию, при этом для ID2D1GeometrySink вызываете Release.

    6. Теперь, что бы отрисовать вашу геометрию вызываете DrawGeometry и первый аргумент передаёте указатель на ID2D1PathGeometry , вторым аргументом кисть, а третий тип FLOAT - ширина. Или заполнить генерацию. FillGeometry - первый аргумент указатель на ID2D1PathGeometry , второй тип кисти.

На этом, по сути, введение завершается. Далее будет рассмотрена отрисовка текстур, текста и т.п. - постепенное углубление в графику.

Также важно отметить, что для сложной геометрии существуют интересные функции, например, встроенные проверки пересечений и другие. Однако, я считаю целесообразным вынести эту тему в отдельную статью вместе с кодом-примером.

Итоговый код-пример со всеми фигурами (логика вынесена в отдельную функцию, добавлена функция SafeRelease, которая освобождает ресурс, если он не nullptr):

Итоговый кодо-пример со всеми примерами рисования
#include <windows.h>
#include <d2d1.h>
#include <d2d1helper.h>
#include <wincodec.h>

#pragma comment(lib, "d2d1.lib")


ID2D1Factory* pFactory = nullptr;
ID2D1HwndRenderTarget* pRenderTarget = nullptr;
ID2D1SolidColorBrush* pBlackBrush = nullptr;
ID2D1SolidColorBrush* pRedBrush = nullptr;
ID2D1SolidColorBrush* pBlueBrush = nullptr;
ID2D1SolidColorBrush* pGreenBrush = nullptr;


template<class T>
void SafeRelease(T** ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = nullptr;
    }
}

// Создание устройство-независимых ресурсов
HRESULT CreateDeviceIndependentResources()
{
    HRESULT hr = S_OK;

    // Создаем фабрику Direct2D
    hr = D2D1CreateFactory(
        D2D1_FACTORY_TYPE_SINGLE_THREADED,
        &pFactory
    );

    return hr;
}

// Создание устройство-зависимых ресурсов
HRESULT CreateDeviceResources(HWND hwnd)
{
    HRESULT hr = S_OK;

    if (!pRenderTarget)
    {
        RECT rc;
        GetClientRect(hwnd, &rc);

        D2D1_SIZE_U size = D2D1::SizeU(
            rc.right - rc.left,
            rc.bottom - rc.top
        );

        // Настройки рендер-таргета
        D2D1_RENDER_TARGET_PROPERTIES rtProps = D2D1::RenderTargetProperties();
        rtProps.pixelFormat = D2D1::PixelFormat(
            DXGI_FORMAT_B8G8R8A8_UNORM,
            D2D1_ALPHA_MODE_IGNORE
        );
        rtProps.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
        rtProps.usage = D2D1_RENDER_TARGET_USAGE_NONE;

        // Настройки HWND рендер-таргета
        D2D1_HWND_RENDER_TARGET_PROPERTIES hwndRtProps = D2D1::HwndRenderTargetProperties(
            hwnd,
            size,
            D2D1_PRESENT_OPTIONS_IMMEDIATELY
        );

        // Создаем рендер-таргет
        hr = pFactory->CreateHwndRenderTarget(
            rtProps,
            hwndRtProps,
            &pRenderTarget
        );

        if (SUCCEEDED(hr))
        {
            // Создаем кисти разных цветов
            hr = pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::Black),
                &pBlackBrush
            );
        }

        if (SUCCEEDED(hr))
        {
            hr = pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::Red),
                &pRedBrush
            );
        }

        if (SUCCEEDED(hr))
        {
            hr = pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::Blue),
                &pBlueBrush
            );
        }

        if (SUCCEEDED(hr))
        {
            hr = pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::Green),
                &pGreenBrush
            );
        }
    }

    return hr;
}

// Освобождение устройство-зависимых ресурсов
void DiscardDeviceResources()
{
    SafeRelease(&pRenderTarget);
    SafeRelease(&pBlackBrush);
    SafeRelease(&pRedBrush);
    SafeRelease(&pBlueBrush);
    SafeRelease(&pGreenBrush);
}

// Функция отрисовки сложной геометрии
void DrawComplexGeometry()
{
    ID2D1PathGeometry* pPathGeometry = nullptr;
    ID2D1GeometrySink* pSink = nullptr;

    // Создаем PathGeometry через фабрику
    HRESULT hr = pFactory->CreatePathGeometry(&pPathGeometry);

    if (SUCCEEDED(hr))
    {
        // Открываем GeometrySink для записи
        hr = pPathGeometry->Open(&pSink);
    }

    if (SUCCEEDED(hr))
    {
        // Начинаем фигуру (замкнутую)
        pSink->BeginFigure(
            D2D1::Point2F(300.0f, 100.0f),
            D2D1_FIGURE_BEGIN_FILLED
        );

        // Добавляем линии
        pSink->AddLine(D2D1::Point2F(400.0f, 200.0f));
        pSink->AddLine(D2D1::Point2F(300.0f, 300.0f));

        // Добавляем квадратичную кривую Безье
        D2D1_QUADRATIC_BEZIER_SEGMENT quadBezier = D2D1::QuadraticBezierSegment(
            D2D1::Point2F(200.0f, 200.0f),  // контрольная точка
            D2D1::Point2F(300.0f, 100.0f)   // конечная точка
        );
        pSink->AddQuadraticBezier(&quadBezier);

        // Завершаем фигуру (замкнутую)
        pSink->EndFigure(D2D1_FIGURE_END_CLOSED);

        // Закрываем sink
        hr = pSink->Close();
    }

    if (SUCCEEDED(hr))
    {
        // Рисуем контур сложной геометрии
        pRenderTarget->DrawGeometry(pPathGeometry, pBlueBrush, 3.0f);

        // Заливаем сложную геометрию
        pRenderTarget->FillGeometry(pPathGeometry, pGreenBrush);
    }

    SafeRelease(&pSink);
    SafeRelease(&pPathGeometry);
}

// Основная функция отрисовки
void OnRender(HWND hwnd)
{
    HRESULT hr = CreateDeviceResources(hwnd);

    if (SUCCEEDED(hr) && !(pRenderTarget->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED))
    {
        pRenderTarget->BeginDraw();

        // Очищаем область белым цветом
        pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));

        // 1. Рисуем линию
        pRenderTarget->DrawLine(
            D2D1::Point2F(50.0f, 50.0f),
            D2D1::Point2F(150.0f, 50.0f),
            pBlackBrush,
            2.0f
        );

        // 2. Рисуем прямоугольник (контур)
        pRenderTarget->DrawRectangle(
            D2D1::RectF(50.0f, 70.0f, 150.0f, 120.0f),
            pRedBrush,
            2.0f
        );

        // 3. Рисуем залитый прямоугольник
        pRenderTarget->FillRectangle(
            D2D1::RectF(170.0f, 70.0f, 270.0f, 120.0f),
            pRedBrush
        );

        // 4. Рисуем скругленный прямоугольник (контур)
        D2D1_ROUNDED_RECT roundedRect = D2D1::RoundedRect(
            D2D1::RectF(50.0f, 140.0f, 150.0f, 190.0f),
            10.0f, 10.0f
        );
        pRenderTarget->DrawRoundedRectangle(
            roundedRect,
            pBlueBrush,
            2.0f
        );

        // 5. Рисуем залитый скругленный прямоугольник
        D2D1_ROUNDED_RECT filledRoundedRect = D2D1::RoundedRect(
            D2D1::RectF(170.0f, 140.0f, 270.0f, 190.0f),
            15.0f, 15.0f
        );
        pRenderTarget->FillRoundedRectangle(
            filledRoundedRect,
            pBlueBrush
        );

        // 6. Рисуем эллипс (контур)
        D2D1_ELLIPSE ellipse = D2D1::Ellipse(
            D2D1::Point2F(100.0f, 250.0f),
            30.0f, 20.0f
        );
        pRenderTarget->DrawEllipse(
            ellipse,
            pGreenBrush,
            2.0f
        );

        // 7. Рисуем залитый эллипс (круг)
        D2D1_ELLIPSE filledEllipse = D2D1::Ellipse(
            D2D1::Point2F(200.0f, 250.0f),
            25.0f, 25.0f
        );
        pRenderTarget->FillEllipse(
            filledEllipse,
            pGreenBrush
        );

        // 8. Рисуем сложную геометрию
        DrawComplexGeometry();

        hr = pRenderTarget->EndDraw();

        // Если устройство потеряно, пересоздаем ресурсы
        if (hr == D2DERR_RECREATE_TARGET)
        {
            DiscardDeviceResources();
        }
    }
}

// Оконная процедура
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_SIZE:
    {
        if (pRenderTarget)
        {
            RECT rc;
            GetClientRect(hwnd, &rc);

            D2D1_SIZE_U size = D2D1::SizeU(
                rc.right - rc.left,
                rc.bottom - rc.top
            );

            pRenderTarget->Resize(size);
            InvalidateRect(hwnd, nullptr, FALSE);
        }
    }
    break;

    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        BeginPaint(hwnd, &ps);
        OnRender(hwnd);
        EndPaint(hwnd, &ps);
    }
    break;

    case WM_DISPLAYCHANGE:
        InvalidateRect(hwnd, nullptr, FALSE);
        break;

    case WM_DESTROY:
        DiscardDeviceResources();
        SafeRelease(&pFactory);
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }

    return 0;
}

// Точка входа
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
    // Регистрируем класс окна
    WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = nullptr;
    wcex.lpszClassName = L"Direct2DDemo";
    wcex.hIconSm = LoadIcon(nullptr, IDI_APPLICATION);

    if (!RegisterClassEx(&wcex))
        return -1;

    // Создаем устройство-независимые ресурсы
    if (FAILED(CreateDeviceIndependentResources()))
        return -1;

    // Создаем окно
    HWND hwnd = CreateWindow(
        L"Direct2DDemo",
        L"Direct2D Demo - Основы графики",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        800, 600,
        nullptr, nullptr, hInstance, nullptr
    );

    if (!hwnd)
        return -1;

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // Цикл сообщений
    MSG msg = { };
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

Комментарии (0)