Learn GTK+ 3.x with the D Programming Language

    6.2 - View


0. Introduction

Before beginning, a number of assumptions need to be made. This guide assumes that you are already familiar with the D programming language. The purpose is not to teach you D by itself, but rather how to use GTK+ with D. If you don't know D, then I recommend, The D Programming Language by Ali Çehreli. It's a freely licensed (Creative Commons) book designed to teach beginners how to use D. More information can be found at the official D website and at the GtkD website.

For the first chapter, when setting up the development environment, I'm going to assume that you are using Windows. Linux and OS X users should be able to figure out how to translate those steps to their appropriate platform. Readers are encouraged to be resourceful. Links on this page may become outdated or the information may change slightly. Always trust the information on the official project site over this tutorial.

This guide covers GTK+ 3.x which is API and ABI incompatible with GTK+ 2.x. Keep that in mind when referencing documentation. This guide also does not assume prior experience with the C version of GTK+, although such knowledge is beneficial as GtkD attempts to be a wrapper around the C APIs.

At the time of this writing, the current version of DMD (Digital Mars D, the compiler for D) is 2.063 and the version of the GtkD bindings is 2.2.0, and the GTK+ version it wraps is at version 3.8.




1. Installing the Development Environment

1.1 - Installing D and D Development Tools (DDT)

1. Download DMD (Digital Mars D), the reference compiler from D Downloads page. Windows users are encouraged to download the executable installer. Linux users may be able to obtain DMD from their distribution's repository. If you already have a D development environment setup and working, you may skip this section entirely.

2. Download and install Eclipse. Although it is not required, Eclipse with the DDT (D Developer Tools) plugin can make writing D programs easier. Otherwise any editor (preferably with D syntax highlighting support) and access to the command line will do fine. Skip to step 8 if you wish to skip the steps on using Eclipse and DDT.

3. Open Eclipse. Select your workspace. The default is acceptable. Next, go up to the menu bar and select Help -> Install New Software. For the "work with" textbox, enter the following URL:

http://updates.ddt.googlecode.com/git/

Once it downloads the updates, select "DDT Project" and proceed with the installation. The rest of the process is intuitive and won't be covered here in detail.

4. Once Eclipse restarts, select File -> New D Project. You may name it anything you wish, such as "GtkDTutorial".

5. On the new project window, click the link marked "configure interpreter" and tell Eclipse where the D compiler is located. In my case, it's at: C:\D\dmd2\windows\bin\dmd.exe

It should automatically find druntime and phobos. We'll also need to add a source folder so it can find GtkD after we've installed it. Click "Add" and select the D source directory. On my system it's located at: C:\D\dmd2\src

6. Click OK and then click Finish to complete the project creation. If you wish to keep track of your progress in case you make a mistake and want to back out to a previous revision, it is recommended you use a revision control system such as Mercurial (hg) or Git. Setup for revision control will not be covered in this tutorial, as it's readily available elsewhere and goes outside the scope of this document.

7. D uses Unicode for its source. Ensure that Eclipse editor is set to save as Unicode. Right click the project in the Project Explorer (the file hierarchy to the left) and click "Properties". Set the text encoding to UTF-8 and the newline to UNIX (yes, even on Windows systems). Then click Apply and OK.

8. Before continuing with the GtkD installation, we'll ensure that D itself is installed correctly. Right click the "src" folder and create a new file named "main.d". Enter the following Hello World program and run it.

main.d

import std.stdio;

void main()
{
    writeln("Hello World!");
}

If you're using an editor and command line, you may compile this program with:

dmd main.d

The output file should be main.exe or simply main on UNIX-like systems. Run the program and you should see Hello World!


1.2 - Installing GtkD

1. Install the GTK+ Runtime. Linux users should already have it installed. If you're using Windows, install the 32-bit version, even if you're using a 64 bit system. The reason is because the DMD port for Windows has not yet been fully ported to 64 bits and the installer used earlier only installed a 32 bit version of D.

GTK+ 3.8 Runtime Installer (32 bit)

2. Download and extract the latest GtkD sources.

GtkD Source Download

3. Open a command line at the GtkD directory you extracted. Then build the source. Windows users will want to build without the 64 bit flag.

rdmd Build.d
or if you're using the 64 bit version on Mac or Linux, then use this:
rdmd -m64 Build.d

4. Open the directory with the DMD binary. There is a file called sc.ini. Open it in a text editor.
C:\D\dmd2\windows\bin\sc.ini

5. Find the DFLAGS line and append the following. Note that you must NOT remove anything that already exists on the DFLAGS line. Only append this to the end. Include the quotes.

