Getting started with GTKD

  • Tutorial
Good day, Habr!

Today I want to tell you about how to write applications using GTK + and the programming language D. I will not talk





about what GTK is. Let's get down to business immediately.

Create a new project, that is, just a folder =)
Add dub.json with the following content:
{
    "name": "hellogtkd",
    "targetPath": "bin",
    "targetType": "executable",
    "dependencies": {
        "gtk-d": "~>3.1.3"
    }
}


The dub build system by default looks for sources in the source folder.
Create it and add the main.d file there:
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();
}

Run the assembly and then the application
dub build && bin/hellogtkd


And voila!


We will not dwell on this and create an extremely simple ui in the glade program , and then load it into our program.



Pay attention to the tree of added components (top right), for the main window, buttons and rendering fields, identifiers are assigned that are different from those given by default (mwindow, btn, plot, respectively), we will use them for convenient obtaining from the builder.
Save the file in the project folder under the name ui.glade and edit main.d:

new 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();
}



I think everyone guessed why the GtkDrawingArea element was added - we will draw.
Add the actions that are performed when the button is pressed:
...
    void prepare()
    {
        prepareMainWindow();
        prepareButtonAction();
    }
...
    void prepareButtonAction()
    {
        // мы просто передаём делегат, который будет вызывается при сигнале Clicked
        obj!Button( "btn" ).addOnClicked( (Button aux)
        {
            obj!DrawingArea( "plot" ).queueDraw(); // заставить переотрисоваться наш plot
        });
    }


Let's prepare an empty class for rendering, create a draw.d file in the source folder:
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;
    }
}


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


Now when you start the application, you will see something like this in the console:
...
draw figure 014855276247
draw figure 014872180248
draw figure 014889286316
...


You may notice that re-rendering is called up not only when the button is pressed, but also when other events occur: moving and resizing the window, animating the color of the button, etc.
For details on redrawing widgets, see the GTK + documentation.
I set the call to redraw at the click of a button in order to show how this is done.
In fact, it simply coincided that pressing a button in itself leads to redrawing (due to a change in the background color of the button), but there may be other events that are not related to changing the GUI, then such a call is necessary.

Add some drawing code:
...
    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;
    }
...


And make the button do at least something that is not done automatically =)
...
    void prepareButtonAction()
    {
        obj!Button( "btn" ).addOnClicked( (Button aux)
        {
            fig.angle += 0.1; // немного поворачиваем фигуру
            obj!DrawingArea( "plot" ).queueDraw();
        });
    }
...




That's all. The training program with Schulte tables (first picture) is here . In it you can find more examples of using gtkd and rendering through cairo (although not the best ones, in terms of code quality).

Also in the gtk-d package there are wrappers for:
  • gtkgl - using OpenGL in GTK + applications
  • sv - SourceView - GTK + extension for editing text with various goodies like syntax highlighting, undo / redo, etc.
  • vte - terminal widget
  • gstreamer - multimedia framework (it surprised and pleased that the binding was created and included in gtkd)

The project documentation is here .

Many more examples are here .

Also popular now: