Skip to content

Tutorial 1

Vincent MAGNIN edited this page Jul 22, 2020 · 18 revisions

Tutorial 1: my first gtk-fortran application

At the end of this tutorial, you will know how to write a GTK application with a window, some widgets and how to draw a bitmap image.

Let's create a GTK application

Let's call our file my_first_gtk_app.f90. Its two main parts will be:

  • a module named handlers which will contain the functions that will handle the events and global variables like pointers toward widgets. Because writing a Graphical User Interface (GUI) needs events oriented programming: the interface will be composed of widgets and of callback functions that will be called when events occurs. To understand the GTK event-driven programming model, read the Main loop and Events documentation page.
  • The main program which will declare the GtkApplication.

Let's write the skeleton of our application:

module handlers
  use iso_c_binding
  use gtk, only:
  use g, only:

  implicit none

  contains

end module handlers

program my_first_gtk_app
  use handlers

  implicit none

end program

The iso_c_binding module, available since Fortran 2003, offers the constants for types interoperable between Fortran and C and functions to interoperate pointers. The gtk module is the main module of gtk-fortran, defined in the src/gtk-auto.f90 and src/gtk.f90 files. The g module is the GLib module (src/glib-auto.f90 file). The only statement is highly recommended to lower the compilation time, because the gtk and g modules contain thousands of functions.

