CHARMING PYTHON #9 (20000052)
TK programming in Python: Tips for Beginners

David Mertz, Ph.D.
Super model, Gnosis Software, Inc.
October 2000

    Built into most distributions of Python you will find the
    'TK' GUI library developed by Scriptics for use with TCL.
    'TK' is available for a large number of computer platforms,
    and its Python interface, [TKinter], is available almost
    equally widely.  This column introduces a programmer to
    [TKinter] by means of source code samples and usage
    explanations.  The project used as an example is a port of
    the Txt2Html front-end discussed in an earlier article to a
    GUI environment.


WHAT IS PYTHON? WHAT IS TK?
------------------------------------------------------------------------

  Python is a freely available, very-high-level, interpreted
  language developed by Guido van Rossum.  It combines a clear
  syntax with powerful (but optional) object-oriented semantics.
  Python is available for almost every computer platform you
  might find yourself working on, and has strong portability
  between platforms.

  TK is a widely used graphics library developed by John
  Ousterhout, and most closely associated with the TCL language
  also developed by Ousterhout.  TK started out--in 1991--as an
  X11 library, but since that time it has been ported to
  virtually every popular GUI.  Bindings for TK have been written
  for many popular languages (and for many small languages too),
  including the [Tkinter] module for Python.  TK is as close as
  Python comes to having a "standard" GUI.


INTRODUCTION
------------------------------------------------------------------------

  This column has a lot of parallels with my earlier "Curses
  programming in Python" column.  Both 'curses' and 'TK' are
  both widely used user interface libraries.  And despite the
  fact that 'curses' targets text consoles, and 'TK' GUIs,
  working with both libraries is surprisingly similar.
  Understanding the basic notions of windows and event loops is
  the first step to programming with either library.  Once you
  have got those concepts down, all you really need is a
  reference to the widgets available.  Well, a good reference,
  and a moderate amount of practice.

  In this column--much as with the 'curses' one--we'll be limiting
  ourselves to the features of [Tkinter] itself.  Since the
  [Tkinter] module is part of many Python distributions, you have
  a good chance of having it available without requiring users to
  download support libraries or other Python modules.  The
  Resources section gives pointers to several collections of
  higher-level widgets for various UI purposes.  But you can do a
  lot with [Tkinter] itself, even construct your own new
  high-level widgets.  Getting used to the base [Tkinter] module
  is a good way to familiarize yourself with the 'TK' way of
  thinking, even if you go on to use extra widget collections.

  The author should also make a confession to readers--but a
  useful one, perhaps.  I am no wizened expert at 'TK' programming.
  In fact, my 'TK' programming experience stretches back about
  three days (with a few glances at some of the references in the
  Resources prior to that).  Maybe it was not an entirely
  painless three days, but at the end of them, I feel like I have
  a pretty good grasp of working with [Tkinter].  The moral here
  is that 'TK' itself, and the Python [Tkinter] wrapper are both
  extraordinarily well-designed and friendly libraries that are
  about the easiest way to start GUI programming I can think of.


