Using DISLIN to make a GUI for a Fortran Program

If you want to provide a Fortran program with a Graphical User Interface you need to use a suitable external library, since there are no GUI-construction facilities built into the language.  This tutorial describes the DISLIN library, which is not only easy to use but also highly portable, with versions for Windows® (using the Windows API) and for GNU/Linux and other Unix-like systems (using the X11/MOTIF widget set).  The GUI-building routines form only a small part of the DISLIN package, which is mainly designed for the production of plots, charts, images, etc.  The library can be called from other languages, but this tutorial only covers Fortran.  DISLIN works well with Open Source Fortran95 compilers such as g95 or gfortran.
  
DISLIN is not an open source product but the library can be downloaded and used free of charge for non-commercial purpose (the DISLIN web-site should be consulted for licence restrictions).  Versions of the DISLIN library are available for a wide variety of Fortran compilers and operating systems.  The GUI-building routines are documented in Section 15 of the DISLIN manual, which is also available in printable form.  The software distribution includes a Fortran90 module file, dislin.f90which is also useful documentation as it contains interface definitions for each routine in the library, including whether each argument has an intent of in, out, or inout in case this is not clear from the user's manual.

There are several ways of using these library routines, depending on the amount of programming effort you want to put in and the level of user interaction you wish to support.  The following sections describe the facilities in increasing order of complexity.

1. Individual Widget Routines

DISLIN includes a set of five very simple routines each of which is completely stand-alone.  Each produces a small GUI window containing a single widget.  The routine DWGMSG simply displays a message, the others are designed for different forms of user input.  As soon as the input is provided, the window vanishes.
 
DISLIN
routine
Description
DWGMSG Outputs a string in a message box; can be multi-line using | as line divider.
DWGBUT Displays of YES and NO buttons which return 1 or 0 respectively. 
DWGTXT Displays a text box allowing input from user.
DWGLIS Selection of a single item from a list, the item number is returned.
DWGFIL File selection via the usual dialogue or by entering its name directly.

Note that if you call more than one of these routines, one window after another is presented to the user, which may be a little disconcerting. A program using all these calls is shown here purely as a demonstration.  Note: I have capitalized Fortran keywords with the idea that this makes the user-chosen symbolic names clearer, but Fortran programs are completely case-insensitive.

PROGRAM mygui1
USE dislin
IMPLICIT NONE
INTEGER :: ival, iselect
CHARACTER :: myname*30, myfile*100
!
CALL dwgmsg('Program mygui1 has started.|Second line of msg')
!

ival = 0                                   ! value is NO upon entry
CALL dwgbut('Does this look ok?', ival)
PRINT *,'Return value (0=no and 1=yes) :', ival
!
myname = ' '                               ! some initial value needed on entry
CALL dwgtxt('What is your name?', myname)
PRINT *,"User's name: ", trim(myname)
!
iselect = 2                                ! default value needed on entry
CALL dwglis('Choose level', 'Easy|Medium|Hard', iselect)
PRINT *,'Chosen level: ', iselect
!
myfile = 'mygui1.f90'                      ! initial value needed on entry
CALL dwgfil('Choose a Fortran file', myfile, '*.f90')
PRINT *,'The chosen file: ', trim(myfile)
END PROGRAM mygui1

The USE DISLIN statement in the second line is not essential, but the module it invokes contains interface declarations of all of the routines in the DISLIN package, which thus provides a valuable defence against making mistakes in the procedure calls.  To use it you need to have compiled the DISLIN interface file dislin.f90 in the current directory, or otherwise ensured that the dislin.mod file is accessible to the compiler.

The appearance of the five successive windows when using Windows-XP is show below.  On Unix/Linux there are superficial differences, but the general effect is the same.
mygui1-1 mygui1-2  mygui1-3mygui1-4
file chooser

