6.2. Some SpecTcl scripting examples.

In this section we're going to build a simple, small bit of a user interface. This GUI will:

  1. Provide a list of all spectra. This list will be udpated as new spectra are created/destroyed

  2. Provide a list of all gates. This list too, will be updated as new gates are created/deleted or modified.

  3. Allow a set of spectra to be selected from the spectrum list

  4. Allow a single gate to be selected from the gate list.

  5. Allow the user to apply the selected gate to the list of selected spectra.

We're going to do this the 'old fashioned way'. In a modern Tcl/Tk program, we'd use one of the object oriented extensions or the built in TclOO package to encapsulate our user interface elements. For this example we'll use raw Tcl/Tk so that we don't have to teach snit, incrTk or TclOO or any other Tcl/Tk extension.

Along the way we're going to introduce the following widgets:

This toy UI will be built into a single top level frame that's passed in to the creation proc. The widgets of the UI will be pasted intot the top level frame using the grid geometry manager. It will be up to the client to grid, pack or place the containing frame it passed us into the appropriate place of the larger user interface. We'll give an example of a fragment from SpecTclRC.tcl that we can use to create the UI and put it in the top level GUI (button bar) produced by the default SpecTclRC.tcl

This toy UI only shows a small fraction of the widgets and capabilities of Tk. See: http://www.tcl.tk/man/tcl8.6/TkCmd/contents.htm for a list of the Tk commands and links to associated reference material.

Let's start off small. We're going to use a ttk::combobox to allow the user to select a gate. But how to we ensure the drop down menu of the combo box is up-to-date? ttk::combobox has an option -postcommand that allows us to attach a script to the event that pops up the menu. The script runs before the menu is generated and, therefore can define the contents of the menu.

Have a look at the code below (elisis means that unrelated code has been omitted):

Example 6-1. A gate selection combobox


namespace eval ::MultiApply {
    variable listbox                    (1)
    variable selectedGate ""

}
...
##
# _FillGateList
#
#    Fills ::MultiApply::gates with the list of gaten names.
#
proc ::MultiApply::_FillGateList {widget} {
    set gates [list]
    foreach gate [gate -list] {
        lappend gates [lindex $gate 0]             (2)
    }
    $widget configure -values $gates               (3)
}
...
##
# GateComboBox
#   Create a combobox.  When posted, the combox box list will contain
#   The current list of gates.
#
# @param widget - name of the widget to create (this is not compound).
# @return widget - The widget requested.
#
proc ::MultiApply::GateComboBox {widget} {

    ttk::combobox $widget -values [list]  \
        -postcommand [list ::MultiApply::_FillGateList $widget] \  (4)
        -textvariable ::MultiApply::selectedGate


    return $widget                           (5)
}

                

The description of this bit of code will skip around a bit so try not to get seasick:

(1)
This block of code creates a new Tcl namespace and puts two variables in it. Namespaces are a mechanism to encapsulate the names (procs and variables) of a package so as to minimize the chances some other package will generate name conflicts.

Namespaces can be nested. Fully qualified Tcl names use the :: characters to separate the path to the variable or proc. Thus we created two variables whose fully qualified names are ::MultiApply::listbox and ::MultiApply::selectedGate.

procs can also live in namespaces. Attempts to locate commands for procs in a namespace are first searched in the proc's namespace. These variables will be used to hold the widget name of the spectrum list box and the gate currently selected by the gate combobox respectively.

(4)
This code creates the combobox from which the user will select a gate. -value provides the current set of values in he combobox. This is empty because -postcommand is used to supply a script that will stock the values at the time the combox list is pulled down. This ensures that the combobox always has the current set of gate names when it's list of values is pulled down.

-textvariable specifies a variable in which the combobox will maintain the current value of the combobox. This is either something the user types in or something picked from the list that can be pulled down in the combobox. Note that this variable is in the MultiApply namespace.

Note that the proc itself is also in the MultiApply namespace.

