Доброго времени суток, хабр!

Сегодня хочу рассказать вам о том, как писать приложения с использованием GTK+ и языка программирования D.





О том, что такое GTK рассказывать не буду. Приступим сразу к делу.

Создадим новый проект, то есть просто папку =)
Добавим туда dub.json с таким содержанием:
{
    "name": "hellogtkd",
    "targetPath": "bin",
    "targetType": "executable",
    "dependencies": {
        "gtk-d": "~>3.1.3"
    }
}


Система сборки dub по умолчанию ищет исходники в папке source.
Создадим её и добавим туда файл main.d:
import gtk.Main;
import gtk.MainWindow;
import gtk.Label;

class HelloWorld : MainWindow
{
    this()
    {
        super( "wintitle" );
        setBorderWidth(20); // чтобы не совсем маленькое было
        add( new Label( "hello habr!" ) );
        showAll();
    }
}

void main( string[] args )
{
    Main.init( args );
    new HelloWorld();
    Main.run();
}

Запустим сборку а затем приложение
dub build && bin/hellogtkd


И вуаля!


Не будем на этом останавливаться и создадим предельно простой ui в программе glade, а затем загрузим в нашу программу.



Обратите внимание на дерево добавленных компонентов (сверху справа), для главного окна, конпки и поля отрисовки присвоены идентификаторы отличные от тех, что даются по умолчанию (mwindow, btn, plot соответственно), мы их будем использовать для удобного получения из билдера.
Сохраним файл в папке проекта под именем ui.glade и отредактируем main.d:

новый main.d
import std.string : format;

import gtk.Main;
import gtk.Builder;
import gtk.Window;
import gtk.Widget;

import std.format;

final class UI
{
    string glade_file;

    Builder builder; // используется для загрузки интерфейса и его хранения

    this( string file )
    {
        builder = new Builder;

        if( !builder.addFromFile( file ) )
            except( "could no load glade object from file '%s'", file );

        glade_file = file;

        prepare();
    }

    void prepare()
    {
        prepareMainWindow();
    }

    void prepareMainWindow()
    {
        auto w = obj!Window( "mwindow" ); // получаем экземпляр Window по идентификатору
        w.setTitle( "glade ui" );
        w.addOnHide( (Widget aux){ Main.quit(); } ); // при закрытии окна нужно закрывать всю программу
        w.showAll(); // показываем главное окно
    }

    // так мы из загруженного файла получаем ссылку на экземпляр необходимого объекта
    auto obj(T)( string name )
    {
        // метод getObject возвращает ссылку на объект класса ObjectG, являющийся родителем для всех обёрнутых классов
        auto ret = cast(T)builder.getObject( name ); // поэтому мы кастуем его к необходимому классу
        if( ret is null ) except( "no '%s' element in file '%s'", name, glade_file );
        return ret;
    }

    void except( string file=__FILE__, size_t line=__LINE__, Args...)( Args args )
    { throw new Exception( format( args ), file, line ); }
}

void main( string[] args )
{
    Main.init( args );
    new UI( "ui.glade" ); // стоит обратить внимание на то, что файл будет искаться в той дирректории, в которой была запущена программа
    Main.run();
}



Думаю все догадались, для чего был добавлен элемент GtkDrawingArea — мы будем рисовать.
Добавим действия, выполняемые при нажатии кнопки:
...
    void prepare()
    {
        prepareMainWindow();
        prepareButtonAction();
    }
...
    void prepareButtonAction()
    {
        // мы просто передаём делегат, который будет вызывается при сигнале Clicked
        obj!Button( "btn" ).addOnClicked( (Button aux)
        {
            obj!DrawingArea( "plot" ).queueDraw(); // заставить переотрисоваться наш plot
        });
    }


Подготовим пока пустой класс для отрисовки, создадим файл draw.d в папке source:
module draw;

import std.stdio;
import std.datetime : Clock;

import gtk.Widget;
import cairo.Context;

class Figure
{
    bool draw( Scoped!Context cr, Widget aux )
    {
        writefln( "draw figure %012d", Clock.currAppTick().length );

        // необходимо вернуть true, если мы хотим остановить обработку события
        // другими обработчиками, иначе возвращаем false, чтобы остальные
        // обработчики были вызваны при вызове сигнала
        // такое поведение касается всех сигналов, принимающих bool delegate(...)
        return false;
    }
}


Привяжем фигуру:
...
    void prepare()
    {
        prepareMainWindow();
        prepareButtonAction();
        prepareDrawing();
    }
...
    Figure fig;

    void prepareDrawing()
    {
        fig = new Figure;
        // для подключения к сигналу мы можем использовать метод класса, это тоже делегат
        obj!DrawingArea( "plot" ).addOnDraw( &fig.draw );
    }


Теперь при запуске приложения вы увидите в консоли примерно такой вывод:
...
draw figure 014855276247
draw figure 014872180248
draw figure 014889286316
...


