DlangUI — кросплатформенный GUI для D (Часть 1)
Мне нравится язык D. Давно слежу за его развитием. Для D есть несколько GUI библиотек и биндингов, но я решил изобрести свой велосипед.Хочу рассказать о своём проекте DlangUI. Надеюсь, что он кому-нибудь будет полезен.
На КДПВ скриншот DlangIDE — приложения, написанного на DlangUI.
Особенности: Кроссплатформенность — поддерживаются Windows, Linux, Mac OSX; легкость портирования на другие платформы Написан на D — легкорасширяемый Использование Layouts для позиционирования элементов интерфейса Масштабирование шрифтов и иконок в приложении в зависимости от разрешения экрана Поддержка Unicode Интернационализация — поддержка перевода UI на несколько языков Аппаратное ускорение с помощью OpenGL (опционально) Возможность отрисовки виджетов поверх OpenGL сцены (например, для UI в игре) Небольшой размер исполняемого файла Внешний вид интерфейса настраивается с помощью тем (две стандартные темы — светлая и темная) Встраивание ресурсов в исполняемый файл Открытый исходный код, под лицензией Boost License 1.0 Еще пара скриншотов Демо DlangUI — example1 Демо DlangUI — Tetris Для D имеется немало GUI библиотек. Полный список можно найти на wiki.dlang.orgЕсли биндинги к GTK, Qt, wxWidgets, FLTK, и даже порт SWT с Java на D (DWT).Но они тянут с собой много зависимостей, сложно расширять набор виджетов, менять их внешний вид.Нативные, написанные на D, DFL и DGUI — работают только под Windows.Поэтому написание своего GUI велосипеда не такая уж и глупая затея. Чтобы собрать и запустить приложение на DlangUI, нам понадобится компилятор D (например, dmd) и DUB (build tool и менеджер зависимостей). Скачайте и установите их, если их еще нет.Создайте директорию для проекта, в ней создайте файл проекта для DUB — dub.json { «name»: «helloworld», «targetPath»: «bin», «targetName»: «helloworld», «targetType»: «executable»,
«dependencies»: { «dlangui»:»~master», } } Также, в поддиректории src создайте файл src/helloworld.d с таким содержимым:
module app; // импортируем библиотеку dlangui import dlangui;
// поместить объявление main или WinMain в этот файл mixin APP_ENTRY_POINT; // точка входа в приложение DlangUI — вызывается из main после инициализации библиотеки extern © int UIAppMain (string[] args) { // создаем окно Window window = Platform.instance.createWindow («DlangUI example — HelloWorld», null); // создаем кнопку и устанавливаем ее как основной виджет окна window.mainWidget = (new Button ()).text («Hello, world! «d).margins (Rect (20,20,20,20)); // показываем окно window.show (); // цикл сообщений return Platform.instance.enterMessageLoop (); } Теперь мы можем запустить приложение. В командной строке, в директории проекта с dub.json выполните команду:
dub run При успешной компиляции приложение сразу запустится. Окно с единственной кнопкой: Также можете посмотреть примеры из dlangui (демо почти всех виджетов example1 и игра tetris):
dub fetch dlangui dub run dlangui: example1 dub run dlangui: tetris Еще одно приложение — DlangIDE:
dub fetch dlangide dub run dlangide
Усложним наше приложение. Добавим несколько виджетов.Будем использовать простые виджеты:
TextWidget — текст Button — кнопка с текстом ImageButton — кнопка с картинкой ImageTextButton — кнопка с картинкой и текстом CheckBox — понятно из названия RadioButton — понятно из названия ImageWidget — картинка EditLine — однострочный редактор ComboBox — комбобокс — для выбора элемента из выпадающего списка Пример создания простой текстовой кнопки:
auto btn = new Button («btn1», «Button 1«d); Здесь «btn1» — это идентификатор виджета, обычно использоваться для его поиска в родительском виджете или для того, чтобы отличать один виджет от другого в общем обработчике событий.
«Button 1«d — текст кнопки. Обратите внимание на суффикс d — это utf32 — dstring. Обычно в конструкторах виджетов DlangUI в качестве текста может передаваться сам текст — как utf32 dstring, или идентификатор строкового ресурса как обычный string — для поддержки перевода интерфейса на несколько языков.
Виджеты могут иметь вложенные виджеты.Layouts — виджеты-контейнеры для выравнивания других виджетов. Похожи на используемые в Android UI:
VerticalLayout — расположить вложенные виджеты по вертикали HorizontalLayout — расположить вложенные виджеты по вертикали TableLayout — расположить вложенные виджеты в несколько столбцов, как в таблице Создание VerticalLayout и добавление в него пары кнопок:
auto vlayout = new VerticalLayout (); // расположить элементы по вертикали vlayout.addChild (new RadioButton («radio1», «Radio Button 1«d)); vlayout.addChild (new RadioButton («radio2», «Radio Button 2«d)); Исправим наш пример — сделаем форму со сложной структурой. module app; // импортируем библиотеку dlangui import dlangui;
// поместить объявление main или WinMain в этот файл mixin APP_ENTRY_POINT; // точка входа в приложение DlangUI — вызывается из main после инициализации библиотеки extern © int UIAppMain (string[] args) { // создаем окно Window window = Platform.instance.createWindow («DlangUI example — HelloWorld», null);
// основной виджет окна — располагаем все, что внутри него по вертикали auto mainWidget = new VerticalLayout ();
mainWidget.addChild (new TextWidget (null, «пример HorizontalLayout: «d)); // заголовок
auto hlayout = new HorizontalLayout (); // расположить элементы по вертикали hlayout.addChild (new Button («btn1», «Кнопка 1«d)); hlayout.addChild (new Button («btn2», «Кнопка 2«d)); hlayout.addChild (new Button («btn3», «Кнопка 3«d)); hlayout.addChild (new CheckBox («btn4», «Пример CheckBox«d)); mainWidget.addChild (hlayout);
mainWidget.addChild (new TextWidget (null, «пример VerticalLayout: «d)); // заголовок
auto vlayout = new VerticalLayout (); // расположить элементы по вертикали vlayout.addChild (new RadioButton («radio1», «Radio Button 1«d)); vlayout.addChild (new RadioButton («radio2», «Radio Button 2«d)); vlayout.addChild (new RadioButton («radio3», «Radio Button 3«d)); mainWidget.addChild (vlayout);
mainWidget.addChild (new TextWidget (null, «пример TableLayout — форма с 2 столбцами: «d)); // заголовок
auto tlayout = new TableLayout (); // таблица / форма tlayout.colCount = 2; tlayout.addChild (new TextWidget (null, «Строка ввода«d)); tlayout.addChild (new EditLine («edit1», «Какой-то текст для редактирования«d)); tlayout.addChild (new TextWidget (null, «ComboBox«d)); tlayout.addChild ((new ComboBox («combo1», [«Значение 1«d, «Значение 2«d, «Значение 3«d])).selectedItemIndex (0)); tlayout.addChild (new TextWidget (null, «Группа RadioButton«d)); // внутри Layout может быть другой Layout: auto radiogroup = new VerticalLayout (); radiogroup.addChild (new RadioButton («rb1», «Значение 1«d)); radiogroup.addChild (new RadioButton («rb2», «Значение 2«d)); radiogroup.addChild (new RadioButton («rb3», «Значение 3«d)); tlayout.addChild (radiogroup); tlayout.addChild (new TextWidget (null, «Кнопка ImageTextButton«d)); tlayout.addChild (new ImageTextButton («btn_ok», «dialog-ok-apply», «Текст кнопки«d));
mainWidget.addChild (tlayout);
// создаем кнопку и устанавливаем ее как основной виджет окна window.mainWidget = mainWidget; // показываем окно window.show (); // цикл сообщений return Platform.instance.enterMessageLoop (); } Вот что у нас получилось:
Рассмотрим обработку сигналов на примере сигнала onClick
Добавим обработчики нажатия на кнопки — пусть переключают тему интерфейса.
В нашем примере меняем кусок кода с RadioButton в VerticalLayout.
mainWidget.addChild (new TextWidget (null, «Выбор темы интерфейса: «d));
auto vlayout = new VerticalLayout (); // addChild () возвращает добавленный вижет, и большинство методов установки свойств виджета возвращают сам виджет, // поэтому можно вызывать несколько методов по цепочке. vlayout.addChild (new RadioButton («radio1», «Обычная«d)).checked (true).onClickListener = delegate (Widget src) { platform.instance.uiTheme = «theme_default»; return true; }; vlayout.addChild (new RadioButton («radio2», «Тёмная«d)).onClickListener = delegate (Widget src) { platform.instance.uiTheme = «theme_dark»; return true; }; mainWidget.addChild (vlayout); Вот что получилось:
Пояснения:
onClickListener — сигнал, доступный в любом виджете.Вот как он описан:
/// interface — slot for onClick interface OnClickHandler { bool onClick (Widget source); } //… class Widget { //… Signal! OnClickHandler onClickListener; //… } Обработчиком может служить делегат подходящего типа.Подключать обработчик событий можно по разному.Пример обрабочика onClick — обычный делегат
auto button1 = new Button («btn1», «Кнопка 1«d); button1.onClickListener = delegate (Widget src) { window.showMessageBox (UIString («Обработчик onClick«d), UIString («Вызван\ndelegate«d)); return true; }; Пример обрабочика onClick — метод класса
class MyOnClickHandler1 { bool onButtonClick (Widget src) { src.window.showMessageBox (UIString («Обработчик onClick«d), UIString («Вызван MyOnClickHandler1.onClick\nиз виджета с id=«d ~ to! dstring (src.id))); return true; } } auto memberFunctionHandler = new MyOnClickHandler1(); auto button2 = new Button («btn2», «Кнопка 2«d); button2.onClickListener = &memberFunctionHandler.onButtonClick; hlayout.addChild (button2); Пример обрабочика onClick — класс, определяющий интерфейс, использованный при определении сигнала
// пример обрабочика onClick — класс, определяющий интерфейс сигнала class MyOnClickHandler2: OnClickHandler { override bool onClick (Widget src) { src.window.showMessageBox (UIString («Обработчик onClick«d), UIString («Вызван MyOnClickHandler2.onClick\nиз виджета с id=«d ~ to! dstring (src.id))); return true; } } auto interfaceHandler = new MyOnClickHandler2(); auto button4 = new Button («btn4», «Показать сообщение 4«d); button2.onClickListener = interfaceHandler; // нужный метод onClick будет взят из интерфейса OnClickHandler Другие полезные сигналы из класса Widget:
onCheckChangeListener — состояние checked изменено (например, для CheckBox, RadioButton) onFocusChangeListener — изменено состояние фокуса этого виджета onKeyListener — перехват событий от клавиатуры onMouseListener — перехват событий мыши margins — отступ от соседних виджетов или границ контейнера (фон виджета рисуется с отступом на margins) padding — отступ от границ виджета до его внутренних элементов backgroundColor — цвет фона (32 bit uint, 0xAARRGGBB) backgroundImageId — фоновое изображение — id ресурса textColor — цвет текста (32 bit uint, 0xAARRGGBB) fontSize — размер шрифта Эти и многие другие свойства можно задавать как напрямую, так и в виде стилей (определяются в файле — теме).Назначить стиль можно с помощью свойства styleId. Например, поменяем стиль заголовка у выбора темы интерфейса. mainWidget.addChild (new TextWidget (null, «Выбор темы интерфейса: «d)).styleId («POPUP_MENU»); Пример: добавим отступов к основному виджету и сделаем ему полупрозрачный желтый фон.
// отступ от границ окна на 10 пикселей, вложенные виджеты будут располагаться с отступом 15 пикселей mainWidget.margins (Rect (10, 10, 10, 10)).padding (Rect (15, 15, 15, 15)); mainWidget.backgroundColor (0xC0FFFF00); // полупрозрачный желтый фон У TableLayout назначим фоновой картинкой «btn_default.png». Id ресурса — это имя файла без расширения. Расширения .9.png обозначают nine-patch — масштабируемое излбражение, как в Android.Добавим также padding — отступ для вложенных виджетов.
tlayout.backgroundImageId («btn_default»); // фон от кнопки — btn_default.9.png из стандартных ресурсов tlayout.padding (Rect (5, 5, 5, 5)); // отступ для вложенных виджетов — 5 пикселей В TextWidget заголовка для TableLayout поменяем размер и цвет шрифта. tlayout.backgroundImageId («btn_default»); // фон от кнопки — btn_default.9.png из стандартных ресурсов tlayout.padding (Rect (5, 5, 5, 5)); // отступ для вложенных виджетов — 5 пикселей Вот, что получилось: Текущий код helloworld.d: module app; // импортируем библиотеку dlangui import dlangui;
// поместить объявление main или WinMain в этот файл mixin APP_ENTRY_POINT; // точка входа в приложение DlangUI — вызывается из main после инициализации библиотеки extern © int UIAppMain (string[] args) { // создаем окно с изменяемым размером, начальный размер — 800×600 Window window = Platform.instance.createWindow («DlangUI example — HelloWorld», null, WindowFlag.Resizable, 600, 400);
// основной виджет окна — располагаем все, что внутри него по вертикали auto mainWidget = new VerticalLayout (); // отступ от границ окна на 10 пикселей, вложенные виджеты будут располагаться с отступом 15 пикселей mainWidget.margins (Rect (10, 10, 10, 10)).padding (Rect (15, 15, 15, 15)); mainWidget.backgroundColor (0xC0FFFF00); // полупрозрачный желтый фон
mainWidget.addChild (new TextWidget (null, «пример HorizontalLayout: «d)); // заголовок
auto hlayout = new HorizontalLayout (); // расположить элементы по вертикали // пример обрабочика onClick — делегат auto button1 = new Button («btn1», «Кнопка 1«d); button1.onClickListener = delegate (Widget src) { window.showMessageBox (UIString («Обработчик onClick«d), UIString («Вызван\ndelegate«d)); return true; }; hlayout.addChild (button1); // пример обрабочика onClick — метод класса class MyOnClickHandler1 { bool onButtonClick (Widget src) { src.window.showMessageBox (UIString («Обработчик onClick«d), UIString («Вызван MyOnClickHandler1.onClick\nиз виджета с id=«d ~ to! dstring (src.id))); return true; } } auto memberFunctionHandler = new MyOnClickHandler1(); auto button2 = new Button («btn2», «Кнопка 2«d); button2.onClickListener = &memberFunctionHandler.onButtonClick; hlayout.addChild (button2);
// можно использовать один и тот же обработчик сигнала для нескольких источников hlayout.addChild (new Button («btn3», «Кнопка 3«d)).onClickListener = &memberFunctionHandler.onButtonClick;
// пример обрабочика onClick — класс, определяющий интерфейс сигнала class MyOnClickHandler2: OnClickHandler { override bool onClick (Widget src) { src.window.showMessageBox (UIString («Обработчик onClick«d), UIString («Вызван MyOnClickHandler2.onClick\nиз виджета с id=«d ~ to! dstring (src.id))); return true; } } auto interfaceHandler = new MyOnClickHandler2(); auto button4 = new Button («btn4», «Показать сообщение 4«d); button2.onClickListener = interfaceHandler; // нужный метод onClick будет взят из интерфейса OnClickHandler hlayout.addChild (button4); mainWidget.addChild (hlayout);
mainWidget.addChild (new TextWidget (null, «Выбор темы интерфейса: «d)).styleId («POPUP_MENU»);
auto vlayout = new VerticalLayout (); vlayout.addChild (new RadioButton («radio1», «Обычная«d)).checked (true).onClickListener = delegate (Widget src) { platform.instance.uiTheme = «theme_default»; return true; }; vlayout.addChild (new RadioButton («radio2», «Тёмная«d)).onClickListener = delegate (Widget src) { platform.instance.uiTheme = «theme_dark»; return true; }; mainWidget.addChild (vlayout);
// в этом заголовке поменяем цвет и размер шрифта, и выровняем его по горизонтали вправо mainWidget.addChild (new TextWidget (null, «пример TableLayout — форма с 2 столбцами: «d)).textColor (0xC00000).fontSize (26).alignment (Align.Right);
auto tlayout = new TableLayout (); // таблица / форма tlayout.backgroundImageId («btn_default»); // фон от кнопки — btn_default.9.png из стандартных ресурсов tlayout.padding (Rect (5, 5, 5, 5)); // отступ для вложенных виджетов — 5 пикселей tlayout.colCount = 2; tlayout.addChild (new TextWidget (null, «Строка ввода«d)); tlayout.addChild (new EditLine («edit1», «Какой-то текст для редактирования«d)); tlayout.addChild (new TextWidget (null, «ComboBox«d)); tlayout.addChild ((new ComboBox («combo1», [«Значение 1«d, «Значение 2«d, «Значение 3«d])).selectedItemIndex (0)); tlayout.addChild (new TextWidget (null, «Группа RadioButton«d)); // внутри Layout может быть другой Layout: auto radiogroup = new VerticalLayout (); radiogroup.addChild (new RadioButton («rb1», «Значение 1«d)); radiogroup.addChild (new RadioButton («rb2», «Значение 2«d)); radiogroup.addChild (new RadioButton («rb3», «Значение 3«d)); tlayout.addChild (radiogroup); tlayout.addChild (new TextWidget (null, «Кнопка ImageTextButton«d)); tlayout.addChild (new ImageTextButton («btn_ok», «dialog-ok-apply», «Текст кнопки«d));
mainWidget.addChild (tlayout);
// создаем кнопку и устанавливаем ее как основной виджет окна window.mainWidget = mainWidget; // показываем окно window.show (); // цикл сообщений return Platform.instance.enterMessageLoop (); } Эта же программа, запущенная на Ubuntu: отличаются только рамка окна и шрифты.Размер helloworld.exe, построенного dmd2 под windows (dub build --build=release) — 1.4Mb, из них 200K занимают ресурсы.При наличии libfreetype-6.dll (700K) и zlib1.dll (84K) — автоматически копируются DUB в директорию bin — использует FreeType для рендеринга шрифтов, иначе — win32 API.
Бинарный файл, построенный в Ubuntu x64 c помощью dmd — 4Mb.
EditBox — многострочный редактор TreeWidget — дерево StringGrid — таблица, похожая на Excel TabWidget — табы — для выбора страниц AppFrame — базовый класс основного виджета окна с меню, тулбарами и стстус-строкой, для удобства написания таких приложений. ScrollBar ToolBar StatusLine ScrollWidget — реализует скроллинг вложенного в него виджета, если размеры превышают доступное место DockHost, DockWidget — для приложений с док-виджетами по краям от основного виджета окна (как в IDE) Нужна ли следующая часть? О чем еще рассказать? Пишите в комментах…