![Tkinter Notes [page banner]](tkinter.gif)
|
By Bill Allen The Tkinter module that comes with Python is as close as Python comes to having an official graphics user interface (GUI), but no documentation is installed. Tkinter resides in the Python folder at /Python/tcl/tk8.3. See the readme file in the /demos folder. If you are using Python for Windows, a good sample of Tkinter work is the IDLE interactive interface that you're probably using to learn Python. Another example is the Pynche color editor that you will find under /Python/Tools. Here are some places online to find Tkinter documentation and help, all based on Python 2.0 with Win98/ME and Mac OS9 (not tried with Unix, but no problems reported from Linux users):
The most complete example of Python and Tkinter programming presently on this site is the PullSDTS utility. Please examine it and feel free to reuse its elements in your own projects. All of the PullSDTS code and all of the author's Python code published via this "Tkinter Notes" page is placed freely into the public domain (see the "License" statement at the top of the "Code Samples" page). |
PullSDTS Python Notes Tkinter 3D Code Samples File Formats SDTS Notes Glossary Index |
The original idea for this section's name was "Tkinter Simplified," but that would have been wildly overoptimistic. Beyond the complexity that comes with power and flexibility, there are some big factors that complicate learning Tkinter programming, and these are not explained to the new user. Here are six things you need to know at the outset:
First, you need to understand that, without GUI tools of its own, Python's developers have embedded in it a completely separate language, Tcl (Tool Command Language), known for its great Tk (toolkit) for building interfaces. In combination, they are called the Python "Tkinter" module. Tcl/Tk, like Python, is open-source freeware, is an interpreted scripting language, and is also Unix-based but cross-platform (visit dev.scriptics.com/scripting/ to learn more).
If you go looking for Tkinter info online, you will find stuff that looks remarkably like Tkinter but is in a very different format. You have found Tcl/Tk help, and it may be of use or may just confuse you further. When you write instructions to Tkinter in Python, it is passing them out to Tcl/Tk, and so many global constants and commands are identical or very similar. (If you know Tcl, you may want to try taking advantage of being able to pass explicit Tcl language statements through Tkinter's eval or call commands to expand Python's functionality. For an example, see StripViewer.py in your
Second, you need to know that, if you are using Python's IDLE interactive programming interface, IDLE itself is a Tkinter application. That can make for a bit of a mess when writing your own Tkinter code. If you follow any of the various introductions to Tkinter, the code you are told to write will invoke a Tkinter outer loop with root.mainloop(), and yet IDLE itself has already got an outer loop going. And creating two outer loops can keep you from closing Python (read further for solutions).
Third, for MS-Windows users: With Python installed, when you click on a .py file in any folder in Windows Explorer, Python comes up with an MS-DOS window where print output and error feedback will appear. This window can be very handy for debugging during development, but it is also screen clutter that may confuse users. You should know that all it takes to suppress the MS-DOS window is to change the script's file name extension from .py to .pyw. In a package with multiple scripts, only the first, top script's extension needs to be changed to .pyw. (There doesn't appear to be a way to similarly suppress the interpreter window when running scripts with Python on Mac or Linux.)
If you use a Python script to boot an MS-DOS program, such as using a command like os.system('rendrib '+ribName) to run the Blue Moon Rendering Tools renderer, you may need to make sure to use the .pyw extension to avoid confusion over whether the DOS window belongs to Python or the booted program. If you need to see Python error messages, then run your script from IDLE or another IDE.Fourth, speaking of extra windows, some introductory Tkinter scripts bring up two windows--the one you expected and an extra that belongs to the Tkinter root. One common solution is to withdraw the root window as soon as you create the root:
root = Tkinter.Tk() root.withdraw() .. // set up your interface code here, then enter the outer loop // .. root.mainloop()
Fifth, less about Tkinter and Python and more about programming in general, make sure to keep your interface code as separate from your application code as is practical. A sand trap for new programmers is thinking that core code isn't working when actually the problem is how it hooks into some dialog or other "widget" (a favorite term for Tk objects).
When you think about it, the interface for most programs consists of elements that 1) are somewhat decorative in function, and 2) are changeable in any number of ways regarding layout arrangement and style. Yet the main get-the-job-done code will remain fairly constant. When something doesn't work in your code, you will want to be able to quickly isolate the problem, starting with, "Is it an interface or application issue?"
As an example, the new PullSDTS 2 here has six primary modules (as of 10 Sept. 2001), of which only two have Tkinter code--the main pullsdts.py script that sets up a very specific user interface, and guinter.py that has all the non-specific graphic user interface functions and classes, from init files to button classes and tip windows.
Sixth, for all who are setting out to do cross-platform or just Mac Tkinter programming, ignore assertions that Tkinter doesn't work on Macintosh. Python/Tkinter 2.0 does work with OS9 (I haven't yet tried with Python 2.1+ or Mac OSX). However, it works much like it works on Unix and Windows, which is to say, "It ain't Mac-like." Get over it. Yes, drag-and-drop would be very nice, but, no, the lack of it won't keep you from writing a slick program that gets a job done well. And complaints about not interfacing with some Mac-specific features may be pointless when it comes to most cross-platform programming.
That said, I can't yet speak to Tkinter's suitability for graphics programming such as image editing under any OS, and there are some things that can definitely confound you in taking your scripts from Windows to Mac, and that will require some special care in your Tkinter code. See "Mac Tkinter" for a list of discontinuities. Notice that there are NO vital insurmountable problems, just a bunch of picky details to attend to in creating a user interface for both Mac and Windows/Linux.
tkex1.py, or TkDemo#1, is a complete but quite minimal Tkinter script to demonstrate generally how to get a GUI Python program going, how to shut it down properly, and how to avoid conflicts with Tkinter-based IDEs such as IDLE. Windows users should try booting it from Explorer both as tkex1.py and as tkex1.pyw to see how the .pyw extension suppresses the usual MS-DOS window.
Note 1: This is a new version of tkex1.py on 30 May 01 for Win98/NT/etc. It also probably works for Unix. A Mac-safe variation is coming.
Note 2: You may find that, when running tkex1.py from IDLE (make sure to set its usingIDLE flag to = 1), you will have to close the Python shell window before each running--not the script window itself, just the shell.
Something you will notice in this script is a departure from the common usage of from Tkinter import *. It is not good practice to import any module's entire "name space" with an import command like that. So much of Tkinter is needed so often, including its global constants, that programmers frequently just import the whole collection. Thus they can write commands like menubar = Menu(..), where Menu is a Tkinter class. This can lead to confusion, especially for new programmers, because, for instance, you now have several variations on "menu" and "Menu" running around in your script. The basic alternative is to import Tkinter and then use dotted commands such as menubar = Tkinter.Menu().
That approach can lead to some awkwardly long lines of code in a language that (wisely) enforces indenting. To remedy this somewhat, you will see in older scripts a shortened renaming of the imported module like this:
import Tkinter tk = Tkinter delete Tkinter #not always used, probably should beWith Python 2.0+, you can use the cleaner syntax, import Tkinter as tk.
With the renamed imported module, you can write menubar = tk.Menu(). The catch is that you don't have access to Tkinter's global constants. For instance, you cannot write self.frame = tk.Toplevel(relief=RIDGE), where "RIDGE" is a global constant from the Tkinter module. The solution is to instead use the alternate form, relief='ridge'.
When I changed from Tkinter import * to import Tkinter as tk on an existing script that had been functioning well enough, if not quite perfectly, suddenly many Tkinter errors became exposed. I wondered, "How had this thing worked at all?!"
Note: Do NOT mix import Tkinter as tk and from Tkinter import * statements in the same script, as Tkinter has a "tk" in its own name space!
As terse as the docs are on how to "build" an interface with Tkinter, even less is said about how to correctly unbuild an interface for a safe program shutdown. In fact, it looks like Python Tkinter interfaces are prone to crashing on exit without forcing an error to your attention. To users of your script, it merely looks like the main window disappeared when expected, and they won't detect remnants of a disorderly shutdown unless they try to run the same or other Tkinter script and find that they have to reboot Python/IDLE, or maybe reboot the computer, to get that next script to run correctly. (Such a program is a "bad neighbor" to other software you use and depend on, so don't consider it to be "good enough" only because it doesn't crash until after you're done with it!)
The IDLE documentation does not come with IDLE and Python, but rather is online at www.python.org/idle/doc/idlemain.html. Under "Special Issues" (a third of the way down on the page version dated 5 March 2000, viewed on 20 Feb. 2001), it says: If you are typing in a Tkinter example, omit adding the final "mainloop()" line that would be necessary if running from the command line. This can cause your whole IDLE session to hang as IDLE is already running in a Tk event loop. It omits that, if you do a Ctrl-F5/Run on a script with a mainloop command, that also will hang IDLE, requiring in Windows a Ctrl-Alt-Del "three-finger salute" to bring up the Close Program dialog, then select "Python Shell" and End Task. (Don't be too impatient, but also don't be shy about closing tasks or rebooting if that's what's needed. Welcome to programming!)
If you examine the Pynche color editor scripts that Python installs in its /Tools folder as an excellent advanced example of Tkinter programming, you will see that Pynche's author tries to detect, and attach to, a Tkinter main loop if one is already in use. For some reason this approach doesn't seem to work with Python 2.0 and IDLE 0.6 on Windows ME. Interestingly, in the TODO.txt that comes with IDLE, there is this: Need to define a standard way whereby one can determine one is running inside IDLE. Yes, indeed, second that motion.
Some might argue that a major Python program with a Tkinter interface shouldn't be booted from IDLE, anyway. Certainly a big package had better run fine when booted on its own, but IDLE is a good working environment, and running from IDLE is useful for debugging.
To summarize, the bottom line is literally this: When using a root.mainloop() as your program's last command, be ready to "comment it out" (put a # number/pound sign at that line's beginning) when running from IDLE, and to undo the commenting when booting it outside of IDLE, such as from Windows Explorer.
root.mainloop() #ready to boot directly
# root.mainloop() #ready to run under IDLEA better solution: In examining newer Tkinter programming examples linked to these pages, including the revised tkex1.py above, you will find a usingIDLE Boolean variable that is set instead of commenting out the root.mainloop() command. This is cleaner and also handier, as it gets used in deciding other factors in how to destroy the top window upon closing, including whether or not the WM_DELETE_WINDOW protocol is employed.
Just to be clear, understand that this issue is in regard to Ctrl-F5/Running a Tkinter script from IDLE. If you boot a script from the OS or Python command line, or from Windows Explorer, then most of the time it doesn't seem to matter whether IDLE is also running.
This overall conflict situation appears to be also true with other Tkinter-based environments, such as the IDE that comes with Mac Python.
Another solution: Don't use root.mainloop() at all, but instead use root.wait_frame(yourToplevelFrame). The first two example scripts on the Tkinter 3D page use this approach, as explained here.
This section deals with three concepts: 1) Getting the GUI benefits of Tkinter without a big setup or the problems of a root.mainloop() command (see the previous section). 2) Reducing installation complications by embedding ASCII-encoded image icons right in your scripts. And, incidentally, 3) how you can write a script to write another script for you.
In preparing a GUI interface for PullSDTS, I wanted to be able to use some graphics, but didn't want people to have to deal with downloading and installing more than a single .py script, so loose GIF images were out.
In Tkinter, you first create a PhotoImage object from a GIF or the less common PGM, PPM, or XBM file types, then reference that object in your script. Somewhere on the Internet, I ran across a script that used Tkinter PhotoImages with embedded base64 data, but saw nothing about how that worked or how you would go about creating your own embedded data.
Base64 is a long standard MIME (Multipurpose Internet Mail Extensions) encoding scheme for sending ASCII superset characters (those with decimal values 128 to 255) and binary data over the Internet as safe 7-bit ASCII characters. In the early days of computer communications, the eighth bit was reserved for error (parity) checking, and the original U.S. ASCII standard was 7-bit. (If you check the RFC references, you will find base64 explained as a 6-bit encoding system, but that's internal, while the external representation is in 7-bit ASCII characters.)
Because Python comes with built-in functions for sending and receiving data streams over the Internet, it has native support for Base64 encoding and decoding. The binascii module's b2a_base64() function is all we need to encode images. While we're at it, however, why not also have our script create the whole PhotoImage statement? And, if we can do that, then why not also put that output into a "workbench" test script that shows us the statement is correct and informs us of the image's dimensions? A beauty of scripted languages is that one script can write another, and may even be able to run the new script.
So... I set out to write a simple, no-interface script to create a PhotoImage from a GIF and display it on a test button. Once that was working, it was time to try using simple Tkinter objects that avoid Toplevel windows and root.mainloop() commands. A utility that needs to run only once through, such as ImageEmbedder, can work much like a simple procedural program, going from dialog to dialog until done, and then exiting without a loop back to the beginning.
img2pytk.py, or ImageEmbedder, is a complete, ready-to-use, cross-platform Python programmer's utility that will take a GIF image and output a Tkinter PhotoImage statement with text-like base64 image encoding that can be copied into any Python script. When you run the program from the command line, or from IDLE or other IDE, you first see a small info dialog (with an MS-DOS window if running in Windows). Click OK on that to get the standard system file name requester looking for "Tk valid images (*.gif)." If you select a valid file, next will appear notice of where to find the resulting Python script ("icon.gif" converts to "icon_gif.py"). If you are on a Windows machine, ImageEmbedder will also boot the test script with a .pyw extension that suppresses the MS-DOS window.
|
|
|
And the generated script, the one that creates the dummy test button, will look like this:
#Embedded image created from C:/Python/MyUtils/pullsdts3.gif
#Done with ImageEmbedder 1.0 utility img2pytk.py from
# http://www.3dartist.com/WP/python/pycode.htm#img2pytk
import Tkinter as tk
root = tk.Tk()
img00 = tk.PhotoImage(format='gif',data=
'R0lGODlhKwAQAJEAACWgA/3bYYNOGgAAACwAAAAAKwAQAAACfIyPqcsrD2M0'
+'oAJqa8h29yAkITiG3HWmKWiUrdtpseZdtcfmJSyjvf2Z5Q671u0wA9I+RtLj'
+'ZcwgfxglTTchjqS34JUrCUMQySOzih07Tb62eeneqSfU8vsmf65xZa8S/zI3'
+'dlLD5deRl1dlxdT4MYIA2TBJuSZ2iZkZVgAAOw==')
newButton = tk.Button(root,image=img00)
t = str(img00.width()) + ' wide x ' + str(img00.height()) + ' high'
newButton.pack()
tk.Label(root,text='The image is\n'+t).pack()
root.mainloop() #comment this out to run from IDLE
The only part that you need to copy into your own script is the PhotoImage statement, with or without the "tk." or something similar, depending on how you have imported the Tkinter module. Everything else in the output script is just for testing the PhotoImage statement.
There's nothing fancy here, but it gets the job done cleanly, and it demonstrates how you can add a very simple GUI interface to quick-and-dirty utilities.
Delinquent dimensions: So, you created a carefully sized frame of specified dimensions in which to place various label, button, image, and other objects. Then you placed the first label, packed it, and the frame shrank to surround just that object. You might add some dummy spreader objects, or you might just give up and switch to the grid or place methods for managing the laying out of your interface. However, if you want to stay with the advantages of the pack method, here's how to tell a frame to keep its dimensions, where import Tkinter as tk is in effect:
self.frame = tk.Frame(master,width=400,height=200,relief='raised',border=4)
self.frame.pack_propagate(0) #no shrinking!
self.frame.pack(side='top',fill=None,expand=0)
For the pack_propagate(0) command to work, it appears that the fill and expand attributes also have to be None and 0, respectively. One would think that either statement by itself should prevent size changes. Both are supposed to be the default and so shouldn't have to be stated, but both are usually needed, perhaps to override contrary inheritance from a parent object.
Note that this approach may not work for some Tkinter objects or situations, especially when placing Button widgets (at least not under Windows with Python 2.0). To constrain button size when using the pack manager, you may find that you have to place the button inside a Frame widget and then follow the above steps for the frame. When placed this way, a button may shrink to surround just its label text, so pack it with fill='y'.
File open requester: When passing the filetypes parameter to tkFileDialog.askopenfilename, the correct form is filetypes=[('TAR & tarballs','*.tar *.tar.gz'),('All Files','*.*')]. Look closely and you will see a tiny variance from how the actual dialog appears to users. On Windows ME, the appearance will be "TAR & tarballs (*.tar,*.tar.gz)," with commas and no spaces between the file name extensions. Tkinter wants a space, not a comma, between extensions.
As a courtesy to your users, always include an "All Files (*.*)" option in file name requesters. It can help make navigation a little less frustrating. (But also make sure that your code is protected from the user selecting an inappropriate file type!)
About font tuples, see "Assigning values to tuples."
Mac Tkinter is a subset of what's available in Tkinter for Windows and Linux, and some parts that are available on the three platforms work differently on Mac. Here is a catalog of what we have found on PowerMac with OS9.x in testing code that originated on Win98/ME. Everything that follows here comes from Python 2.0. I'm not aware that anything has changed for Tkinter with Python 2.1 or 2.2 on any platform.
Since Mac Tkinter is more limited, it has been suggested that cross-platform developers would do well to develop their Tkinter code on Mac first. My experience has been starting on Win98/ME and then seeing what works on a colleague's Mac. This has worked very well, learning first what's possible and how to do it, then finding out what problems if any there are on Mac. That leads to workaround branch code or to finding a different approach that works on both platforms. It's a lot of extra effort to be Mac-compatible, but the resulting code comes out better for Windows, too.
The following comments are only for Python 2.0 and its version of Tkinter. These comments also presume that you plan to work with standard installations and are not going to get into modifying and replacing standard Python modules written in Python, Tcl/Tk, or C.
What works differently:
def rgb2hex(r,g,b):
return '#%02X%02X%02X'%(r,g,b)
..
winGray = rgb2hex(204,204,204)
..
if os.name == 'mac':
widget.config(bg=winGray,highlightbackground=winGray)
(The double dots .. mean, "other code goes in here.") If the class is a regular button, only the highlightbackground color should be specified, to avoid tiny white spots at the button's four corners. You can see examples of such steps in the mainWindowClass in pullsdts.py and buttClass in guinter.py (look for if os.name == 'mac').
Many thanks to Cecilia Ziemer for many hours of testing code variations, suffering happily through countless failures, and feeding back with detailed comments and screen shots by Internet. It's difficult to have a non-programmer run proxy crash-and-burn cycles on a machine a thousand miles away, but her detail-orientation and enthusiasm are what has made it possible to include Mac support in this Python project.
See also "What's New on the Project Site."