Note that only the file selection window has a title - the rest are anonymous.  As far as I can tell, the default directory (folder) for file selection is simply the same one you last used when selecting a file.  If you clicking on a file-name the subroutine argument returns the full name including the path; if you type a name in the box but omit the path, just that name gets returned, and there seem to be no checks that this is actually the name of an existing file. 

2. Building an Multi-widget GUI

The next step in complexity is to build a window containing several widgets.  These can be arranged in a vertical row (the default), or a horizontal line, or (with a bit more effort) in several rows or columns.  The main routines producing widgets are shown in this table:

Make Widget Widget Description Get Results
WGBOX Box of toggle buttons - only one can be selected GWGBOX
WGBUT Single button with label - click to turn on or off GWGBUT
WGDLIS Dropping list - like WGLIS but saves screen space GWGLIS
WGDRAW Drawing widget for DISLIN graphics - see later section for details
WGFIL File selection widget returns filename GWGFIL
WGLAB Label which just displays character string
WGLIS List widget - select one item from those displayed GWGLIS
WGLTXT Labelled text box for input GWGTXT
WGOK Displays just an OK button - if pressed the GUI display ends, execution continues
WGQUIT Display just a QUIT button - if pressed the program terminates
WGPBUT Push button with label GWGBUT
WGPOP Pop-up menu in menu-bar: create entries with WGAPP, needs call-back routine
WGSCL Scale-bar - horizontal or vertical GWGSCL
WGSTXT Scrolled text output - can create entries with SWGTXT

The general sequence of calls is:
  1. Optional calls to SWGxxx routines to set parameters for the GUI, such as window size or title.
  2. Call WGINI to initialise the GUI.
  3. Create widgets using calls to various WGxxxx routines as listed in the table above.
  4. Call WTFIN to complete the window and display the results.  Execution continues when the user clicks on "Exit | OK" from the menu bar, or presses an "OK" button if one is provided separately.
  5. Get user responses via calls to GWGxxx routines as in the third column of the table above. 
  6. The rest of your program containing whatever you want.
Program mygui2 uses three of these routines to get from the user a day, month, and year.  After OK is pressed it converts these to an absolute day number (Modified Julian Day) and from that determines the day of the week, which it prints on the usual console.  Of course one could alternatively call DGWMSG to create a new message window for the results.

PROGRAM mygui2
USE dislin
IMPLICIT NONE
INTEGER :: ip, idscale, idlist, idbox, idok, &
           month, year, day, boxyear, mm, mjd
REAL :: realday
CHARACTER :: dayname(0:6)*9
DATA dayname / "Wednesday", "Thursday", "Friday", "Saturday", &
               "Sunday",    "Monday",   "Tuesday" /
