Using libgtk2 widgets from Forth

The source code for his chapter can be found in the directory Examples/GTK. Additional directories provide tutorial code, including a small editor.

GTK provides a GUI interface and graphical toolkit for many operating systems. It also includes a GUI designer program called Glade whose output can be read and used by GTK applications. This chapter is not a tutorial on GTK programming.

Several GTK+ tutorials exist, including

  http://developer.gnome.org/gtk-tutorial/

The book we used during development is Foundations of GTK+ Development, by Andrew Krause, published by APress, ISBN 978-1-59059-793-4.

The file gtkbindings.fth links to the libgtk library and allows GTK calls to be made from Forth. This provides VFX Forth with a powerful widget library that works on Windows, Linux and Mac OS X.

The file gtktools.fth provides basic functions including the main GTK message loop.

The file graphics.fth provides a graphics interface in the style of the old Borland BGI interface. This is still very useful for many applications.

The file GraphicsDemo.bld loads all needed files and provides a number of demonstrations, of which ShowGraphics3 is the most useful.

The file TextEd/editor.fth contains a simple text editor based on a tutorial by Micah Carrick.

External Linkages

The code loads required libraries and defines necessary API calls. There are two points to be noted when using this code:

: CCB:          \ #in #out "<name>" -- ; -- entry
Behaves as CallBack: but enforces a C parameter-passing convention.

: CCBproc:      \ #in #out "<name>" -- ; -- entry
Behaves as CallProc: but enforces a C parameter-passing convention.

Signal Connection

Recommended

extern: long "C" g_signal_connect_object(
  void * instance, char * detailed_signal, void * c_handler,
  void * gobject, uint connect_flags
);
extern: long "C" g_signal_connect_data(
  void * instance, char * detailed_signal, void * c_handler, void * data,
  int destroy_data, uint connect_flags
);

: g_signal_connect      \ instance detailed_signal c_handler data -- ulong
  0 0 g_signal_connect_data
;

Macros

The GTK function gtk_signal_connect_full() is the library signal connection function; the C header file defines the other connect-style functions as macros. These words create the interface as it is more usually used.

extern: unsigned int "c" gtk_signal_connect_full(
  int * object,
  char * name,
  int * func,
  int * unsupported,
  int * data,
  void * destroy_func,
  int object_signal,
  int after
);

: gtk_signal_connect \ object name func data --
Connect an event handler to a signal.

: gtk_signal_connect_after \ object name func data --
Connect an event handler to a signal, but at the end of the event chain.

: gtk_signal_connect_object   \ object name func slot_object
Connect an event handler which takes a GtkObject pointer as its argument to a signal.

: gtk_signal_connect_object_after   \ object name func slot_object
Connect an event handler which takes a GtkObject pointer as its argument to a signal, at the end of the event chain.

GTK Types

GTK operates on a number of different object types - and these are frequently checked on call (with assertion failures if the wrong type is used). These words should cause the object to be cast correctly.


\ object -- cast_object
GTK_OBJECT
GTK_CONTAINER
GTK_BOX
GTK_WIDGET
GTK_WINDOW

struct /GError  \ -- len
Definition of a GError structure.

GTK message pump

: gtk-step      \ -- flag
Iterate GTK's inner loop, process any events and exit immediately. flag is returned true if a GTK event was processed.

: gtk-step-blocking     \ -- flag
Iterate gtk's inner loop, process any events, wait until there are events if none are waiting when the function was called.

: gtkPump       \ --
Empty the GTK event queue.

Windows

: gui-Idle      \ --
Wait for message or GTK events and process it/them.

: gui-BusyIdle  \ --
If a message or GTK events are available, process them.

: gui-EmptyIdle \ --
While messages and GTK events are available, process them.

: gui-AppIdle   \ --
Process messages until the GetMessage API call returns zero in response to a WM_QUIT message. The VFX Forth variable ExitCode is set to the wParam value of this message. -- This version includes the GTK inner loop. --

: installGTKhooks       \ --
Install the GTK+ versions of the message pumps.

Linux

: gui-WaitIdle  \ --
Wait for message or GTK events and process it/them.

: gui-BusyIdle  \ --
If a message or GTK events are available, process them.

