Building graphical user-interfaces with Fortran and Tcl/Tk

Arjen Markus (29 january 2023) I have been working on an interface library for building GUIs with Tcl/Tk and Fortran. In its current state it is proof of concept, not a full-fledged library. While only some basic features have been implemented, it does show the possibilities of leveraging Tcl/Tk for this purpose.

The full code can be found at https://github.com/arjenmarkus/ftk .

Some Considerations

The philosophy of the interface is that much can be done via _evaluating_ Tcl commands, rather than calling the Tcl API directly. Of course, this means that some roundabout processing is required: building the string representing the command first in Fortran and then having it evaluated by Tcl/Tk. Still, for the purpose of setting up the GUI this is fast enough and it saves detailed interfacing between C and Fortran.

Another consideration of this interface is that much if not all can be done via Fortran directly, thanks to the standardised C-Fortran binding. That does mean that some things are a trifle difficult: Both the stubs mechanism and the lifetime management of Tcl_Obj data structures rely on macros, and Fortran can not deal with C macros. That means that, at least for the moment, the string interface must suffice.

Demo

The project contains a single demo program. It needs to be built as a library that can be loaded by Tcl/Tk's wish program. The sample build script demonstrates this.

A convenient aspect of simply loading a library is that the Fortran part does not need to initialise the interpreter.

Note: I have not paid much attention to the layout yet, so the rows are quite far apart. This has to be improved, of course, but I wanted to present this little project.

Note: The Fortran code below is not FORTRAN 77, it uses several modern features, such as the standardised C binding and polymorphic variables.

The code for the demo program is shown below:

! demo_gui.f90 --
!     Very simple GUI to show that the basics are working:
!     It presents a column of three variables, start, stop and colour,
!     as well as two buttons, one "Draw" and one "Exit" and a canvas.
!     When you press "Draw", it draws a circle sector in the canvas
!     using the given colour. Any previous sector is removed first.
!
!     The GUI is starting from the start_gui routine - the name is
!     fixed and it should appear outside any modules.
!
subroutine start_gui
    use ftk_module

    implicit none

    !
    ! Widgets we create: they must be saved!
    !
    character(len=:), allocatable, save  :: canvas, title_label, param_label, &
                                            label_1, entry_1, &
                                            label_2, entry_2, &
                                            label_3, entry_3, &
                                            button_frame, draw_button, exit_button
    type(ftk_option), dimension(3) :: option

    !
    ! Set up the window:
    !
    !    --- Text to explain the GUI ----
    !    Parameters:          +-----------------------------+
    !    Start:      [    ]   |                             |
    !    Stop:       [    ]   |                             |
    !    Colour:     [    ]   |                             |
    !                         |                             |
    !                         |                             |
    !                         |                             |
    !                         |                             |
    !                         +-----------------------------+
    !                    [Draw]      [Exit]
    !

    !
    ! Open the console, for debugging and so on
    !
    call ftcl_evaluate( "console show" )

    canvas      = ftk_create_widget( default_toplevel, ftk_canvas, "canvas" )

    option(1)   = ftk_option( "-text", "Draw a circle sector in a given colour" )
    option(2)   = ftk_option( "-text", "Parameters:" )
    title_label = ftk_create_widget( default_toplevel, ftk_label, "title", option(1:1) )
    param_label = ftk_create_widget( default_toplevel, ftk_label, "param", option(2:2) )

    option(1)   = ftk_option( "-text", "Start angle:" )
    option(2)   = ftk_option( "-text", "Stop angle:" )
    option(3)   = ftk_option( "-text", "Colour:" )
    label_1     = ftk_create_widget( default_toplevel, ftk_label, "label_1", option(1:1) )
    label_2     = ftk_create_widget( default_toplevel, ftk_label, "label_2", option(2:2) )
    label_3     = ftk_create_widget( default_toplevel, ftk_label, "label_3", option(3:3) )

    option(1)   = ftk_option( "-textvariable", "start" )
    option(2)   = ftk_option( "-textvariable", "stop" )
    option(3)   = ftk_option( "-textvariable", "colour" )
    entry_1     = ftk_create_widget( default_toplevel, ftk_entry, "entry_1", option(1:1) )
    entry_2     = ftk_create_widget( default_toplevel, ftk_entry, "entry_2", option(2:2) )
    entry_3     = ftk_create_widget( default_toplevel, ftk_entry, "entry_3", option(3:3) )

    button_frame = ftk_create_widget( default_toplevel, ftk_frame, "frame" )

    option(1)   = ftk_option( "-text", "Draw" )
    option(2)   = ftk_option( "-text", "Exit" )
    draw_button = ftk_create_widget( button_frame, ftk_button, "draw", option(1:1) )
    exit_button = ftk_create_widget( button_frame, ftk_button, "exit", option(2:2) )

    call ftk_grid_add_row( [draw_button, exit_button] )

    call ftk_grid_add_row( [title_label, ftk_add_left, ftk_add_left] )
    call ftk_grid_add_row( [param_label, ftk_add_left, canvas]       )
    call ftk_grid_add_row( [label_1, entry_1, ftk_add_top]         )
    call ftk_grid_add_row( [label_2, entry_2, ftk_add_top]         )
    call ftk_grid_add_row( [label_3, entry_3, ftk_add_top]         )
    call ftk_grid_add_row( [button_frame, ftk_add_left, ftk_add_left] )

    !
    ! Initialise
    !
    call ftcl_evaluate( "set start 60" )
    call ftcl_evaluate( "set stop 180" )
    call ftcl_evaluate( "set colour red" )

    !
    ! Add the commands
    ! TODO:
    ! Find out why we need to have a dummy first entry. It is bizarre.
    !
    call ftk_button_add_command( ".a"       , do_not_use, ""  )
    call ftk_button_add_command( draw_button, draw_sector, ""  )
    call ftk_button_add_command( exit_button, stop_program, ""  )

    ! TODO: the rest

    !
    ! SImply return, the event loop takes over
    !