(5)
In order to make the ::MultiApply::GateComboBox look, as much as possible like a Tk widget creating command, the widget passed in is returned to the caller.
(2)
Recall that the ::MultiApply::_FillGateList is the sxcript run when the drop down is about to become visible. This loop iterates through the set of gate definitions making a list, gates, that only contains the current gate names.
(3)
Configuring the combobox's -values option set the list of values (gate names) that will be displayed in the pull down. Note that gate -list is documented to return the list of gates in alphabetial order of gate name.

Next let's look at how the spectrum selector is created and maintained. In this case we need a widget that allows more than one spectrum to be selected. The Tk listbox widget does that. We'll also need a mechanism to maintain the contents of the list box as spectra are added and, potentially deleted.

The mechanism we have for maintaining the spectrum list is the -trace option in the spectrum command. This option allows a script to be called when spectra are added or deleted from SpecTcl's spectrum dictionary.

Let's look at the code.

Example 6-2. Spectrum selection listbox


...
##
# _FillSpectrumListbox
#
#   - Delete all entries in the list box.
#   - Fill the list box with names of the spectra.
#
# @param l - list box.
# @param n - Name of spectrum added or deleted.
#
proc ::MultiApply::_FillSpectrumListbox {l name} {  (1)
    $l delete 0 end                                 (2)
    foreach spectrum [spectrum -list] {
        $l insert end [lindex $spectrum 1]          (3)
    }
}

...
##
# SpectrumListBox
#
# Create a list box and an associated ttk::scrollbar.
# The list box is then stocked with the existing
# spectra.  The spectrum -trace command is used
# to restock the list box when the set of spectra change
# either through creation or deletion.
#
#
# @param parent -  Name of desired widget
# @return parent.
proc ::MultiApply::SpectrumListBox {parent} {
    variable listbox                               (4)

    # Create the UI elements and make the scroll bar work.

    ttk::frame $parent                               (5)
    listbox    $parent.list  -yscrollcommand [list $parent.yscroll set] \
        -selectmode extended                         (6)
    ttk::scrollbar  $parent.scroll -command [$parent.list yview] \
        -orient vertical                             (7)

    set listbox $parent.list                         (8)

    # Stock the widget and arrange for it to get restocked:

    
    spectrum -trace add [list ::MultiApply::_FillSpectrumListbox $parent.list]
    spectrum -trace delete \                        (9)
        [list after idle [list ::MultiApply::_FillSpectrumListbox $parent.list]]
    _FillSpectrumListbox $parent.list junk          (10)
    
    
    # Layout the widgets so that they also scale nicely and return $parent

    grid $parent.list $parent.scroll -sticky nsew   (11)
    grid rowconfigure $parent 0 -weight 1;          
    grid columnconfigure $parent 0 -weight 1;       (12)
    grid columnconfigure $parent 1 -weight 0;       

    return $parent
}

                

As before we're not going to cover this code in the order in which it appears. We'll start by explaining ::MultiApply::SpectrumListBox which creates the list box that will display and allow users to select the set of spectra a selected gate will be applied to.

(4)
Recall that in the previous example we created a namespace, MultiApply and that one of the variables we created in it was named listbox. When the variable command is used within a proc that lives in a namespace, as this one does, the variable in the same namespace is made available to the proc as if it were local. This can also be used instead of the older global command.

The listbox variable is going to hold the full path to the listbox widget that we are creating. This is neede in order to make it available to the proc that will later handle the actual application.

(5)
The number of spectra defined can be rather large. Therefore it's important to allow the user to access all spectrum names even if there are more than can fit in the visible area of the listbox. A scrollbar is the natural way to do this in GUIs. Therefore we're gong to build two widgets, a listbox and a vertical scroll bar.

The simplest way to ensure that we can layout these widgets as we want to is to first build a frame and then build and layout the widgets in that frame. A frame is just a container for other widgets. Frames can have their own geometry management independent of the geometry management of the rest of the application.

