Здравствуйте. Эта статья посвящена самому началу работы с Direct2D(Первая в серии, дальше будут продолжения), поскольку в документации от Microsoft мало что сказано о том, что вообще нужно сделать для создания окна и прочих вещей, а также слишком много «воды». Таким образом, это альтернатива началу документации от Microsoft.

Структура будет построена как ряд вопросов и ответов на них. Так как всё сводится к понятию окно, первым делом будет дано объяснение, что это такое в Windows.

Что такое окно в Windows?

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

Но прямого доступа к структуре окна мы не имеем, лишь к её части - структуре класса окна WNDCLASSEXW. Обратившись к MSDN, получим следующее:

typedef struct WNDCLASSEXW {
    UINT      cbSize;
    UINT      style;
    WNDPROC   lpfnWndProc;
    int       cbClsExtra;
    int       cbWndExtra;
    HINSTANCE hInstance;
    HICON     hIcon;
    HCURSOR   hCursor;
    HBRUSH    hbrBackground;
    LPCWSTR   lpszMenuName;
    LPCWSTR   lpszClassName;
    HICON     hIconSm;
} WNDCLASS;

Сообщения окна. Оконная процедура

Прежде чем начать заполнение структуры, важно рассказать об этих двух вещах. Каждое ваше действие в Windows - это сообщение. Сообщение обрабатывается оконной процедурой (функцией). Её синтаксис:

LRESULT CALLBACK WindowProc(
    HWND hwnd,      // Получатель (может отличаться от MSG.hwnd для дочерних окон)
    UINT uMsg,      // Идентификатор сообщения
    WPARAM wParam,  // Дополнительные данные
    LPARAM lParam   // Дополнительные данные
);

hwnd - это ID (идентификатор) окна; uMsg - число, по которому определяется тип сообщения (системные - от 0x0000 до 0x03FF, пользовательские, возникающие из-за действий пользователя, - от 0x0400 до 0x7FFF и от 0xC000 до 0xFFFF). Ссылка подробней; в wParam и lParam передаются дополнительные данные (например, если uMsg == WM_MOUSEMOVE, то в wParam - состояние кнопок мыши, а в lParam - координаты x и y (младшее и старшее слово)). Список всех сообщений можно найти в документации. Пример функции:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_CREATE:
            // Инициализация при создании окна
            return 0;
        case WM_PAINT:
            // Обработка перерисовки
            return 0;
        case WM_DESTROY:
            PostQuitMessage(0); //Отправляет сообщение WM_QUIT
            return 0;
        default:
            // Сообщения, которые мы не обработали, передаем на обработку по умолчанию
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

Заполнение WNDCLASSEXW

Зная, что для регистрации нужна заполненная структура, произведём необходимое:
1.Размер, достаточно указать в значение результат sizeof(WNDCLASSEXW)

1.Стиль окна определяет как визуальное отображение, так и его поведение. Список стилей.

2.Ссылка на оконную процедуру (описана выше).

3. и 4. Название говорит само за себя, но четвёртый параметр - для каждого отдельного дочернего окна.

5. HInstance - это базовый адрес в памяти, по которому загружен исполняемый модуль (EXE или DLL); его значение передаётся через точку входа.

6. и 7. и 8. Названия говорят сами за себя.

9. То самое меню, которое отображается у программ.

10. Название класса окна в Unicode.

11. Иконка окна.

Минимальный пример заполнения:

    WNDCLASSEXW wcew = { 0 }; 
    wcew.cbSize = sizeof(WNDCLASSEXW);           // Обязательно: размер структуры
    wcew.lpfnWndProc = WindowProc;              // Указатель на оконную процедуру
    wcew.hInstance = hInstance;                 // Экземпляр приложения
    wcew.hCursor = LoadCursor(NULL, IDC_ARROW); // Курсор - стрелка
    wcew.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // Фон - цвет окна
    wcew.lpszClassName = L"MyWindowClass";            // Уникальное имя класса