contains
subroutine draw_sector( dummy )
    class(*) :: dummy

    integer                       :: start, stop
    character(len=:), allocatable :: colour
    character(len=200)            :: string

    call ftcl_getvar( "start",  start )
    call ftcl_getvar( "stop",   stop  )
    call ftcl_getvar( "colour", colour )

    if ( colour == "" ) then
        colour = "white"
    endif

    call ftcl_evaluate( canvas // " delete all" )

    write( string, * ) canvas, " create arc ", 10, 10, 210, 210, " -start ", start, " -extent ", stop-start, " -fill ", colour

    call ftcl_evaluate( string )

end subroutine draw_sector

subroutine stop_program( dummy )
    class(*) :: dummy

    stop

end subroutine stop_program

!
! I do not know why it is necessary to register an extra routine, but if
! I do it like this, the program works fine.
!
subroutine do_not_use( dummy )
    class(*) :: dummy

end subroutine do_not_use

end subroutine start_gui

You can build the library like this (just an example - you can also use the gfortran compiler and you porbably need to change the path to the Tcl library):

rem The stubs library is not useable!

ifort -c ftcl_mod_interp.f90
ifort -c ftcl_variables.f90
ifort -c ftk_module.f90
ifort demo_gui.f90 ftk_module.obj ftcl_variables.obj ftcl_mod_interp.obj -exe:ftk.dll c:\ActiveTcl\lib\tcl86t.lib -dll

Finally, to run it via wish:

# test_ftk.tcl --
#     Test the Ftk module
#
load ftk.dll

To give you an idea, here is a screenshot:

Screenshot demo-ftk


arjen - 2023-03-23 07:46:57

I have now realised this example as a standalone Fortran program. Instead of the regular Tcl/Tk libraries I use the tclkit DLL from KitCreator. It took me a bit of work to iron out a somewhat mysterious bug (which with hindsight is completely understandable), but it definitely works. Now I need to clean up the code a bit.


arjen - 2024-01-03 07:32:06

As a fun project for the dark days around Christmas (yes, this is North-European-centric, but it is really dark for me in this time of year) I picked this up again and now have the humble beginnings of a new Fortran-Tcl interface. It is not my goal (because of time limitations) to encapsulate all of Tcl's public API, but I intend to select a decent subset. For now, it is all Fortran, using the standardised C interfacing capabilities of the Fortran 2003 standard. I will need to wrap a few C macros (like Tcl_DecrRefCount) using C, but my hopes are that that is only a very limited amount of code. The nice thing about the Tclkit library is that it is a single-file distribution, which makes the distribuion of programs built with it much simpler.