By passing a frame back to the caller, the application can then put the user interface element we create anywhere it wants as a unit without knowing anything about its internal construction. ttk::frame creates a frame that should have a natural appearance in the platform in which the UI is running.

(6)
This creates the list box that will hold the spectrum. Listboxes have several selection models. The -selectmode option sets the desired one. In this case extended provides the most natural selection model for a box that can have multiple items selected.

The -yscrollcommand option may seem a bit unsual. Scroll bars and scrollable widgets require a round trip interaction. Just as the scrollbar can tell its target widget what should be visible, the target widget must tell the scroll bar how it should appear (where the elevator should be in the groove and how big it should be).

The -yscrollcommand provides a script that is executed when the listbox changes what is visible. Note that the script references a scrollbar, $parent.scroll that we've not created yet. The -yscrollcommand when it is executed will append two fractions that are the fraction of the list at the top of its display and the fraction that are at the bottom.

(7)
This line creates a vertical scrollbar. Two things make this scrollbar vertical. First the -orient option means that the scrollbar will be drawn up and down rather than side to side. Second the -command script asks the target widget to execute the yview (instead of xview).

The -command option provides a script that's called in response to scrollbar manipulations. The listbox's yview command controls what set of list elements are visible in the listbox.

Thus the listbox can control what the scrollbar looks like since its -yscrollcommand executes the scrollbar's set command. Conversely the scrollbar can control which list elements the listbox displays because its -command script invokes the listbox's yview command.

If we wanted to provide an X scrollbar we'd need it to invoke the listbox's xview command and to add a -xscrollcommand that would invoke the x scrollbar's set command.

(8)
Once the list box is created its widget command is saved int MultiApply::listbox. Note again that since we imported MultiApply::listbox into the proc, we don't need to fully qualify its name.
(9)
In SpecTcl spectra can be created and deleted during a run. We want our spectrum list to be maintained when the list of spectra is modified as a result of creation and deletion. The SpecTcl -trace option on the Spectrum command allows us to associate a script with the creation or deletion of a spectrum. The script is called with the spectrum affected passed in. The script is called while the spectrum exists. This means after creation is complete and before deletion is actually performed.

In our case, we just recreate the list box after each creation and deletion for simplicity rather than attempting to figure out what the creation or deletion means in terms of updating the contents of the listbox in place. The proc ::MultiApply::_FillSpectrumListbox accepts the list box as a parameter and the trace will add to that the spectrum name as a parameter, which we'll ignore.

The processing the deletion in this manner is problematic, since the spectrum still exists at the time the trace fires. We have two choices. The first is to have a different proc which looks at the spectrum name, finds it in the listbox and removes it. The second is to defer repopulation of the listbox until the spectrum has been deleted.

The after idle command executes the script that follows when the application event loop next has nothing to do. SpecTcl won't return to the event loop until the spectrum is completely deleted so using after idle to schedule execution of ::MultiApply::_FillSpectrumListbox from the event loop ensures that when it's called the deleted spectrum is no longer in the SpecTcl spectrum dictionary.

(10)
Since the trace proc ignores the spectrum parameter we can use it directly to load the initial set of spectra into the list box. We just need to pass some dummy spectrum name so that Tcl sees that we provided the right number of parameters.

We could also have used the args method in ::MultiApply::_FillSpectrumListbox so that we don't even have to provide that. See The proc manpage for more information on variable length parameter lists and defaulted parameters (which are another way to go).

(11)
Since the scrollbar is a vertical scrollbar, grid is used to put the listbox and the scrollbar side by side. The -sticky option is used to ensure the widgets fully fill the cells they've been placed in. If there were more rows of widgets that might horizontally enlarge the scrollbar cell, we might want to lay them out separately so that the scrollbar could come free on the left (east) side:


grid $parent.list -row 0 -column 0 -sticky nsew;
grid $parent.scroll -row 0 -column 1 -sticky nsw
                        