Можно заметить, что переотрисовка вызывается не только при нажатии кнопки, но и при других событиях: перемещение и изменение размеров окна, анимация цвета кнопки и тд.
За подробностями по поводу перерисовки виджетов обратитесь к документации GTK+.
Вызов переотрисовки при нажатии кнопки я поставил для того, чтобы показать как это делается.
По сути просто совпало, что нажатие кнопки само по себе ведёт к переотрисовке (из-за изменения цвета фона кнопки), но могут быть и другие события, не связанные с изменением GUI, тогда такой вызов необходим.

Добавим немного кода рисования:
...
    float angle = 0;
    bool draw( Scoped!Context cr, Widget aux )
    {
        writefln( "draw figure %012d", Clock.currAppTick().length );

        import std.math;

        auto w = aux.getAllocatedWidth();
        auto h = aux.getAllocatedHeight();

        auto xc = w / 2.0;
        auto yc = h / 2.0;

        auto radius = fmin(w,h) / 2.0;

        auto x1 = cos( angle ) * radius + xc;
        auto y1 = sin( angle ) * radius + yc;

        auto x2 = cos( angle + PI / 3 * 2 ) * radius + xc;
        auto y2 = sin( angle + PI / 3 * 2 ) * radius + yc;

        auto x3 = cos( angle + PI / 3 * 4 ) * radius + xc;
        auto y3 = sin( angle + PI / 3 * 4 ) * radius + yc;

        cr.setSourceRgb( 1.0, 0.0, 0.0 );
        cr.moveTo( x1, y1 );
        cr.lineTo( x2, y2 );
        cr.lineTo( x3, y3 );
        cr.closePath();
        cr.fill();

        // необходимо вернуть true, если мы хотим остановить обработку события
        // другими обработчиками, иначе возвращаем false, чтобы остальные
        // обработчики были вызваны при вызове сигнала
        // такое поведение касается всех сигналов, принимающих bool delegate(...)
        return false;
    }
...


И заставим кнопку делать хоть что-нибудь, что не делается автоматически =)
...
    void prepareButtonAction()
    {
        obj!Button( "btn" ).addOnClicked( (Button aux)
        {
            fig.angle += 0.1; // немного поворачиваем фигуру
            obj!DrawingArea( "plot" ).queueDraw();
        });
    }
...




На этом всё. Программа для тренировки с таблицами Шульте (первая картинка) лежит здесь. В ней Вы сможете найти больше примеров использования gtkd и отрисовки через cairo (хоть и не самых лучших, с точки зрения качества кода).

Так же в пакете gtk-d есть обёртки для:
  • gtkgl — использование OpenGL в приложениях GTK+
  • sv — SourceView — расширение GTK+ для редактирования текста с разными плюшками вроде подсветки синтаксиса, undo/redo и тд
  • vte — виджет терминала
  • gstreamer — мультимедийный фреймворк (удивило и порадовало, что биндинг создан и включён в gtkd)

Документация по проекту лежит здесь.

Ещё много примеров лежит здесь.

Комментарии (5)


  1. Pugnator
    07.06.2015 09:50
    +2

    Очень люблю GTK+, но сейчас перехожу на QT просто потому, что устал от ужасов Glade:
    Падения на сохранении проекта, фантомные объекты внутри XML после удаления объекта, который приходится удалять из файла руками, глюки при изменении текста полей: если начать править текст, он удалялся и прочее, прочее прочее.
    Постепенно просто устал от этого. Последний раз им пользовался с полгода назад, собирая из исходников. То, что было в репах убунты было ещё хуже.
    А жаль, тот же Broadway очень заинтересовал в свое время


  1. ntfs1984
    07.06.2015 10:15
    +3

    Хороший язык.

    Минутка ненависти
    А я вот вовсе не программист, но встала задача написать индикатор клавиатуры на elementary, ибо родной глючный достал.
    Полез в дебри GTK+ и ужаснулся. На Qt целых полчаса писал, учитывая то что я первый раз его в глаза вижу.

    На Гиктаймсе обитает статейка Инфляция программного обеспечения с точки зрения ресурсов процессора — почему новые версии приложения порой гораздо медленнее старых?
    Ответ нашел в этой: чтобы вывести Hello world, мы подключаем три библиотеки (со странными названиями Main и MainWindow — будто вторая может\будет работать без первой), создаем класс с несколькими функциями, и все это занимает 19 строк.


  1. baka_cirno
    07.06.2015 18:46

    Тем, кто уже знаком с C#, рекомендую глянуть в сторону языка Vala. Когда-то пришлось написать на нем существенный объем кода, и в целом он меня порадовал. Ввиду разнобоя в экосистеме вы не получите такого цельного фреймворка как Qt, однако это, на мой взгляд, лучший способ написать родное приложение для Gnome и других сред, основанных на GTK. Судя по рассылке, он все еще жив.


    1. Pugnator
      08.06.2015 19:18

      А Вала разве не всё?


      1. baka_cirno
        08.06.2015 21:05

        Рассылка вроде еще живая. Не слежу за ней давно, но надеюсь, что не все.