: gui-EmptyIdle \ --
While messages and GTK events are available, process them.

: installGTKhooks       \ --
Install the GTK+ versions of the message pumps.

Mac OS X

: gui-WaitIdle  \ --
Wait for message or GTK events and process it/them.

: gui-BusyIdle  \ --
If a message or GTK events are available, process them.

: gui-EmptyIdle \ --
While messages and GTK events are available, process them.

: installGTKhooks       \ --
Install the GTK+ versions of the message pumps.

Operating System Dependencies

Linux

If GTK+ and Glade are not already installed, install them using the system's package manager.

Windows

Under Windows, we use the MinGW version of GTK+. Like much Open Source software built with gcc, it uses a C calling convention both for external functions and for callbacks.

GTK+

We get our GTK + binaries from:

  http://www.gtk.org/download/win32.php

The Windows version of GTK+ used by MPE is supplied as a directory tree that you place on your PC. During development, the simplest way to work is to add the bin folder to your PC's system path. Depending on your version of Windows, this is done from Control Panel -> System -> Advanced -> Environment Variables. You may have to reboot after changing the path.

If your application is being distributed into environments controlled by an IT department, it may not be possible to alter the system path. In this case make sure that the GTK libraries and executables are in the same folder as your application.

Glade

Glade is the GU designer for GTK+. At first, we tried to install Glade to use the version of GTK+ used for application run-time. This is a mistake! Installing a separate matched Glade/GTK+ pair permits much more reliable development. MPE gets Glade and GTK+ as one package from:

  http://ftp.gnome.org/pub/GNOME/binaries/win32/glade3/

If you are feeling adventurous, go to:

  http://glade.gnome.org/

Mac OS X

We use the GTK-OSX package. You can either use the package built by MPE and supplied as gtkbins.tgz or you can work from the source package. At application run time, it is usually sufficient to use the prebuilt binaries if you use absolute paths to the GTK shared libraries. For development, install from the source package. One day, we'll find a suitable incantation to run Glade from the binary package.

Installing gtkbins.tgz

Launch a terminal, select your home folder and unpack the package:


  tar -zxvf gtkbins.tgz

Executables will then be in ~/gtkbins/bin and shared libraries in ~/gtkbins/lib. Edit gtkbindings.fth and other files such as gladetest.fth so that library references are correct.

Installing from source

During devlopment, it is convenient to use the source distribution, although it is tedious to install.

If you want to use the Glade designer and you already have MacPorts or Fink installed, you will need to create a new user and install GTK+ for that user in order to avoid conflicts.

Instructions for installing it are at:

 http://sourceforge.net/apps/trac/gtk-osx/wiki/Build

Read these instructions fully and carefully before you start. In particular read the Snow Leopard section and apply the changes for a 32 bit build before running jhbuild. Then perform the three main GTK+ builds:


$ jhbuild bootstrap
$ jhbuild build meta-gtk-osx-bootstrap
$ jhbuild build meta-gtk-osx-core

Then build the libglade library needed at runtime and the Glade designer needed for development.


jhbuild build libglade
jhbuild build glade3

Executables will then be in ~/gtk/inst/bin and shared libraries in ~/gtk/inst/lib. Edit gtkbindings.fth and other files such as gladetest.fth so that library references are correct.

In order to run Glade properly, it should be launched by


$ jhbuild shell
$ glade-3 &

This incantation is required to persuade Glade to find its icons.

Loading GTK Builder files

: zFindCallback \ zCbName -- entry true | 0
Search the default search order for the given callback name. On success, the entry point is returned.

7 0 CCBproc: BuilderConnect_cb  \ *Builder *object zSignal zHandler *ConnObj flags *User --
The main Glade signal connection callback. This is called for each signal handler specified in the Glade file, i.e. handlers named by the GUI designer. Handler names are looked up in the current context. The words found are linked to the appropriate object by way of the GTK signal connection bindings.

variable CurrBuilder    \ -- addr
Holds the current builder handle.

: loadBuilderXML        \ z$ -- builder|0
Load a Glade GtkBuilder XML file and return the pointer or zero on error. On success, the connections are made and the variable CurrBuilder is set.

: BuilderObject \ z$ -- *widget
Obtain the widget pointer for the given named widget from the current builder object.

