Загрузка 0
ПОДЕЛИТЬСЯ

Мой блог

Листай вниз

WinUI 3 без XAML: как написать декларативный GUI на чистом C++23 с помощью библиотеки wxl

WinUI 3 без XAML: как написать декларативный GUI на чистом C++23 с помощью библиотеки wxl

Современный фреймворк WinUI 3 позиционируется компанией Microsoft в качестве ключевого фундамента для разработки десктопных приложений под Windows. Однако традиционная связка C++/WinRT и XAML нередко отпугивает разработчиков: долгая компиляция, запутанная иерархия на основе CRTP-миксинов, огромное количество шаблонной рутины и громоздкий синтаксис. Библиотека wxl (WinUI XAML-Less) предлагает кардинально иной подход — создание декларативного графического интерфейса на чистом C++23 с минимизацией накладных расходов WinRT.

Почему именно wxl?

Название wxl расшифровывается как WinUI XAML-Less. Это компактная архитектурная надстройка над WinUI 3, позволяющая описывать пользовательский интерфейс прямо в коде на C++23 в декларативном стиле — аналогично тому, как это сделано во Flutter или SwiftUI. При этом тяжелая стандартная проекция C++/WinRT изолирована внутри библиотеки и больше не раздувает единицы трансляции конечного проекта мегабайтами заголовков и скрытыми вызывами COM-интерфейсов.

Оптимизация компиляции C++/WinRT в библиотеке wxl
Схема сокращения заголовочных файлов и ленивого кэширования COM-интерфейсов.

Библиотека отлично показывает себя как в небольших утилитах из пары диалоговых окон, так и в масштабных приложениях. В отличие от декларативной разметки XAML, стандартный C++ позволяет легко декомпозировать GUI, повторно использовать фрагменты интерфейса, применять обычные циклы и условные операторы. Для примера, панель кнопок калькулятора в wxl описывается следующим образом:

Реклама
[&](iterate<L"C÷×√" L"789-" L"456+" L"123%" L"±0.="> key) {
    return Button {
        key.text(),
        fontSize = 18,
        fontWeight = 600,
        style = face(key),
        hAlign.stretch,
        vAlign.stretch,
        row = key.index / 4 + 1,
        column = key.index % 4,
        onClick = [symbol = key.value]() { type(symbol); },
    };
}

Роль XAML в современном GUI под Windows

Возникает естественный вопрос: оправдан ли полный отказ от XAML? На инфраструктурном уровне XAML решает важные задачи — управление шаблонами контролов, презентерами, словарями ресурсов и стилизацией тем. Он позволяет изменять внешний вид приложения путем подгрузки сторонних XAML-ресурсов без пересборки C++ кода.

Разработчики wxl не отказываются от продуманной системы ресурсов WinUI 3, а используют её в качестве надежного фундамента. Однако для описания структуры окон и логики взаимодействия декларативный C++ оказывается значительно удобнее и быстродейственнее.

Графический бэкенд и мощь Windows Composition API

WinUI 3 тесно интегрирован с системным графическим композитором Windows Composition API (Visual Layer). Корни этой технологии уходят в DirectComposition времен Windows 8, но именно в Windows 10/11 композитор достиг технологической зрелости. Это высокопроизводительный графический движок, работающий выше базового уровня DirectX, но ниже уровня готовых виджетов.

Реклама

Композитор берёт на себя отрисовку фигур, теней, эффектов, а также управление анимациями в отдельном потоке. Благодаря этому плавный скроллинг, эффекты наведения мыши (hover) и трансформации выполняются без задержек, даже если основной поток GUI временно загружен вычислениями. Нативные приложения, работающие поверх Windows Composition API, показывают производительность, сравнимую с оптимизированными 3D-играми.

Пример окна приложения HelloHere на wxl C++23
Интерфейс тестового приложения HelloHere, созданный полностью на C++23 без использования XAML.

Три фундаментальные сложности сложного GUI

Создание графической библиотеки с нуля — крайне трудоемкая задача. На определенном этапе любой разработчик сталкивается с тремя масштабными проблемами:

  1. Рендеринг и редактирование текста: Полноценный текстовый контрол (TextBox) по сложности сопоставим с отдельным текстовым редактором, а RichTextControl требует реализации форматирования уровня офисных пакетов.
  2. Автоматизация интерфейса (GUI Automation): Внешние утилиты и алгоритмы искусственного интеллекта должны считывать структуру окна, вводить текст и нажимать кнопки для автоматического тестирования и управления.
  3. Доступность (Accessibility): Поддержка экранных дикторов, контрастных тем и обхода визуального дерева для пользователей с ограниченными возможностями.

Именно поэтому рациональным решением является использование готовых высокоуровневых компонентов WinUI 3, обернутых в удобный интерфейс на C++.

Декларативный интерфейс на чистом C++23

Архитектура wxl решает основные проблемы традиционной разработки под Windows, обеспечивая лаконичный синтаксис, исключая проникание типов WinRT в публичные заголовки и выстраивая полноценную иерархию классов C++ с кешированием COM-интерфейсов.