The recommended way to create a GTK application is to use the GtkApplication class (https://developer.gnome.org/gtk3/stable/GtkApplication.html), which will initialise the library and manage various operations to integrate the application in the system. Let's add in our main program the following lines :

  type(c_ptr)    :: app
  integer(c_int) :: status

  app = gtk_application_new("gtk-fortran.my_first_gtk_app"//c_null_char, &
                          & G_APPLICATION_FLAGS_NONE)
  status = g_application_run(app, 0_c_int, c_null_ptr)
  call g_object_unref(app)

The gtk_application_new() function is returning a C pointer. We therefore declare our app variable using the c_ptr constant defined in the ISO_C_BINDING module. The first argument to pass is the application (unique) identifier: a string composed of alphanumerical characters containing at least one dot (see https://developer.gnome.org/gio/stable/GApplication.html#g-application-id-is-valid). But Fortran strings having a defined length, contrary to C strings terminated by a null character, we must append a c_null_char each time we pass a Fortran string to GTK. The second argument, G_APPLICATION_FLAGS_NONE, is a flag (see https://developer.gnome.org/gio/stable/GApplication.html for other values).

GTK is based on GLib and a GTK application is therefore a GLib application. We launch our application using the g_application_run() function. Here we don't manage the command line arguments: the 0_c_int, c_null_ptr arguments are the classical int argc, char **argv used in C language. Calling that function means your program will enter the main loop of the GUI. That loop is idle most of the time, and waits for something to happen (an event). When quitting the application, the function will return the exit status in the status variable, whose type must be interoperable with the C int type.

When you read the GTK documentation, be conscious that GTK is using the GLib types instead of the C types: for example, gint just stands for int, gdouble for double, etc. See https://developer.gnome.org/glib/stable/glib-Basic-Types.html

The call g_object_unref(app) line will free the memory. Note that C functions which return nothing (void) are interfaced in Fortran by subroutines and therefore called by call.

But don't forget to declare in the handlers module GTK functions and constants you are using:

  use gtk, only: gtk_application_new, G_APPLICATION_FLAGS_NONE
  use g, only: g_application_run, g_object_unref

Note that in gtk-fortran all the enums, like G_APPLICATION_FLAGS_NONE, were placed in the gtk module, whatever library they come from (so don't trust the prefix).

Let's verify that the code compiles:

$ gfortran my_first_gtk_app.f90 $(pkg-config --cflags --libs gtk-3-fortran)

Note that depending on your system, you may also need to export the PKG_CONFIG_PATH environment variable with the pkgconfig path (see installation instructions).

You can now launch your application:

$ ./a.out

(a.out:16004): GLib-GIO-WARNING **: 16:27:17.982: Your application does not implement g_application_activate() and has no handlers connected to the 'activate' signal.  It should do one of these.

Or if you are under Windows (MSYS2): ./a.exe

Nothing appears and you get a warning. It is normal because we have not yet defined any widget.

My first window

Most of the time, your GTK program will be based on a Window widget. And that window must be created when our GTK application is activated. Just before launching our application with status = g_application_run(app, 0_c_int, c_null_ptr), we need to add this line in the main program:

  call g_signal_connect(app, "activate"//c_null_char, c_funloc(activate), c_null_ptr)

We connect our app to a function named activate that will be called by the main loop when the "activate" signal is emitted. It is a callback function. Don't forget to append the c_null_char to the signal name. The c_funloc() function of the ISO_C_BINDING module returns a C pointer toward the Fortran callback function. The c_null_ptr means no particular data will be passed to the callback function.

Now, let's write an activate function in our handlers module, just after the contains statement:

  subroutine activate(app, gdata) bind(c)
    type(c_ptr), value, intent(in) :: app, gdata
    type(c_ptr) :: window

    window = gtk_application_window_new(app)
    call gtk_widget_show_all(window)
  end subroutine activate

This subroutine activate() will be called by the GTK main loop when you launch your application. You will define there all the widgets of your GUI. The input arguments are two C pointers toward the GTK application and the data defined in the g_signal_connect() function (i.e. c_null_ptr in our case). Note firstly the bind(c) statement (this Fortran subroutine will be called by a C function in GTK) and secondly that the pointers are passed by value.

Our first widget in our app is an empty window: we have defined a C pointer toward that object and we have created it using gtk_application_window_new(app). Don't forget to call the gtk_widget_show_all() function if you want your window to appear on screen! Note that in GTK 3, all widgets are hidden by default and that is why we use _show_all(). In GTK 4, on the contrary, the widgets inside a window are all shown by default and the gtk_widget_show_all() function was removed: you will use instead call gtk_widget_show(window).

Don't forget to add those function names in the use gtk, only: statement of the module. Note that g_signal_connect must be added also to the use gtk although it is part of GLib (g module): for some reason, it is an exception in gtk-fortran and was placed in the gtk module.

If you run you application, felicitations, you will see your first GTK window! ![](CHANGELOG file

You can change its title using:

    call gtk_window_set_title(window, "Hello world!"//c_null_char)

**You now know you have to add the function name in the use gtk, only: statement of the module. I won't repeat anymore that advice in the following steps of this tutorial. If you forget, the compiler will tell you! **

At this stage, your code should be similar to that file: my_first_gtk_app3.f90

Adding a Compute button

To place your widgets in the window, you need a widget that will manage the layout, for example a GtkBox where the widgets will be arranged into column or row (code to add into the activate subroutine):

    type(c_ptr) :: box, my_button
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10_c_int)
    call gtk_container_add(window, box)

Here, we have chosen to arrange them vertically and separated by 10 pixels (note the _c_int suffix to be sure the integer will be interoperable with the C int).

Our box is then added to the window. In GTK 4, gtk_container_add was removed and should be replaced by gtk_window_set_child.

Now, we define our button and add it to the box. We connect it to the my_button_clicked subroutine that we want to be called when it is clicked:

    my_button = gtk_button_new_with_label("Compute"//c_null_char)
    call gtk_container_add(box, my_button)
    call g_signal_connect(my_button, "clicked"//c_null_char, c_funloc(my_button_clicked))

In GTK 4, gtk_container_add should be replaced by gtk_box_append.

Let's add that callback function into the handlers module:

  subroutine my_button_clicked(widget, gdata) bind(c)
    type(c_ptr), value, intent(in) :: widget, gdata
    print *, "Button clicked!"
  end subroutine

But how can you know the interface of that callback function? You can find it by looking in the GTK documentation for the "clicked" signal of a button: https://developer.gnome.org/gtk3/stable/GtkButton.html#GtkButton-clicked When you see a star * in a prototype, don't bother with the type of the object. You just need to know it's a pointer and declare it as type(c_ptr) in your Fortran code.

At this stage your code should be: my_first_gtk_app4.f90 And your app should look like this:

As you can see, the window size was automatically adapted to the only widget inside. We could of course override that behaviour by using: https://developer.gnome.org/gtk3/stable/GtkWindow.html#gtk-window-set-default-size

Adding entry widgets

The objective of the next steps is to study the chaotic behaviour of the Logistic Map: https://en.wikipedia.org/wiki/Logistic_map We add in our program the following module, with a function that will compute a great number of terms of that sequence, starting from x0 between 0 and 1 and with r in the [0, 4] range:

module math
  use iso_c_binding, only: dp=>c_double
  implicit none

  contains

  pure real(dp) function iterate(x0, r) result(x)
    real(dp), intent(in) :: x0, r
    integer  :: i

    x = x0
    do i = 0, 20000
      x = r * x * (1_dp - x)
    end do
  end function iterate
end module math

Note that we use a real type interoperable with the C double type.

x0 will be chosen randomly between 0 and 1. We will study the effect of the r parameter. We will therefore add into the activate subroutine a widget to enter the r value:

    label_r = gtk_label_new("r parameter"//c_null_char)
    call gtk_container_add(box, label_r)

    r_spin_button = gtk_spin_button_new(gtk_adjustment_new(3._dp, 0._dp, 4._dp, &
                                    & 0.1_dp, 0._dp, 0._dp), 0.0_dp, 15_c_int)
    call gtk_container_add(box, r_spin_button)

In fact, two widgets: a label to show the name of the parameter and a GtkSpinButton as an entry field. See: https://developer.gnome.org/gtk3/stable/GtkSpinButton.html

The last argument is the number of decimals to display. The first argument is a GtkAdjustment object: the default value is 3.0, the min is 0.0 and the max 4.0, the step is 0.1.

We update the my_button_clicked callback function to choose randomly x0, get r from the the GtkSpinButton and print the results in the terminal:

  subroutine my_button_clicked(widget, gdata) bind(c)
    type(c_ptr), value, intent(in) :: widget, gdata
    real(dp) :: r, x0

    call random_number(x0)
    r = gtk_spin_button_get_value(r_spin_button)
    print *, r, x0, iterate(x0, r)
  end subroutine

The r_spin_button pointer must therefore be declared as a global variable in the handlers module:

  type(c_ptr) r_spin_button

If x0=0 (improbable) the result of the iterations will of course be zero. In the other cases, depending on the r value, if you press several times the Compute button, you will see that the iterations can be attracted toward one value, two values, four values, etc. And when r>3.569946..., it becomes chaotic (except in a few values ranges).

At this stage your code should be: my_first_gtk_app5.f90 And your app should look like this:

Adding a drawing area

We have used a print * to print the results into the terminal. We could of course also have used a label and its gtk_label_set_text() function or more elaborate solutions like a GtkTextView. But our real objective is to draw the final result of the iterations over the r value.

Below our GtkSpinButton, we define a GtkDrawingArea:

    my_drawing_area = gtk_drawing_area_new()
    pixwidth  = 1000
    pixheight = 600
    call gtk_widget_set_size_request(my_drawing_area, pixwidth, pixheight)
    call g_signal_connect (my_drawing_area, "draw"//c_null_char, c_funloc(draw))
    call gtk_container_add(box, my_drawing_area)

The variables my_drawing_area and pixwidth and pixheight will be declared at the top of the handlers module, respectively with type(c_ptr) and integer(c_int). We will later write the callback function for the draw signal.

Following the previous code, we use the GdkPibux library: https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-Image-Data-in-Memory.html
https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-The-GdkPixbuf-Structure.html

    my_pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8_c_int, &
              & pixwidth, pixheight)
    nch = gdk_pixbuf_get_n_channels(my_pixbuf)
    rowstride = gdk_pixbuf_get_rowstride(my_pixbuf)
    call c_f_pointer(gdk_pixbuf_get_pixels(my_pixbuf), pixel, &
                   & (/pixwidth*pixheight*nch/))
    pixel = char(0)

A pixbuf is a representation of an image in the memory. The FALSE value means there will be no transparency (no alpha channel). And 8 is the number of bits per colour. The gdk_pixbuf_get_n_channels() function should return 3 (Red, Green, Blue). The gdk_pixbuf_get_rowstride will generally return pixwidth*nch, but it can be greater: it is the "number of bytes between the start of a row and the start of the next row". my_pixbuf, nch and rowstride will be declared at the top of the module. And also pixel:

  character(kind=c_char), dimension(:), pointer :: pixel

It is a pointer toward the C array containing the pixel data. That pointer is obtained by gdk_pixbuf_get_pixels and transformed into a Fortran pointer with c_f_pointer. That array is initialized with zeros and the image will therefore be black (red=0, green=0, blue=0 for each pixel). It is a one dimensional array: the three first bytes are the intensities of the three colours of the first pixel (0,0) at the top left, the three following bytes are for the pixel (1,0), etc.

Each time the GtkDrawingArea will need to be redrawn (for example if another window was on top), the draw signal will be emitted and the callback function called:

  function draw(widget, my_cairo_context, gdata) result(ret) bind(c)
    type(c_ptr), value, intent(in) :: widget, my_cairo_context, gdata
    integer(c_int)                 :: ret

    call gdk_cairo_set_source_pixbuf(my_cairo_context, my_pixbuf, 0d0, 0d0)
    call cairo_paint(my_cairo_context)
    ret = FALSE
  end function draw

The drawing will be made via the Cairo library. Our pixbuf is placed at the top left (0,0) of what is called a Cairo context, which us then painted by cairo_paint.

At this stage your code should be: my_first_gtk_app6.f90 And your app should look like this:

In GTK 4, the draw signal has been removed. The GTK 4 version of those parts of the code is given in the Appendix at the bottom of this page.

Perhaps you have the feeling it was quite fastidious, but you now have a template to use every time you want to draw a bitmap image!

Drawing the Feigenbaum fractal

The algorithm to draw the bifurcation diagram of the logistic map, also called the Feigenbaum fractal, being short, we will put it directly into the my_button_clicked callback function:

  subroutine my_button_clicked(widget, gdata) bind(c)
    type(c_ptr), value, intent(in) :: widget, gdata
    real(dp) :: r, x0
    real(dp) :: rmin, rmax
    integer :: p, n, xp, yp, xpmax, ypmax

    call random_seed()

    rmin = gtk_spin_button_get_value(r_spin_button)
    rmax = 4_dp
    xpmax = pixwidth-1
    ypmax = pixheight-1
    pixel = char(0)

    do xp = 0, xpmax
      r = rmin + xp * (rmax - rmin) / xpmax

      do n = 1, 100
        call random_number(x0)
        yp = ypmax - nint(iterate(x0, r) * ypmax)

        p = 1 + xp*nch + yp*rowstride
        pixel(p)   = char(255)
        pixel(p+1) = char(150)
        pixel(p+2) = char(120)
      end do
    end do

    call gtk_widget_queue_draw(my_drawing_area)
  end subroutine my_button_clicked

The GtkSpinButton will now correspond to the minimal r value, the maximum begin 4. For each r value, we compute the iterations starting from 100 x0 random values. The position of the (xp,yp) pixel in the one dimensional array is given by p = 1 + xp*nch + yp*rowstride (in Fortran arrays indexing start at 1). The red, green, blue values are 255, 150, 120. Note also that the graphical coordinate system has the origin (0,0) at the top left and the ordinate axis is oriented downward.

The gtk_widget_queue_draw() function tells the main loop that the area needs redrawing.

At this stage your code should be: my_first_gtk_app7.f90 And your app should look like this: Note that the computation takes several seconds and that the image appears only when it is finished.

Conclusion

Felicitations, you have learned a lot: you can know create your own GtkApplication in Fortran. You know how to add widgets, you have understood what is a callback function, and you can even draw a bitmap image in Fortran!

Of course, this is just a first step. We could improve some details:

  • the GtkSpinButton and Button widths are identical to the width of the image. For the layout we have used the simple GtkBox container. But we could have used a more complex one like GtkGrid.
  • Another problem is that the image content appears only at the end of the computation, when we return to the main loop of the GUI. One solution could be to manage directly that main loop (see the mandelbrot_pixbuf.f90 example) to update the image during the computation.
  • We have not saved our image. See the pixbuf_without_gui.f90 example: it is easy to save a pixbuf as a PNG file.

Appendix: GTK 3 -> GTK 4 changes

  • replace gtk_widget_show_all by gtk_widget_show
  • Replace gtk_container_add by gtk_window_set_child and gtk_box_append
  • Replace:
    call g_signal_connect (my_drawing_area, "draw"//c_null_char, c_funloc(draw))

by

    call gtk_drawing_area_set_draw_func(my_drawing_area, &
                   & c_funloc(my_draw_function), c_null_ptr, c_null_funptr)
  • Replace:
  function draw(widget, my_cairo_context, gdata) result(ret) bind(c)
    type(c_ptr), value, intent(in) :: widget, my_cairo_context, gdata
    integer(c_int)                 :: ret

    call gdk_cairo_set_source_pixbuf(my_cairo_context, my_pixbuf, 0d0, 0d0)
    call cairo_paint(my_cairo_context)
    ret = FALSE
  end function draw

by:

  subroutine draw(widget, my_cairo_context, width, height, gdata) bind(c)
    type(c_ptr), value, intent(in)    :: widget, my_cairo_context, gdata
    integer(c_int), value, intent(in) :: width, height

    call gdk_cairo_set_source_pixbuf(my_cairo_context, my_pixbuf, 0d0, 0d0)
    call cairo_paint(my_cairo_context)
  end subroutine draw
Clone this wiki locally