: freeBuilder   \ --
Free the current builder.

Dialogs

: runDlg        \ dialog -- response
Given a *GtkDialog, run the dialog as a modal dialog and return the result.

: ErrorBox      \ zMessage zTitle parent --
Displays an error box with the given message, title and parent window.

: AskSaveFileNameBox    \ zaddr len parent -- flag
Given a buffer and parent window, ask for a file name. On success, the filename is returned (clipped as required) in the buffer as a zero terminated string. If no file name is given the buffer is left unchanged.

: AskOpenFileNameBox    \ zaddr len parent -- flag
Given a buffer and parent window, ask for an existing file name. On success, the filename is returned (clipped as required) in the buffer as a zero terminated string. If no file name is given the buffer is left unchanged.

Event Callbacks

The GTK library calls back into Forth on various events. Here are some sample definitions. You may need to modify them or add new ones according to the complexity of your UI. You can also use this code to test your GTK installation.

Compile the file gtkbindings.fth. Then run

 .libs  .badExterns

This will display the load addresses of the libraries and show you any unresolved external functions. A library value of zero indicates that the library has not been found. If everything is good at this point, run the test window:

  gtktest

which should display a "hello world" window.

: delete_event_fn \ *widget *event *data -- 0/1
Handles delete events. It returns zero so that the widget is destroyed.

3 1 CCB: delete_event \ -- addr ; *widget *event *data -- 0/1
Callback for the delete event. Return 0 to perform the default handling or return 1 to indicate that the callback has done everything necessary.

: destroy_fn         \ *widget data --
Handles the destroy event for the application. It calls gtk_main_quit().

2 0 CCB: destroy_event
Callback for the destroy event.

: wd_destroy    \ addr --
Destroy the widget

1 0 CCB: widget_destroy_cb      \ -- entry
Callback to handle the destroy event.

GTK startup and shutdown

0 value GTKstarted?     \ -- x
Non-zero when GTK has been started.

0 value AppFinished?    \ -- x
Non-zero when app must quit.

0 value gtk_main?       \ -- x
Non-zero if gtk_main is in use.

: noVfxGtk      \ --
Mark the VFX Forth to GTK interface as unused.

: do_gtk_init   { | temp[ cell ] -- }
Run gtk_init.

: do_gtk_main   \ --
The tools version of gtk_main.

: initGTK       \ --
Call this to initialise the GTK system. Always call this word to start the GTK system.

: GtkAppQuit    \ --
The GTK+ app should finish.

GTK test code

: hello_world_window    \ --
Launch the "hello world" window.

: gtktest       \ --
Start GTK+, launch the hello world window, and wait until it closes.

: hw            \ --
As gtktest but does not initialise GTK+ again.

Graphics in the Borland style

Global Data

struct /gwindow \ -- len
Structure to control a graphics window.

variable windows        \ -- addr
Anchors chain for keeping track of all created windows.

CELL +USER CANVAS       \ -- addr
The current drawing window.

Internal operations

: window>       \ -- window
Get the current drawing window.

: penx          \ -- addr
The current window's X coordinate for subsequent drawing commands.

: peny          \ -- addr
The current window's Y coordinate for subsequent drawing commands.

: filled?       \ -- addr
The current window's internal flag affecting drawing commands; see filled.

CREATE mycolor   0xffff0000 , 0xffff w, 0 w, 0 w,
Initial foreground color for new windows

: window-dims   \ window -- w h
Get the dimensions of a window from the window structure.

: load-pixbuf   \ zaddr -- pixbuf
Load an image into a Gtk_Pixbuf, which can be drawn to the current window using PUT.

: redraw        { window -- }
Make changes to a window's graphics visible.

: ?redraw   { window -- }
Internal - redraw a window only if it is "dirty" (affected by any drawing commands). Immediately resets the window's dirty flag, making it "clean" again

: ChainEach \ ... xt anchor -- ...
Execute xt on the contents of chain with the following structure:

  link | ...

The given xt must have the stack effect

  ... link -- ...

Where link is the address of the link field in the structure.

You can pass your own values to each link, just remember to clean up afterwards.

1 1 CCBproc: timeout_event_cb   \ 0 -- true ; -- entry
Callback run from a timer to redraw all "dirty" windows.

