8.5. Tailoring the SpecTcl main window

The SpecTcl main window, the top level window named . in Tk parlance is special. If that window is destroyed, SpecTcl will exit. The sample SpecTclRC.tcl script produces a pair of vertically oriented buttons that allow you to exit the program or clear all the spectra. This GUI is composed by the $SpecTclHome/Script/gui.tcl.

In this section, we will produce our own custom replacement for this GUI.

I advocate producing Tcl GUI's by following this recipe:

  1. Determine what the GUI should do.

  2. Incrementally produce code that performs all of the GUI functions and test them independent of the GUI, where possible

  3. Create a simple, and possibly ugly GUI that invokes your functions.

  4. Create the final form GUI

Lets list the functions our sample GUI will have:

  1. Display of analysis efficiency

  2. Clear all or some list of spectra

  3. Exit SpecTcl after confirmation

This is a small set of functions but sufficient to show some typical Tcl/Tk programming techniques.

Having listed the functions of our GUI, we now need to create procedures that execute the functions and we need to test these functions.

Computing analysis efficiency. In this section we develop a script that allows us to compute the analysis efficiency. By analysis efficiency I mean the fraction of data analyzed by SpecTcl. When SpecTcl is used in the online mode, it is allowed to skip over buffers that it does not have time to process. This ensures that SpecTcl does not bottleneck data taking.

It is often important to be able to estimate actual counts in peaks or other spectrum features. To do this, we need to know how much data has not been analyzed as well as how much data has been analyzed.

SpecTcl provides two global variables that allow the fraction of data analyzed to be estimated. These are:

BuffersAnalyzed

A count of the number of event buffers analyzed.

LastSequence

The sequence number of the last event buffer seen

Since sequence numbers only count data buffers, the ratio of buffers analyzed to the last sequence number is a good estimator of the fraction of the total events analyzed. This leads us to the following proc definition:

Example 8-18. Computing the fraction of the data analyzed.


#
#   Return the percentage of data analyzed.  At most 2 decimal places
#   are kept.
# Returns:
#   Estimate of percentage of data analyzed.
#
proc analysisPercentage {} {
    global BuffersAnalyzed
    global LastSequence
    
    if {$LastSequence == 0} {
        return 0
    }
    
    return [format "%.2f" [expr {100.0*$BuffersAnalyzed/$LastSequence}]]
    
}
            

We can test this in any Tcl interpreter by manually setting specific values for BuffersAnalyzed and LastSequence. You should test for edge cases as well as a few typical cases. Edge cases include LastSequence set to zero, BuffersAnalyzed and LastSequence nonzero and equal. Values for the globals which are easy for you to compute and compare.

If you want to be serious about this sort of testing, you may want to look into the tcltest package. That package provides the ability to build up automated test suites for Tcl scripts. Using tcltest, helps you ensure that when you modify existing software you don't break features that used to work. http://www.tclscripting.com/articles/apr06/article1.html provides an introduction to tcltest.

Clearing a list of spectra. Our next proc will accept a parameter that is a list of spectrum names. It will clear the channels of each spectrum in the list.

This leads to the proc:

Example 8-19. Clearing a list of spectra


#
#   Clear a list of spectra
# Parameters:
#   spectra  - Tcl list of names of spectra to clear.
#
proc clearSpectrumList spectra {
    foreach spectrum $spectra {
        clear $spectrum
    }
}
            

This sort of proc is a bit harder to test. You can start SpecTcl with a set of spectra loaded, invoke clearSpectrumList with known sets of spectra as the parameter and check with Xamine that the correct spectra have been cleared. You can also write a proc that uses the SpecTcl chan command to verify that a spectrum has been cleared, and use that to write an automated test with tcltest.

Exiting SpecTcl with confirmation. For the final piece of functionality, we will use a built in prompting dialog called tk_messageBox. The tk_messageBox is described at http://tcl.activestate.com/man/tcl8.3/TkCmd/messageBox.htm. We will use it to prompt for exit confirmation.

Example 8-20. Exiting with a prompt


