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:
Determine what the GUI should do.
Incrementally produce code that performs all of the GUI functions and test them independent of the GUI, where possible
Create a simple, and possibly ugly GUI that invokes your functions.
Create the final form GUI
Lets list the functions our sample GUI will have:
Display of analysis efficiency
Clear all or some list of spectra
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:
BuffersAnalyzedA count of the number of event buffers analyzed.
LastSequenceThe 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:
A label widget to hold this text
A procedure that is periodically called to update the lable.
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 snitsnit::widget spectrumPrompter {
hulltype toplevel
variable result {}
constructor args {
# Create the megawidget subwidgets. listbox $win.list -width 32 \ -selectmode extended \ -xscrollcommand [list $win.hscroll set] \ -yscrollcommand [list $win.vscroll set]
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 \
-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
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
$self FillListBox
} # # 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 } } }

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.

spectrumPrompter .spprompt
to create a spectrum prompter.



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.

-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.

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.

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.


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.
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
}
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.
execute method returns,
we'll release the grab just in case they don't.
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
button.
execute method.
The tkwait command exits when the
$win.hidden frame is destroyed.
This line allows the execute method
to continue to execute.
execute
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:
The status line should be at the bottom of the page.,
The Exit command should probably be a menu item in a 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