(12)
Making widgets resize appropriately in complex user interfaces can be challenging. Fortunately this user interface isn't complex. We want both the listbox and the scrollbar to be able to vertically resize and only the listbox should be able to take advantage of additional horizontal space.

The grid command has this concept of row (vertical scaling) and column (horizontal scaling) weights. Weights allow you to specify the proportion of extra space each widget will get, or lose in a resize operation. See The grid manpage for more about this.

By setting the row weight to 1 we allow the widgets in the row to resize vertically. By setting the weight of column 0 to 1 and column 1 to 0, we allow the widget in column 0 (listbox) to resize horizontally while fixing the horizontal size of the widget in column 1 (the scrollbar).

Finally lets look at the implementation of ::MultiApply::_FillSpectrumListbox:

(1)
Note that in addition to the listbox widget, the proc is passed a spectrum name. This name is appended to the command specified by the spectrum -trace command when the trace is fired. In fact we ignore this parameter.
(2)
Since we're regenerating the entire contents of the listbox, the first step is to delete all items from the listbox.
(3)
This loop extracts the name from each spectrum description and adds it in the last position of the listbox. The spectrum -list command is documented to return the spectra listed in alphabetical order by spectrum name. Therefore the listbox will also be alphabetical by spectrum name.

We pretty much have everything we need. Most of the hard work is done. All we need to do is layou the spectrum chooser and gate chooser side by side and put a button underneath all that. We also need to write code for the button to execute when clicked:

Example 6-3. Multiselect final layout and logic


...

##
# _apply
#
#   Called to apply the gates:
#   - There must be at least one spectrum selected.
#   - A gate must be selected.
#   - Since the user can type into a combo box, the gate
#     must exist.
#   - Apply the selected gate to all selected spectra.
#
# @note ::MultiApply::listbox is the widget tha has the spectrum list.
# @note ::MultiApply::selectedGate has the name of the selected gate.
#
proc ::MultiApply::_apply {}  {
    variable selectedGate                     (1)
    variable listbox


    set spectrumIndices [$listbox curselection]  (2)

    # Check for the error listed above:          (3)

    if {[llength $spectrumIndices] == 0} {
        tk_messageBox -icon error -type ok -title "Need Spectrum" \
            -message {At least one spectrum must be selected}
        return
    }

    if {$selectedGate eq ""} {
        tk_messageBox -icon error -type ok -title "Need Gate" \
            -message {A gate must have beenselected}
        return
    }

    if {[llength [gate -list $selectedGate]] == 0} {
        tk_messageBox -icon error -type ok -title "Invalid Gate" \
            -message "There isn't a gate named $selectedGate"
        return
    }

    #  Build up a list of spectrum names:

    foreach item $spectrumIndices {
        lappend spectrumNames [$listbox get $item]     (4)
    }

    apply $selectedGate {*}$spectrumNames             (5)
}

...
##
# Selectors
#   Create a frame that has the spectrum selection on the left and
#   the gate combobox on the right.  gridding is done so that the
#   spectrum selector is the only one that can expand.
#
#  @param widget -widget to hold this (ttk::frame we create).
#  @return widget.
#
proc ::MultiApply::Selectors {widget} {
    ttk::frame $widget                              (6)

    set s [::MultiApply::SpectrumListBox $widget.s] (7)
    set g [::MultiApply::GateComboBox    $widget.g]

    grid $s   -sticky nsew                          (8)
    grid $g  -row 0 -column 1 -sticky ew

    grid rowconfigure    $widget 0 -weight 1
    grid columnconfigure $widget 0 -weight 1       (9)
    grid columnconfigure $widget 1 -weight 0

    return $widget
}