#
#  Exit the program with a confirmation prompt:
#
proc Exit {} {
    set answer [tk_messageBox -icon question  -type yesno -title "Are you sure"\
                              -message {Are you sure you want to exit?}]
    if {$answer eq "yes"} {
        exit
    }
}
            

Now that we have the functional components, lets look at the minimal code needed to drive them from a GUI. Let's start with Exit as that's the simplest. We connect it to a button thus:

Example 8-21. Connecting the Exit proc to a button


button .exit -text {Exit...} -command Exit
pack .exit
            

The analysis fraction is a bit more work. We'll produce a display of the form: xxx Buffers analyzed out of yyy zz.zz % efficient. To do this we need three things:

  1. A label widget to hold this text

  2. A procedure that is periodically called to update the lable.

  3. An initial call to schedule the update procedure.

Tcl's mechanism to schedule code to run after some time is the after command. This command is described at: http://tcl.activestate.com/man/tcl8.3/TclCmd/after.htm. Here's the code that gets all this done:

Example 8-22. Displaying analysis efficiency


#
#  Updates the analysis efficiency lable.
# Parameters:
#   label   - Label widget to modify.
#   seconds - Seconds between updates (note that after scheduls in milliseconds)
#
proc updateEfficiency {label seconds} {
    global BuffersAnalyzed
    global LastSequence
    
    after [expr $seconds*1000] [list updateEfficiency $label $seconds]
    
    $label config -text \
        [format "%d Buffers analyzed out of %d %s %% efficient" \
            $BuffersAnalyzed $LastSequence \
            [analysisPercentage]]
            
}
# 
#  Create the label with dummy text that is about the normal size.
#
label .efficiency -text {xxxxx Buffers analyzed out of yyy zz.zz efficient}
pack  .efficiency

# Start updating.

updateEfficiency .efficiency 1

            

Our final an most complicated 'simple' GUI, is the one that clears the list of spectra. To do this, we must first prompt for the set of spectra to clear. There are many ways to prompt for spectra. The simplest method, that is consistent with a GUI is to provide a dialog that allows the user to choose a set of spectra from a list box. Typically such prompter dialogs are application modal, which means that you cannot interact with any other parts of the application's GUI until you dismiss the modal dialog.

You can think of the prompting dialog box as a sort of compound widget, that consists of a listbox, with a scrollbar, and buttons for Ok and Cancel. In Tcl/Tk, compound widgets are normally referred to as megawidgets.

Tcl/Tk have several add on packages that support the creation of megawidgets. We're going to use a relatively simple package called Snit. The reference page for Snit is at: http://tcllib.sourceforge.net/doc/snit.html. The Snit FAQ provides more information about how to use Snit to do object oriented programming with Tcl as well as how to create megawidgets with Snit (http://tcllib.sourceforge.net/doc/snitfaq.html).

Let's take apart a snit megawidget that will prompt for a set of spectra from the user:

Example 8-23. Snit megawidget spectrum prompter


package require snit                   (1)

