- PVSM.RU - https://www.pvsm.ru -
Мне нравится язык D. Давно слежу за его развитием. Для D есть несколько GUI библиотек и биндингов, но я решил изобрести свой велосипед.
Хочу рассказать о своём проекте DlangUI [1]. Надеюсь, что он кому-нибудь будет полезен.

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


Для D имеется немало GUI библиотек. Полный список можно найти на wiki.dlang.org [2]
Если биндинги к GTK, Qt, wxWidgets, FLTK, и даже порт SWT с Java на D (DWT).Но они тянут с собой много зависимостей, сложно расширять набор виджетов, менять их внешний вид.
Нативные, написанные на D, DFL и DGUI — работают только под Windows.
Поэтому написание своего GUI велосипеда не такая уж и глупая затея.
Чтобы собрать и запустить приложение на DlangUI, нам понадобится компилятор D (например, dmd [3]) и 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 (C) 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
Усложним наше приложение. Добавим несколько виджетов.
Будем использовать простые виджеты:
Пример создания простой текстовой кнопки:
auto btn = new Button("btn1", "Button 1"d);
Здесь «btn1» — это идентификатор виджета, обычно использоваться для его поиска в родительском виджете или для того, чтобы отличать один виджет от другого в общем обработчике событий.
«Button 1»d — текст кнопки. Обратите внимание на суффикс d — это utf32 — dstring. Обычно в конструкторах виджетов DlangUI в качестве текста может передаваться сам текст — как utf32 dstring, или идентификатор строкового ресурса как обычный string — для поддержки перевода интерфейса на несколько языков.
Виджеты могут иметь вложенные виджеты.
Layouts — виджеты-контейнеры для выравнивания других виджетов. Похожи на используемые в Android UI:
Создание 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 (C) 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.onClicknиз виджета с 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.onClicknиз виджета с 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:
Эти и многие другие свойства можно задавать как напрямую, так и в виде стилей (определяются в файле — теме).
Назначить стиль можно с помощью свойства 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 [4].
Добавим также 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 пикселей
Вот, что получилось:

module app;
// импортируем библиотеку dlangui
import dlangui;
// поместить объявление main или WinMain в этот файл
mixin APP_ENTRY_POINT;
// точка входа в приложение DlangUI - вызывается из main после инициализации библиотеки
extern (C) int UIAppMain(string[] args) {
// создаем окно с изменяемым размером, начальный размер - 800x600
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.onClicknиз виджета с 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.onClicknиз виджета с 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();
}
Размер helloworld.exe, построенного dmd2 под windows (dub build --build=release) — 1.4Mb, из них 200K занимают ресурсы.
При наличии libfreetype-6.dll (700K) и zlib1.dll (84K) — автоматически копируются DUB в директорию bin — использует FreeType для рендеринга шрифтов, иначе — win32 API.
Нужна ли следующая часть? О чем еще рассказать? Пишите в комментах…
Автор: Buggins
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/86871
Ссылки в тексте:
[1] DlangUI: https://github.com/buggins/dlangui
[2] wiki.dlang.org: http://wiki.dlang.org/Libraries_and_Frameworks#GUI_Libraries
[3] dmd: http://dlang.org/download.html
[4] Android: http://habrahabr.ru/post/113623/
[5] Онлайн — документация: http://buggins.github.io/dlangui/ddox/
[6] Багтрекер: https://github.com/buggins/dlangui/issues
[7] Скриншоты: http://buggins.github.io/dlangui/screenshots.html
[8] Страница проекта DlangIDE на GitHub: https://github.com/buggins/dlangide
[9] Чат в Gitter: https://gitter.im/buggins/dlangui
[10] Источник: http://habrahabr.ru/post/253923/
Нажмите здесь для печати.