Pull to refresh

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

Reading time 12 min
Views 17K
Мне нравится язык 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



Зачем нужна еще одна GUI библиотека?


Для D имеется немало GUI библиотек. Полный список можно найти на wiki.dlang.org
Если биндинги к GTK, Qt, wxWidgets, FLTK, и даже порт SWT с Java на D (DWT).Но они тянут с собой много зависимостей, сложно расширять набор виджетов, менять их внешний вид.
Нативные, написанные на D, DFL и DGUI — работают только под Windows.
Поэтому написание своего GUI велосипеда не такая уж и глупая затея.

Начнем знакомство с Hello, World


Чтобы собрать и запустить приложение на 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 (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 - с помощью dub init

Есть более удобный способ создания нового проекта DUB. Воспользуйтесь командой
dub init helloworld

DUB создаст директорию с указанным именем, файл dub.json, .gitignore и source/app.d
Начальное содержимое dub.json:
{
	"name": "helloworld",
	"description": "A minimal D application.",
	"copyright": "Copyright © 2015, username",
	"authors": ["username"],
	"dependencies": {
	}
}

Просто добавьте сюда зависимость dlangui
    "dependencies": {
        "dlangui": "~master",
    }

Содержимое файла app.d замените на код helloworld


Теперь мы можем запустить приложение. В командной строке, в директории проекта с 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


Widgets & Layouts


Усложним наше приложение. Добавим несколько виджетов.

Будем использовать простые виджеты:
  • 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 (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.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 (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.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)


Ссылки




Продолжение следует...



Нужна ли следующая часть? О чем еще рассказать? Пишите в комментах…

Tags:
Hubs:
+43
Comments 48
Comments Comments 48

Articles