![]() |
Tcl/Tk GUI Programming
|
| Course content | Enter chat room | Send email
to mailing list |
Check calendar |
In memory of Linda. Worldwide Cancer
sites here.
Canadian Cancer donations here.
In this lesson we are going to continue to work with the menu widget. In particular you are going to learn how to disable (dim out) and reenable menu items from within your program.
We are going to introduce our first text animiation concepts. We are going to apply these to building a ticker tape text area.
Since we gave you a break from homework after the last lesson there
is no solution to the exercises to present.
| HINT:
Some of you have no doubt discovered that although Tcl/Tk is a friendly enough programming language it does have its share of quirks. For some reason this lesson brings those out more than others. Recall from a much earlier lesson that all Tcl/Tk statements are built
up from a
WHEN IN DOUBT ADD A SPACE. It is not always easy to pickup where they are on the webpage. |
Step 0: preparation for lesson#7 |
Some of you may have noticed that programmers are inherently a lazy bunch. We generally try not to have to retype things if we can pinch something close enough from somewhere else and modify it to suit. Let's do just that for lesson7.tcl ... lesson6.tcl is close enough to save us some typing so let's make a copy of lesson6.tcl into our lesson7 folder and rename it as lesson7.tcl.
Using your editor modify the main area of the program as follows.
#======================================== # explicitly setup our main window #======================================== wm geometry . 400x75+10+10 wm title . "lesson 7" |
This will make our wish window a little wider and squatter in anticipation
that our ticker will be only needing that type of space.
Step 1: adding the ticker area |
In the same main area of the code modify the text widget lines to look
as follows:
#======================================== # add the ticker area as a text widget #======================================== text $f.t \ -bd 2 \ -bg white \ -height 1 \ -relief ridge \ -wrap none \ -state disabled pack $f.t -side bottom pack $f -side top -expand true -fill both |
Notice that we have reintroduced the "statement continues on the next line" character ... the backslash "\". This is a convenient way to display long statement lines in your code listing to enhance readability.
Notice also that we have added some additional attributes to our new text widget.
-relief ridge gives our text area a raised border
effect
-wrap none will be necessary when we come to have
our text drop off the screen to the left
-state disabled this prevents anyone from typing
into this text area
Next we need to remove the statements which attempt to add text to the
text widget.
#========================== # add a line of text #========================== $f.t insert end "abc\n" tag0 $f.t insert end "def" tag1 |
Run your program with these changes and you should see the new look
for our text widget.
Step 2: the Menus |
Using your editor modify the menu section to look like that below:
#======================================== # create a pull down menu with a label #======================================== set File [menu .menubar.mFile] .menubar add cascade -label File -menu .menubar.mFile #======================================== # add the Open menu item #======================================== $File add command -label Open -command openFile #======================================== # add the menu item #======================================== $File add command -label Quit -command exit #======================================== # create a Ticker pull down menu #======================================== set Ticker [menu .menubar.mTicker] .menubar add cascade -label Ticker -menu .menubar.mTicker #======================================== # add the Start menu item #======================================== $Ticker add command -label Start -command toggleTicker -state disabled |
Rerun your program now.
| HINT:
Recall in a previous lesson we introduced the concept of a programming cycle: change it, save it, run it, look at it ... repeat until satisfied. |
Notice that we removed the Save menu item from the File menu. We added a new Ticker menu with a "dimmed out" Start item. ie. the text shows up "dimmed" and the mouse will not hilite it in any way.
In the next section we are going to see how to manipulate this dimmed
menu item from inside our program.
Step 3: manipulating the Ticker menu |
Notice in the code snip above that we introduced a new - as yet to be written - command called toggleTicker.
Before we demonstrate how to do this, let me explain what we want our toggleTicker procedure to do.
When this program is completed we will want to be able to load some text into our Ticker tape and then Start it "ticking". We will want that to happen via this Start menu item. However, when we do that we want that menu item to change it's label to read Stop. So that the next time we select the Ticker menu we can Stop our ticker tape from running. When we click stop we want our label to go back to Start.
Start/Stop/Start/Stop ... in other words we want our menu item to behave like a toggle switch with two positions. Each time we press it , it reverts to the other position.
Using your text editor once again let's create a procedure called toggleTicker
and place it at the beginning of our program.
#==================================
# toggleTicker - entry point
# Note: spaces are very important on a proc line
#==================================
proc toggleTicker { } {
set fn "toggleTicker"
puts stdout [format "%s:ding" $fn]
puts stdout [format "%s:done" $fn]
} ;#end toggleTicker
|
Let's run our program and see what happens.
Oops !!! Our procedure can never run because we have disabled the menu item.
Let's temporarily change that until we get our little procedure more "fleshed out". This is a great programming technique called "commenting out".
Using your text editor once again locate the menu line below and make
the following changes
# TEMPORARILY COMMENTED OUT FOR TESTING #$Ticker add command -label Start -command toggleTicker -state disabled # FOR TESTING ONLY $Ticker add command -label Start -command toggleTicker |
Notice how we used the "#" to temporarily disable the original statement. We then duplicated it and reenabled it without the -state disabled parameter set.
Now when you run your program and select the Start menu item the toggleTicker messages should be appearing on your wish console window.
While we are at it let's temporarily disable the Open menu option
which we are not using yet.
# TEMPORARILY COMMENTED OUT FOR TESTING #$File add command -label Open -command openFile # FOR TESTING ONLY $File add command -label Open -command openFile -state disabled |
| REMEMBER:
This programming stuff may not come easily at first. Not to worry. If you are having difficulties don't hesitate to contact the mailing list or use the chat room resources for assistance. At this beginner level there is no such thing as a stupid question to ask. |
Let us see if we can now make our toggleTicker actually do some toggling.
The first thing we are going to need is a variable which is going to change value alongside the toggle. This is how our program is going to keep track of what state the toggle is current set at.
Just below the Ticker menu stuff at the end of our program listing let's
add a section for initialization of some variables. These
variables are sometimes called "global variables" because they are going
to be used in more than one procedure
#===================================== # initialize some global variables #===================================== set tickerState "stopped" |
Here we are going to initialize our tickerState to "stopped". In other words the ticker tape is not "running" ... which incidently will be the other state that this variable will take on.
Now let's enhance out toggleTicker procedure to accept this global variable
and print it out at the incoming point and just before it returns.
#==================================
# toggleTicker - entry point
#==================================
proc toggleTicker { } {
set fn "toggleTicker"
global tickerState
puts stdout [format "%s:ding tickerState=<%s>" $fn $tickerState]
puts stdout [format "%s:done tickerState=<%s>" $fn $tickerState]
} ;#end toggleTicker
|
It should now say "stopped" at both the "ding" and "done" points.
Let's add some toggle logic. This will be very similar to the stoplight logic that you used in a previous lesson.
So if you want to stop here and try it on your own ... DON'T PEEK AT
THE SOLUTION BELOW.
#==================================
# toggleTicker - entry point
#==================================
proc toggleTicker { } {
set fn "toggleTicker"
global tickerState
puts stdout [format "%s:ding tickerState=<%s>" $fn $tickerState]
if { $tickerState == "running" } {
set tickerState "stopped"
puts stdout [format "ticker is now stopped"]
} else {
set tickerState "running"
puts stdout [format "ticker is now running"]
} ;# end if
puts stdout [format "%s:done toggleState=<%s>" $fn $toggleState]
} ;#end toggleTicker
|
When this program is run repeated selection of the Ticker/Start menu item produces the desired toggle effect on the messages appearing in the Wish console window.
What we want to achieve as well is a toggling of the menu label from Start to Stop as well.
For this we need to introduce a menu configuration statement into each
branch of the if/else as shown below.
global tickerState
global Ticker
puts stdout [format "%s:ding tickerState=<%s>" $fn $tickerState]
if { $tickerState == "running" } {
set tickerState "stopped"
$Ticker entryconfigure Stop -label Start
puts stdout [format "ticker is now stopped"]
} else {
set tickerState "running"
$Ticker entryconfigure Start -label Stop
puts stdout [format "ticker is now running"]
} ;# end if
|
When this program is run you should see the Ticker menu toggling between Start and Stop in a consistent context with the state message showing on the console.
We are now ready for the Ticker tape portion.
Step 4: the Ticker tape |
Remember text widget we created on the bottom of our window. We are going to work on turning that area into a scrolling text area commonly called a ticker tape.
For this we are going to create another new procedure at the top of
our program listing called "ticker" as follows:
#==========================
# ticker - entry point
#==========================
proc ticker { t } {
set fn "ticker"
global text_messages
global maxMessages
puts stdout [format "%s:ding" $fn ]
puts stdout [format "%s:done" $fn ]
} ;#end ticker
|
In the main area at the very bottom of the program let's add some temporary
lines to call our ticker procedure.
#========================== # THIS IS TEMPORARY # TO TEST ticker PROCEDURE #========================== ticker $f.t |
If all is well when you run this program you should see the
ticker:dingmessage pair appear in the Wish console window. GOOD. You have your tickle procedure at least being called.
ticker:done
Let's begin to add some meat to this procedure to have it do something useful. In particular we want to have it put our ticker message into the text area at the bottom of our window.
Notice we added some global variables (text_messages and maxMessages)
to this procedure. We are now going to make use of them:
#==========================
# ticker - entry point
#==========================
proc ticker { t } {
set fn "ticker"
global text_messages
global maxMessages
puts stdout [format "%s:ding" $fn ]
set text_messages(0) Zero
set maxMessages 1
for { set j 0 } { $j < $maxMessages} { incr j } {
puts stdout [ format "array(%d)=<%s>" $j $text_messages($j) ]
} ;#end for
puts stdout [format "%s:done" $fn ]
;#end ticker
|
Run it and then we'll explain.
We have introduced a completely new Tcl/Tk construct in this section called an array.
The lines in question are:
set text_messages(0) Zero puts stdout [ format "array(%d)=<%s>" $j $text_messages($j) ] |
Arrays in any programming language are ways to collect a list of items under a single variable name. It would be like creating a grocery list and then labeling each item on that list as:
grocery1The array is a variable with two parts. The name and the index. Tcl/Tk is rather unique as a language because the array index can be anything at all. Most computer languages restrict array indices to being numeric values. We won't be exploiting Tcl/Tk's special capabilities and our array indices will all be numeric.
grocery2
grocery3
etc.
The first of the lines above illustrates how we declare an array variable in Tcl/Tk. You simply include the "(index)" part and it is done! In other words we have told the Wish program that text_messages is an array and the 1st item is to be set to Zero.
Wait a minute !!! The first item?
That's right if you recall we encountered a similar "starting from 0" counting scheme in our previous lesson on loops. Don't worry about it ... it is common for computer programming language designers to want to start counting from 0,1,2 etc.
The second of our lines above illustrates how we can extract the contents of our new array variable.
$text_messages($j)where our index in this case is now a variable itself ($j).
This ability forms the basis of the power of arrays as a programming construct. The ability to index into them with variables as indices.
Let's try to illustrate by adding some more items to our little array.
set text_messages(0) Zero set text_messages(1) One set text_messages(2) Two set text_messages(3) Three # maxMessages is always set to the next available array index set maxMessages 4 |
Arrays are a very useful programming construct indeed.
We now need to get our array contents to show up in our ticker area.
for { set j 0 } { $j < $maxMessages } { incr j } {
puts stdout [ format "array(%d)=<%s>" $j $text_messages($j) ]
$t configure -state normal
$t insert end $text_messages($j) tag$j
$t configure -state disabled
} ;#end for
|
Notice the "t" variable that we passed into this procedure is actually the set the to "$f.t" variable which is how we represented our ticker text widget. In addition we also left the text area disabled so that we could not interact with it via the mouse or keyboard. Unfortunately, this disabled state prevents even our program itself from interacting with the text widget.
Hence we have to temporarily reenable the widget (-state normal) ... do our thing ... and then disable it once more.
When you run the program now you should see:
ZeroOneTwoThreeAppear in the ticker area.
This is good, but we'd like to achieve some separation between the individual ticker items.
We need to do this separation in a general way, so let's start by altering
our ticker procedure call and parameter list.
#========================== # THIS IS TEMPORARY # TO TEST ticker PROCEDURE #========================== ticker $f.t " +++ " |
and
#==========================
# ticker - entry point
#==========================
proc ticker { t filler } {
|
If we now make use of this filler string we can space apart each item
from our array as it gets added to the ticker widget.
$t configure -state normal $t insert end $text_messages($j) tag$j $t insert end $filler fill $t configure -state disabled |
All that remains to be done in this lesson is to make the ticker actually
move across the screen.
Step 5: making the Ticker tape move |
#========================== # THIS IS TEMPORARY # TO TEST ticker PROCEDURE #========================== ticker $f.t 400 " +++ " 95 |
and
#==========================
# ticker - entry point
#==========================
proc ticker { t delay filler minlen } {
|
We also need to add a new global variable called index.
This is to be added in the "initialize some global variables" section near
the bottom of the main area in the program as below:
#===================================== # initialize some global variables #===================================== set tickerState "stopped" set index 0 |
These changes should have no effect on our program yet.
We also need to modify the "else" branch inside the toggleTicker procedure
as follows
} else {
set tickerState "running"
$Ticker entryconfigure Start -label Stop
ticker $f.t 400 " +++ " 95
puts stdout [ format "ticker is now running" ]
} ;# end if
|
We will be needing the "ticker" line here to act as a trigger for our
ticker tape animation to follow. Since we have made use of
the previously defined "f" frame variable here we need to add a
| global f |
statement along with the other "global" calls near the top of the toggleTicker
procedure.
Now let's modify our ticker procedure to look like:
#==========================
# ticker - entry point
#==========================
proc ticker { t delay filler minlen } {
set fn "ticker"
global text_messages
global maxMessages
global index
puts stdout [format "%s:ding" $fn ]
set text_messages(0) Zero
set text_messages(1) One
set text_messages(2) Two
set text_messages(3) Three
set maxMessages 4
scan [ $t index 1.end ] %d.%d line len
# add to ticker
if { $len < $minlen } {
if { $index >= $maxMessages } { set index 0 }
set j $index
puts stdout [ format "array(%d)=<%s>" $j $text_messages($j) ]
$t configure -state normal
$t insert end $text_messages($j) tag$j
$t insert end $filler fill
$t configure -state disabled
incr index
} ;#end if
# move ticker one character to the left
$t configure -state normal
$t delete 1.0
$t configure -state disabled
puts stdout [format "%s:done" $fn ]
} ;#end ticker
|
Run this program and toggle the Start/Stop menu item under Ticker menu a few times. You should notice that the text in the ticker area begins to change and scroll slowly off to the left.
We have introduced a new statement here called scan. You can best think of scan as an opposite to format. With format we are converting variables into displayable strings. With scan we are converting strings of data back into individual variables.
The construct inside the [ ] on the scan line is used to return the text widget index for the end of the 1st line. For the text widget this index will be in the form of:
linenumber.characternumberWe are simply using the scan statement to extract the linenumber into a variable called line and the characternumber into a variable called len. In other words this is a "clever" way to extract the length of the line.
To understand our ticker motion more clearly we need to identify two distinct operations: the adding to the end of the ticker tape; the removal of text on the left side of the ticker tape. The adding part insures that the ticker always is replenished. The deletion of the leftmost character in the text widget is what makes the ticker move from right to left.
The addition block is inside the if statement noted by "add to ticker"
comment. This warrants a small explaination.
# add to ticker
if { $len < $minlen } {
if { $index >= $maxMessages } { set index 0 }
|
Rather than develop some complex logic to add to text to the end of the ticker area on a character by character basis, we have chosen a simpler approach. Firstly we are going to work in the text widget area that is beyond the visible portion. In other words in the area that would be to the right of the visible text window. This is why we set "minlen" to be 95 characters. At any given time only 50 or so characters are visible in the ticker area.
Our algorithm has the actual text len compared to this "minlen". Once we detect that the text in the widget has dropped below this minlen threshold we are going to add the next complete word from the array and a filler message following it. The position in the array is marked by our variable called "index". The if statement and the incr statement
if { $index >= $maxMessages } { set index 0 }will insure that index counts 0,1,2,3,4,0,1,2,3,4etc. If you don't understand how this works study the lines a little more and then ask the mailing list or chat room for some help.
incr index
The last remaining thing we need to do is to "animate" the ticker. In other words we need to make it operate on its own.
Tcl/Tk has some very simple but powerful animation capabilities built into the language. We are going to take advantage of one of the simplest of these. Tcl/Tk has the ability to tell the program to "queue" up a statement for execution after some time has elapsed.
All we need to do is to add the following statements to the end of our
"ticker" procedure. NOTE: that we commented out the puts statement
near the end. You probably want to do the same with the "ding"
puts statement at the top of the ticker procedure. This
will save activity on your console window once you start the animation.
$t configure -state normal
$t delete 1.0
$t configure -state disabled
if { $tickerState == "running" } {
after $delay [ list ticker $t $delay $filler $minlen ]
} ;#end if tickerState
#puts stdout [ format "%s:done" $fn ]
} ;#end ticker
|
and a new global statement near the top of the ticker procedure.
| global tickerState |
![]() |
Congratulations !
You have just written your very first animated Tcl/Tk program. |
Before we wrap up this lesson let's examine the "after" statement in
more detail.
after $delay [ list ticker $t $delay $filler $minlen ] |
The "after" statement tells the Tcl/Tk Wish program to queue something
for execution at some future point in time. The key concept
here is "queue". Your Wish program will continue
execution beyond the "after" statement. This is
often a difficult concept to understand. All the "after"
statement did is tell the Wish program to set something up to happen in
the future but continue on as usual once that is done.
When that future time arrives the statement we "queued" will be run
in much the same way that clicking the mouse on a button in an earlier
lesson caused a procedure to run. This would be very
analogous to setting your alarm clock or the automatic bake feature on
an oven. Bake start time is reached and oven goes on.
| This brings up an characteristic of programming for GUI's as we are doing here: we trade ease off writing a complex program against the loss of "cause - effect" control. The Wish program is doing a tremendous amount of the work for us in behind the scenes. If we are trying to follow the flow of our little program in a way we might follow the plot in a good novel we find that the "execution flow" is very disconnected. The computer itself isn't seeing a disconnected "plot" in the least ... it is being told to do this ... then that ... then the next thing in a very orderly fashion. The problem is most of that activity is originating in the Wish program itself and we can't see it in our source code. |
Returning to our "after" statement ...the 1st argument ($delay in our case) represents the number of milliseconds in the future we wish to set our "alarm clock" for.
When that "alarm clock" goes off we are going to arrange to run the
statement that results from the operation inside the [ ] ... how's that
for a mouthful.
list ticker $t $delay $filler $minlen |
This introduces our last Tcl/Tk statement of this lesson ... the "list" statement. What list does is effectively substitute for each of the $ variables and produce a result which would read.
ticker .myarea.t 400 " +++ " 95It is this statement which we will be running 400ms into the future ... when the "alarm clock" sounds.
Step 6: final listing |
The full listing of the source code for lesson #7 is below.
#==========================
# ticker - entry point
#==========================
proc ticker { t delay filler minlen } {
set fn "ticker"
global text_messages
global maxMessages
global index
global tickerState
#puts stdout [format "%s:ding" $fn ]
set text_messages(0) Zero
set text_messages(1) One
set text_messages(2) Two
set text_messages(3) Three
set maxMessages 4
scan [$t index 1.end] %d.%d line len
# add to ticker
if { $len < $minlen } {
if { $index >= $maxMessages } { set index 0 }
set j $index
puts stdout [format "array(%d)=<%s>" $j $text_messages($j)]
$t configure -state normal
$t insert end $text_messages($j) tag$j
$t insert end $filler fill
$t configure -state disabled
incr index
} ;#end if
# move ticker one character to the left
$t configure -state normal
$t delete 1.0
$t configure -state disabled
if { $tickerState == "running" } {
after $delay [ list ticker $t $delay $filler $minlen ]
}; #end if tickerState
#puts stdout [format "%s:done" $fn ]
} ;#end ticker
#==================================
# toggleTicker - entry point
#==================================
proc toggleTicker { } {
set fn "toggleTicker"
global tickerState
global Ticker
global f
puts stdout [format "%s:ding tickerState=<%s>" $fn $tickerState]
if { $tickerState == "running" } {
set tickerState "stopped"
$Ticker entryconfigure Stop -label Start
puts stdout [format "ticker is now stopped"]
} else {
set tickerState "running"
$Ticker entryconfigure Start -label Stop
ticker $f.t 400 " +++ " 95
puts stdout [format "ticker is now running"]
} ;# end if
puts stdout [format "%s:done tickerState=<%s>" $fn $tickerState]
} ;#end toggleTicker
#==========================
# saveFile - entry point
#==========================
proc saveFile { } {
set fn "saveFile"
global f
set myFile [tk_getSaveFile]
puts stdout [format "%s:myFile=<%s>" $fn $myFile]
set fileID [open $myFile w]
set line [$f.t get 1.0 1.end]
for {set i 2} {[string length $line] > 0 } {incr i } {
puts stdout [format "line=<%s>" $line]
puts $fileID $line
set line [$f.t get $i.0 $i.end]
} ; end for loop
close $fileID
} ;#end saveFile
#==========================
# openFile - entry point
#==========================
proc openFile { } {
set fn "openFile"
global f
set myFile [tk_getOpenFile]
puts stdout [format "%s:myFile=<%s>" $fn $myFile]
set fileID [open $myFile r]
set i 1
$f.t delete 1.0 end
while { [ gets $fileID line ] >= 0 } {
puts stdout [format "line(%d)=%s" $i $line]
$f.t insert end [format "%s\n" $line]
incr i
} ;#end while
close $fileID
} ;#end openFile
#========================================
# main - entry point
#========================================
#========================================
# explicitly setup our main window
#========================================
wm geometry . 400x75+10+10
wm title . "lesson 7"
#========================================
# setup the frame stuff
#========================================
destroy .myArea
set f [frame .myArea -borderwidth 5 -background blue]
#========================================
# add the ticker area as a text widget
#========================================
text $f.t \
-bd 2 \
-bg white \
-height 1 \
-relief ridge \
-wrap none \
-state disabled
pack $f.t -side bottom
pack $f -side top -expand true -fill both
#========================================
# create a menubar
#========================================
menu .menubar
. config -menu .menubar
#========================================
# create a pull down menu with a label
#========================================
set File [menu .menubar.mFile]
.menubar add cascade -label File -menu .menubar.mFile
#========================================
# add the Open menu item
#========================================
#$File add command -label Open -command openFile
#TEMPORARY FOR TESTING
$File add command -label Open -command openFile -state disabled
#========================================
# add the menu item
#========================================
$File add command -label Quit -command exit
#========================================
# create a Ticker pull down menu
#========================================
set Ticker [menu .menubar.mTicker]
.menubar add cascade -label Ticker -menu .menubar.mTicker
#========================================
# add the Start menu item
#========================================
# TEMPORARILY COMMENTED OUT FOR TESTING
#$Ticker add command -label Start -command toggleTicker -state disabled
# FOR TESTING ONLY
$Ticker add command -label Start -command toggleTicker
#=====================================
# initialize some global variables
#=====================================
set tickerState "stopped"
set index 0
#==========================
# THIS IS TEMPORARY
# TO TEST ticker PROCEDURE
#==========================
ticker $f.t 400 " +++ " 95
|
For those running PCWindows the result of running this program should be something like:
Summary |
You have been introduced to a bunch of new Tcl/Tk statements and constructs in this lesson. Let's review:
Exercises |
Lesson #8 will be your final lesson for this course. In that we will attempt to wrap together the file editor we built in lesson #6 with the ticker we built in this lesson.
As an exercise see if you can reenable the Open menu option and have the contents of the file you open be placed into the ticker tape.
End of Lesson 7.