"-I%@P%\..\..\src"


6. Copy the GtkD src folder into D's src folder.

There several sub-folders beginning with "atk" and ending with "pango". Copy these sub-folders and paste them into D's src:
C:\D\dmd2\src. They should sit alongside phobos and druntime. You must copy all the folders, not just the gtk folder. This is because GTK itself has its own set of dependencies.

7. Copy GtkD's library file gtkd.lib to D's Windows library folder at: C:\D\dmd2\windows\lib\

Proceed to the next chapter and try compiling and running the "Hello World" program to ensure the installation was successful.



2. Hello World

2.1 - Our First GtkD Program

hello.d

import gtk.MainWindow;
import gtk.Label;
import gtk.Main;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("Hello World");
    win.setDefaultSize(200, 100);
    win.add(new Label("Hello World"));
    win.showAll();
    Main.run();
}






Now we need to compile and run it. If you are compiling from the command line, it's quite simple. You just need to pass an argument telling it you wish to link with GtkD and it will compile successfully.

dmd hello.d -L+gtkd

If you are compiling with Eclipse and DDT, you'll have to modify the project's properties. Right click the project folder in the Project Explorer and click Properties. Under compile options add "-L+gtkd". Leave all the existing options in place. Here is a screenshot of what my settings look like.

D Compile Options

Here's an analysis of the Hello World program. The code is straight forward, but some simple explanation will be provided.

Main.init(args);
This call is necessary, even if you don't intend to have any command line arguments. It's needed to setup GTK.

Main.run();
Tells GTK to enter its main event loop. An event is an action which the program will respond to. Examples include the user clicking a button, typing some text, or even just mousing over a widget. Events can also be non-user generated such as packets coming in from the network, changes to a file, etc.

MainWindow win = new MainWindow("Hello World");

A MainWindow is different from a normal window in that once all windows of the MainWindow class are closed, the application will exit. Had a normal window been used, we would have had to setup this callback ourselves. Otherwise, the application will stay open even if all windows are closed. This is sometimes desirable for daemon tools (background processes), but not usually for end user programs. The argument provided to the constructor is the title of the window which will appear on the window border and task bar.

win.setDefaultSize(200, 100);
The window is resizable by the user, but we can specify its default size in terms of width by height.


2.2 - Exercises

1. What happens when you try to add more than one label to the window? Why do you think this happens?

2. Try setting the window size to (0,0), and observe the result.

3. What happens when you call win.show() instead of showAll()? Why would calling methods on the window affect other widgets?




3. Buttons and Callbacks

3.1 - Creating Your Own Button Widget

Although we could create a button just as we did with a label, we need a way to establish a callback. A callback is a delegate which is called when the button is clicked. We'll be creating our own class which inherits from GtkD's Button class. It's very common to create your own widgets this way and set the properties for that widget in the constructor. This makes it much less work to reuse widgets with similar attributes.

main.d

import gtk.Button;
import gtk.Main;
import gtk.MainWindow;

import gdk.Event;
import gtk.Widget;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("Example");
    win.setDefaultSize(200, 100);
    win.add(new QuitButton("Exit Program"));
    win.showAll();
    Main.run();
}

class QuitButton : Button
{
    this(in string text)
    {
        super(text);
        modifyFont("Arial", 14);
        addOnButtonRelease(&quit);
    }
   
    private bool quit(Event event, Widget widget)
    {
        Main.quit();
        return true;
    }
}


Button Example



















import gdk.Event;
This is not a typo. The Event class is actually apart of GDK, The GIMP Drawing Kit, which handles more primitive tasks like doing the actual drawing to the screen and listening for events. GTK+ is an abstraction over GDK.

modifyFont("Arial", 14);
This changes the button's font to be Arial, at size 14. This same method works on Labels as well.

addOnButtonRelease(&quit);
Adds an event handler. This specifies which method to call when the button is clicked. Typically, buttons will only handle the event after the mouse button has been released from its pressed state. Although you can make the event happen on mouse-down, it can subtly surprise users to see an event happen mid-click.

3.2 - Event Propagation

Our "quit" method, must be of a particular form. It must return a bool, and it must take an Event and Widget as arguments. In this simple example, we ignore the Event and Widget since we don't need any information from them. The bool serves a special purpose called Event Propagation. It asks the question, "Did you fully handle the event?", true or false. If we return false, then the event is passed up to the parent widget.

