www.gibmonks.com




  Previous section   Next section

Practical Programming in Tcl & Tk, Third Edition
By Brent B. Welch

Table of Contents
Chapter 22.  Tk by Example


The Example Browser

Example 22-3 is a browser for the code examples that appear in this book. The basic idea is to provide a menu that selects the examples, and a text window to display the examples. Before you can use this sample program, you need to edit it to set the proper location of the exsource directory that contains all the example sources from the book. Example 22-4 on page 329 extends the browser with a shell that is used to test the examples.

Example 22-3 A browser for the code examples in the book.
#!/usr/local/bin/wish
#  Browser for the Tcl and Tk examples in the book.

# browse(dir) is the directory containing all the tcl files
# Please edit to match your system configuration.

switch $tcl_platform(platform) {
   "unix" {set browse(dir) /cdrom/tclbook2/exsource}
   "windows" {set browse(dir) D:/exsource}
   "macintosh" {set browse(dir) /tclbook2/exsource}
}

wm minsize . 30 5
wm title . "Tcl Example Browser"

# Create a row of buttons along the top

set f [frame .menubar]
pack $f -fill x
button $f.quit -text Quit -command exit
button $f.next -text Next -command Next
button $f.prev -text Previous -command Previous

# The Run and Reset buttons use EvalEcho that
# is defined by the Tcl shell in Example 22? on page 329

button $f.load -text Run -command Run
button $f.reset -text Reset -command Reset
pack $f.quit $f.reset $f.load $f.next $f.prev -side right

# A label identifies the current example

label $f.label -textvariable browse(current)
pack $f.label -side right -fill x -expand true

# Create the menubutton and menu
menubutton $f.ex -text Examples -menu $f.ex.m
pack $f.ex -side left
set m [menu $f.ex.m]

# Create the text to display the example
# Scrolled_Text is defined in Example 30? on page 430

set browse(text) [Scrolled_Text .body \
    -width 80 -height 10\
    -setgrid true]
pack .body -fill both -expand true

# Look through the example files for their ID number.

