3 Tutorial example: a notepad application

This chapter describes by an example how a GUI (graphical user interface) is built using the QTk module. The creation of widgets, their geometry inside the window and their interaction with Mozart are described.


Figure 3.1: Notepad application


3.1 Geometry management

The example application consists of a window composed by

The construction of the window is completely described by a record. This record defines the widgets that compose the window, how these widgets must be placed and what is their behavior upon window resizing. The label of the record defines the widget type while the features define its parameters. Some widgets act as containers for other widgets. The two main containers are:

In this application, the toolbar is composed of button widgets. The text parameter defines the text inside the button.


Figure 3.2.


The toolbar is described by:

Toolbar=lr(button(text:"Save")
           button(text:"Load")
           button(text:"Quit"))

which means: place these three buttons respectively from left to right, each button taking the size it needs to display itself. Combining with the text widget, we obtain:

Description=td(Toolbar
               text)

which means: place the toolbar and the text respectively from top to bottom, each widget taking the size it needs to display itself.

Now we can build a window from this description by the command:

Window={QTk.build Description}

This window is hidden by default and must be explicitly shown:

{Window show}

This is already a first version of the application UI, but it still requires more work. Something that is easily noticeable by resizing the window, is that the widgets don't resize in a very clever way inside the window.


Figure 3.3.


One expects the toolbar to stick itself to the top left of the window, and the text widget to take all remaining available size below. All widgets have a glue parameter that specifies constraints to their geometry management. Valid values for the glue parameters are atoms that are combinations of n, s, w and e. By default a widget takes as much place as it needs to draw itself, and if it receives more space than needed, the widget is centered inside that space. By specifying n (resp. s, w, e) you enforce the north (resp. south, west and east) border of the widget to glue to its top (resp. down, left, right) neighbor. If you specify both ns (resp. we) you enforce both opposite border to stick to their respective neighbors, resulting in the widget taking all the vertical (resp. horizontal) space available.

Using the glue parameter we define:

Toolbar=lr(glue:nw                     % the toolbar glues itself to the top left of the window
           button(text:"Save" glue:w)  % the button glues itself to the left of the lr widget
           button(text:"Load" glue:w)  % idem
           button(text:"Quit" glue:w)) % idem
 
Description=td(Toolbar
               text(glue:nswe))

Note that the very first td or lr widget is always implicitly glue:nswe. Building and showing the window again, we obtain an application that has a complete graphical user interface, except that it is an empty shell: there is no interaction defined between the application and the user.

3.2 Interaction with the user

An action can be associated to buttons. It is executed when the user clicks the button:

button(text:"Quit" glue:w action:proc{$} {Application.exit 0} end)}

This definition makes the oz application terminate when the user clicks the Quit button. Another possibility is to just close the window using this particular parameter:

button(text:"Quit" glue:w action:toplevel#close)

Let's make procedures for the Save and Load buttons. The new definitions for these buttons are:

button(text:"Save" glue:w action:SaveText)
button(text:"Load" glue:w action:LoadText)

Where LoadText and SaveText do these:

We need a way to dynamically change or get the state of the text widget, i.e. interact with it. This is what handles are used for:

TextHandle
Description=td(Toolbar
               text(glue:nswe handle:TextHandle))

After the window is built, the (unbound) variable TextHandle is bound to an object that controls the text widget. The current text can be obtained by:

{TextHandle get($)}

and set by:

{TextHandle set(SomeValue)}

We can now write the SaveText and LoadText procedures:

proc{SaveText}
  Name={QTk.dialogbox save($)}
in 
  try 
    File={New Open.file init(name:Name flags:[write create truncate])}
    Contents={TextHandle get($)}
  in 
    {File write(vs:Contents)}
    {File close}
  catch _ then skip end 
end 
 
proc{LoadText}
  Name={QTk.dialogbox load($)}
  Contents={TextHandle get($)}
in 
  try 
    File={New Open.file init(name:Name)}
    Contents={File read(list:$ size:all)}
  in 
    {TextHandle set(Contents)}
    {File close}
  catch _ then skip end 
end

And the application is now complete.

3.3 Enhancements

Instead of using standard buttons, we might want to use a toolbar look and feel. A QTk widget exists for that purpose: tbbutton. As its interface is the same as standard buttons, we just have to change the label of this record:

Toolbar=lr(glue:we
           tbbutton(text:"Save" glue:w)
           tbbutton(text:"Load" glue:w)
           tbbutton(text:"Quit" glue:w))

If there is a lot of text, scrollbars are required for easy mouse navigation. A vertical scrollbar can be added with this parameter:

text(glue:nswe handle:TextHandle tdscrollbar:true)

Also one may prefer the white color as background color for the text:

text(glue:nswe handle:TextHandle tdscrollbar:true bg:white)

As you can see, widgets are highly configurable by parameters. Most of these parameters can be dynamically changed:

{TextHandle set(bg:white)}

Sets the background to white, at any time as long as the window containing that particular widget exists (ie is created and not yet closed). Interfaces to widgets were made as uniform as possible and similar widgets have similar parameters name and use.

3.4 The complete code

Source File

declare 
[QTk]={Module.link ["http://www.info.ucl.ac.be/people/ned/qtk/QTk.ozf"]}
 
proc{SaveText}
   Name={QTk.dialogbox save($)}
in  
   try  
      File={New Open.file init(name:Name flags:[write create truncate])}
      Contents={TextHandle get($)}
   in  
      {File write(vs:Contents)}
      {File close}
   catch _ then skip end  
end  
 
proc{LoadText}
   Name={QTk.dialogbox load($)}
in  
   try  
      File={New Open.file init(name:Name)}
      Contents={File read(list:$ size:all)}
   in  
      {TextHandle set(Contents)}
      {File close}
   catch _ then skip end  
end 
 
Toolbar=lr(glue:we
           tbbutton(text:"Save" glue:w action:SaveText)
           tbbutton(text:"Load" glue:w action:LoadText)
           tbbutton(text:"Quit" glue:w action:toplevel#close))
 
TextHandle
 
Window={QTk.build td(Toolbar
                     text(glue:nswe handle:TextHandle bg:white tdscrollbar:true))}
 
{Window show}
 


Donatien Grolaux
Version 1.3.0 (20040413)