Декларативное описание UI на C++20
Несколько лет назад у меня появилась идея как можно реализовать декларативное описание UI на современном C++. Наконец-то нашёл время и реализовал маленькую экспериментальную библиотеку.
Ключевая идея заключается в том, что с помощью Class Template Argument Deduction (CTAD), добавленного в C++17, и Variadic Templates, которые существуют ещё с C++11, можно в compile time описывать сложные иерархические структуры данных с помощью фигурных скобок, без явного указания параметров шаблонов.
Например, мы можем объявить класс widget
для описания виджетов UI следующим образом:
template <typename ... Elements>
class widget {
public:
explicit widget(Elements && ... elements) { /*...*/ }
};
Теперь этот класс можно использовать с другими классами, объявленными подобным же образом, для построения дерева разметки UI:
auto w = widget {
title {"My Widget"},
widget {
/* ... */
},
other_widget {
/* ... */
}
};
Компилятор сам выведет параметры Elements
шаблона widget
для переменной w
, в данном случае тип переменной w
будет widget<title, widget<...>, other_widget<...>>
. Имея типы дочерних элементов в виде параметров шаблона класса мы можем с помощью метапрограммирования делать много интересных вещей,
например отделить дочерние виджеты (widget
и other_widget
) от свойств (title
), сохранить их в переменную-член и организовать к ним доступ по индексу со статической типизацией:
// child1 type is widget<...> &
auto & child1 = w.child<0>();
// child2 type os other_widget<...> &
auto & child2 = w.child<1>();
В C++20 добавили возможность использования literal class type в качестве параметров шаблона, и с помощью этого можно реализовать параметризацию шаблонных классов и функций строковым литералом. Применив всё это вместе можно реализовать поиск дочерних виджетов в разметке по строковому ID со статическиой типизацией:
auto w = widget {
some_widget {
id<"my_widget">
}
}
// child type is some_widget<...> &
auto & child = w.child<"my_widget">();
Отдельной проблемой была реализация возможности добавления обработчиков событий прямо в разметке. С помощью обобщённых лямбда-функций (из C++14) и небольших хитростей удалось реализовать возможность доступа из обработчиков к дочерним элементам разметки по ID или индексу:
auto w = root_widget {
some_widget {
id<"child_widget">
}
button {
on_click { [](auto & root) {
// child type is some_widget &
auto & child = root.template child<"child_widget">();
} }
}
}
Идея заключается в том, чтобы добавить специальные типы виджетов (root_widget
, window
, dialog
, и т. д.), которые будут корнями разметки. Конструкторы этих виджетов будут устанавливать себя в качестве параметра, передаваемого в обобщенные лямбда-функции всех обработчиков событий.
Если всё это сложить вместе, то реализация простого диалога с кнопкой и счётчиком выглядит примерно так:
auto dlg = ui::dialog {
ui::vbox {
ui::hbox {
ui::label {
ui::text("Counter:")
},
ui::line_edit {
ui::id<"counter">,
ui::text("0")
}
},
ui::button {
ui::text("Button"),
ui::on_click { [](auto & root) {
auto & edit = root.template child<"counter">();
edit.set_text(std::to_wstring(std::stoi(edit.text()) + 1));
} }
}
}
};
Реализация библиотеки для Qt с примером использования лежит здесь: https://github.com/cxx-ui/ull.
Это пока только экспериментальная версия, proof of concept. Осталось понять что с этим делать дальше. Я склоняюсь к тому, что не стоит пытаться делать отдельную новую библиотеку для UI на C++, а сделать это для начала в виде вспомогательной библиотеки, которую можно будет использовать для описания разметки виджетов в любом существующем коде UI, написанном с помощью разных библиотек: Qt, wxWidgets, Gtk.
comments powered by Disqus