foreach f [lsort -dictionary [glob [file join $browse(dir) *]]] {
   if [catch {open $f}in] {
      puts stderr "Cannot open $f: $in"
      continue
   }
   while {[gets $in line] >= 0} {
      if [regexp {^# Example ([0-9]+)-([0-9]+)}$line \
             x chap ex] {
         lappend examples($chap) $ex
         lappend browse(list) $f
         # Read example title
         gets $in line
         set title($chap-$ex) [string trim $line "# "]
         set file($chap-$ex) $f
         close $in
         break
      }
   }
}

# Create two levels of cascaded menus.
# The first level divides up the chapters into chunks.
# The second level has an entry for each example.

option add *Menu.tearOff 0
set limit 8
set c 0; set i 0
foreach chap [lsort -integer [array names examples]] {
   if {$i == 0} {
      $m add cascade -label "Chapter $chap..." \
         -menu $m.$c
      set sub1 [menu $m.$c]
      incr c
   }
   set i [expr ($i +1) % $limit]
   $sub1 add cascade -label "Chapter $chap" -menu $sub1.sub$i
   set sub2 [menu $sub1.sub$i ]
   foreach ex [lsort -integer $examples($chap)] {
      $sub2 add command -label "$chap-$ex $title($chap-$ex)" \
         -command [list Browse $file($chap-$ex)]
   }
}

# Display a specified file. The label is updated to
# reflect what is displayed, and the text is left
# in a read-only mode after the example is inserted.

proc Browse { file } {
   global browse
   set browse(current) [file tail $file]
   set browse(curix) [lsearch $browse(list) $file]
   set t $browse(text)
   $t config -state normal
   $t delete 1.0 end
   if [catch {open $file}in] {
      $t insert end $in
   } else {
      $t insert end [read $in]
      close $in
   }
   $t config -state disabled
}

# Browse the next and previous files in the list

set browse(curix) -1
proc Next {} {
   global browse
   if {$browse(curix) < [llength $browse(list)] - 1} {
      incr browse(curix)
   }
   Browse [lindex $browse(list) $browse(curix)]
}
proc Previous {} {
   global browse
   if {$browse(curix) > 0} {
      incr browse(curix) -1
   }
   Browse [lindex $browse(list) $browse(curix)]
}

# Run the example in the shell

proc Run {} {
   global browse
   EvalEcho [list source \
      [file join $browse(dir) $browse(current)]]
}

# Reset the slave in the eval server

proc Reset {} {
   EvalEcho reset
}

More about Resizing Windows

This example uses the wm minsize command to put a constraint on the minimum size of the window. The arguments specify the minimum width and height. These values can be interpreted in two ways. By default they are pixel values. However, if an internal widget has enabled geometry gridding, then the dimensions are in grid units of that widget. In this case the text widget enables gridding with its setgrid attribute, so the minimum size of the window is set so that the text window is at least 30 characters wide by five lines high:

wm minsize . 30 5

In older versions of Tk, Tk 3.6, gridding also enabled interactive resizing of the window. Interactive resizing is enabled by default in Tk 4.0 and later.

Managing Global State

The example uses the browse array to collect its global variables. This makes it simpler to reference the state from inside procedures because only the array needs to be declared global. As the application grows over time and new features are added, that global command won't have to be adjusted. This style also serves to emphasize what variables are important. The browse array holds the name of the example directory (dir), the Tk pathname of the text display (text), and the name of the current file (current). The list and curix elements are used to implement the Next and Previous procedures.

Searching through Files

The browser searches the file system to determine what it can display. The tcl_platform(platform) variable is used to select a different example directory on different platforms. You may need to edit the on-line example to match your system. The example uses glob to find all the files in the exsource directory. The file join command is used to create the file name pattern in a platform-independent way. The result of glob is sorted explicitly so the menu entries are in the right order. Each file is read one line at a time with gets, and then regexp is used to scan for keywords. The loop is repeated here for reference:

foreach f [lsort -dictionary [glob [file join $browse(dir) *]]] {
   if [catch {open $f}in] {
      puts stderr "Cannot open $f: $in"
      continue
   }
   while {[gets $in line] >= 0} {
      if [regexp {^# Example ([0-9]+)-([0-9]+)}$line \
             x chap ex] {
         lappend examples($chap) $ex
         lappend browse(list) $f
         # Read example title
         gets $in line
         set title($chap-$ex) [string trim $line "# "]
         set file($chap-$ex) $f
          close $in
          break
      }
   }
}

The example files contain lines like this:

# Example 1-1
# The Hello, World! program

The regexp picks out the example numbers with the ([0-9]+)-([0-9]+) part of the pattern, and these are assigned to the chap and ex variables. The x variable is assigned the value of the whole match, which is more than we are interested in. Once the example number is found, the next line is read to get the description of the example. At the end of the foreach loop the examples array has an element defined for each chapter, and the value of each element is a list of the examples for that chapter.

Cascaded Menus

The values in the examples array are used to build up a cascaded menu structure. First a menubutton is created that will post the main menu. It is associated with the main menu with its menu attribute. The menu must be a child of the menubutton for its display to work properly:

menubutton $f.ex -text Examples -menu $f.ex.m
set m [menu $f.ex.m]

There are too many chapters to put them all into one menu. The main menu has a cascade entry for each group of eight chapters. Each of these submenus has a cascade entry for each chapter in the group, and each chapter has a menu of all its examples. Once again, the submenus are defined as a child of their parent menu. Note the inconsistency between menu entries and buttons. Their text is defined with the -label option, not -text. Other than this they are much like buttons. Chapter 27 describes menus in more detail. The code is repeated here:

set limit 8 ; set c 0 ; set i 0
foreach key [lsort -integer [array names examples]] {
   if {$i == 0} {
      $m add cascade -label "Chapter $key..." \
         -menu $m.$c
      set sub1 [menu $m.$c]
      incr c
   }
   set i [expr ($i +1) % $limit]
   $sub1 add cascade -label "Chapter $key" -menu $sub1.sub$i
   set sub2 [menu $sub1.sub$i]
   foreach ex [lsort -integer $examples($key)] {
      $sub2 add command -label "$key-$ex $title($key-$ex)" \
         -command [list Browse $file($key-$ex)]
   }
}

A Read-Only Text Widget

The Browse procedure is fairly simple. It sets browse(current) to be the name of the file. This changes the main label because of its textvariable attribute that links it to this variable. The state attribute of the text widget is manipulated so that the text is read-only after the text is inserted. You have to set the state to normal before inserting the text; otherwise, the insert has no effect. Here are a few commands from the body of Browse:

global browse
set browse(current) [file tail $file]
$t config -state normal
$t insert end [read $in]
$t config -state disabled

      Previous section   Next section
    Top