In this section we're going to build a simple, small bit of a user interface. This GUI will:
Provide a list of all spectra. This list will be udpated as new spectra are created/destroyed
Provide a list of all gates. This list too, will be updated as new gates are created/deleted or modified.
Allow a set of spectra to be selected from the spectrum list
Allow a single gate to be selected from the gate list.
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:
ttk::frame A container widget that allows us to layout children and then slap that layout as a unit into another container.
listbox A widget that contains a list of items. We're going to use this to present a list of the spectra currently defined in SpecTcl.
ttk::combobox a drop down menu widget that allows you to select one item from a list. We'll use this widget to let the user select one of the gates.
ttk::button we'll use this to allow the user to trigger the application of the gates.
tk_messageBox we'll use this to display error and confirmation messages to the user.
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 listboxvariable 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]
} $widget configure -values $gates
} ... ## # 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] \
-textvariable ::MultiApply::selectedGate return $widget
}
The description of this bit of code will skip around a bit so try not to get seasick:
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.
-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.
gates
, that only
contains the current gate names.
-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} {$l delete 0 end
foreach spectrum [spectrum -list] { $l insert end [lindex $spectrum 1]
} } ... ## # 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
# Create the UI elements and make the scroll bar work. ttk::frame $parent
listbox $parent.list -yscrollcommand [list $parent.yscroll set] \ -selectmode extended
ttk::scrollbar $parent.scroll -command [$parent.list yview] \ -orient vertical
set listbox $parent.list
# Stock the widget and arrange for it to get restocked: spectrum -trace add [list ::MultiApply::_FillSpectrumListbox $parent.list] spectrum -trace delete \
[list after idle [list ::MultiApply::_FillSpectrumListbox $parent.list]] _FillSpectrumListbox $parent.list junk
# 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.
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.
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.
-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.
-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.
MultiApply::listbox
. Note again
that since we imported MultiApply::listbox
into the proc, we don't need to fully qualify its name.
-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.
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).
-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:
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
:
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 selectedGatevariable listbox set spectrumIndices [$listbox curselection]
# Check for the error listed above:
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]
} apply $selectedGate {*}$spectrumNames
} ... ## # 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
set s [::MultiApply::SpectrumListBox $widget.s]
set g [::MultiApply::GateComboBox $widget.g] grid $s -sticky nsew
grid $g -row 0 -column 1 -sticky ew grid rowconfigure $widget 0 -weight 1 grid columnconfigure $widget 0 -weight 1
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
::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.
::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.
::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.
-text
option). The
::MultiApply::_apply
proc specified
as the -command
script so that it will
be called when the button is clicked.
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.
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.
At least one spectrum must be selected in the spectrum listbox.
A gate must have been specified (the textbox in the combobox must not be empty).
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.
spectrumNames
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: