-
Notifications
You must be signed in to change notification settings - Fork 43
Tutorial 1
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 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.
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
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:
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.
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:
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!
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.
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.
If you use GTK 3 instead of GTK 4, you should:
- replace
gtk_widget_show
bygtk_widget_show_all
- Replace
gtk_window_set_child
andgtk_box_append
bygtk_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
- Installation
- My first gtk-fortran application
- Drawing an image in a PNG file (without GUI)
- A program also usable without GUI
- Using Glade3 and gtkf-sketcher (GTK 3)
- Using gtk-fortran as a fpm dependency
- Debugging with GtkInspector
- Learning from examples
- Video tutorials
- How to start my own project from a gtk-fortran example
- git basics
- CMake basics
- Alternatives to CMake
- How to migrate to GTK 4
- How to contribute to gtk-fortran
- How to hack the cfwrapper