Even though the Tcl/Tk-based UI module called tkinter is a fairly standard part of the community and ActivePython distributions and has been for ages, it often gets ignored by developers who have come to Python from other directions.
My own background is mostly in the C/C++ world, where I had deep experience with Qt and wxWidgets. That made it natural and easy to focus on wxPython when I started using Python ten years ago. I knew tkinter existed, but didn’t pay it much attention.
I thought Tcl/Tk was old, odd, and obscure, which turns out not to be the best technical judgment I’ve ever made.
In modern Tcl/Tk, tkinter is simple, powerful, and fast. It looks pretty good too. Modern Tk includes a styling mechanism via themed widgets that allow it to be made even prettier.
Comparing wxPython and tkinter
Comparing wxPython and tkinter using basic examples we get a sense of what they look like, in code and on-screen. I don’t mean to knock wxPython, which is powerful, well-documented, and has been my go-to Python UI solution for many years. But tkinter has some features I really like, particularly the way it handles layout management.
Here are screenshots of two similar examples, one using tkinter (taken from: http://www.tkdocs.com/tutorial/grid.html), one using wxPython (slightly modified from: http://zetcode.com/wxpython/layout/):
They have comparable controls and layouts. The wxPython version is 76 lines and the tkinter version is 48 lines, most of which is accounted for by layout code.
The wxPython example uses nested HBOX and VBOX sizers, which is my preferred way to handle layout using that toolkit because I find it easier to reason about, and therefore, easier to maintain and modify. The tkinter example uses a grid layout, and this does account for some of the difference in program length. However, it also points to quite a different design choice between the two toolkits: wxPython externalizes the layout classes in its sizer hierarchy, whereas tkinter internalizes layout so that each widget manages its own children using a variety of policies, of which grid is just one.
UI layout in wxPython is not a lot of fun, and I’ve never found GUI builders–from DialogBlocks to BoaConstructor and beyond–to be much help. Managing the parallel hierarchies of sizers and windows adds complexity without a lot of additional functionality – in that delightful 1990’s object-oriented way that seemed like such a good idea to all of us at the time.
tkinter does away with all that by hiding layout policy behind the widget interface. You just add children to their parent’s grid. You don’t have to create the grid sizer, add the children to it, and then set it as the sizer on the parent. This inevitably creates a bunch of names like “gridSizer1” along the way that you’ll regret when it comes time to edit the UI code.
Then there is the speed comparison. For example, how long does it take from typing
at the command prompt to the UI being shown on the screen? The difference is negligible on a desktop machine, but on a little embedded ARM processor the wall-clock seconds for these simple example programs come out like this:
wxPython: 6 seconds
tkinter: 1 second
It wasn’t the world’s most sophisticated test–I just used my watch for the timings–but the difference is so big it doesn’t have to be. The canonical absolute response time that users are willing to tolerate is 2 seconds, going from three times that to less than half is kind of a big deal.
I’ve done a lot of embedded work over the years, and even got wxX11 to compile on an ARM board that has almost as much computing power as a toaster. The C++ version is fast enough that users don’t experience perceptible lag. I’ve run some wxPython-based tools on the same system (maintenance scripts for field engineers, who will tolerate anything) and have always been disappointed at how slow they were. I would love to re-write the main application UI in Python and let C++ do all the low-level stuff underneath, but it just didn’t seem practical given even wxGTK/C++ was unacceptably slow on that board.
I’ve looked at a lot of different toolkits for embedded UI over the years: Qt Embedded, GTK and GTK+, FLTK, and so on. None of them met the criteria of power, maturity, i18n/l10n, and speed that I needed for embedded systems running on boards in the “Raspberry Pi or a bit smaller” category.
Now, I feel like the search may be over, and the solution was right under my nose the whole time. I just never paid attention to it because of an outdated and incorrect attitude toward Tcl/Tk and tkinter.
If you haven’t had a chance to try ActivePython it comes pre-compiled with all the most popular Python packages and is free to use in development and testing.
NOTE: In modern Python the old Tkinter module has been renamed tkinter, and the ttk module has become a submodule of it, so when legacy code did
from Tkinter import *
, modern (Python 3) code will do
from tkinter import *
. Also, if you get an error on these imports saying the module
could not be loaded, your Python install can’t find _tkinter.so or equivalent on its LD_LIBRARY_PATH or equivalent, which may be an issue with your environment settings or a problem with your Python build. I had to build Python 2.7.10 from scratch on ARM and then do some fiddling to get _tkinter.so to build. Watch out for error messages during the build and note that tkinter is built as part of “make install” not “make”, at least on Linux. The build can have trouble finding the Tcl and Tk headers and shared libs if your paths aren’t set correctly Setting CPPFLAGS, LDFLAGS and LD_LIBRARY_PATH as described in the answer to this question on Stackoverflow may help.
Title photo courtesy of Hitesh Choudhary on Unsplash.