Rapid Gui Development using Tcl/Tk Course Text Part Two: Tk (some text drawn from Brent Welch's book, "Practical Programming in Tcl/ Tk") Aidan Low aidan@mit.edu Student Information Processing Board MIT January 1999 This class is sponsored by SIPB, the MIT Student Information Processing Board, a volunteer student group that provides computer- related services to the MIT community. This class is one of those services. For more information, see our web page, www.mit.edu, or drop by the SIPB office, W20-557. Class Homepage: This entire document and the text from the first class are available from the webpage at: http://www.mit.edu/iap/tk Class outline: Tk * Overview - Widgets, Packer * Pack Geometry Manager - side, fill, pad, ipad * Widgets * Labels * Message * Buttons * Menus * Frames * Scale * Entry * Text * Scrollbars * Listbox * Canvas * New Windows * Binding Example * Word Processing Program Advanced Topics * Tcl Stuff: + Dynamic Scoping + Namespaces + Eval + Regexp, regsub + Advanced Input/Output + Tcl and CGI + Safe Tcl, Tcl Plugin + C & Tcl * Tk Stuff: + Grid Geometry Manager + Place Geometry Manager + Send + Bitmaps, Images + Window Managers, Window Information + Writing new Tk widgets in C Background Tcl is an intepreted high-level programming language designed to be easily understandable and easily customizable. It was developed by John Ousterhout when he wanted to give his students an editor that they could alter and extend themselves. Tk is an associated graphical toolkit which provides the same power to users in the graphical domain. Now complex GUIs can be written in a few dozen lines of code, rather than the pages it requires in more complicated arenas. Outline This course will cover Tcl and Tk at a basic level, and looking at features of the language one by one along with examples of the code at work. In the end, we'll see that a basic word processing program can be written very simply using Tcl and Tk. We'll close by briefly looking at a number of advanced topics which are beyond the scope of this class, but that you can learn about on your own if you like. In the first class, we concentrated on Tcl, and in this, the second class, we'll look at how Tk can extend Tcl to allow the creation of graphical interfaces very simply. Writing Conventions Text in Courier font set a [expr $b * 12] represents code exactly as typed into the interpreter. Text in italics represents an abstract thing to be filled in. This text set f [open filename r] means that the command takes a filename in that space. (i.e. set f [open "foo.txt" r] Text wrapped in ? ? means that it is optional. puts ?-nonewline? textstring Finally, ... means that there are multiple things there. foo arg ... arg Starting up (on Athena) Tcl Interpreter This will bring up the tcl interpreter in the window you run it in. add tcl tclsh Tk Interpreter This will bring up the Tcl interpreter in the window you run it in, but this tcl interpreter can run tk commands as well. A Tk window will also be created. add tcl wish Tk Overview - Widgets, Packer, Binding The basic idea of Tk is that you create "widgets" (small GUI components) and then place them on the screen with a geometry manager. You can then "bind" events that happen in those widgets to Tcl commands. This three step process of create/place is the general way of creating all Tk applications. We'll talk about the first two steps now, and talk about binding after we've talked about some other things. The simplest of these geometry managers is the pack geometry manager, which we will use here. These components are very simple to create, as evidenced by the following Tk program which creates a window with Hello World written in it. label .l -text "Hello World" pack .l This program, written in a more complicated GUI system like Win32, takes about two pages of code to write. Tk allows GUIs to be created simply and effectively, providing lots of functionality for you that you can use if you like. Example This example is a program for a basic text editor. By the end of the class, you should understand everything in this code and be able to extend and modify the text editor to add any features that you like. This example was a lot more ambitious before, but I ran out of time. Right now, it loads and saves and lets you edit and has a bunch of "normal" editor features. proc tkSetup {} { menu .menubar -tearoff 0 . config -menu .menubar menu .menubar.file -tearoff 0 menu .menubar.file.recent -tearoff 0 .menubar add cascade -label "File" -menu .menubar.file .menubar.file add command -label "New" -command {newCommand} .menubar.file add command -label "Open" -command {openCommand} .menubar.file add command -label "Save" -command {saveCommand} .menubar.file add separator .menubar.file.recent add command -label "tcltk.txt" -command {openCommand "tcltk.txt"} .menubar.file.recent add command -label "thesis.txt" -command {openCommand "tcltk.txt"} .menubar.file add cascade -label "Recent Files" -menu .menubar.file.recent .menubar.file add command -label "Quit" -command {quitCommand} frame .textframe frame .statusbar text .textframe.text -wrap word -width 80 -height 24 -font "Arial 9" -yscrollcommand ".textframe.yscroll set" -xscrollcommand ".textframe.xscroll set" scrollbar .textframe.yscroll -orient vert -command ".textframe.text yview" scrollbar .textframe.xscroll -orient horiz -command ".textframe.text xview" label .statusbar.label -text "Ready" pack .textframe -side top -fill both -expand yes pack .statusbar -side top -fill both -expand yes pack .textframe.yscroll -side right -fill y pack .textframe.xscroll -side bottom -fill x pack .textframe.text -fill both -expand yes pack .statusbar.label -side left -anchor w } proc globalSetup {} { } proc newCommand {} { .textframe.text delete 0.0 end } proc openCommand {{fname "test.txt"}} { newCommand set f [open $fname r] while {![eof $f]} { .textframe.text insert insert [gets $f] .textframe.text insert insert "\n" } close $f } proc saveCommand {} { set f [open "test.txt" w] puts $f [.textframe.text get 0.0 end] close $f } proc quitCommand {} { exit } # actual initialization code tkSetup globalSetup Widget Names All widgets are given named when they are created. The name of the top-level window that is created when you run the wish interpreter is simply a period. "." Any widgets placed within this top level window must be something like ".label1", ".button1", or something. When you create frames within this window, you can nest widgets within the frames. For example, if you created a frame within the top level window named ".frame1", you could create labels within that frame named ".frame1.label1" or something. Note that the names can be anything, so instead of .frame1.label1. you could have .foo.bar.baz. So the object .foo.bar.baz has the name "baz" for itself, and it's parent is named ".foo.bar". One thing somewhat confusing about Tk is that objects may be placed into any child of their parent, so if .foo.bar had another frame called .foo.bar.meow, you could place .foo.bar.baz into .foo.bar.meow. This is, again, sort of a confusing way of doing things, and I recommend against it, but it's there if you want to use it. Labels To create a label in tk, you use the label command, which takes in the name of the label you wish to create followed by a number of options. For example, the simplest label is just this: label .l This creates a label named .l with no parameters. Note that the label command, as in all widget creators, returns the name of the widget created. Therefore, you could say set a [label .l] pack $a A label with text looks like this: label .l -text "Hello World" A label can use the text of a variable as well, which will update the label on screen if the variable changes: label .l -textvariable myVariable A label with a background color looks like this: label .l -text "Hello World" -background blue Below are options available for labels: -anchor Relative position of the label within its packing space (more later) -background Background color (also bg) -bitmap Name of a bitmap to display instead of a text string -borderWidth Extra space around the edge of the label -cursor Cursor to display when the mouse is over the label -font Font for the label's text (more on fonts later) -foreground Foreground color (also fg) -height In screen units for bitmaps, in lines for text -highlightBackground Focus highlight color when widget does not have focus -highlightColor Focus highlight color when widget has focus -highlightThickness Thickness of focus highlight rectangle -image Specifies image to display instead of bitmap or text -justify Text justification (left, right, or center) -padX Extra space to the left and right of the label -padY Extra space above and below the label -relief flat, sunken, raised, groove, solid, or ridge -takeFocus Control focus changes from keyboard traversal -underline Index of character to underline -width In characters for text labels -wrapLength Length at which text is wrapped in screen units Pack Geometry Manager - side, fill, pad, ipad Once we've created a label, we want to display it on the screen. The simplest way to do this is to use the pack command. The command below will pack the label into the main Tk window. pack .l You can have the widget request additional space, to make the widget bigger. pack .l -ipadx 20 -ipady 10 You can pad the widget with extra space, so that it grabs space within the top but doesn't use it.: pack .l -padx 20 -pady 10 You can pack the widget to a particular side of the window, which doesn't do anything the first time, but specifies how widgets packed later will be placed in relation to this widget. pack .l -side top pack .l2 -side top Packing a widget without a side specified will use the side from the last pack command issued. The first pack commands default to "top". You can pack multiple widgets at the same time, which does the same thing as packing them in left-to-right order, all with the same options afterwards. pack .l .l2 -side top You can make a widget fill the available space with the fill options pack .l -fill x pack .l -fill y pack .l -fill both You can also specify that a widget will expand to fill available space when possible, which will make the widget respond to the display area changing, whether because other widgets are added or removed or if the user resizes the window. pack .l -expand true A widget can be anchored in place if it has more packing space than display area. The default anchoring is center, though you can use n, ne, e, se, s, sw, w, nw. (compass points) pack .l -anchor nw While packing widgets sequentially will normally result in one being packed after the other, you can specify the order by using the "before" and after commands. pack .l .l2 .l3 pack .l4 -after .l2 pack .l5 -before .l3 You can unpack a widget by using the pack forget command. This will remove the widget from the screen, and delete all information that was given when it was packed, (pad, ipad, anchor, expand, etc) though the widget itself will not be removed from memory, so you can pack it again. pack forget .l To delete the widget forever, use destroy. destroy .l You can also specify where a widget will be packed. By default, the widget will be packed within its parent. For example, .foo.bar will be packed in .foo by default. pack .foo.bar -in .foo.meow There are a number of other pack commands, but these basic ones are enough for most applications. Message A message is a multi-line label, essentially, designed for use in a dialog box. message .msg -justify center -text "When in the \ course of human \ events" pack .msg The backslashed newlines will be translated into newlines in the message when it is displayed. Menus Menus are in some sense collections of other widgets. Menu entries can act like buttons, checkbuttons, radiobuttons, frames, and menus themselves. The main foundation of a menu is the menubar that goes along the top of the application. Once you create this bar, you can add cascade buttons to it that will have the pulldown menus that the application will use. Menus are created with menu, and can be associated with a window with the config command. menu .menubar . config -menu .menubar this configures the root window (".") to use .menubar as its menu Pulldown To add pulldown menus, create the menus first, and then add them as cascade to the main menu menu .menubar.file menu .menubar.optionst menu .menubar.format .menubar add cascade -label "File" -menu .menubar.file .menubar add cascade -label "Options" -menu .menubar.options .menubar add cascade -label "Format" -menu .menubar.format Menus (except for the one in the menubar) will have this annoying dashed-line called a "tearoff". To turn this off, use the tearoff flag. menu .menubar.file -tearoff 0 Menu Commands To add a normal menu selection, add a command .menubar.file add command -label "Quit" -command {exit} Note that the choice of braces or quotes determines when the command is evaluated. This code will evaluate the command string when the menu entry is added, evaluating the variable foo at that time. .menubar.file add command -label "Borp" -command "myProc $foo" This code, however, will evaluated the command string only when it is executed, and will use the value of foo when the menu item is clicked: .menubar.file add command -label "Borp" -command {myProc $foo} Cascade Menus To add a cascading menu from a pulldown menu, just create the cascaded menu, then add it to the pulldown menu as a cascade. menu .menubar.file.recent .menubar.file.recent add command -label "monica.jpg" .menubar.file.recent add command -label "clinton.jpg" .menubar.file add cascade -label "Recent Files" -menu .menubar.file.recent Radio Buttons To add a radio button, you create groups of radio buttons that are all associated with one variable, and by clicking the radio buttons you can change the value of that. This example adds two entries to toggle the variable "datatype" between "HTML" and "ASCII" .menubar.options add radio -label "HTML" -variable datatype -value "HTML" .menubar.options add radio -label "ASCII" -variable datatype -value "ASCII" The variable is global, so it can be accessed anywhere in the code by using the command "global daytatype". Checkbuttons To add a checkbutton, you specify the name of the variable, and if the user "checks" it, then the global variable is set to 1 or 0. .menubar.format add check -label "Bold" -variable boldface .menubar.format add check -label "Italics" -variable boldface .menubar.format add check -label "Underline" -variable boldface Buttons A button is just that. It has text and a command that is run when it is executed. button .b -text "push me" -command {puts stdout "Ouch! That hurt!"} Recall the notes about the quotes vs. curly braces from menu commands. Those things apply here as well. Frames You'll probably notice that the packer can't handle very complicated organizations of widgets. However, you can introduce frames, or subwindows. You can then pack items into those subframes as you would into the normal window. frame .f frame .g label .f.label1 label .f.label2 label .g.label1 label .fglabel2 pack .f .g -side top pack .f.label1 .f.label2 -side left pack .g.label1 .g.label2 -side bottom Recall that items will be packed in their parents as per the hierarchical names, but you can also pack a widget into any child of its parent with the in command. frame .f frame .f.g label .f.label pack .f pack .f.g pack .f.label -in .f.g Scale A scale is a slider widget that allows users to input a numerical value between two bounds. The flags are pretty self-explanatory. scale .scale -from 10 -to 20 -length 200 -variable x -orient horiztonal -label "The value of X" -tick interval 5 -showvalue true pack .scale -showvalue says that the value the slider is current at should be displayed above the slider. Entry An entry is a widget that allows a user to enter text. It can be associated with a textvariable, in which case a change to the entry or to the variable will change the value of the other as well. entry .entry -textvariable foo -relief sunken -width 30 -font "Times 9" There are also a number of operations you can use on the entry itself. .entry delete first ?last? <- delete characters. first and last can be integers, or last can be end. There are other valid values, see docs. .entry get <- returns the text in the entry .entry insert index <- inserts text into the entry .entry select range start end <- selects some text from the entry as if the user selected it with the mouse. There are other operations as well, which you can get in the documentation and I'll add to this document later. Text The text widget is one of the most powerful widgets in Tk. It implements a pane of text, and automatically comes with a blend of keyboard shortcuts on the text from Windows and Emacs. It allows the user to enter text, and allows the use of tags and marks to declare ranges of text and enact formatting on that text. You can search through the text, declare justification, count characters and words, use different fonts, different colors, and different tab settings. It's a very powerful widget, and too complex to explain fully, but the documentation is very clear on this. text .t -wrap word -width 42 -height 14 -font "Times 9" pack .t You can insert and delete text from text widgets. Indexes are stored in a "line,char" format, but there are special indices as well. line.char <- Lines count from 1, Characters count from 0 @x,y <- The character under the given screen position (in pixels) current <- The character currently under the mouse end <- the last character in the text widget image <- the position of the embedded image insert <- the position right after the insert cursor mark <- just after the named mark tag.first <- the first character in the range tagged with tag tag.last <- just after the last character in the range tagged with tag window <- the position of the embedded window Once you have these indexes, you can do a bunch of stuff: .text delete i1 ?i2? <- delete from i1 to i2, or just the character at i1 if i2 is not defined .text insert i1 text <- insert text at index i1 .text get i1 ?i2 <- get the text from i1 to i2, or just the character at i1 There are tons of other options, too many to list. You can create tags by using the tag configure command .text tag configure bold -font {times 12 bold} You can then set text in the widget to be tagged with the tag add command .text add bold 0.0 end You can remove the tag from some of the text. Even if you remove the tag from all text, the tag still "exists" with its configuration information. .text remove bold 0.0 3.0 To delete the tag completely, use delete .text delete bold .Scrollbars You can create scrollbars to associate with text widgets. The following example shows the changes that need to be made to the text widget and add the scrollbars properly. Note that the scrollbars must be told to fill in the appropriate directions, or they'll be tiny. text .textframe.text -wrap word -width 80 -height 24 -font "Arial 9"\ -yscrollcommand ".textframe.yscroll set" -xscrollcommand ".textframe.xscroll set" scrollbar .textframe.yscroll -orient vert -command ".textframe.text yview" scrollbar .textframe.xscroll -orient horiz -command ".textframe.text xview" pack .textframe.yscroll -side right -fill y pack .textframe.xscroll -side bottom -fill x pack .textframe.text -fill both -expand yes Note that the scrollbars are packed first, and then the text widget that they scroll. This is important, or else the widgets will be sized strangely. Listbox A listbox is a listbox; there's not much else to say. They are very useful and very common in Tk apps, however, so they bear some discussion. listbox .list -xscrollcommand ".xscroll set" -yscrollcommand ".yscroll set" pack .list And the scrollbars need to be created and packed just like for the text widget. You can insert items into the listbox at the end .list insert end itemString You can get items from the listbox .list get first ?last? <- get items between first and last, or just first if last is not specified Indexes for first and last can be simply integers (0 is the first), or they can be the following special indices: active <- the active index (the one the user has clicked) anchor <- the index of the anchor point of the selection if the user has selected multiple entries end <- the index of the last line @x,y <- the index of the line closest to the given coords Canvas The canvas is another really powerful widget, but I'm totally out of time to write about it. Suffice it to say that it allows you to make a panel that you can draw on. canvas .c -width 320 -height 320 Here's a basic painting program: (we'll get to binding later) canvas .c -width 300 -height 300 pack .c bind .c {startline %x %y} bind .c {moveline %x %y} proc startline {x y} { global startx starty set startx $x set starty $y } proc moveline {x y} { global startx starty .c create line $startx $starty $x $y set startx $x set starty $y } New Windows You can do lots of things within just the one window that wish brings up for you, but you can create new windows as well. Technically, they are children of the first window, but they are their own windows, and not in that window. toplevel .newwindow You can then pack things into .newwindow just as you did into .. label .newwindow.label pack .newwindow.label Note that you do not need to pack the toplevel window. There are tons of window operations you can do on windows, the simplest of which is setting the title. wm title .newwindow "My New Window" See the documentation or the updated version of this file for those details. Configure You can change the widget's configuration after they have been created, as well as when created, by using the configure command. To configure a widget, you use the widget's name, followed by "configure", followed by the flags you'd like to set. label .l -text "Hello world" pack .l .l configure -text "Hello World" -font "Times 9" Binding The last major topic for Tk, binding is probably the most important. Binding allows you to associate Tcl commands with Tk widgets, so they these commands are triggered when the user does something. The format for this is bind bindingTag ?eventSequence? ?command? bindingTag is either a widget name (".foo.button1") or a widget class name ("Button") If called with no other arguments, bind returns the bindings of the widget or widget class. If called with an eventSequence only, bind returns the binding of the widget for that event sequence. If called with both an eventSequence and a command, it creates a new binding. The valid eventSequences depend on the widget, but some common ones are: <- mouse button1 pressed <- mouse button1 released <- the mouse pointer has entered the widget's bounding box <- a key was pressed when this widget had the focus <- a key was released when this widget had the focus For example, you could make an entry print out its contents whenever the user changes them by the following code: entry .e -width 20 -textvariable foo bind .e {puts stdout $foo} Finally, you can use special variable in the bind command. You can use %x to get the current x position of the cursor, %y to get the y position, and %W to get the current window. For example: bind .e {puts "%W %x %y"} You may not use these %x-type variables anywhere but in a bind command. That's pretty much all there is to binding. You'll have to look at the documentation to see which eventSequences are valid for different widgets, but the bind command gives your application a tremendous amount of power. Armed with the ability to create widgets, place them on the screen, and bind arbitrary Tcl commands to events that occur within those widgets, you should be able to do almost anything. Advanced Topics We've covered the basics of the Tcl and Tk languages, but there are a number of other things about the language that you can look at if you're interested. Look at the documentation linked at the bottom of this document or one of the books out there for more information. I'm not going to go very in depth about any of this, but you can look it up if you're interested. Advanced Tcl Dynamic scoping Most languages resolve free variables lexically, but enclosing scopes in the definition of the procedure. Tcl resolves its free variables dynamically, in terms of the call stack when the procedure is called. Namespaces In the latest version of Tcl, you can define multiple namespaces to control large programs and make them more manageable. Eval You can use the eval command to execute string as if they were Tcl commands, and thus dynamically generate Tcl code and execute it without writing it to a file and sourcing the file. Regexp, regsub You can use regular expressions and regular expression substitutions, but explaining regular expressions would take a while. I don't use them, but they're very powerful. Tcl and CGI You can use Tcl scripts as CGI scripts to allow server-side scripting on the web. Safe Tcl, Tcl Plugin You can use a Tcl plugin to do client-side scripting on the web, running Tcl and Tk within the web browser. C & Tcl You can interface C and Tcl by compiling Tcl code into a C program by linking against the proper libraries.(I haven't forgotten you, jkhu@mit.edu, I'll look up which libraries you need soon :)) Advanced Tk Grid Geometry Manager The grid geometry manager allows you to create a grid within windows and assign widgets blocks of grid cells. Place Geometry Manager The place geometry manager allows you to place widgets at specified coordinates within a window. Send Send allows Tk applications running in different interpreters to talk to each other using the X11 message queue. This is also implemented in some way on Windows and Macintosh platforms. Bitmaps, Images You can put bitmaps or other images (gif, jpeg, etc) within your Tk applications. Window Managers, Window Information You can do a bunch of window manager type stuff, moving the focus between different windows, minimizing and maximizing windows, and defining the stacking order. Writing new Tk widgets in C It's possible to create your own Tk widgets by writing C code for them. You can then use those widgets just like any other widget in Tk. Links of interest: The Tcl Platform Company :http://www.scriptics.com/ Brent Welch's book (excellent): http://www.beedub.com/book/ Tcl/Tk Man pages: http://www.scriptics.com/man/tcl8.0/contents.htm Tcl/Tk Plugin for Netscape, Internet Explorer: http://www.scriptics.com: 8123/plugin/