Tkinter Folklore

by Russell Owen.

Tkinter is a Python interface to the Tk GUI toolkit. This document provides tidbits of information about Tkinter that I found difficult to obtain or that caught me off guard.

Much of the information that was originally in this document has been moved to Tkinter Summary, a quick reference with links to more extensive information.

Tk Variables

Tk variables (StringVar, IntVar, BooleanVar and DoubleVar) have several uses including live update of widget contents (change the variable, the widget updates) and executing callback functions when the value of the variable is changed. Live update of widgets is straightforward: simply specify the variable as an option (usually named "textvariable") when creating or configuring the widget. Traces are described here.

To trace a changed value, use the trace_variable method. Details:

Example

import Tkinter
# you must create a root window before you can create Tk variables
root = Tkinter.Tk()

# create a Tk string variable
myVar = Tkinter.StringVar()

# define a callback function that describes what it sees
# and also modifies the value
def callbackFunc(name, index, mode):
  print "callback called with name=%r, index=%r, mode=%r" % (name, index, mode)
  varValue = root.globalgetvar(name)
  print "    and variable value = %r" % varValue
  # modify the value, just to show it can be done
  root.globalsetvar(name, varValue + " modified by %r callback" % (mode,))

# set up a trace for writing and for reading;
# save the returned names for future deletion of the trace
wCallbackName = myVar.trace_variable('w', callbackFunc)
rCallbackname = myVar.trace_variable('r', callbackFunc)

# set a value, triggering the write callback
myVar.set("first value")

# get the value, triggering a read callback and then print the value;
# do not perform the get in the print statement
# because the output from the print statement and from the callback
# will be blended together in a confusing fashion
varValue = myVar.get() # trigger read callback
print "after first set, myVar =", varValue

# set and get again to show that the trace callbacks persist
myVar.set("second value")
varValue = myVar.get() # trigger read callback
print "after second set, myVar =", varValue

# delete the write callback and do another set and get
myVar.trace_vdelete('w', wCallbackName)
myVar.set("third value")
varValue = myVar.get() # trigger read callback
print "after third set, myVar =", varValue
root.mainloop()

Output is:
callback called with name='PY_VAR0', index='', mode='w'
    and variable value = 'first value'
callback called with name='PY_VAR0', index='', mode='r'
    and variable value = "first value modified by 'w' callback"
after first set, myVar = first value modified by 'w' callback modified by 'r' callback
callback called with name='PY_VAR0', index='', mode='w'
    and variable value = 'second value'
callback called with name='PY_VAR0', index='', mode='r'
    and variable value = "second value modified by 'w' callback"
after second set, myVar = second value modified by 'w' callback modified by 'r' callback
callback called with name='PY_VAR0', index='', mode='r'
    and variable value = 'third value'
after third set, myVar = third value modified by 'r' callback

Preferences in Tk Applications

The trick in setting preferences is propagating the changes. The simplest solution is to make the user quit and restart (and some Tk applications do this), but live update is much nicer if you can justify the extra code complexity. For most preferences you'll simply have to set up callback functions to watch preference values. Welch includes a complete preferences implementation that works by setting Tk variables (which are watched by setting up traces).

However, in the case of widget colors and fonts, Tk is willing to take care of live update for you. Those mechanisms are described here.

Colors

The call tk_setPalette will automatically update one or more widget color options, though it has a limited set it can handle. See Grayson or any good Tk reference, such as Welch for details. tk_setPalette does two things:

Fonts

If you set a widget's font to a named font (in Tk parlance; the Tkinter equivalent is a tkFont.Font object), then that widget's font will live-update when you change the named font!

To make this work automatically, use named fonts to set the option database before creating any widgets. Then your widgets will automatically be created with live-updatable fonts (unless you explicitly set their font option to some other value). The following example code reads the default font settings (by creating temporary widgets) and then sets the option database. On my system, Entry and Text widgets use one font and all other widgets use another (by default), so I only allow setting those two categories of fonts. This code should do very well if you wish to honor the standard Tk option database. A different means of getting initial font settings for the Font objects is called for if you wish to store preferences in some other fashion.

import Tkinter
import tkFont # required to use Font objects!

root = Tkinter.Tk()

# Create named fonts, reading the current defaults
# (thus if users use option database files, this honors their settings).
mainFontDescr = Tkinter.Button()["font"] # same as Label, Checkbutton, Menu...
entryFontDescr = Tkinter.Entry()["font"] # same as Text
mainFont = tkFont.Font(font=mainFontDescr)
entryFont = tkFont.Font(font=entryFontDescr)
# Set the option database; be sure to set the more general options first.
root.option_add("*Font", mainFont)
root.option_add("*Entry*Font", entryFont)
root.option_add("*Text*Font", entryFont)

# This is a quick and dirty demo of live font update
Tkinter.Label(root, text="Test Label").pack()
fontList = tkFont.families()
entryVar = Tkinter.StringVar()
entryVar.set(mainFont.cget("family"))
def setMainFont(varName, *args):
	mainFont.configure(family = root.globalgetvar(varName))
entryVar.trace_variable("w", setMainFont)
mainMenu = Tkinter.OptionMenu(root, entryVar, *fontList)
mainMenu.pack()
root.mainloop()

Warnings:

Hiding Widgets

To hide a widget you have to make the geometry manager forget about it. For the packer that is widget.pack_forget(), which forgets all about where the widget goes -- a potential pain when it comes time to show the widget again. For grid there's a nicer method widget.grid_remove() that remembers the grid settings for the widget, so you can show it again by simply using widget.grid().

Problem: an enclosing frame will shrink as widgets are removed until it contains only one widget; when you remove that one the frame does not shrink further (at least this is true on a Mac using MacPython 2.0) This is a pain if you wish to be able to remove all widgets from a frame and have it shrink to zero. The cheap fix is to include an empty (hence tiny) frame in the enclosing frame and never remove that. The enclosing frame will then shrink to basically nothing (the size of the empty frame) when the last visible widget is removed.

History