! Set a title for the main window
CALL SWGTIT("Date to day")
! Create parent window with vertical column of widgets
CALL WGINI('VERT', ip)
! Select day of month using a slider - initial value 15 near mid-month
realday = 15.0
CALL WGSCL(ip, "Day of month", 1.0, 31.0, realday, 0, idscale)
! Select month using a drop-down list, initial value 1 for January
month = 1
CALL WGDLIS(ip, "January|February|March|April|May|June|July|August|&
                &September|October|November|December", month, idlist)
! Select year by pressing a radio button, just a small number to choose from
boxyear = 2
CALL WGBOX(ip, "2006|2007|2008|2009|2010", boxyear, idbox)
! Add an OK button for user convenience
CALL WGOK(ip, idok)
CALL WGFIN
! Now use the widget identifiers to get the values entered by the user
CALL GWGSCL(idscale, realday)
CALL GWGLIS(idlist,  month)
CALL GWGBOX(idbox,   boxyear)
day  = NINT(realday)          ! Convert real day to nearest integer
year = boxyear + 2005         ! Apply year offset as button 1 means 2006
mm   = (month-14)/12          ! mm is -1 for Jan, Feb, zero for other months
mjd  = day - 2432076 + 1461*(year + 4800 + mm)/4 + 367 * &
       (month - 2 - 12*mm) / 12 - 3*((year + 4900 + mm)/100)/4
WRITE(*,*) 'Date ', year, month, day, " is ", dayname(mod(mjd,7))
END PROGRAM mygui2


The initial appearance of this GUI under Windows is:
mygui2 output
Using a slider to get an integer number is not an ideal technique, but I wanted to demonstrate three different selection widgets. As you will see if you run this program, it has the disadvantage that the user interaction only occurs at the start.  After the user presses the OK button (or uses the Exit|OK drop-down option) the GUI disappears and only then does the main calculation begin.  It is also not possible to make one option depend upon another, e.g. so that the user simply cannot select an invalid date, such as the 31st February.  To make a GUI which is fully interactive, or which lasts while the calculation proceeds it is necessary to use call-back routines as shown in the next section.

3. A Persistent GUI using Call-back Routines

In order to make a GUI which stays active while the main computation proceeds it is necessary for program to obey an interrupt whenever a GUI event occurs, such as the the user clicking on a button or otherwise manipulating a widget.  This is not easy to arrange with a traditional program design in which the order of execution is simply that specified by the programmer.  What is needed is an event-driven program design. In this the role of the main program is to create widgets and connect each one to a user-written subroutine called a call-back routine.  Using DISLIN this connection is made by means of calls to SWGCBK.  After the widgets are all specified WGFIN is called to invoke the GUI management system, which runs in an idle loop and waits for the user to interact with a widget. Whenever this happens the linked call-back routine is called.  In such a design the call-back routines have the responsibility not just of reading the user input but, when appropriate, running the main computation.

Each call-back routine has only a single integer argument which provides it with the appropriate widget identifier.  When the call-back routine is to run the main calculation it is likely to need access to much more information than this, including results from other widgets.  The best way to do this in Fortran is to have the call-back routines in a module, and set up module variables which are accessible to all the module procedures and contain the information necessary to run the main computation. In older versions of Fortran it was the practice to use common blocks for this, but modules storage is much more, well, modular. 

Since the call-back routine is provided with the identifier of the widget which invoked it, it is possible to have one call-back routine handling more than one widget (though this is easier if they are all of the same type).  This example shows a persistent GUI running the same date calculation as before; it contains two call-back routines: changeyear when the year is altered using the set of radio buttons (produced by WGBOX), and changedm invoked when either day or month is changed, using the list widgets produced by WGDLIS or WGLIS.  The setmjd routine also has a modest error trap: if you select a date of 30 or 31 in February it calls MSGBOX to display an error message which the user has to acknowledge execution continues.  It would be easy to extend this to cover other invalid dates.

MODULE cbmod
USE dislin
IMPLICIT NONE
INTEGER :: day = 1, month = 1, year = 2007, &
           idbox, idtext, iddaylist, idmonthlist
CHARACTER :: result*20
CONTAINS
!--------------------------------------------------
SUBROUTINE changedm(id)
INTEGER, INTENT(IN) :: id
IF(id == iddaylist) THEN
   CALL GWGLIS(id, day)
ELSE
   CALL GWGLIS(id, month)
END IF
CALL setmjd
END SUBROUTINE changedm
!--------------------------------------------------
SUBROUTINE changeyear(id)
INTEGER, INTENT(IN) :: id
CHARACTER :: syear*5
INTEGER :: nbutton
CALL GWGBOX(id, nbutton)
year = nbutton + 2005     ! because button 1 is year 2006
CALL setmjd
END SUBROUTINE changeyear
!--------------------------------------------------
SUBROUTINE setmjd   
! Gets day, month, year from module variables
! Returns: result (also in module header)
INTEGER :: mm, mjd
CHARACTER :: dayname(0:6)*9, dayofweek*9
DATA dayname / "Wednesday", "Thursday", "Friday", "Saturday", &
               "Sunday",    "Monday",   "Tuesday" /
mm   = (month-14)/12          ! mm is -1 for Jan, Feb, zero for other months
mjd  = day - 2432076 + 1461*(year + 4800 + mm)/4 + 367 * &
       (month - 2 - 12*mm) / 12 - 3*((year + 4900 + mm)/100)/4
if(day > 29 .and. month == 2) CALL MSGBOX('February date error')
dayofweek = dayname(mod(mjd,7))
WRITE(result, '(I6,2X,A)') mjd, dayofweek
CALL SWGTXT(idtext, result)
END SUBROUTINE setmjd
END MODULE cbmod
!==================================================
PROGRAM guicb
USE cbmod
USE dislin
IMPLICIT NONE
INTEGER :: ip, idok, isel
CALL SWGTIT("Date to MJD and day")
! Create parent window with vertical column of widgets
CALL WGINI('VERT', ip)
CALL WGDLIS(ip, "1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21&
                &|22|23|24|25|26|27|28|29|30|31", day, iddaylist)
CALL WGLIS(ip, "January|February|March|April|May|June|July|August|&
                &September|October|November|December", month, idmonthlist)
isel = year - 2005
CALL WGBOX(ip, "2006|2007|2008|2009|2010", isel, idbox)
CALL WGSTXT(ip, 1, 1, idtext)
CALL SWGCBK(iddaylist,   changedm)
CALL SWGCBK(idmonthlist, changedm)
CALL SWGCBK(idbox,       changeyear)
CALL WGOK(ip, idok)
CALL setmjd
CALL WGFIN
END PROGRAM guicb

The initial appearance of the GUI under Windows is:
guicb.png

You will see that whenever you change the date, the month, or the year, the MJD and day-of-week are immediately recalculated and both are displayed in the text box underneath.  Note that the MJD is the Julian Date - 2,400,000.5 days.

4. Displaying Execution Progress

When you have a GUI which is visible while the program executes you might think that in the case of a long-running program it would be nice to show the how the execution is proceeding, e.g. through a progress bar.  Although the DISLIN library does not include a progress-bar widget, you can in fact use a scale-bar widget or a text box to do the same thing.  This is not perfect, but seems adequate. The next program simulates a program running for 10 seconds by using a one-second sleep ten times over, and updates the slider on the scale-bar and the text box every second.

MODULE progmod

USE dislin
IMPLICIT NONE

INTEGER :: idtext, idbut, idscal
CONTAINS
!--------------------------------------------------
SUBROUTINE runprog(id)
INTEGER, INTENT(IN) :: id
INTEGER :: j, iarray(8)
CHARACTER :: time*8
DO j = 1,10
   CALL sleep(1)    ! wait for 1 second between each update
   CALL DATE_AND_TIME(values=iarray)
   WRITE(time, "(i2.2,2(':',i2.2))") iarray(5:7)
   CALL swgtxt(idtext, time)
   CALL swgscl(idscal, 10.0*j)
   WRITE(*,'(A)') time
END DO
END SUBROUTINE runprog
END MODULE progmod
!==================================================
PROGRAM guiprogress
USE progmod

USE dislin
IMPLICIT NONE

INTEGER :: ip, ival, idquit
CALL swgtit("Show calculation progress")
CALL wgini('VERT', ip)
ival = 0
CALL wgbut(ip, 'Start', ival, idbut)
CALL swgcbk(idbut, runprog)
CALL wgscl(ip, 'Percent completed', 0.0, 100.0, 0.0, 0, idscal)
CALL wgquit(ip, idquit)
CALL wgtxt(ip, 'Time', idtext)
CALL swgcbk(idtext, runprog)
CALL wgfin
WRITE(*,*)"End of program guiprogress."
END PROGRAM guiprogress

From the code above you will see that after the Start button is pressed the program executes the call-back routine runprog which at 1-second intervals updates the slider bar position and outputs the current clock time to a text-box.  This snapshot shows the GUI when the program was 80% complete.

guiprogress.png

When executed on Windows the slider-bar is indeed altered in 10% steps, and the text box is also updated (in this example it just shows the current time). 

5. A GUI with a Drawing Widget

The next (and final) example here shows how to produce graphical output from a program within a GUI widget.  This example takes its map-making code from EX13_4 in the DISLIN manual.  The GUI here allows the user to choose a European country which causes the map to be re-drawn with that country high-lighted in red.  With a more clever design one could perhaps avoid the complete re-drawing of the country borders, but this seems so fast that the inefficiency is not an issue.

MODULE gui2mod
USE dislin
IMPLICIT NONE
INTEGER :: idraw, idlist
CONTAINS


! callback routine - called when new list item selected or PLOT button pressed

SUBROUTINE doplot(id)
INTEGER, intent(in) :: id
INTEGER :: j, ncolour, ncountry
CALL gwglis(idlist, ncountry)
CALL metafl('CONS')
CALL setxid(idraw, 'WIDGET')
CALL page(2500, 2500)
CALL metafl('CONS')
CALL disini
CALL errmod('ALL', 'OFF')
CALL erase
CALL pagera
CALL complx
CALL intax
CALL ticks(1, 'XY')
CALL frame(3)
CALL axslen(1500, 1800)
CALL name('Longitude', 'X')
CALL name('Latitude',  'Y')
CALL titlin('Conformal Conic Projection', 3)
CALL labels('MAP', 'XY')
CALL projct('CONF')
CALL grafmp(-10.0, 30.0, -10.0, 10.0, 30.0, 70.0, 30.0, 10.0)
CALL gridmp(1,1)
CALL shdeur( (/0/), (/0/), (/255/), 1)  
ncolour = indrgb(1.0, 0.0, 0.0)           ! get colour index of red colour
CALL shdeur((/ncountry/), (/16/), (/ncolour/), 1)
CALL height(50)
CALL title
CALL disfin
END SUBROUTINE doplot
END MODULE gui2mod

!-------------------------------------------------------
PROGRAM guimap

USE dislin
USE gui2mod
IMPLICIT NONE
INTEGER :: ip, idplot, idok, ival, idquit, isel=8
! ip is top level widget id
CALL swgtit('Program guimap')
CALL swgwth(-40)
CALL wgini('VERT', ip)
CALL wgquit(ip, idquit)
CALL wgdlis(ip, &
  &'Albania|Andorra|Belgium|Bulgaria|Germany|Denmark|Cyprus|Great Britain&
  &|Finland|France|Greece|Ireland|Iceland|Italy|Ex-Yugoslavia&
  &|Liechtenstein|Luxemburg|Malta|Netherlands|Northern Ireland|Norway&
  &|Austria|Poland|Portugal|Romania|Sweden|Switzerland|Spain|Ex-Czechoslovakia&
  &|Turkey|Ex-USSR|Hungary|Belarus|Bosnia|Croatia|Czech|Estonia|Latvia|Lithuania&
  &|Macedonia|Moldova|Russia|Serbia|Slovakia|Slovenia|Ukraine&
  &', isel, idlist)
CALL swgcbk(idlist, doplot)
ival = 0
CALL wgbut(ip, 'press here for initial plot', ival, idplot)
CALL swgcbk(idplot, doplot)
CALL wgdraw(ip, idraw)
CALL scrmod('REVERSE')
CALL wgfin
END PROGRAM guimap

I have provided a "press here for initial plot" button as well, as otherwise the map is only drawn if one changes the country using the drop-down list.  A QUIT button is also provided as a more convenient alternative to the "Exit | Quit" option in the left-corner of the menu-bar.  Incidentally one can also click to get focus within the drop-down list and then use the keyboard's up and down arrow keys to select one country after another, and have them each high-lighted in turn.
 
guimap.png

This actually offers another way of displaying the progress of a program, as graphical output within the draw widget is displayed in real time, and one could arrange for information to be updated at intervals during the execution.

©Clive G. Page, 2007 July 12
Made with Nvu