Skip to content

Tutorial 1

vmagnin edited this page May 17, 2022 · 18 revisions

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, using around 110 lines of Fortran code. This tutorial is for GTK 4, but the modifications needed for GTK 3 are also given.

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 occur. To understand fully the GTK event-driven programming model, you may read the The Main Event Loop documentation page.
  • The main program which will declare the GtkApplication.

Let's write the skeleton of our application:

module handlers
  use, intrinsic :: 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 Fortran kinds 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.f90 file (which includes src/gtk-auto.inc). The g module is the GLib module (src/glib-auto.f90 file).

You should always add to your use statements the only: option and list only what you need, because the gtk and g modules contain thousands of functions. The compilation will be 10 to 20 times faster if you follow that advice!

The recommended way to create a GTK application is to use the GtkApplication class, which will initialize 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://docs.gtk.org/gio/type_func.Application.id_is_valid.html). 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 the default flag (see https://docs.gtk.org/gio/flags.ApplicationFlags.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 causes your program to 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 using directly the C types: for example, gint just stands for int, gdouble for double, etc.

The final 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.

Don't forget to declare in the handlers module the 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 for constants). You can consult their declarations in the src/gtkenums-auto.inc file.

Let's verify that the code compiles (we use GFortran in this tutorial but any modern Fortran compiler should work):

$ gfortran my_first_gtk_app.f90 $(pkg-config --cflags --libs gtk-4-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 our Fortran callback function. The c_null_ptr means that no data will be passed to the callback function.

Concerning the names of the modules you need, you can generally just look at the beginning of the names of the functions you use: gtk_window_new is in the gtk module, cairo_curve_to is in cairo... If you have a doubt, you can consult the src/gtk-fortran-index.csv file which lists all the GTK functions available in gtk-fortran (Fortran module, name, Fortran file, C header file, C prototype). Note that the Fortran interfaces of the functions g_signal_connect*, although belonging to GLib, are in the gtk module (they can not be parsed by our wrapper because they are defined by C macros; see the Known issues and limitations page).

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(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 from the GTK main loop) and secondly that the C pointers are passed by value (memory addresses).

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() function if you want your window to appear on screen! In GTK 4, the widgets inside a window are all shown by default. It was the contrary in GTK 3, where the gtk_widget_show_all() function should be used instead.

Don't forget to add those function names in the use gtk, only: statement of the module.

If you run your application, felicitations, you will see your first GTK window!

You can change its title using:

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

You now know you have to add that 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 complain!

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_window_set_child(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 type).

Our box is then added to the parent window as a "child". In GTK 3, gtk_container_add must be used instead of gtk_window_set_child. A GtkBox is indeed what it called a container.

Now, we define our button and add it to the box: the button is therefore contained in a box which is in the window. We connect that button 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_box_append(box, my_button)
    call g_signal_connect(my_button, "clicked"//c_null_char, c_funloc(my_button_clicked))

In GTK 3, gtk_container_add must be used instead of 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. When you see a star * in a prototype, don't bother with the type of the object. You generally 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 the gtk_window_set_default_size() function. But it would be useless as we are now going to add bigger widgets in our window.

Adding entry widgets

The objective of the next steps is to study the chaotic behaviour of the Logistic Map, a seemingly simple recurrence relation. 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, intrinsic :: 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. And 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_box_append(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_box_append(box, r_spin_button)

In fact, two widgets: a GtkLabel to show the name of the parameter and a GtkSpinButton as an entry field. The last argument of the spin button 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.

For GTK 3, don't forget to use gtk_container_add instead of gtk_box_append.

We update the my_button_clicked callback function to choose randomly x0, get r from 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 be declared as a global variable in the handlers module:

  type(c_ptr) :: r_spin_button

If x0=0 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 gtk_drawing_area_set_draw_func(my_drawing_area, &
                  & c_funloc(draw), c_null_ptr, c_null_funptr)
    call gtk_box_append(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 draw function which will be called each time the drawing area is drawn.

Following the previous code, we use the GdkPibuf library:

    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. We use the C char type which is stored in one byte. The 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 automatically emitted and the callback function called:

  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

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 is 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 3, the draw signal must be used instead (see 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 being 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 starts 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. The main loop will manage it as soon as possible.

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 our app:

  • 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 examples/mandelbrot_pixbuf.f90 example) to update the image during the computation.
  • We have not saved our image. See the examples/pixbuf_without_gui.f90 example: it is easy to save a pixbuf as a PNG file.

Appendix: GTK 4 -> GTK 3 changes

If you use GTK 3 instead of GTK 4, you should:

  • replace gtk_widget_show by gtk_widget_show_all
  • Replace gtk_window_set_child and gtk_box_append by gtk_container_add
  • Replace:
    call gtk_drawing_area_set_draw_func(my_drawing_area, &
                   & c_funloc(my_draw_function), c_null_ptr, c_null_funptr)

by

    call g_signal_connect (my_drawing_area, "draw"//c_null_char, c_funloc(draw))
  • Replace:
  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

by

  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
Clone this wiki locally