Как выглядит декларативный код wxl

Ниже приведен пример создания окна с карточкой, темой и кнопкой без единой строчки XAML-кода:

#include "pch.h"
using namespace wxl;
using namespace wxl::dsl;

wxl::Teardown wxl_launched() {
    auto window = Window {
        title = L"Hello from WXL",
        minSize = {500, 430},
        Grid {
            rowDefinitions = L"auto,*",
            Card {
                row = 1,
                Rows {
                    TextBlock { L"A modern C++23 library", styles.textBlock.subtitle },
                    Button {
                        content = L"Click",
                        toolTip = L"Похвалить библиотеку",
                        onClick = {content = L"Thank You!"},
                    },
                },
            },
        },
    };
    window.activate();
    return {};
}

Присваивания внутри фигурных скобок вычисляются в отложенные операции, а вариативный конструктор последовательно применяет их к созданному объекту. Конструкция onClick = {content = L"Thank You!"} создает обработчик, который автоматически транслирует новые значения обратно объекту-источнику.

Борьба с мегабайтами заголовков C++/WinRT

Официальная проекция C++/WinRT включает свыше 30 МБ заголовочных файлов, распределенных по 252 файлам. Компилятор вынужден повторно анализировать этот массив данных в каждой единице трансляции. В библиотеке wxl применяется генератор winui-srcgen, который формирует минимальную проекцию по JSON-профилю с учетом графа зависимостей.

В результате объем заголовочных файлов снизился до 574 КБ (почти в 50 раз меньше оригинала), а вся бинарная реализация занимает около 1 МБ. Публичный интерфейс wxl полностью изолирован от имен winrt::, ABI-указателей и типов HRESULT. Исходники C++/WinRT парсятся лишь единожды при сборке самой библиотеки wxl.

Замеры времени сборки демонстрируют существенный прирост скорости:

  • Пересборка main.cpp тестового приложения с линковкой исполняемого файла занимает всего 4,7 секунды.
  • Полная пересборка файла библиотеки с повторным разбором проекции cppwinrt требует 30,8 секунды.

Настоящая иерархия классов вместо CRTP-миксинов

В стандартном C++/WinRT класс Button не наследуется от UIElement в привычном понимании ООП. Вместо этого используется набор шаблонов CRTP (consume_X<D>), а приведение к базовым типам выполняется через вызов .as<T>(). Это приводит к раздуванию бинарного кода за счет генерации дубликатов функций для каждого типа.

В wxl реализовано прямое наследование: wxl::Button наследует wxl::ButtonBase, который восходит к wxl::UIElement и wxl::Object. Публичные обертки не содержат полей данных — все состояние скрыто в приватной цепочке Impl. Благодаря этому скомпилированный код методов базовых классов существует в единственном экземпляре для всех наследников.

Ленивое кэширование и однократный QueryInterface

Каждый объект WinRT поддерживает множество интерфейсов (например, UIElement реализует IUIElement, IAnimationObject, IVisualElement и др.). В стандартной проекции обращение к методам базовых интерфейсов сопровождает цепочка вызовов QueryInterface, AddRef и Release с атомарными операциями.

В wxl интерфейсы запрашиваются лениво через поля структуры Impl при первом обращении:

template <auto member>
member_type<member> const& Object::Impl::get() {
    auto& field = static_cast<class_type<member>&>(*this).*member;
    if (!field) {
        field = inspectable_.as<member_type<member>>(); // Выполняется один раз
    }
    return field;
}

Все последующие обращения выполняют только быструю проверку указателя на nullptr. Поскольку все виджеты создаются и функционируют исключительно в главном STA-потоке (Single-Threaded Apartment), операции со счетчиками ссылок обходятся без межпотоковых блокировок.

Удобные хелперы для рутинных задач

Для ускорения разработки библиотека wxl содержит набор встроенных DSL-хелперов:

  • minSize = {500, 430} — перехват системного сообщения WM_GETMINMAXINFO для ограничения размеров окна;
  • placement — сохранение и корректное восстановление позиции окна на активном мониторе;
  • rowDefinitions = L"auto,*" — декларативная разметка сетки Grid без ручного наполнения коллекций;
  • toolTip = L"..." — единовременная установка всплывающей подсказки и имени для экранного диктора;
  • brushes.Card.BackgroundFillColor — автоприменение цвета из активной системной темы;
  • wxl::DrawingSurface — прямая интеграция Direct2D с поверхностью композитора;
  • wxl::accept_file_drops — встроенная обработка Drag-and-Drop файлов;
  • wxl::UiThread — безопасный вызов функций в интерфейсном потоке из фоновых задач.

Где изучить исходники и примеры

Ознакомиться с практической реализацией библиотеки можно в репозитории проекта:

  • samples/HelloHere — базовое окно с карточкой, настройкой темы и кнопкой;
  • samples/Calculator — пример полнофункционального калькулятора с сеткой кнопок и обработкой ввода;
  • wxl.gen/profiles/ — настраиваемые профили генератора проекций под задачи конкретного проекта.

Источник: habr.com

01.