: +gtimer       \ --
Start the graphics event timer.

: -gtimer       \ --
Stop the graphics event timer

: winResized    \ window --
Perform this when the window has been resized.

3 0 CCBProc: GframeCallback     \ window event data -- ; -- entry
Callback to force window size to be updated and the window redrawn.

3 0 CCBProc: GExposeCallback    \ window event data -- ; -- entry
Callback to force window to be redrawn.

: filled>       \ -- flag
Fetch the filled flag

: drawdest>     \ -- pixmap gc
Fetch the 2 objects from the current window that are passed to all GDK graphics functions.

: dirty \ --
Mark the current window as dirty, which signals the GUI's internal timer to make changes to that window visible. Note that the flag is reset by redraw.

Application words

: COLOR:        \ -- ; --
Builds a new GTK color. When the color is executed, the foreground color is set. Use in the form:

 COLOR: red      0xffff0000 , 0xffff w, 0x0000 w, 0x0000 w,

The following colours are predefined:

 red     green   blue    yellow  orange  magenta
 cyan    white   black   ltgrey  grey
 dkred   dkgreen dkblue  dkyellow brown
 violet  dkcyan  dkgrey

: onto          \ window --
Set the current target window structure for graphics commands.

: pen           \ -- x y
Get the current drawing coordinates.

: at            \ x y --
Set the current drawing coordinates.

: filled        \ --
Makes the next command, such as rectangle or circle, filled instead of stroked.

: line          { destx desty -- }
Draw a line from the pen to (destx,desty).

: lineto        \ destx desty --
Draw a line from the pen to (destx,desty) and set the pen to (destx,desty).

: linerel       \ dx dy --
Draw a line relative to the pen.

: linerelto     \ dx dy --
Draw a line relative to the pen and move the pen to the end of the line

: ellipse       { width height -- }
Draw an ellipse defined by width, height. The ellipse is positioned such that the pen points to the top left corner of an imaginary rectangle around the ellipse.

: circle        \ diameter --
Draw a circle.
The circle is positioned such that the pen points to the top left corner of an imaginary square around the circle.

: rectangle     { width height -- }
Draw a rectangle.

: putpixel      \ --
Plot a single pixel.

: cleardevice   \ --
Clear the current drawing window using the current color.

: put           { pixbuf -- }
Draw a Gtk_Pixbuf to the current window at the current pen position.

: gwin:         \ <name> -- ; -- window
Declare a named graphics window. The returned window is the address of a /gwindow structure.

  gwin: MyWin    \ -- window

: setupGwin     { w h window -- }
Initialize a window control structure. This word is used to create a new window. SetupGwin cannot be used with windows defined in Glade.

: initGladeGwin \ z$name builder window --
Use the Glade widget name (usually a drawing area) in the Glade builder to set up the given /gWindow structure.

: initGwin      \ *widget gwindow --
Use the widget to set up the given /gWindow structure.

: addEvent      \ cbentry zname event window --
Add a callback to handle the name and event for a window structure.

: enable-graphics { window -- }
Enable the window for graphics operations and set the initial state.

A text editor in Glade

The original design and C code is by Micah Carrick, whose tutorial is well worth studying. It is at:

  http://www.micahcarrick.com/gtk-glade-tutorial-part-1.html

The Forth code presented here is liberally derived from that presentation and code.

To compile the text editor demo, CD to the directory containing editor.fth and then:

  include TextEdDemo.bld

To run the editor from the Forth console:

  runTextEd

The code will run unchanged on VFX Forth for Windows, Mac and Linux.

struct /TextEd  \ -- len
Everything we need to know about the editor can be derived from this structure.

0 value pTextEd \ -- addr
Holds the address of a structure for the current text editor.

#1024 constant /NameBuffer      \ -- len
Largest file name.

/NameBuffer buffer: zFilenameBuffer     \ -- zaddr
Buffer to hold current file name.

#2048 constant /StatusBuffer    \ -- len
Size of the status buffer

/StatusBuffer buffer: zStatusBuffer     \ -- zaddr
Text buffer for status bar.

Tools

: EdErrBox      \ zmessage --
Displays an error message box.

Status bar operations