##
# MultiApply
#   Makes the full UI:
#     Selectors on top and an Apply Gate button on the bottom.
#     All that in a frame.
#
# @param widget - Top of the widget hierachy created ttk::frame.
# @return widget
#
proc ::MultiApply::MultiApply widget {
    ttk::frame $widget                                 (10)
    ::MultiApply::Selectors $widget.selectors          (11)
    ttk::button $widget.apply -text {Apply Gate} \
        -command [list ::MultiApply::_apply]           (12)

    grid $widget.selectors -sticky nsew                (13)
    grid $widget.apply     -sticky ns

    grid columnconfigure $widgxet 0 -weight 1
    grid rowconfigure    $widget 0 -weight 1          (14)
    grid rowconfigure    $widget 1 -weight 0

    return $widget
}

                

Let's start with the two procs that glue together all the widgets and then conclude with the proc called to do the actual application.

(6)
::MultiApply::Selectors creates a compound widget that consists of both the spectrum chooser/scrollbar bundle and the gate selection combobox.

These two objects will be placed in a frame which is given the name passed in as a parameter and returned if the proc executes successfully.

(7)
The two selectors get created as children of the frame. While not always necessary, in general Tk is better behaved if widgets that live an a container are descendents in the widget tree of the container.
(8)
The layout places the gate selector to the right of the spectrum selector and, while the gate selector is stuck to all four sides of its cell (the better to expand in x/y),
(9)
By now hopefully you have some familiarity with column and row weights. The weights given here allow the widgets to expand in Y and the listbox only to expand in X. Since the gate chooser is unstuck from the top and bottom it's not going to expand in Y but stay vertically centered.
(10)
::MultiApply::MultiApply creates the entire user interface. A frame is used to place the composite widgets. The frame is named as requested by the widget parameter, which is also returned to the caller.
(11)
The frame that contains both of the selectors is created.
(12)
A button is created and labeled Apply Gate (-text option). The ::MultiApply::_apply proc specified as the -command script so that it will be called when the button is clicked.
(13)
This gridding places the selectors above the button. The button floats into the middle of the horizontal extent but is stuck to the top and bottom.
(14)
The row and column weights are set so that both rows can expand in X but only the top row can expand in Y. Since the button is unstuck from the X edges, it won't expand at all.

Let's look at ::MultiApply::_apply which executes when the button is clicked. This function has the actual logic. As is not uncommon, the bulk of body of this proc is concerned with error checking, reporting and handling.

(1)
These two lines import the namespace scoped variables. Recall that selectedGate contains the name of the gate that's been selected. More precisely, it contains the value of the text field of the gate selection combo box. listbox contains the name/command of the Spectrum selection listbox.
(2)
The curselection command of the listbox widget returns a (possibly empty) list of the indices of the items selected in the listbox.
(3)
The long section of code below checks for and reports, via a pop up dialog, the following error conditions:

  1. At least one spectrum must be selected in the spectrum listbox.

  2. A gate must have been specified (the textbox in the combobox must not be empty).

  3. The text in the combobox must be a gate that exists. Note that since the value of that textbox can be typed in, it is possible to try to apply a non-existent gate. The same is not true for spectra as the only spectra that can be chosen are those in the spectrum listbox, which shows exactly the set of spectra that are currently defined.

(4)
This loop fetches the names of the selected spectra and puts them in the list spectrumNames
(5)
Applies the gate. There's a bit of Tcl syntax that we've not talked about in play here. The {*} or argument expansion operator (called by some the Polyphemus operator because it looks like a single eye in a head), takes a single command word that is a valid Tcl list and turns it into one command word per list item.

Thus if there are two spectra selected; spec1 and spec2, The command:


apply somegate {*}{spec1 spec2}                       
                    

Becomes:


apply somegate spec1 spec2
                    

The apply command does not accept a list of spectra as a single parameter but the latter form where each spectrum name is a single parameter.

This completes the user interface. If all of this code is in a file named multiapply.tcl you can add the following commnds to SpecTclRC.tcl to add it to the buttonbox (which may need to) be strecthed:


source mulitapply.tcl
MultiApply::MultiApply .mapply
pack .mapply -fill both -expand 1