DlangUI — кросплатформенный GUI для D (Часть 1)

Мне нравится язык D. Давно слежу за его развитием. Для D есть несколько GUI библиотек и биндингов, но я решил изобрести свой велосипед.Хочу рассказать о своём проекте DlangUI. Надеюсь, что он кому-нибудь будет полезен.54fb63b5a87749e18bfb6f0777fc2c08.png

На КДПВ скриншот DlangIDE — приложения, написанного на DlangUI.

Особенности: Кроссплатформенность — поддерживаются Windows, Linux, Mac OSX; легкость портирования на другие платформы Написан на D — легкорасширяемый Использование Layouts для позиционирования элементов интерфейса Масштабирование шрифтов и иконок в приложении в зависимости от разрешения экрана Поддержка Unicode Интернационализация — поддержка перевода UI на несколько языков Аппаратное ускорение с помощью OpenGL (опционально) Возможность отрисовки виджетов поверх OpenGL сцены (например, для UI в игре) Небольшой размер исполняемого файла Внешний вид интерфейса настраивается с помощью тем (две стандартные темы — светлая и темная) Встраивание ресурсов в исполняемый файл Открытый исходный код, под лицензией Boost License 1.0 Еще пара скриншотов Демо DlangUI — example1 3151b283b1974473b317495b389124a1.pngДемо DlangUI — Tetris 6831c0e4ebe54f00a3f5dbef71be490d.pngДля 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 При успешной компиляции приложение сразу запустится. Окно с единственной кнопкой: 60b863627c244ca79d28a868b4518638.pngТакже можете посмотреть примеры из 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 (); } Вот что у нас получилось: f499985982d8441eaab772d2c70b9cac.png

Рассмотрим обработку сигналов на примере сигнала 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); Вот что получилось: 33ff7daf4782497595b84569bb81ca25.png

Пояснения:

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 пикселей Вот, что получилось: d4ca209b1c03498d8c38ba456e2e7307.pngТекущий код 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: отличаются только рамка окна и шрифты.cecbc4ce03df4dfba26fad3eef886363.pngРазмер 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) Нужна ли следующая часть? О чем еще рассказать? Пишите в комментах…

© Habrahabr.ru