WindowProc и hInstance указаны как бы вне; полноценный пример будет позже.

Регистрация WNDCLASSEXW. Создание окна

Для регистрации достаточно вызвать RegisterClassExW, передав в аргументы ссылку на структуру WNDCLASSEXW, то есть: RegisterClassExW(&wcew).

Теперь создание окна происходит при помощи вызова функции CreateWindowExW, результатом которого является ID окна - HWND. Аргументы функции:

  1. dwExStyle - дополнительный стиль окна, изменяющий поведение. Список;

  2. lpClassName - тип создаваемого окна или элемента управления;

  3. lpWindowName - название окна;

  4. dwStyle - определяет внешний вид и поведение окна. Таблица(прокрутить вниз);

  5. X - позиция окна по оси X;

  6. Y - позиция окна по оси Y;

  7. nWidth - ширина окна;

  8. nHeight - высота окна;

  9. hWndParent - родительское окно;

  10. hMenu - меню или ID контрола;

  11. hInstance - экземпляр приложения;

  12. lpParam - дополнительные параметры (указатель на любые данные).

Зная все нужные параметры, можно просто написать:

HWND hwnd = CreateWindowExW(0, wcew.lpszClassName, L"Minimal Window",
                              WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                              400, 300, NULL, NULL, hInst, NULL);

Если при создании не указать стиль его видимости, то это необходимо сделать вручную через ShowWindow. Данная функция управляет отображением окна: первый аргумент - это ID окна (HWND), второй - тип видимости (список). Также чтобы не было проблем с отображением окна, вызывается функция UpdateWindow, которая немедленно перерисовывает окно и принимает один аргумент - ID окна. В итоге имеем следующее:

ShowWindow(hwnd,3);
UpdateWindow(hwnd);

Точка входа

До этого момента точка входа не рассматривалась, так как были более важные вещи. Теперь рассмотрим и её:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmdLine, int nCmdShow)
{

return 0;
}
  1. WINAPI - это макрос, который определяет, как функция получает параметры (справа налево) и кто очищает стек (сама функция).

  2. hInstance - это ID приложения.

  3. hPrev - ID предыдущего приложения; в x32 и x64 Windows не используется.

  4. lpCmdLine - аргументы, указанные при запуске в командной строке (аргумент под индексом 0 не содержит названия программы).

  5. int nCmdShow - флаг отображения окна(Список. Прокрутить вниз). Передаётся от программы-инициатора. То есть если программа запускается с скрытым окном, то это поведение сохранится и для нового.

Приём и обработка сообщений, передача оконной процедуре. Финал

И сразу код:

MSG msg;
        while (GetMessage(&msg, NULL, 0, 0)) 
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
  1. MSG - cодержит информацию о сообщении из очереди сообщений (описание).

  2. GetMessage - получение сообщения. Первый аргумент - ссылка на структуру; второй - ID окна (null - все окна текущего потока); третий аргумент - первое сообщение для фильтрации; четвёртый аргумент - последнее сообщение для фильтрации (0 - нет фильтра).

  3. TranslateMessage - преобразует сообщения от клавиатуры в специальные сообщения; если сообщение не от клавиатуры - пропускает. Принимает ссылку на структуру сообщения.

  4. DispatchMessage - передаёт структуру MSG оконной процедуре. Принимает ссылку на структуру сообщения.

Имея все пункты, соединяем воедино:

#include <Windows.h>

// Глобальное имя класса окна

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);  // Отправляем WM_QUIT для завершения приложения
        return 0;

    case WM_PAINT:
        return 0;

    }

    // Сообщения, которые мы не обработали, передаем на обработку по умолчанию
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