Suppose for example, you wanted to ask the user if they're sure they want to quit when they click the close button. Returning true, would keep the program open (user clicks no), because GTK+ stops operating on the event if it has been fully handled. If we return false (user clicks yes), then GTK+ sees the event hasn't been fully handled and the event will propagate up and GTK+ will exit the program.

Because we're exiting the program directly with a call to Main.quit(), the events never actually propagate further. So for this example, it doesn't matter if we return true or false. In general, you'll want to return true.

4. Containers

As you may have observed from the exercises in chapter 2, some widgets have a minimum size. Labels aren't scrollable so they'll force a window to be a particular size to display itself.

What might have surprised you the most was the discovery that multiple labels couldn't be added to a window. Windows can only contain one widget. Wait... what?! Yes, that's correct. Windows are known as "containers". A container is any widget which can hold other widgets. Containers can be nested inside one another as many levels deep as you need. Although a Window can only hold one widget, what we can do is add another container; one which does allow multiple widgets.

Widgets are designed to have a single purpose. It's good software engineering to not try to make a single component do too much. A window is more concerned with things like minimize, maximize, resizing, and those tasks. The layout which describes how multiple widgets are displayed is the job of another container. There are multiple ways to do layout. The simplest is known as Box Layout, using the Box container.

4.1 - Box Layout

main.d

import gtk.MainWindow;
import gtk.Box;
import gtk.Button;
import gtk.Main;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("Hello World");
    win.setDefaultSize(200, 100);
  
    Box box = new Box(Orientation.VERTICAL, 30);
    box.add(new Button("Hello World"));
    box.add(new Button("Goodbye World"));
  
    win.add(box);
    win.showAll();
    Main.run();
}













Box box = new Box(Orientation.VERTICAL, 30);
Boxes can be created to be either horizontal or vertical. They also have a "spacing" argument. This tells the Box how many pixels of space to put between each of the elements added.

win.add(box);
Instead of adding the labels directly, we add the box which contains the labels. The labels are considered "children" of the box container and likewise, the box is a child of the window. This is why the showAll() method works. It recursively calls show() on all of a widgets children. It's much more convenient.

4.2 - Packing Widgets

Packing can be thought of as putting our widgets inside an invisible box before adding it to a container. By putting it inside this invisible box we can better control the layout. In particular, we're interested in how things will be handled when the container is resized. Do we want our child widgets to become larger or do we want padding around them? Packing gives us this kind of control.

main.d

import gtk.MainWindow;
import gtk.Box;
import gtk.Button;
import gtk.Main;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("Hello World");
    win.setDefaultSize(200, 200);
 
    Box box = new Box(Orientation.VERTICAL, 10);
    box.packStart(new Button("Hello World"), true, true, 0);
    box.packStart(new Button("Click Me!"), true, false, 0);
    box.packStart(new Button("Goodbye World"), false, false, 10);
 
    win.add(box);
    win.showAll();
    Main.run();
}


Packing Example





There are two methods for packing. One is packStart which will start packing widgets from top to bottom, left to right. The packEnd method does the reverse. So if you wanted the widgets to appear in reverse order from how they appear in your code, you can use packEnd.

box.packStart(new Button("Hello World"), true, true, 0);
The first argument is the widget we want to pack. The second argument is the "expand" option. It specifies whether or not the invisible box we packed our widget into will grow in size when the parent container grows in size. The next boolean is the "fill" option. It specifies whether or not the widget we packed will grow in size to fill the box (true), or whether the packing box will simply add padding. Notice how the "Hello World" button is bigger because we specified that it fill it's space. Meanwhile, the "Click Me" button has space around it. The final argument is the "padding" option. It specifies the padding outside of the invisible packing box. The padding of 10, on "Goodbye World" button makes it so that the button doesn't touch the bottom edge of the window.

4.3 - Exercises

1. Play with the packing options. Try changing the expand and fill options to true/false and resize the window. See how the button widgets respond to these changes. Try all the combinations.

2. Create a program that displays a label and a button. Have the label say "Hello World" and the button say "Update". When clicking the update button, the text from the label will alternate between "Hello" and "Goodbye". Is it necessary to call the show method for the changes to the label to become visible?



5. MenuBar

The MenuBar class is used to create the kinds of menus you see at the top of windows. They'll typically include things such as File, Edit, Tools, etc. Creating a MenuBar can be kind of tricky unless you understand some core concepts behind it.

The MenuBar as a widget is what displays at the top of the window. The items inside of it (the clickable labels) are part of the MenuItem class. A drop-down list of these clickable labels is part of the Menu class. Understanding the difference between these three classes is important to knowing how to build menus.