: sbParams      \ -- sb context
REturn the status bar parameters

: setStatus     \ z$1 z$2 --
set the status buffer, merging the two strings. Then update the status bar.

: clearStatus   \ --
Clear the status bar.

TextViews and buffers

: modified?     \ -- flag
Return true if the current text has been modified and not saved.

: modified      \ --
Mark the current text buffer as modified.

: unmodified    \ --
Mark the current text buffer as unmodified.

: inactive      \ --
Set the current text window as inactive (unresponsive).

: active        \ --
Set the current text window as active (responsive).

: getCurrText   \ -- text
Get a copy of the current text. When you are finished with it, you must release it with g_free ( text -- ).

Loading and saving text

: CurrFilename  \ -- z$
REturn the current text file name.

: MustSave?     \ -- flag
Return true if the buffer has been modified and the user wants to save it.

: getSaveFileName       \ --
Set the current text file name for saving.

: getOpenFileName       \ --
Set the current text file name for loading.

: sbSaving...   \ --
Show status bar as saving.

: sbLoading...  \ --
Show status bar as loading.

: fileStatus    \ --
Show filename on status bar.

: loadCurrFile  \ -- *text 0 | -1
Load the contents of the current file. On success, return a pointer to the text and zero. On failure, just return -1. When finished with, the text pointer must be freed with g_free.

: loadCurrText  \ --
Load the text of the current window from the current file. No action is taken if the filename is null.

: writeCurrText \ --
Save the text of the current window to the current file. No action is taken if the filename is null.

: saveCurrText  \ --
As writeCurrText but asks for a file name if one has not been set.

: saveAsCurrText        \ --
As writeCurrText but always asks for a file name.

: checkedSave   \ --
If text has been modified and the user wants saving, write the text to a file.

: openCurrText  \ --
Open a new file.

: newCurrText   \ --
Start with an empty buffer.

Clipboard

: CurrSelection \ -- clipboard
Get the clipboard item for the current selection.

: doCut         \ --
Do the cut operation.

: doCopy        \ --
Do the copy operation.

: doPaste       \ --
Do the paste operation.

: doDelete      \ --
Do the delete operation.

Callbacks

File Menu

2 0 CCBproc: on_new_MainFileMenu_activate       \ *widget *editor --
Callback for the "New" button.

2 0 CCBproc: on_Open_MainFileMenu_activate      \ *widget *editor --
Callback for the "Open" button.

2 0 CCBproc: on_save_MainFileMenu_activate      \ *widget *editor --
Callback for the "Save" button.

2 0 CCBproc: on_SaveAs_MainFileMenu_activate    \ *widget *editor --
Callback for the "Save As" button.

2 0 CCBproc: on_MainWindow_destroy              \ *wiget *editor --
Callback for final destroy of main window.

3 1 CCBProc: on_MainWindow_delete_event         \ *widget *event *editor -- 0/1
When the window is requested to be closed, we need to check if they have unsaved work. We use this callback to prompt the user to save their work before they exit the application. From the "delete-event" signal, we can choose to effectively cancel the close based on the value we return.

2 0 CCBproc: on_quit_MainFileMenu_activate      \ *wiget *editor --
Callback for the "Quit" button.

Edit menu

2 0 CCBproc: on_Cut_MainEditMenu_activate
Callback for the "Cut" button.

2 0 CCBproc: on_Copy_MainEditMenu_activate
Callback for the "Copy" button.

2 0 CCBproc: on_Paste_MainEditMenu_activate
Callback for the "Paste" button.

2 0 CCBproc: on_Delete_MainEditMenu_activate
Callback for the "Delete" button.

Help menu

2 0 CCBproc: on_About_MainHelpMenu_activate     \ *widget *user -- ; -- entry
Callback to run the About dialog.

2 0 CCBproc: on_aboutdialog1_close      \ *widget *user -- ; -- entry
Callback when about box is closed.

Initialisation and termination

: loadTeGUI     \ --
Load the text editor's GUI. After the file has been loaded, the widgets we need to access are extracted and their object pointers saved in a /TextEd structure. The design file object is then released.

: termTextEd    \ --
free up the application data and perform termination actions.

: RunTextEd     \ --
Run the text editor.