const wchar_t* CLASS_NAME = L"MyWindowClass";

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmdLine, int nCmdShow)
{
    // 1. Заполнение структуры класса окна
    WNDCLASSEX wc = { 0 };
    wc.cbSize = sizeof(WNDCLASSEX);           // Обязательно: размер структуры
    wc.lpfnWndProc = WindowProc;              // Указатель на оконную процедуру
    wc.hInstance = hInstance;                 // Экземпляр приложения
    wc.hCursor = LoadCursor(NULL, IDC_ARROW); // Курсор - стрелка
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // Фон - цвет окна
    wc.lpszClassName = CLASS_NAME;            // Уникальное имя класса

    // 2. Регистрация класса окна
    RegisterClassEx(&wc);

    // 3. Создание окна
    HWND hwnd = CreateWindowEx(
        0,                      // Расширенные стили
        CLASS_NAME,             // Имя класса окна
        L"Test Window",         // Заголовок окна
        WS_OVERLAPPEDWINDOW,    // Стиль окна
        100, 100,               // Позиция (x, y)
        400, 300,               // Размер (width, height)
        NULL,                   // Родительское окно
        NULL,                   // Меню
        hInstance,              // Экземпляр приложения
        NULL                    // Дополнительные параметры
    );

    // 4. Показать и обновить окно
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // 5. Цикл обработки сообщений
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}