5.1 - A Simple MenuBar

main.d

import gtk.MainWindow;
import gtk.Box;
import gtk.Main;
import gtk.MenuBar;
import gtk.MenuItem;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("MenuBar Example");
    win.setDefaultSize(250, 200);
  
    MenuBar menuBar = new MenuBar();
    MenuItem fileMenuItem = new MenuItem("File");
  
    menuBar.append(fileMenuItem);
 
    Box box = new Box(Orientation.VERTICAL, 10);
    box.packStart(menuBar, false, false, 0);
 
    win.add(box);
    win.showAll();
    Main.run();
}











We keep the vertical box so that our menu appears at the top of the window and we have room for other widgets beneath it. Otherwise the menu would take up the whole space and appear stretched out. This example is very simple and doesn't include any actual menus when clicking on the MenuItems. Because menus can quickly grow in complexity, the next example will create a class that represents the file menu item.

5.2 - Adding Menus

As mentioned, menus are the actual drop-down that appears when a MenuItem is clicked. Let's create a class to represent the file MenuItem and it will have a Menu object as a member to represent it's drop down selections. We'll then add an exit MenuItem to quit the application.

main.d

import gtk.MainWindow;
import gtk.Box;
import gtk.Main;
import gtk.Menu;
import gtk.MenuBar;
import gtk.MenuItem;
import gtk.Widget;
import gdk.Event;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("MenuBar Example");
    win.setDefaultSize(250, 200);
  
    MenuBar menuBar = new MenuBar();  
    menuBar.append(new FileMenuItem());
 
    Box box = new Box(Orientation.VERTICAL, 10);
    box.packStart(menuBar, false, false, 0);
 
    win.add(box);
    win.showAll();
    Main.run();
}

class FileMenuItem : MenuItem
{
    Menu fileMenu;
    MenuItem exitMenuItem;
   
    this()
    {
        super("File");
        fileMenu = new Menu();
       
        exitMenuItem = new MenuItem("Exit");
        exitMenuItem.addOnButtonRelease(&exit);
        fileMenu.append(exitMenuItem);
       
        setSubmenu(fileMenu);
    }
   
    bool exit(Event event, Widget widget)
    {
        Main.quit();
        return true;
    }
}
































6. TreeView

A TreeView creates a widget with a tree structure. The most common example of this is seen when browsing a file dialog and seeing the hierarchy of the directory structure.

Because GTK+ uses a Model-View-Controller (MVC) style architecture, the data for the tree and the widget that renders it are separate things. We'll talk about a tree model and adding data to it before showing it in a tree. Here's an example program with two columns and three rows.


6.1 - Model

There are two types of models. A ListStore is the simplest of the two. It creates a model without any hierarchical data. It's just rows and columns, like the simple table shown above. There are no sub-rows. Here is the source code for the model.

CountryListStore.d

module CountryListStore;

private import gtk.ListStore;
private import gtk.TreeIter;
private import gtkc.gobjecttypes;

class CountryListStore : ListStore
{
    this()
    {
        super([GType.STRING, GType.STRING]);
    }
   
    public void addCountry(in string name, in string capital)
    {
        TreeIter iter = createIter();
        setValue(iter, 0, name);
        setValue(iter, 1, capital);
    }
}



super([GType.STRING, GType.STRING]);
This calls the constructor of the ListStore class and says that the types of both columns will be strings.

TreeIter iter = createIter();
In order to insert data into a row, we need to create it. The parent method createIter adds a new row and returns a TreeIter object that points to that row.

setValue(iter, 0, name);
setValue(iter, 1, capital);

The setValue method takes 3 arguments. The first is the iter of the row we want to add the data too. The second is an integer representing the index of the column. The third is the data to display.

6.2 - View

Here we pass the model to the view and create the column headers.

CountryTreeView.d

module CountryTreeView;

private import gtk.TreeView;
private import gtk.TreeViewColumn;
private import gtk.ListStore;
private import gtk.CellRendererText;
private import gtk.ListStore;

class CountryTreeView : TreeView
{
    private TreeViewColumn countryColumn;
    private TreeViewColumn capitalColumn;
   
    this(ListStore store)
    {       
        // Add Country Column
        countryColumn = new TreeViewColumn(
            "Country", new CellRendererText(), "text", 0);
        appendColumn(countryColumn);
       
        // Add Capital Column
        capitalColumn = new TreeViewColumn(
            "Capital", new CellRendererText(), "text", 1);
        appendColumn(capitalColumn);
       
        setModel(store);
    }
}