snit::widget spectrumPrompter {        (2)
    hulltype toplevel                  (3)

    variable result {}                 (4)

    constructor args {                   (5)

	# Create the megawidget subwidgets.

	listbox $win.list      -width 32   \
                               -selectmode extended \
	                       -xscrollcommand  [list $win.hscroll set] \
	                       -yscrollcommand [list $win.vscroll set]  (6)
	scrollbar $win.vscroll -command [list $win.list yview] \
                               -orient vertical
	scrollbar $win.hscroll -command [list $win.list xview] \
                               -orient horizontal

	frame  $win.actions
	button $win.actions.ok     -text Ok     \
                                   -command [mymethod onOk]
	button $win.actions.cancel -text Cancel \                  (7)
                                   -command [mymethod onCancel]

	# Use grid to layout the interface:

	grid $win.list    -row 0 -column 0 -sticky nsew
	grid $win.vscroll -row 0 -column 1 -sticky nsw              (8)
	grid $win.hscroll -row 1 -column 0 -sticky new -columnspan 2

	grid $win.actions.ok $win.actions.cancel
	grid $win.actions -row 2 -column 0 -sticky w                (9)

	$self FillListBox                                          (10)

    }
    #
    #  This is called by by the client to start the dialog modality.
    #  We create a hidden window used to synchronize the end of
    #  the modality.
    #  $win is given focus and grab
    #  we wait until $win.hidden is destroyed.
    #  The method returns the value of the result variable which
    #  is left empty by onCancel, and widget destruction but
    #  filled with the list of selected spectra by onOk
    # 
    #  On return, the user should destroy this object.
    #
    method execute {} {                           (11)

	set result [list]

	frame $win.hidden
	focus $win                               (12)
	grab  $win
	
	tkwait window $win.hidden               (13)

	grab release $win                      (14)

	if {[winfo exists $win]} {
	    return $result                     (15)
	} else { 
	    return [list]
	}
    }
    # Called when the ok button is clicked.
    # Fetch the selection into the result variable
    # and destroy the $win widget.. which will finish the
    # tkwait.
    #
    method onOk {} {                                   (16)
	set selectionIndices [$win.list curselection]
	foreach index $selectionIndices {               
	    lappend result [$win.list get $index]
	}
	destroy $win.hidden                            (17)
    }
    # Called when the cancel button is clicked.
    # Just destroy the window...leaving the result
    # unset:
    #
    method onCancel {} {                              (18)
	destroy $win.hidden
    }

    method FillListBox {} {
	foreach spectrum [spectrum -list] {           (19)
	    set name [lindex $spectrum 1]
	    $win.list insert end $name
	}
    }
}
            
(1)
snit is a package that is part of the TclLib. In order to use it in a Tcl script you must use the package require command. That command locates the scripts that make up the snit package and source them into the running script.
(2)
The snit::widget command creatse a snit megawidget. When our definition is complete, we'll be able to issue commands like:

spectrumPrompter .spprompt
                    
to create a spectrum prompter.
(3)
snit megawidgets are built by laying them out in a container widget. In snit terminology this container widget is the hull of the megawidget. By default, the hull is a frame widget. Since we want our widget to be a dialog box, we need the hull to be a toplevel widget. This command arranges for that to be the case.
(4)
Snit megawidgets definitions can be thought of as class definitions in the same sense as C++ classes. Just as C++ classes can have instance variables, so can snit megawidgets. the variable command creates, and optionally initializes an instance variable.
(5)
Continuing the analogy to C++ classes, the constructor for a snit megawidget initializes the megawidget, when it's first created. Normally, this means creating the primitive widgets that make up the megawidget, setting up the options that tie them together and laying them out in the hull.

Constructors can make use of the win variable, which holds the name of the hull widget, and the the parameters which are the remaining items on the creating command line, which, to maintain compatibility with Tk widgets are normally -option value pairs.

(6)
This chunk of code creates a listbox and horizontal and vertical scroll bars. Using -selectmode extended makes it possible to select more than one item in the listbox and allows the selected items to be discontiguous groups in the box.
(7)
Dialogs by convention have a strip of buttons at the bottom that govern how to handle the list when the user is ready to dismiss the box. These buttons are Ok which accepts the selection and dismisses the dialog, and Cancel which dismisses the dialog without taking any action.

This chunk of code creates a frame in which the Ok and Cancel buttons are made. The buttons are made within a frame so that they can be laid out along the bottom of the dialog left justified.

Later we'll see that there is another way a dialog can be dismissed that our code will need to handle.

(8)
We use the grid layout manager to position the widgets on the top level hull. grid uses a table-like layout method. The hull is divided into a table of cells of unequal size. Each widget is placed in one or more cells.

This part of the code, lays out the list box with its scroll bars. The horizontal scroll bar is laid out to fill the horizontal extent of the widget vai the -columnspan 2 option.