THE APPLICATION
------------------------------------------------------------------------

  As a test application for this article, the author will discuss
  a wrapper he has written for the 'Txt2Html' program introduced
  in "Charming Python #3", whose techniques were discussed
  further in subsequent columns.  'Txt2Html' works in several
  ways, but for purposes of this article, we are interested in
  'Txt2Html' as a command-line format conversion program.  One
  way to operate 'Txt2Html' is to feed it a bunch of command-line
  arguments indicating various aspects of the conversion to be
  performed, and let the application run as a batch process.  For
  occassional usage, it might be friendlier for users to be
  presented with an interactive selection screen that leads users
  through conversion options, and provides visual feedback of
  options selected, before performing the actual conversion.

  The application 'tk_txt2html' is structured in terms of
  a familiar topbar menu with drop-downs and nested submenus.
  Even beyond the similarities of 'TK' and 'curses' event loops,
  this application -looks- a lot like the 'curses' version
  discussed in "Charming Python #6" -- at least in terms of the
  basic parts of the screens and UI techniques.  'TK' gives us a
  bit more starting material than 'curses' did, so things like
  the menus can rely on inherent [Tkinter] classes, rather than
  being built "from scratch."  'tk_txt2html' has somewhat fewer
  lines of code than does 'curses_txt2html', while simultaneously
  doing a bit more.  But they are in the same ballpark.  Beyond
  the capability for selecting each configuration option, a
  scrolling help box is created with the TK Text widget, an about
  box used the Message widget, and a series of history lines help
  flex 'TK's dynamic geometry management a little bit.  Of
  course, as with most interactive applications, some user input
  is caught with 'TK's Entry widget.

  Probably it is worth looking at the application in action
  before going on with the explanation of its code:

  {Screenshot of tk_txt2html.py:
   http://gnosis.cx/publish/programming/cp9.gif}


The Basics
------------------------------------------------------------------------

  There are really exactly three things that a [Tkinter] program
  has to do:

      #--------- Minimum possible [Tkinter] program -----------#
      import Tkinter        # import the Tkinter module
      root = Tkinter.Tk()   # create a root window
      root.mainloop()       # create an event loop

  The sample program is a perfectly legitimate [Tkinter] program
  (maybe not a perfectly -good- on, since it doesn't even manage
  "hello world").  But the only thing actually missing from our
  first sample program is some widgets to populate the root
  window we have created.  Once we enhance our program with
  widgets, this same root '.mainloop()' method call will handle
  all the interaction with our widgets without further programmer
  intervention.

  Let's take a look at the more realistic main() function of
  'tk_txt2html.py'.  Notice that I prefer to perform an 'import
  Tkinter' statement to the 'from Tkinter import *' that John
  Grayson uses throughout his book (see Resources).  It is not so
  much that I am worried about namespace pollution (the usual
  caveat for 'from ... import *' statements); rather, I want to
  make it explicit when I am using [Tkinter] classes, and not
  risk confusion with my own functions and clases.  I recommend
  you do this, at least as you begin working with [Tkinter]:

      #------------ tk_txt2html main() function ---------------#
      def main():
          global root, history_frame, info_line
          root = Tkinter.Tk()
          root.title('Txt2Html TK Shell')
          init_vars()

          #-- Create the menu frame, and menus to the menu frame
          menu_frame = Tkinter.Frame(root)
          menu_frame.pack(fill=Tkinter.X, side=Tkinter.TOP)
          menu_frame.tk_menuBar(file_menu(), action_menu(), help_menu())

          #-- Create the history frame (to be filled in during runtime)
          history_frame = Tkinter.Frame(root)
          history_frame.pack(fill=Tkinter.X, side=Tkinter.BOTTOM, pady=2)

          #-- Create the info frame and fill with initial contents
          info_frame = Tkinter.Frame(root)
          info_frame.pack(fill=Tkinter.X, side=Tkinter.BOTTOM)

          # first put the column labels in a sub-frame
          LEFT, Label = Tkinter.LEFT, Tkinter.Label   # shortcut names
          label_line = Tkinter.Frame(info_frame, relief=Tkinter.RAISED, borderwidth=1)
          label_line.pack(side=Tkinter.TOP, padx=2, pady=1)
          Label(label_line, text="Run #", width=5).pack(side=LEFT)
          Label(label_line, text="Source:", width=20).pack(side=LEFT)
          Label(label_line, text="Target:", width=20).pack(side=LEFT)
          Label(label_line, text="Type:", width=20).pack(side=LEFT)
          Label(label_line, text="Proxy Mode:", width=20).pack(side=LEFT)

          # then put the "next run" information in a sub-frame
          info_line = Tkinter.Frame(info_frame)
          info_line.pack(side=Tkinter.TOP, padx=2, pady=1)
          update_specs()

          #-- Finally, let's actually do all that stuff created above
          root.mainloop()

  There are a number of things to notice about our simple 'main()' function.

    - Every widget has a parent.  Whenever we create a widget,
    the first argument to the instance creation is the parent of
    that new widget.

    - When other arguments are used besides a parent, they are
    passed in Python's pass-by-name style.  This gives us lots of
    flexibility about what options we want to override the
    defaults on, and which we are happy to leave be.

    - A number of (Frame) widget instances are global variables.
    It would be possible to pass these around from function to
    function, and maintain a theoretical purity about scoping.
    Doing that is much more trouble than it is worth.  The basic
    UI elements of our application are perfectly appropriate for
    any function to play with; making them global just makes this
    explicit.  Of course, you should use a good naming convention
    when you use global variables (Pythonistas seem to hate
    Hungarian notation, so don't use that ^-)).

    - After a widget is created, it needs to call a geometry
    manager method to let 'TK' know where to put the widget.  A
    lot of magic goes into 'TK' 's calculation of the details,
    especially when windows are resized or widgets are added
    dynamically.  But you need to let 'TK' know which set of
    incantations to use, for your part.


GEOMETRY MANAGERS
------------------------------------------------------------------------

  'TK' (and therefore [Tkinter]) has three geometry managers to
  choose from:  '.pack()', '.grid()' and '.place()'.  Only the
  first two are used by 'tk_txt2html', although '.place()' can be
  used for the most fine-grained (and therefore complicated)
  control.  Most of the time, you will use '.pack()'.

  You are certainly allowed to call the '.pack()' method of a
  widget with no arguments.  If you do that, you can certainly
  count on the widget winding up -somewhere- in your application.
  But you probably want to provide some slight hints by way of
  named argument.  The most important such hint is the 'side'
  argument.  Options are LEFT, RIGHT, TOP, and BOTTOM (note that
  those words are variables in the [Tkinter] namespace).

  A lot of the magic of '.pack()' comes from the fact that
  widgets can be nested.  In particular, the Frame widget does
  little more than act as a container for other widgets (maybe
  show borders of various types).  So a particularly handy way of
  organizing things is to pack together several frames in the
  orientations you want them, then later add other widgets within
  each frame.  Frames (or any other widgets) get packed in the
  order their '.pack()' methods are called.  So if two widgets
  both want to have 'side=TOP', it is first-come-first-serve.

  '.grid()' is also used a bit in 'tk_txt2html' (mostly just to
  play with it though).  The idea of the grid geometry manager is
  that a parent widget is divided into invisible graph-paper
  lines.  When a widget calls '.grid(row=3, column=4)' it is
  requesting (of its parent) that if be placed on the third row
  and fourth column.  The total number of rows and columns is
  just a matter of looking at the requests made by all the
  siblings.

  Don't forget to apply a geometry manager to your widgets, or
  else you'll be surprised not to see them in your application.


MENUS
------------------------------------------------------------------------

  [Tkinter] makes creating menus quite painless.  If you want,
  you can even have different fonts, pictures, checkboxes, and
  all sorts of fancy child widgets populate your menus.  Our
  application is simpler than that though.  The menus for
  'tk_txt2html' were all created with the line we saw above:

      menu_frame.tk_menuBar(file_menu(), action_menu(), help_menu())

  But this line might mystify as much as it clarifies, by itself.
  Most of the work (but still a small amount) lives in the
  functions I have called  '*_menu()'.  Let's look at the simplest
  one:

      #------------- Creating a drop-down menu ----------------#
      def help_menu():
          help_btn = Tkinter.Menubutton(menu_frame, text='Help', underline=0)
          help_btn.pack(side=Tkinter.LEFT, padx="2m")
          help_btn.menu = Tkinter.Menu(help_btn)
          help_btn.menu.add_command(label="How To", underline=0, command=HowTo)
          help_btn.menu.add_command(label="About", underline=0, command=About)
          help_btn['menu'] = help_btn.menu
          return help_btn

  Basically, a drop-down menu consists of a Menubutton widget
  that has a Menu widget as a child.  The Menubutton needs to be
  '.pack()'d to the appropriate location (or '.grid()'d, etc.),
  but the Menu widget instead has items added with the
  '.add_command()' method.  There is an odd little assignment to
  the Menubutton's dictionary in the above:  just do the same in
  your own code.


GETTING USER INPUT
------------------------------------------------------------------------

  We have seen to display output (the Label widget was used
  above, see the full source for some use of the Text widget and
  the Message widget also).  And we have also seen how to create
  menus.  Probably the most significant remaining UI issue is
  getting user field input (and the last UI issue for this
  introduction).

  The basic widget for field input is Entry.  Using this is
  simple, but might be a little bit different than you would
  expect from Python's 'raw_input()' or [curses]' '.getstr()'.
  That is, 'TK's Entry widget does not return a value for an
  assignment context, but rather takes an argument for the field
  object to be populated.  For example, this is the function that
  allows the user to specify an input file:

      #------------- Receiving user field input ---------------#
      def GetSource():
          get_window = Tkinter.Toplevel(root)
          get_window.title('Source File?')
          Tkinter.Entry(get_window, width=30,
                        textvariable=source).pack()
          Tkinter.Button(get_window, text="Change",
                         command=lambda: update_specs()).pack()

  Again, there are a few things to look at here.  We have created
  a new Toplevel widget for this input.  That is, input occurs in
  its own dialog box in this example.  The input field is created
  by creating an Entry widget, and specifying a 'textvariable'
  argument.  But there is a bit more to this still.

  The 'textvariable' argument does not specify a simple string
  variable, but is instead a StringVar object.  In our case, the
  'init_vars()' function that was called from 'main()' contained
  these lines:

      source = Tkinter.StringVar()
      source.set('txt2html.txt')

  What this did was create an object suitable for taking user
  input, and then give it an initial value.  Once this object
  exists, it is modified immediately every time a change is made
  within an Entry widget that links to it.  The change occurs for
  every keystroke within the Entry widget, not just upon
  termination of a read, in the style of 'raw_input()'.

  Once we want to do something with the value a user entered, we
  use the '.get()' method of our StringVar instance, for example:

      source_string = source.get()


FINALLY
------------------------------------------------------------------------

  The techniques outlined here--and especially those additional
  ones used in the full application source code should get you
  started with [Tkinter] programming.  Play with it a bit, it is
  not hard to work with.  One nice thing is that the 'TK'
  library may be accessed by many languages other than Python
  also, so what you learn using Python's [Tkinter] module is
  mostly transferrable elsewhere.


RESOURCES
------------------------------------------------------------------------

  A good online starting point for [Tkinter] information (and
  downloads) is:

    http://python.org/topics/tkinter/

  Several extra widget collections are available to save you some
  time in constructing complex UIs. PMW (Python Mega Widgets) is
  one written 100% in Python, and widely used in the Python
  community.  Several widget collections can be found at:

    http://python.org/topics/tkinter/widgets.html

  Fredrik Lundh has written a good tutorial for [Tkinter] that
  contains much more detail than this article:

    http://www.pythonware.com/library/tkinter/introduction/index.htm

  A couple printed books are worth checking out also.  The first
  is a good intro to TK itself.  The second is specific to
  Python, with a lot of use of the PMW collection in its
  examples:

    _TCK/TK in a Nutshell_, Paul Raines & Jeff Tranter, O'Reilly,
    1999. ISBN 1-56592-433-9

    _Python and Tkinter Programming_, John E. Grayson, Manning,
    2000. ISBN 1-884777-81-3

  A very nice distribution of Python has been created recently by
  ActiveState.  This distribution includes [TKinter] and a
  variety of other nice packages and modules not contained in
  most other distributions.  (They even have an ActivePerl
  distribution for those inclined towards that other scripting
  language).  Find it at:

    http://activestate.com/Products/ActivePython/Download.html

  Scriptics (the maintainers and creators of TK) has been renamed
  as Ajuba Solutions.  It can still be found at:

    http://www.scriptics.com/home.html

  For many comparisons, take a look at "Curses programming in
  Python:"

    http://gnosis.cx/cgi/txt2html.cgi?source=http://gnosis.cx/publish/programming/charming_python_6.txt

  Files used and mentioned in this article:

    http://gnosis.cx/download/charming_python_9.zip

ABOUT THE AUTHOR
------------------------------------------------------------------------

  {Picture of Author: http://gnosis.cx/cgi/img_dqm.cgi}
  David Mertz writes many apocopetic articles.  David may be
  reached at mertz@gnosis.cx; his life pored over at
  http://gnosis.cx/publish/.  Suggestions and recommendations on
  this, past, or future, columns are welcomed.