На этом, по сути, и всё. Дерзайте! Далее будет рассказ о рисовании, отображении различного контента и т.п.

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


  1. Einherjar
    05.11.2025 19:09

    Эта статья посвящена самому началу работы с Direct2D

    Ну и где тут хоть одна строка имеющая отношение к Direct2D?


    1. Johnny_Depp Автор
      05.11.2025 19:09

      Не совсем вас понял. Буквально всё, имеет к нему отношение. Ведь без окна , графика не возможна. Но если я выдам всё сразу, получится большее полотно, и используя здравый смысл - я разделил на множество статей.

      Хотя прочитав ваш комментарий, я теперь слегка изменю и сделаю пометку, что это первая статья в серии.


      1. Johnny_Depp Автор
        05.11.2025 19:09

        "Direct2D разработан компанией Microsoft для создания приложений под управлением операционных систем Windows" - прямая цитата с поисковика. Ну вот, собственно, первая статья это начало, которое объясняет создание окна.


        1. Einherjar
          05.11.2025 19:09

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

          Я уж не говорю про то что те, кому зачем-то может понадобиться в 2025 году напрямую работать с низкоуровневвм графическим апи вроде d2d, как правило уже в курсе что такое окно.


  1. Serpentine
    05.11.2025 19:09

    статья посвящена самому началу работы с Direct2D

    Тогда что она делает в сишном хабе? Нет, если автор покажет рабочий код Direct2D на чистых сях (без костылей на плюсах), это будет номер!

    в документации от Microsoft мало что сказано о том, что вообще нужно сделать для создания окна

    А это что? Целый учебник с картинками: Get Started with Win32 and C++ (даю линк на английскую версию, т.к. перевод ИИ в MSDN на русский — кровь из глаз).

    И почему в затравочном абзаце речь про DX, а тут резко упали до D2D?

    структуре класса окна _WNDCLASS. Обратившись к MSDN, получим следующее

    Почему WNDCLASSEX в "цитате" из MSDN и в статье обозвали "_WNDCLASS", когда у последнего (сиречь WNDCLASS) полей меньше? И ссылка дана на ansi версию, когда по ходу пьесы у нас вовсю юникод будет?


    1. Serpentine
      05.11.2025 19:09

      UPD. Что-то странное с примерами кода:

      int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmd, int nCmdShow)

      ...

      lpCmdLine - аргументы, указанные при 

      Это который из четырех?

      Имея все пункты, соединяем воедино:

      Соединили:
      #include <Windows.h>
      
      LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
      {
          switch (uMsg)
          {
          case WM_DESTROY:
              PostQuitMessage(0);  //Отправляет сообщение WM_QUIT
              return 0;
      
          case WM_PAINT:
          {
      
              return 0;
          }
      
          // Сообщения, которые мы не обработали, передаем на обработку по умолчанию
          return DefWindowProc(hwnd, uMsg, wParam, lParam);
      }
      
      const wchar_t* CLASS_NAME = L"MyWindowClass";
      
      int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmd, int nCmdShow)
      {
          // 1. Заполнение структуры класса окна
          WNDCLASSEX wc = { 0 };
          wc.cbSize = sizeof(WNDCLASSEX);           // Обязательно: размер структуры
          wc.lpfnWndProc = WindowProc;              // Указатель на оконную процедуру
          wc.hInstance = hInstance;                 // Экземпляр приложения
          wc.hCursor = LoadCursor(NULL, IDC_ARROW); // Курсор - стрелка
          wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // Фон - цвет окна
          wc.lpszClassName = CLASS_NAME;            // Уникальное имя класса
      
          // 2. Регистрация класса окна
          RegisterClassEx(&wc));
      
          // 3. Создание окна
          HWND hwnd = CreateWindowEx(
              0,                      // Расширенные стили
              CLASS_NAME,             // Имя класса окна
              L"Test Window",         // Заголовок окна
              WS_OVERLAPPEDWINDOW,    // Стиль окна
              100, 100,               // Позиция (x, y)
              400, 300,               // Размер (width, height)
              NULL,                   // Родительское окно
              NULL,                   // Меню
              hInstance,              // Экземпляр приложения
              NULL                    // Дополнительные параметры
          );
      
          // 4. Показать и обновить окно
          ShowWindow(hwnd, nCmdShow);
          UpdateWindow(hwnd);
      
          // 5. Цикл обработки сообщений
          MSG msg;
          while (GetMessage(&msg, NULL, 0, 0))
          {
              TranslateMessage(&msg);
              DispatchMessage(&msg);
          }
      
          return 0;  // Код завершения из WM_QUIT
      }

      Я нашел 2 причины, почему этот код вообще не скомпилируется. Это нейруха сгенерировала что ли, или просто влом было собирать перед публикацией?

      Ну и:

      return 0; // Код завершения из WM_QUIT

      Нет, это обычный ноль!


      1. Johnny_Depp Автор
        05.11.2025 19:09

        Спасибо за замечания, были некоторые варианты, не до конца провёл форматирование итоговое и не заметил, хотя перечитывал


    1. Johnny_Depp Автор
      05.11.2025 19:09

      1. Возьмите любую функцию и откройте её в MSDN пролистайте вниз, внизу будет указано в какой она DLL , ну а дальше используете DLL как обычно.(Если хотите могу пример и на ассемблере x86 (NASM) скинуть, но в личку)

      2. Ну вы сами понимаете, что есть только на английском, а это на русском.

      3. Как ранее говорил, ошибка форматирования статьи, думал что исправил. Уже исправлено.

        Спасибо за комментарий


  1. morgot
    05.11.2025 19:09

    Спасибо , конечно, за пересказ книги Петцольда 95 года. Надеюсь, дальше будет что-то интереснее.


  1. Wolf4D
    05.11.2025 19:09

    А зачем вообще выбирать Direct2D? Штука прямо ОЧЕНЬ специфическая, со своими приколами. Почему не Direct 3D, как делает 95% разработчиков? Почему не DirectDraw какой-нибудь?


    1. Johnny_Depp Автор
      05.11.2025 19:09

      Насчёт вопроса почему 2D, ну потому что рисуя двухмерное, не важное что это, при помощи Direct3D придётся игнорировать ещё одну ось(об этом кстати даже в интернете есть обсуждения).

      Ну то есть это просто странно.

      А насчёт DirectDraw - он давно устарел, "DirectDraw уже давно не используется, он был актуален в определённых версиях DirectX. Direct2D появился с DirectX10 и считается актуальным для 2D-графики." Вот ссылка подробней.