(9)
Both action buttons (Ok and Cancel) are placed at the left of a single cell by first gridding them into the frame and only then gridding the frame into the hull.
(10)
The built-in variable self contains the name of the executing object (kind of like the this pointer in C++. Here we call our FillListBox method to stock the list box with the names of the defined spectra.
(11)
Since snit constructors can't actually return a value, the execute method is defined to turn the megawidget into a modal dialog, allow the user to interact with the dialog, and finally, return the names of the spectra that have been selected by the user. The expected use of the dialog is something like:

spectrumPrompter .sp
set spectra [.sp execute]
destroy .sp
foreach spectrum $spectra {
   # operate on spectra
}
                    
(12)
A top level becomes modal by ensuring that events in the application GUI can only be dispatched to that top level and its children This is done via the Tk grab command. It's important to ensure that the top level has keyboard focus or keyboard events may go nowhere since it in some window managers it may be impossible for the top level to gain focus while it's holding a grab.
(13)
A dialog will typically hold the grab until some event occurs that tells it to release the grab. We can either implement an event processing loop within the execute method, or we can ask Tk to re-enter its own event loop using the tkwait command.

tkwait as we've used it enters the event loop, dispatching events until the specified window ($win.hidden) is destroyed. As we will see in onOk and onCancel, those methods destroy that window explicitly.

(14)
While the user should destroy the object when the execute method returns, we'll release the grab just in case they don't.
(15)
When tkwait exist, the method that destroyed $win.hidden will set the member variable result to the value to return to the caller. If the top level still exists, we return that member variable.

If, on the other hand the user clicks the window manager's X decoration rather than cancel, the top leve window will be destroyed as will the object that is running the execute method. In this case, $win.hidden will be destroyed only because it is a child of $win. Furthermore, since the object is being destroyed, result is no longer well defined or even defined at all. Therefore, an empty list is explicitly returned, making this equivalent to the Cancel button.

(16)
This method is called when the user clicks the Ok button. It retreives the selected list items into the result member variable.
(17)
Recall that at this time, presumably the application is hanging in the tkwait command of the execute method. The tkwait command exits when the $win.hidden frame is destroyed. This line allows the execute method to continue to execute.
(18)
This method is invoked when the Cancel button is clicked. It does not modify the result member variable, but destroys $win.hidden which triggers the exit from the tkwait in execute
(19)
Fills the list box with the set of spectra that are currently defined. This is done by analyzing the output of the spectrum -list command.

Now that we have a mechanism for prompting for a list of spectra, we can write a proc that handles our Clear... needs:

Example 8-24. Clearing selected spectra


# Prompt for and clear selected spectra:

proc clearSelectedSpectra {} {
    spectrumPrompter .sp
    set spectra [.sp execute]
    destroy .sp
    clearSpectrumList $spectra
}
            

And the means to invoke this:


button .clearselected -text {Clear...} -command clearSelectedSpectra
pack   .clearselected
        

Trying all this out gives us a strip that has the status buttons and status label laid out vertically. We can click all the buttons and check that everything works. Now let's get to the esthetics of the user interface:

  1. The status line should be at the bottom of the page.,

  2. The Exit command should probably be a menu item in a File menu.

Let's first gather together all the user interface elements and the code that lays them out:

Example 8-25. Sample GUI user interface elements and layout


                
button .exit -text {Exit...} -command Exit
pack .exit

label .efficiency -text {xxxxx Buffers analyzed out of yyy zz.zz efficient}
pack  .efficiency

button .clearselected -text {Clear...} -command clearSelectedSpectra
pack   .clearselected

            

We can move the status line to the bottom of the GUI by swapping the packing order. The menu is a bit trickier. We need to create a menubar (which is itself a menu), and a File Menu in the menu bar, which has the Exit command.

Example 8-26. Re-laying out the GUI:


                
menu .menubar
. configure -menu .menubar
menu .menubar.file
.menubar add cascade -label File -menu .menubar.file
.menubar.file add command -label {Exit...} -command Exit

button .clearselected -text {Clear...} -command clearSelectedSpectra
pack   .clearselected

label .efficiency -text {xxxxx Buffers analyzed out of yyy zz.zz efficient}
pack  .efficiency
updateEfficiency .efficiency 1