countryColumn = new TreeViewColumn(
        "Country", new CellRendererText(), "text", 0);

The first argument is the title we want to appear on the column header. The second and third arguments specify that the column will contain text data. The last argument is the column index.

6.3 - Controller

Here, we'll use "main" to act like our controller and combine the model and view into a complete program.

main.d

import gtk.MainWindow;
import gtk.Box;
import gtk.Main;

import CountryListStore;
import CountryTreeView;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("ListStore Example");
    win.setDefaultSize(250, 200);
 
    Box box = new Box(Orientation.VERTICAL, 0);
   
    auto countryListStore = new CountryListStore();
    countryListStore.addCountry("Denmark", "Copenhagen");
    countryListStore.addCountry("Norway", "Olso");
    countryListStore.addCountry("Sweden", "Stockholm");
   
    auto countryTreeView = new CountryTreeView(countryListStore);
    box.packStart(countryTreeView, true, true, 0);
 
    win.add(box);
    win.showAll();
    Main.run();
}


6.4 - Adding Children

For TreeViews with child nodes, we will use TreeStore instead of ListStore. The only difference is that TreeStore allows child nodes. We'll also need to keep a reference to the parent node to add children to it.



LocationTreeStore.d

module LocationTreeStore;

private import gtk.TreeStore;
private import gtk.TreeIter;
private import gtkc.gobjecttypes;

class LocationTreeStore : TreeStore
{
    this()
    {
        super([GType.STRING, GType.UINT]);
    }
   
    // Adds a location and returns the TreeIter of the item added.
    public TreeIter addLocation(in string name, in uint population)
    {
       
        TreeIter iter = createIter();
        setValue(iter, 0, name);
        setValue(iter, 1, population);
        return iter;
    }
   
    // Adds a child location to the specified parent TreeIter.
    public TreeIter addChildLocation(TreeIter parent,
            in string name, in uint population)
    {
        TreeIter child = TreeStore.createIter(parent);
        setValue(child, 0, name);
        setValue(child, 1, population);
        return child;
    }
}














TreeIter child = TreeStore.createIter(parent);
This is the main difference of the tree store. When we create a new row, we pass a parent TreeIter so the new row is a child of that parent.

LocationTreeView.d

module LocationTreeView;

private import gtk.TreeView;
private import gtk.TreeViewColumn;
private import gtk.TreeStore;
private import gtk.CellRendererText;
private import gtk.ListStore;

class LocationTreeView : TreeView
{
    private TreeViewColumn countryColumn;
    private TreeViewColumn capitalColumn;
   
    this(TreeStore store)
    {       
        // Add Country Column
        countryColumn = new TreeViewColumn(
            "Location", new CellRendererText(), "text", 0);
        appendColumn(countryColumn);
       
        // Add Capital Column
        capitalColumn = new TreeViewColumn(
            "Population", new CellRendererText(), "text", 1);
        appendColumn(capitalColumn);
       
        setModel(store);
    }
}


No major changes on the TreeView.

main.d

import gtk.MainWindow;
import gtk.Box;
import gtk.Main;

import LocationTreeStore;
import LocationTreeView;

void main(string[] args)
{
    Main.init(args);
    MainWindow win = new MainWindow("TreeStore Example");
    win.setDefaultSize(250, 280);
 
    Box box = new Box(Orientation.VERTICAL, 0);
   
    auto store = new LocationTreeStore();
   
    // Add Asia
    auto asia = store.addLocation("Asia", 4_165_440_000);
    auto japan = store.addChildLocation(asia, "Japan", 128_056_026);
    store.addChildLocation(japan, "Tokyo", 35_682_000);
   
    // Add Europe
    auto europe = store.addLocation("Europe", 742_452_000);
    auto finland = store.addChildLocation(europe, "Finland", 5_180_000);
    store.addChildLocation(finland, "Helsinki", 1_361_000);
   
    // Add North America
    auto northAmerica = store.addLocation("North America", 528_700_000);
    auto usa = store.addChildLocation(northAmerica,
            "United States", 313_900_000);
    store.addChildLocation(usa, "New York", 8_245_000);
   
    auto locationTreeView = new LocationTreeView(store);
    box.packStart(locationTreeView, true, true, 0);
 
    win.add(box);
    win.showAll();
    Main.run();
}


Notice how we keep a reference to the added node so we can add children. You can of course find the children another way, such as iterating through them, or sorting and searching. But for the simplicity of this example, we simply store the parent in a variable and pass it to addChildLocation.

6.5 - Iteration

Coming Soon...