Началось все почти два года назад в декабрe, наш основной проект (видео мессенджер) использовал WTL для Windows и GTKmm для Linux. Поддержки мака не было. Огромной неприятностью было тащить два идентичных клиента, которые, по идее, должны делать все строго одно и тоже. Разумеется, это никогда не получалось. От мысли что надо бы сделать ещё один нативный клиент для мака начинался нервный тик...
На резонный вопрос - почему сразу делалось не на Qt могу лишь ответить, что это связано с, так скажем, гурманскими предпочтениями и, отчасти, с любовью к монолитным exe. Да и не требовалось на старте ничего кроме винды.
В течение шести лет жизни с двумя кодовыми базами одного и того же, неспешно подбирались легковесные UI библиотеки написанные хотя бы в стиле C++11.
Надо сказать, что мы активно используем boost и всей душой, как можем, его любим...
В 2021 году видимо Гугл работал плохо или звёзды так сошлись, но не нашлось ничего стоящего. Все что попадалось - основанные на рендеринге html проекты и обертка над wxWidgets. Сейчас то мы знаем про lvgl, да... А вообще, тысячи их.
wxWidgets не плох, но хотелось своего рисования, без окошек под кнопки, поля ввода и списки, boost/bsd подобной лицензией, максимально лаконичной, и в идеале работающей от Windows XP / CentOS 6 на стандартном GDI / X11 до Vulkan на современных машинах.
В итоге, все же было принято волевое решение сделать минимальный UI фреймворк для этого проекта, и сразу выпускать его в Open Source под лицензией boost.
Задачи для UI фреймворка
- Работать на Windows (Как минимум 7, но работает и на XP)
- Работать на Linux (Начиная от условной Ubuntu 16 / CentOS6)
- Работать на macOS
- Открывать окна и отображать на них контролы.
- Предоставлять общий интерфейс к подсистеме рисования, который скрывает платформенно-зависимые методы. Это позволят написать контрол один раз, на любой платформе, и он будет выглядеть и вести себя одинаково на всех.
- Предоставлять общий интерфейс к событиям. Любой контрол или пользователь может подписаться на любую группу сообщений, в том числе пользовательскую, с возможностью асинхронной отправки/получения сообщений.
- Принимать системные сообщения, реагировать на мышь, клавиатуру и прочие события.
- Иметь возможность менять цветовую гамму / стиль / пиктограммы / изображения всех контролов / окон из одного места. Хранить все визуальные настройки приложения в json, в том числе иметь возможность хранить в нем же изображения.
- Предоставлять систему текстовых констант для заголовков и надписей в зависимости от выбранного языка.
- Иметь возможность откреплять / прикреплять окна друг от друга.
- Предоставлять реализацию основных UI контролов и иметь понятную и доступную возможность добавления новых контролов сторонними разработчиками для своих приложений.
- Иметь удобный интерфейс для работы с конфигами приложений. Поддерживается реестр Windows и ini файлы. Естественно, с возможностью изменения.
Общая схема фреймворка
Все базируется на двух сущностях - Window и Control. Окно может содержать контролы, также само окно является контролом.
Control - это любой визуальный элемент для взаимодействия с пользователем - кнопка, поле ввода, список, меню и т.д. Control знает, как обрабатывать события, поступающие от Window, хранит свои состояния и рисует себя на графическом контексте, который предоставляется содержащим его окном.
Window - принимает системные события и обеспечивает их рассылку подписчикам. Так же окно дает команду на перерисовку своих контролов и предоставляет им свой graphic. Кроме этого, окно управляет фокусом ввода, может сделать модальность и отправить подписанному пользователю или в систему событие.
Graphic - предоставляет интерфейс к системным методам рисования. В настоящий момент, реализовано рисование на Windows GDI/GDI+ и Linux xcb/cairo. Разумеется, нет никаких препятствий реализовать рисование на vulcan/bare metal/etc.
В библиотеке также есть вспомогательные средства для работы - структуры common
(содержит такие основные типы, как rect, color, font
), event
(события мыши, клавиатуры, внутренние и системные события), graphic
(для физической отрисовки на системном графическом контексте) theme
(система констант для удобной поддержки визуальных тем) , locale
(подсистема для удобного хранения текстового контента), config
(для удобной, единообразной работы с настройками приложения)
Некоторые основополагающие принципы
В общих чертах процесс работы приложения выглядит следующим образом:
Окно принимает системные события такие как: необходимость отрисовки, ввод с мыши и клавиатуры, изменения устройств, пользовательские сообщения. Данные сообщения передаются подписчикам событий окна, это, во-первых, содержащиеся на окне контролы, во-вторых, пользовательский код приложения, при необходимости. Для упрощения работы, имеется возможность получать только события, относящиеся к контролу: мышь — в прямоугольнике, занимаемом контролом; клавиатура — если контролу принадлежит фокус ввода.
Получение сообщений/событий от контрола производится специфическими для этого контрола коллбэками. Это позволило радикально упростить систему событий, не потеряв в функциональности, и, на наш взгляд, выиграв в лаконичности.
При необходимости отрисовки части окна, производится поиск попадающих в область перерисовки контролов и последовательно, по порядку добавления контролов на окно, вызывается метод draw() каждого контрола. Контролы отвечающие в topmost() true рисуются в последнюю очередь, чтобы оказаться наверху стека контролов.
Чтобы немного прояснить, как построена система, предлагаем рассмотреть интерфейсы окна, контрола и графика
Методы window
i_window.hpp
Создание / уничтожение окна
bool init(const std::string &caption, const rect &position, window_style style, std::function<void(void)> close_callback);
void destroy();
Добавление/удаление контрола
void add_control(std::shared_ptr<i_control> control, const rect &position);
void remove_control(std::shared_ptr<i_control> control);
Перерисовывает часть окна с имеющимися на данном участке контролами. Этот метод вызывается контролом, когда ему нужно себя перерисовать. В ответ окно вызывает draw() контрола с подготовленным graphic (контекстом рисования).
void redraw(const rect &position, bool clear = false);
Методы подписки на события, которые получает окно. События бывают от системы, внутренние или от приложения.
std::string subscribe(std::function<void(const event&)> receive_callback, event_type event_types, std::shared_ptr<i_control> control = nullptr);
void unsubscribe(const std::string &subscriber_id);
При подписке указываются желаемые к получению типы событий и подписчику возвращается токен, который можно передать для принудительной отписки через unsubscribe().
Послать сообщение через системный шедулер сообщений (Win32 / X11)
void emit_event(int32_t x, int32_t y);
Ссылка на структуру, содержащую платформо зависимые сущности. Например дескриптор окна HWND
в Windows или xcb_connection / Display
в Linux.
system_context &context();
Методы control
Каждый контрол должен реализовывать как минимум эти методы. Так же окно, само являясь контролом, должно их имплементировать.
i_control.hpp
Метод где контрол должен нарисовать себя. Вызывается только окном, когда необходима перерисовка контрола. Если контролу необходимо перерисовать себя, он должен вызвать redraw()
своего родительского окна.
void draw(graphic &gr, const rect &paint_rect);
Пользовательский метод для изменения положения контрола на окне. Координаты задаются в пикселях относительно левого верхнего угла родительского окна.
void set_position(const rect &position, bool redraw = true);
Пользовательский метод, возвращает положение контрола относительно окна
rect position() const;
Метод, вызываемый родительским окном при вызове add_control(), позволяет контролу получить указатель на свое родительское окно.
void set_parent(std::shared_ptr<window> window_);
Возвращает указатель на родительское окно
std::weak_ptr<window> parent() const;
Метод, вызываемый родительским окном при вызове remove_control() очищает указатель на родительское окно контрола
void clear_parent();
Сообщает родительскому окну, нужно ли рисовать контрол поверх всех остальных контролов
bool topmost() const;
Изменяет визуальную тему контрола. Если параметр равен nullptr то используется тема приложения по умолчанию.
void update_theme(std::shared_ptr<i_theme> theme_ = nullptr);
Методы управления видимостью
void show();
void hide();
bool showed() const;
Методы управления “включенностью”
virtual void enable();
void disable();
bool enabled() const;
Методы для выстраивания отношений контрола с клавиатурным фокусом ввода
bool focused() const; /// Returns true if the control is focused
bool focusing() const; /// Returns true if the control receives focus
Возвращает структуру, содержащую подробности последней ошибки. Следует вызывать после конструирования контрола или если контрол содержит методы типа bool init() подразумевающего наличие внешних проблем.
error get_error() const;
Методы graphic
Каждое окно имеет свой график для рисования своих контролов. Но никто не мешает создать свой дополнительный график внутри контрола или из приложения. Для отрисовки контрола, окно предоставляет ссылку на свой график через вызов метода draw()
контрола.
graphic.hpp
graphic(system_context &context);
Методы инициализации/деинициализации
void init(const rect &max_size, color background_color);
void release();
Устанавливает цвет фона, этим цветом будет залит холст при вызове clear()
void set_background_color(color background_color);
void clear(const rect &position);
Сброс (отрисовка) области на системный графический контекст
void flush(const rect &updated_size);
Нарисовать точку, линию
void draw_pixel(const rect &position, color color_);
void draw_line(const rect &position, color color_, uint32_t width = 1);
Измерить размер текста с выбранным шрифтом
rect measure_text(const std::string &text, const font &font_);
Написать текст выбранным шрифтом
void draw_text(const rect &position, const std::string &text, color color_, const font &font_);
Нарисовать простой прямоугольник
void draw_rect(const rect &position, color fill_color);
Нарисовать прямоугольник со скругленными краями
void draw_rect(const rect &position, color border_color, color fill_color, uint32_t border_width, uint32_t round);
Нарисовать буфер RGB32
void draw_buffer(const rect &position, uint8_t *buffer, size_t buffer_size);
Нарисовать содержимое другого графика
void draw_graphic(const rect &position, graphic &graphic_, int32_t left_shift, int32_t right_shift);
Доступ к системному DC
#ifdef _WIN32
HDC drawable();
#elif linux
xcb_drawable_t drawable();
#endif
Список методов рисования может и будет расширяться по мере необходимости.
Главный цикл приложения
В случае работы на Windows запускается стандартный бесконечный цикл:
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int) msg.wParam;
Каждое запущенное, не дочернее окно, становится получателем сообщений через имеющейся в нем wnd_proc. Далее, в зависимости от типа события, производится либо перерисовка контролов, работа с положением/размером окна, либо событие посылается подписчикам. Срок жизни первого созданного окна определяет срок жизни приложения.
На линукс картина слегка отличается, но для пользователя и контролов выглядит аналогично. Каждое не дочернее окно, запускает отдельный тред для ожидания событий в xcb_wait_for_event() и по мере их поступления рассылает их подписчикам.
bool window::init(...)
{
...
thread = std::thread(std::bind(&window::process_events, this));
}
void window::process_events()
{
xcb_generic_event_t *e = nullptr;
while (runned && (e = xcb_wait_for_event(context_.connection)))
{
switch (e->response_type & ~0x80)
{
case XCB_EXPOSE:
...
Весь этот код скрыт в window
и framework
. framework
- это новый компонент появившийся благодаря критике первоначального варианта с торчащим за ifdef'ами платформо зависимым кодом в main
.framework
имеет всего 3 главные функции init(), run()
и stop(). init()
нужно вызвать в первой строке main(), run()
после window->init(...),
а stop()
когда нужно завершить процесс (например пользователь нажал "крестик").
int main(..)
{
wui::framework::init();
MainFrame mainFrame;
mainFrame.Run();
wui::framework::run();
return 0;
}
Здесь wui::framework::end();
вызывается в коллбеке закрытия главного окна:
void MainFrame::Run()
{
window->init(wui::locale("main_frame", "caption"), { -1, -1, width, height },
wui::window_style::frame, [this]() {
wui::framework::stop();
});
}
Транзиентность
Приложения не мыслимы без модальных диалогов. Для их реализации окно имеет метод:
void set_transient_for(std::shared_ptr<window> window_, bool docked = true);
Этим методом, родительскому окну сообщается, что другое окно нужно сделать модальным относительно него. Флаг docked указывает, что модальное окно должно отображаться в базовом окне, без создания физического системного окна. Если модальное окно больше родительского, этот флаг игнорируется и создается новое системное окно.
Строго говоря, модальности в привычном смысле WinAPI в библиотеке нет. Т. е. вызов init()
транзиентного окна не блокирует вызывающий код, но это обходится продолжением логики в коллбэке close_callback
передаваемом в init()
.
Как показала практика, с этим вполне можно жить, а для некоторых вещей это даже оказывается удобнее.
Ресурсы
Для удобного и единообразного отображения множества контролов и надписей приложения, удобства работы не программистов, например дизайнеров, переводчиков реализованы подсистемы theme
и locale
.
Приложение всегда имеет текущую тему и локаль. Это по сути конфиги, выдающие значения по паре секция + ключ. В каждый контрол (и окно, так как оно тоже контрол) передается тема для получения этим контролом значений своих цветов, размеров, толщин, шрифтов и прочего. По умолчанию, в контрол можно не передавать кастомную тему, тогда он будет использовать общую текущую тему приложения.
У пользовательского кода для формирования надписей есть доступ к текущей локали приложения. Это позволяет собрать все текстовые ресурсы в одном месте и также, в одном месте, менять их для всего приложения.
Приложение имеет возможность менять текущую тему и локаль, что вызывает автоматическую смену внешнего вида контролов и окон / языка всего приложения.
Технически подсистемы реализованы схоже, рассмотрим на примере theme
Тема представляет из себя json содержащий значения параметров для контролов, например для окна и надписи и изображений.
Тема dark
{
"controls": [
{
"type": "window",
"background": "#131519",
"border": "#404040",
"border_width": 1,
"text": "#f5f5f0",
"active_button": "#3b3d41",
"caption_font": {
"name": "Segoe UI",
"size": 18,
"decorations": "normal"
}
},
{
"type": "text",
"color": "#f5f5f0",
"font": {
"name": "Segoe UI",
"size": 18
}
},
{
"type": "image",
"resource": "IMAGES_DARK",
"path": "~/.hello_wui/res/images/dark"
},
. . .
}
Тема light
{
"controls": [
{
"type": "window",
"background": "#fffffe",
"border": "#9a9a9a",
"border_width": 1,
"text": "#191914",
"caption_font": {
"name": "Segoe UI",
"size": 18
}
},
{
"type": "text",
"color": "#191914",
"font": {
"name": "Segoe UI",
"size": 18
}
},
{
"type": "image",
"resource": "IMAGES_LIGHT",
"path": "~/.hello_wui/res/images/light"
}
...
}
Данный подход предоставляет приложению и контролам прозрачный, централизованный механизм управления отображением. При необходимости создать кастомный контрол (например красную кнопку) можно просто добавить в json новый раздел:
{
"type": "red_button",
"calm": "#c61818",
"active": "#e31010",
"border": "#c90000",
"border_width": 1,
"focused_border": "#dcd2dc",
"text": "#f0f1f1",
"disabled": "#a5a5a0",
"round": 0,
"focusing": 1,
"font": {
"name": "Segoe UI",
"size": 18
}
}
А при создании контрола указать имя контрола: “red_button”
, например:
cancelButton(new wui::button(wui::locale("button", "cancel"), this { window->destroy(); }, "red_button"))
Для работы с пиктограммами и подобными изображениями используется контрол image
. Он также использует theme
для получения идентификатора win32 ресурса или пути к файлу изображения. Это позволяет создать изображение
logoImage(new wui::image(IMG_LOGO))
где:
#ifdef _WIN3
#define IMG_LOGO 4010
#else
static constexpr const char* IMG_LOGO = "logo.png";
#endif
Логотип будет загружен в соответствии с заданной темой.
Вопросы многопоточности
WUI не использует ни одного мьютекса. Коллбэки контролов и системные события приходят только из одного потока на Windows (proc_wnd
) или из потока окна ожидающего xcb_wait_for_event()
.
Рекомендуется все манипуляции с UI производить либо в коллбеках / полученных системных событиях, либо в одном специальном UI треде приложения.
Если же планируется window.add_control()
/ window.remove_control()
из разных тредов, то необходимо осуществить защиту на уровне кода приложения.
Unicode
Используется только UTF-8 передаваемый в обычных std::string / char *
.
Для взаимодействия с WinAPI которой нужен utf16
в wchar, используется boost::nowide::widen() / boost::nowide::narrow()
. boost::nowide
не имеет зависимостей от boost и поставляется вместе с WUI в thirdparty. Таким образом, если в вашем проекте нет boost вам не придется включать его в зависимости для WUI.
Приложение также должно использовать boost::nowide
для работы WUI совместно с WinAPI.
Подробнее о том, почему wchar
не нужен, написано здесь: https://utf8everywhere.org/
На Linux boost::nowide
не требуется, и зависимость от него исключается.
Обработка ошибок
WUI не использует исключения. Методы, которые могут завершиться ошибкой возвращают bool. Для получения подробностей о возникшей проблеме используется метод get_error() возвращающий структуру
struct error
{
error_type type;
std::string component, message;
bool is_ok() const;
};
Ошибки, возможно возникшие в конструкторе объекта, нужно проверять так:
newObject(new wui::image(IMG_LOGO))...
if (!newObject->get_error().is_ok()) { log(“error”, newObject->get_error().str()); }
Hello world app
В качестве основы для любого проекта использующего WUI предлагается минимальное приложение, которое, впрочем сразу сделано под возможность его расширения до крупного проекта.
Данное приложение находится в examples/hello_world
и включает полное наличие необходимых ресурсных файлов. На Windows приложение собирается в монолитный exe, на Linux/Mac хранит ресурсы в папке ”res/”
рядом с исполняемым файлом. Для реальных приложений лучше указать пути “~/.app_name/res”
или, если приложение ставится из под root, что-то вроде “/opt/app_name/res”
.
Показано использование theme, locale
и config
, в приложении, имеющем две цветовые схемы (темная и светлая), два языка и хранящем свою конфигурацию в реестре на Windows и в ini файле на Linux.
main.cpp
#ifdef _WIN32
int APIENTRY wWinMain(_In_ HINSTANCE,
_In_opt_ HINSTANCE,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
#elif __linux__
int main(int argc, char *argv[])
#endif
{
wui::framework::init();
auto ok = wui::config::create_config("hello_world.ini", "Software\\wui\\hello_world");
if (!ok)
{
std::cerr << wui::config::get_error().str() << std::endl;
return -1;
}
wui::error err;
wui::set_app_locales({
{ wui::locale_type::eng, "English", "res/en_locale.json", TXT_LOCALE_EN },
{ wui::locale_type::rus, "Русский", "res/ru_locale.json", TXT_LOCALE_RU },
});
auto current_locale = static_cast<wui::locale_type>(wui::config::get_int("User", "Locale",
static_cast<int32_t>(wui::get_default_system_locale())));
wui::set_current_app_locale(current_locale);
wui::set_locale_from_type(current_locale, err);
if (!err.is_ok())
{
std::cerr << err.str() << std::endl;
return -1;
}
wui::set_app_themes({
{ "dark", "res/dark.json", TXT_DARK_THEME },
{ "light", "res/light.json", TXT_LIGHT_THEME }
});
auto current_theme = wui::config::get_string("User", "Theme", "dark");
wui::set_default_theme("dark");
wui::set_current_app_theme(current_theme);
wui::set_default_theme_from_name(current_theme, err);
if (!err.is_ok())
{
std::cerr << err.str() << std::endl;
return -1;
}
MainFrame mainFrame;
mainFrame.Run();
wui::framework::run();
return 0;
}
Демонстрационное приложение показывает логотип, выводит надпись и предоставляет поле ввода. При нажатии на кнопку, происходит вывод окна сообщения и приложение закрывается. Также показано отслеживание закрытия окна пользователем с выводом сообщения подтверждения.
На следующем скриншоте, тема изменена на светлую, язык на английский и нажата кнопка “Приятно познакомиться”
Код главного окна
//////// Header
class MainFrame
{
public:
MainFrame();
void Run();
private:
static const int32_t WND_WIDTH = 400, WND_HEIGHT = 400;
std::shared_ptr<wui::window> window;
std::shared_ptr<wui::image> logoImage;
std::shared_ptr<wui::text> whatsYourNameText;
std::shared_ptr<wui::input> userNameInput;
std::shared_ptr<wui::button> okButton;
std::shared_ptr<wui::message> messageBox;
void ReceiveEvents(const wui::event &ev);
void UpdateControlsPosition();
};
//////// Impl
MainFrame::MainFrame()
: window(new wui::window()),
logoImage(new wui::image(IMG_LOGO)),
whatsYourNameText(new wui::text(wui::locale("main_frame", "whats_your_name_text"), wui::text_alignment::center, "h1_text")),
userNameInput(new wui::input(wui::config::get_string("User", "Name", ""))),
okButton(new wui::button(wui::locale("main_frame", "ok_button"), [this](){
wui::config::set_string("User", "Name", userNameInput->text());
messageBox->show(wui::locale("main_frame", "hello_text") + userNameInput->text(),
wui::locale("main_frame", "ok_message_caption"), wui::message_icon::information, wui::message_button::ok, [this](wui::message_result) {
runned = false; window->destroy(); }); })),
messageBox(new wui::message(window)),
runned(false)
{
window->subscribe(std::bind(&MainFrame::ReceiveEvents,
this,
std::placeholders::_1),
static_cast<wui::event_type>(static_cast<int32_t>(wui::event_type::internal) |
static_cast<int32_t>(wui::event_type::system) |
static_cast<int32_t>(wui::event_type::keyboard)));
window->add_control(logoImage, { 0 });
window->add_control(whatsYourNameText, { 0 });
window->add_control(userNameInput, { 0 });
window->add_control(okButton, { 0 });
window->set_default_push_control(okButton);
window->set_min_size(WND_WIDTH - 1, WND_HEIGHT - 1);
}
void MainFrame::Run()
{
if (runned)
{
return;
}
runned = true;
UpdateControlsPosition();
window->set_control_callback([&](wui::window_control control, std::string &tooltip_text, bool &continue_) {
switch (control)
{
case wui::window_control::theme:
{
wui::error err;
auto nextTheme = wui::get_next_app_theme();
wui::set_default_theme_from_name(nextTheme, err);
if (!err.is_ok())
{
std::cerr << err.str() << std::endl;
return;
}
wui::config::set_string("User", "Theme", nextTheme);
window->update_theme();
}
break;
case wui::window_control::lang:
{
wui::error err;
auto nextLocale = wui::get_next_app_locale();
wui::set_locale_from_type(nextLocale, err);
if (!err.is_ok())
{
std::cerr << err.str() << std::endl;
return;
}
wui::config::set_int("User", "Locale", static_cast<int32_t>(nextLocale));
tooltip_text = wui::locale("window", "switch_lang");
window->set_caption(wui::locale("main_frame", "caption"));
whatsYourNameText->set_text(wui::locale("main_frame", "whats_your_name_text"));
okButton->set_caption(wui::locale("main_frame", "ok_button"));
}
break;
case wui::window_control::close:
if (runned)
{
continue_ = false;
messageBox->show(wui::locale("main_frame", "confirm_close_text"),
wui::locale("main_frame", "cross_message_caption"), wui::message_icon::information, wui::message_button::yes_no,
[this, &continue_](wui::message_result r) {
if (r == wui::message_result::yes)
{
wui::framework::stop();
}
});
}
break;
}
});
auto width = wui::config::get_int("MainFrame", "Width", WND_WIDTH);
auto height = wui::config::get_int("MainFrame", "Height", WND_HEIGHT);
window->init(wui::locale("main_frame", "caption"), { -1, -1, width, height },
static_cast<wui::window_style>(static_cast<uint32_t>(wui::window_style::frame) |
static_cast<uint32_t>(wui::window_style::switch_theme_button) |
static_cast<uint32_t>(wui::window_style::switch_lang_button) |
static_cast<uint32_t>(wui::window_style::border_all)), [this]() {
wui::framework::stop();
});
}
void MainFrame::ReceiveEvents(const wui::event &ev)
{
if (ev.type == wui::event_type::internal)
{
switch (ev.internal_event_.type)
{
case wui::internal_event_type::window_created:
break;
case wui::internal_event_type::size_changed:
if (window->state() == wui::window_state::normal &&
ev.internal_event_.x > 0 && ev.internal_event_.y > 0)
{
wui::config::set_int("MainFrame", "Width", ev.internal_event_.x);
wui::config::set_int("MainFrame", "Height", ev.internal_event_.y);
}
UpdateControlsPosition();
break;
case wui::internal_event_type::window_expanded:
case wui::internal_event_type::window_normalized:
UpdateControlsPosition();
break;
case wui::internal_event_type::window_minimized:
break;
}
}
}
void MainFrame::UpdateControlsPosition()
{
const auto width = window->position().width(), height = window->position().height();
const int32_t top = 40, element_height = 40, space = 30;
wui::rect pos = { space, top, width - space, top + element_height };
whatsYourNameText->set_position(pos);
wui::line_up_top_bottom(pos, element_height, space);
userNameInput->set_position(pos);
wui::line_up_top_bottom(pos, element_height * 2, space);
int32_t center = width / 2;
pos.left = center - element_height, pos.right = center + element_height;
logoImage->set_position(pos);
okButton->set_position({center - 90,
height - element_height - space,
center + 90,
height - space
});
}
Окно и контролы создаются в конструкторе MainFrame. Там же осуществляется подписка приложения на события и добавляются на окно контролы. Коллбеки контролов для краткости отрабатываются при помощи лямбд.
Метод Run()
запускает окно и содержит лямбду, обрабатывающую коллбеки от контролов окна (кнопки смена языка и темы).ReceiveEvents()
получает события от окна и используется для реагирования на ресайз окна вызывая UpdateControlsPosition()
. который и пересчитывает новые координаты контролов.
Контролы
На момент написания статьи реализовано 14 контролов в составе WUI и несколько специфичных в составе нашего приложения. Список имеющихся контролов:
button
Кнопка может быть следующих видов:
text
image
image_right_text
image_bottom_text
switcher
radio
anchor
sheet
image
image нужен для единообразного отображения пиктограмм с учетом визуальной темы. Например button
использует image
для рисования пиктограмм на себе. image рисует себя из ресурса, соответствующего визуальной теме.
Пример использования image
:
Создаем в конструкторе содержащего image класса
logoImage(new wui::image(IMG_LOGO))...
IMG_LOGO
определен в resourse.h
приложения следующим образом:
#ifdef _WIN32
#define IMG_LOGO 109
#else
static constexpr const char* IMG_LOGO = "logo.png";
#endif
Таким образом, изображение будет взято из ресурса exe на Windows или из файла на других системах.
Магия смены изображения при смене темы реализована следующим образом. image имеет в theme свои настройки:
light.json:
{
"type": "image",
"resource": "IMAGES_LIGHT",
"path": "res/images/light"
}
dark.json:
{
"type": "image",
"resource": "IMAGES_DARK",
"path": "res/images/dark"
}
Путь к файлу ресурса составляется из пути указанном в theme и имени файла в image
что приводит к автоматической замене всех изображений приложения при смене темы.
На Windows стоит упомянуть как организован rc
файл приложения.
IMG_LOGO IMAGES_DARK "res\images\dark\logo.png"
IMG_LOGO IMAGES_LIGHT "res\images\light\logo.png"
Таким образом, замена группы IMAGES_DARK / IMAGES_LIGHT
вызывает аналогичный эффект как с файлами, без необходимости менять ID ресурса.
input
Данный контрол реализовывает стандартное поле ввода. Так как реализация своя, в данный момент нет Undo / Redo, но в перспективе там должен появиться спелл чекинг, подсказки, валидация.
list
Вертикальный список item
’ов со скроллингом. Отрисовка элементов производится пользовательским кодом через callback. Имеется возможность создавать item
’ы с разной высотой. С его помощью можно сделать чат, таблицу к БД, в принципе любой список.
menu
Меню ленточные, без боковых ответвлений. Вложения раскрываются вниз, удлиняя меню.
Меню задается в декларативном стиле, вектором:
menu->set_items({
{ 0, wui::menu_item_state::separator, "Bla bla bla", "", menuImage1, {}, [](int32_t i) {} },
{ 1, wui::menu_item_state::normal, "Expand me 1", "", nullptr, {
{ 11, wui::menu_item_state::normal, "Expanded 1.1", "", nullptr, {}, [](int32_t i) {} },
{ 12, wui::menu_item_state::normal, "Expanded 1.2", "", nullptr, {
{ 121, wui::menu_item_state::normal, "Expanded 1.1.1", "", nullptr, {}, [](int32_t i) {} },
{ 122, wui::menu_item_state::normal, "Expanded 1.1.2", "Shift+Del", menuImage2, {}, [](int32_t i) {} },
{ 123, wui::menu_item_state::separator, "Expanded 1.1.3", "", nullptr, {}, [](int32_t i) {} },
}, [](int32_t i) {} },
{ 13, wui::menu_item_state::normal, "Expanded 1.3", "", nullptr, {}, [](int32_t i) {} },
}, [](int32_t i) {} },
{ 2, wui::menu_item_state::separator, "Expand me 2", "Ctrl+Z", nullptr, {
{ 21, wui::menu_item_state::normal, "Expanded 2.1", "", nullptr, {}, [](int32_t i) {} },
{ 22, wui::menu_item_state::normal, "Expanded 2.2", "", nullptr, {}, [](int32_t i) {} },
{ 23, wui::menu_item_state::separator, "Expanded 2.3", "", nullptr, {}, [](int32_t i) {} },
}, [](int32_t i) {} },
{ 3, wui::menu_item_state::normal, "Exit", "Alt+F4", nullptr, {}, [&window](int32_t i) { window->destroy(); } }
});
Внутри, меню использует list
message
Имеет стандартные наборы кнопок и пиктограмм представленных в перечисленияхmessage_icon, message_button
и message_result
. При использовании есть одна особенность, которая по началу покажется непривычной, а именно, вызов message::show()
не блокирует вызывающий поток. Поэтому, получение ID нажатой кнопки производится в коллбеке. Пример:
messageBox->show(“message”, "header", wui::message_icon::information, wui::message_button::yes_no, [this](wui::message_result result) {
if (result == wui::message_result::yes)
{
/// Продолжаем здесь
}
});
panel
Простой контрол, для того чтобы нарисовать прямоугольник цвета theme на окне.
select
Выпадающий список, он же combo box. Не имеет редактора, т е работает только для выбора из имеющегося. Реализован также на list.
slider
Он же “регулятор громкости”. Как и progress может быть горизонтальным и вертикальным.
Здесь text, slider и button
splitter
Используется для ресайза внутренних окон.
text
Текстовая строка. Позволяет задать выравнивание текста и ставит многоточие если текст не влезает в отведенную область.
tooltip
Всплывающая подсказка.
trayicon
Позволяет управлять иконкой в трее и информировать пользователя плашками.
Зависимости
WUI использует три библиотеки в thirdparty
. Это: boost::nowide, nlohman::json
и utf8
от Nemanja Trifunovic. Последние две, header only и хлопот не вызывают. boost::widen
поставляется в виде “вырезки” из boost, имеются сборки на vs 2017 и 2019 версия boost: 1.82. Если в вашем проекте уже используется boost (тем более другой версии), лучше указать для wui путь к вашему boost.
Внешние зависимости отсутствуют на Windows. На Linux, в данный момент, для работы требуется xcb и cairo.
Вместо завершения
Библиотека в составе нашего приложения прошла опытную эксплуатацию на нескольких крупных предприятиях промышленного и медицинского характера. Использовались различные версии Windows от XP до 11 и Linux от CentOS 6 до Ubuntu 22.
По надежности и производительности система показывает весьма достойные результаты (нет падений, артефактов, утечек памяти), что подтвердило допустимость принятых концепций.
Основные направления развития проекта - это конечно, поддержка macOS и добавление новых контролов. Например нужен календарь, многострочный текстовый редактор, грид для базы данных, чарты и прочее. Нужен графический редактор для создания хотя бы диалогов. Также очень хочется заменить X11 на Wayland и сделать графику на Vulkan.
Будем благодарны за конструктивную критику, помощь в развитии проекта идеями, кодом, распространением.
Спасибо за интерес!
Github: https://github.com/ud84
Project site: https://libwui.org/