A First (Real) Example
With that out of the way, let's try a slightly more substantial example, which will give you an initial feel for what the code behind a real Tk program looks like.
Design
We'll create a simple GUI tool to convert a distance in feet to the equivalent distance in meters. If we were to sketch this out, it might look something like this:
A sketch of our feet to meters conversion program.
So it looks like we have a short text entry widget that will let us type in the number of feet. A "Calculate" button will get the value out of that entry, perform the calculation, and put the result in a label below the entry. We've also got three static labels ("feet," "is equivalent to," and "meters"), which help our user figure out how to work the application.
The next thing we need to do is look at the layout. The widgets we've included seem to be naturally divided into a grid with three columns and three rows. In terms of layout, things seem to naturally divide into three columns and three rows, as illustrated below:
The layout of our user interface, which follows a 3 x 3 grid.
Code
Now here is the Python code needed to create this entire application using Tkinter.
from tkinter import *
from tkinter import ttk
def calculate(*args):
try:
value = float(feet.get())
meters.set(int(0.3048 * value * 10000.0 + 0.5)/10000.0)
except ValueError:
pass
root = Tk()
root.title("Feet to Meters")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
feet = StringVar()
feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet)
feet_entry.grid(column=2, row=1, sticky=(W, E))
meters = StringVar()
ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E))
ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W)
ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W)
ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E)
ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W)
for child in mainframe.winfo_children():
child.grid_configure(padx=5, pady=5)
feet_entry.focus()
root.bind("<Return>", calculate)
root.mainloop()
Now here is the Tcl/Tk code needed to create this entire application.
package require Tk
wm title . "Feet to Meters"
grid [ttk::frame .c -padding "3 3 12 12"] -column 0 -row 0 -sticky nwes
grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1
grid [ttk::entry .c.feet -width 7 -textvariable feet] -column 2 -row 1 -sticky we
grid [ttk::label .c.meters -textvariable meters] -column 2 -row 2 -sticky we
grid [ttk::button .c.calc -text "Calculate" -command calculate] -column 3 -row 3 -sticky w
grid [ttk::label .c.flbl -text "feet"] -column 3 -row 1 -sticky w
grid [ttk::label .c.islbl -text "is equivalent to"] -column 1 -row 2 -sticky e
grid [ttk::label .c.mlbl -text "meters"] -column 3 -row 2 -sticky w
foreach w [winfo children .c] {grid configure $w -padx 5 -pady 5}
focus .c.feet
bind . <Return> {calculate}
proc calculate {} {
if {[catch {
set ::meters [expr {round($::feet*0.3048*10000.0)/10000.0}]
}]!=0} {
set ::meters ""
}
}
vwait forever
Now here is the Ruby/Tk code needed to create this entire application.
require 'tk'
require 'tkextlib/tile'
root = TkRoot.new {title "Feet to Meters"}
content = Tk::Tile::Frame.new(root) {padding "3 3 12 12"}.grid( :sticky => 'nsew')
TkGrid.columnconfigure root, 0, :weight => 1; TkGrid.rowconfigure root, 0, :weight => 1
$feet = TkVariable.new; $meters = TkVariable.new
f = Tk::Tile::Entry.new(content) {width 7; textvariable $feet}.grid( :column => 2, :row => 1, :sticky => 'we' )
Tk::Tile::Label.new(content) {textvariable $meters}.grid( :column => 2, :row => 2, :sticky => 'we');
Tk::Tile::Button.new(content) {text 'Calculate'; command {calculate}}.grid( :column => 3, :row => 3, :sticky => 'w')
Tk::Tile::Label.new(content) {text 'feet'}.grid( :column => 3, :row => 1, :sticky => 'w')
Tk::Tile::Label.new(content) {text 'is equivalent to'}.grid( :column => 1, :row => 2, :sticky => 'e')
Tk::Tile::Label.new(content) {text 'meters'}.grid( :column => 3, :row => 2, :sticky => 'w')
TkWinfo.children(content).each {|w| TkGrid.configure w, :padx => 5, :pady => 5}
f.focus
root.bind("Return") {calculate}
def calculate
begin
$meters.value = (0.3048*$feet*10000.0).round()/10000.0
rescue
$meters.value = ''
end
end
Tk.mainloop
Now here is the Perl/Tkx code needed to create this entire application.
use Tkx;
Tkx::wm_title(".", "Feet to Meters");
Tkx::ttk__frame(".c", -padding => "3 3 12 12");
Tkx::grid( ".c", -column => 0, -row => 0, -sticky => "nwes");
Tkx::grid_columnconfigure( ".", 0, -weight => 1);
Tkx::grid_rowconfigure(".", 0, -weight => 1);
Tkx::ttk__entry(".c.feet", -width => 7, -textvariable => \$feet);
Tkx::grid(".c.feet", -column => 2, -row => 1, -sticky => "we");
Tkx::ttk__label(".c.meters", -textvariable => \$meters);
Tkx::grid(".c.meters", -column => 2, -row => 2, -sticky => "we");
Tkx::ttk__button(".c.calc", -text => "Calculate", -command => sub {calculate();});
Tkx::grid(".c.calc", -column => 3, -row => 3, -sticky => "w");
Tkx::grid( Tkx::ttk__label(".c.flbl", -text => "feet"), -column => 3, -row => 1, -sticky => "w");
Tkx::grid( Tkx::ttk__label(".c.islbl", -text => "is equivalent to"), -column => 1, -row => 2, -sticky => "e");
Tkx::grid( Tkx::ttk__label(".c.mlbl", -text => "meters"), -column => 3, -row => 2, -sticky => "w");
foreach (Tkx::SplitList(Tkx::winfo_children(".c"))) {
Tkx::grid_configure($_, -padx => 5, -pady => 5);
}
Tkx::focus(".c.feet");
Tkx::bind(".", "<Return>", sub {calculate();});
sub calculate {
$meters = int(0.3048*$feet*10000.0+.5)/10000.0 || '';
}
Tkx::MainLoop();
As we'll see in the next chapter, there's another, more object-oriented way to do exactly the same thing. Are we surprised?
And the resulting user interface:
Screenshot of our completed feet to meters user interface.
A Note on Coding Style
Each of the languages included in this tutorial has a variety of coding styles and conventions available to choose from, which help determine conventions for variable and function naming, procedural, functional or object-oriented styles, and so on.
Because the focus on this tutorial is Tk, this tutorial will keep things as simple as possible, generally using a very direct coding style, rather than wrapping up most of our code in procedures, modules, objects, classes and so on. As much as possible, you'll also see the same names for objects, variables, etc. used across the languages for each example.
Step-by-Step Walkthrough
Let's take a closer look at that code, piece by piece. For now, all we're trying to do is get a basic understanding of the types of things we need to do to create a user interface in Tk and roughly what those things look like. We'll go into details later.
Incorporating Tk
Our program starts by incorporating Tk.
from tkinter import *
from tkinter import ttk
These two lines tell Python that our program needs two modules. The first, tkinter
, is the
standard binding to Tk. When imported, it loads the Tk library on your system.
The second, ttk
, is a submodule of tkinter
. It implements Python's binding to the
newer "themed widgets" that were added to Tk in 8.5.
Notice that we've imported everything (*
) from the tkinter module. That way, we can call tkinter functions, etc., without
prefixing them with the module name. This is standard Tkinter practice.
However, because we've imported just ttk
itself, we'll need to prefix anything inside that submodule. For example, calling Entry(...)
would refer to the Entry
class inside the tkinter
module (classic widgets). We'd need ttk.Entry(...)
to use the Entry
class inside ttk
(themed widgets).
As you'll see, several classes are defined in both modules.
Sometimes you will need one or the other, depending on the context. Explicitly requiring the ttk
prefix facilitates this and will be the style used
in this tutorial.
One of the first things you'll find if you're migrating Python 2.x code is that the name of the Tkinter module
is now lowercase, i.e., tkinter
, rather than Tkinter
. In Python 2.x, Ttk
was also its own module, not a sub-module of Tkinter
.
package require Tk
First thing we do is tell Tcl that our program needs Tk. Though not strictly necessary, it's considered good form to include this line. It can also be used to specify exactly what version of Tk is needed.
require 'tk'
require 'tkextlib/tile'
These two lines tell Ruby that our program needs two packages. The first, tk
, is the
Ruby binding to Tk, which, when loaded, also causes the existing Tk library on your system to be loaded.
The second, tkextlib/tile
, is Ruby Tk's binding to the newer "themed widgets" that were added to Tk
in 8.5.
The themed widget set evolved out of an earlier Tk add-on called Tile, hence the nomenclature. Despite that, the Tk::Tile::* calls you'll see in the programs are actually using the proper ttk versions in 8.5. Expect this to get better rationalized in the future.
use Tkx;
The first thing that we need to do is tell Perl to load the "Tkx" module, which provides the Perl interface to Tk that we are using.
As mentioned here, there are other Perl bindings to Tk. However, we very strongly recommend using Tkx for development, and that will be the only binding we will be describing here. Tkx has the advantage of being a very thin layer above Tk's native Tcl API, which means that in almost all cases it automatically tracks the latest changes to Tk (something which became a considerable issue with Perl/Tk, which was extremely popular in earlier years, but has not been recently updated). As well, by avoiding introducing another layer of code, API errors are reduced, and we can also take advantage of available reference documentation for Tk (which is usually Tcl oriented).
If you set the global variable $Tkx::TRACE
to any true value, Tkx will print to stderr
all calls into Tcl of the translated Tkx commands. This is very useful for debugging.
Setting up the Main Application Window
Next, the following code sets up the main application window, giving it the title "Feet to Meters."
root = Tk()
root.title("Feet to Meters")
Yes, the calculate
function appeared before this. We'll describe it below but need to include it
near the start because we reference it in other parts of the program.
wm title . "Feet to Meters"
root = TkRoot.new {title "Feet to Meters"}
Tkx::wm_title(".", "Feet to Meters");
Creating a Content Frame
Next, we create a frame widget, which will hold the contents of our user interface.
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
grid [ttk::frame .c -padding "3 3 12 12"] -column 0 -row 0 -sticky nwes
grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1
content = Tk::Tile::Frame.new(root) {padding "3 3 12 12"}.grid(:sticky => 'nsew')
TkGrid.columnconfigure root, 0, :weight => 1; TkGrid.rowconfigure root, 0, :weight => 1
Tkx::ttk__frame(".c", -padding => "3 3 12 12");
Tkx::grid( ".c", -column => 0, -row => 0, -sticky => "nwes");
Tkx::grid_columnconfigure( ".", 0, -weight => 1);
Tkx::grid_rowconfigure(".", 0, -weight => 1);
After the frame is created, grid
places it directly inside our main application window.
The columnconfigure/rowconfigure
bits tell Tk that the frame should expand to fill any extra space
if the window is resized.
Why do we put a frame inside the main window? Strictly speaking, we could just put the other widgets in our interface directly into the main application window without the intervening content frame. That's what you'll see in older Tk programs.
However, the main window isn't itself part of the newer "themed" widgets. Its background color doesn't match the themed widgets we will put inside it. Using a "themed" frame widget to hold the content ensures that the background is correct. This is illustrated below.
Placing a themed frame inside a window.
On macOS, where this problem is most prominent, you can also set the window's background color (via its background
configuration option) to the predefined color systemWindowHeaderBackground
.
Creating the Entry Widget
The first widget we'll create is the entry to input the number of feet to convert.
feet = StringVar()
feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet)
feet_entry.grid(column=2, row=1, sticky=(W, E))
grid [ttk::entry .c.feet -width 7 -textvariable feet] -column 2 -row 1 -sticky we
$feet = TkVariable.new;
f = Tk::Tile::Entry.new(content) {width 7; textvariable $feet}.grid( :column => 2, :row => 1, :sticky => 'we' )
Tkx::ttk__entry(".c.feet", -width => 7, -textvariable => \$feet);
Tkx::grid(".c.feet", -column => 2, -row => 1, -sticky => "we");
We need to do two things: create the widget itself and then place it onscreen.
When we create a widget, we need to specify its parent. That is the widget that the new widget will be placed inside.
In this case, we want our entry placed inside the content frame. Our entry, and other widgets we'll create shortly, are said to be
children of the content frame. In Tcl and Perl, the widget name is used to specify the parent-child relationship, i.e. .c.feet
is a child of .c
. In Python and Ruby, the parent
is passed as the first parameter when instantiating a widget object.
When we create a widget, we can optionally provide it with certain configuration options.
Here, we specify how wide we want the entry to appear, i.e., 7 characters.
We also assign it a mysterious textvariable
; we'll see what that does shortly.
When widgets are created, they don't automatically appear on the screen; Tk doesn't know
where you want them placed relative to other widgets. That's what the grid
part does.
Remember the layout grid when we sketched out our application? Widgets are placed in the appropriate
column (1, 2, or 3) and row (also 1, 2, or 3).
The sticky
option to grid describes how the widget should
line up within the grid cell, using compass directions. So w
(west) means to anchor the widget
to the left side of the cell, we
(west-east) means to attach it to both the left and right sides,
and so on. Python also defines constants for these directional strings, which you can provide as a list, e.g. W
or (W, E)
.
Creating the Remaining Widgets
We then do exactly the same thing for the remaining widgets. We have one label that will display the resulting number of meters that we calculate. We have a "Calculate" button that is pressed to perform the calculation. Finally, we have three static text labels to make it clear how to use the application. For each of these widgets, we first create it and then place it onscreen in the appropriate cell in the grid.
meters = StringVar()
ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E))
ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W)
ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W)
ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E)
ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W)
grid [ttk::label .c.meters -textvariable meters] -column 2 -row 2 -sticky we
grid [ttk::button .c.calc -text "Calculate" -command calculate] -column 3 -row 3 -sticky w
grid [ttk::label .c.flbl -text "feet"] -column 3 -row 1 -sticky w
grid [ttk::label .c.islbl -text "is equivalent to"] -column 1 -row 2 -sticky e
grid [ttk::label .c.mlbl -text "meters"] -column 3 -row 2 -sticky w
$meters = TkVariable.new
Tk::Tile::Label.new(content) {textvariable $meters}.grid( :column => 2, :row => 2, :sticky => 'we');
Tk::Tile::Button.new(content) {text 'Calculate'; command {calculate}}.grid( :column => 3, :row => 3, :sticky => 'w')
Tk::Tile::Label.new(content) {text 'feet'}.grid( :column => 3, :row => 1, :sticky => 'w')
Tk::Tile::Label.new(content) {text 'is equivalent to'}.grid( :column => 1, :row => 2, :sticky => 'e')
Tk::Tile::Label.new(content) {text 'meters'}.grid( :column => 3, :row => 2, :sticky => 'w')
Tkx::ttk__label(".c.meters", -textvariable => \$meters);
Tkx::grid(".c.meters", -column => 2, -row => 2, -sticky => "we");
Tkx::ttk__button(".c.calc", -text => "Calculate", -command => sub {calculate();});
Tkx::grid(".c.calc", -column => 3, -row => 3, -sticky => "w");
Tkx::grid( Tkx::ttk__label(".c.flbl", -text => "feet"), -column => 3, -row => 1, -sticky => "w");
Tkx::grid( Tkx::ttk__label(".c.islbl", -text => "is equivalent to"), -column => 1, -row => 2, -sticky => "e");
Tkx::grid( Tkx::ttk__label(".c.mlbl", -text => "meters"), -column => 3, -row => 2, -sticky => "w");
Adding Some Polish
We then put a few finishing touches on our user interface.
for child in mainframe.winfo_children():
child.grid_configure(padx=5, pady=5)
feet_entry.focus()
root.bind("<Return>", calculate)
foreach w [winfo children .c] {grid configure $w -padx 5 -pady 5}
focus .c.feet
bind . <Return> {calculate}
TkWinfo.children(content).each {|w| TkGrid.configure w, :padx => 5, :pady => 5}
f.focus
root.bind("Return") {calculate}
foreach (Tkx::SplitList(Tkx::winfo_children(".c"))) { Tkx::grid_configure($_, -padx => 5, -pady => 5); }
Tkx::focus(".c.feet");
Tkx::bind(".", "<Return>", sub {calculate();});
The first part walks through all of the widgets contained within our content frame
and adds a little bit of padding around each so they aren't so scrunched together.
(We could have added these options to each grid
call when we first put the widgets
onscreen, but this is a nice shortcut.)
The second part tells Tk to put the focus on our entry widget. That way, the cursor will start in that field, so users don't have to click on it before starting to type.
The third line tells Tk that if a user presses the Return key (Enter on Windows), it should call our calculate routine, the same as if they pressed the Calculate button.
Performing the Calculation
Speaking of which, here we define our calculate procedure. It's called when a user presses the Calculate button or hits the Return key. It performs the feet to meters calculation.
def calculate(*args):
try:
value = float(feet.get())
meters.set(int(0.3048 * value * 10000.0 + 0.5)/10000.0)
except ValueError:
pass
proc calculate {} {
if {[catch {
set ::meters [expr {round($::feet*0.3048*10000.0)/10000.0}]
}]!=0} {
set ::meters ""
}
}
def calculate
begin
$meters.value = (0.3048*$feet*10000.0).round()/10000.0
rescue
$meters.value = ''
end
end
sub calculate {
$meters = int(0.3048*$feet*10000.0+.5)/10000.0 || '';
}
As you can clearly see, this routine takes the number of feet from our entry widget, does the calculation, and places the result in our label widget.
Say what? It doesn't look like we're doing anything with those widgets! Here's where
the magic textvariable
options we specified when creating the widgets come into play.
We specified the global variable feet
as the textvariable for the entry.
Whenever the entry changes, Tk will automatically update the global variable feet
.
Similarly, if we explicitly change the value of a textvariable associated with a widget
(as we're doing for meters
which is attached to our label), the widget will automatically
be updated with the current contents of the variable.
For Python, the only caveat is that these variables must be an instance of the StringVar
class.
Slick.
The multiplying and dividing by 10000.0 is to avoid the rounding problems inherent in floating-point math.
A simple calculation, e.g., 0.3048*1.5
, could result in a number like
0.45720000000000005
, which would neither be correct or visually appealing when displayed.
There are other ways to do this. of course.
Start the Event Loop
Finally, we need to tell Tk to enter its event loop, which is necessary for everything to appear onscreen and allow users to interact with it.
root.mainloop()
vwait forever
We actually don't need to explicitly provide this (most of the time), as wish
will automatically enter the event loop
after it's read your script.
Tk.mainloop
Tkx::MainLoop();
What's Missing
We've now seen how to create widgets, put them onscreen, and respond to users interacting with them. It's certainly not fancy, could probably do with some error checking, but it's a fully functional GUI application.
It's also worth examining what we didn't have to include in our Tk program to make it work. For example:
- we didn't have to worry about redrawing the screen as things changed
- we didn't have to worry about parsing and dispatching events, hit detection, or handling events on each widget
- we didn't have to provide a lot of options when we created widgets; the defaults seemed to take care of most things, and so we only had to change things like the text the button displayed
- we didn't have to write complex code to get and set the values of simple widgets; we just attached them to variables
- we didn't have to worry about what happens when users close the window or resizes it
- we didn't need to write extra code to get this all to work cross-platform
One More Thing...
As this tutorial emphasizes Tkinter, our examples use standalone script code, global variables, and simple functions. In practice, you'll likely organize anything beyond the simplest scripts in functions or classes. There are different ways to do this: using modules, creating classes for different parts of the user interface, inheriting from Tkinter classes, etc.
Often though, you just want to do something simple to encapsulate your data rather than putting everything into the global variable space. Here is the
feet to meters example, rewritten to encapsulate the main code into a class. Note the use of self
on callbacks (which execute at the global scope)
and StringVar
's.
from tkinter import *
from tkinter import ttk
class FeetToMeters:
def __init__(self, root):
root.title("Feet to Meters")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
self.feet = StringVar()
feet_entry = ttk.Entry(mainframe, width=7, textvariable=self.feet)
feet_entry.grid(column=2, row=1, sticky=(W, E))
self.meters = StringVar()
ttk.Label(mainframe, textvariable=self.meters).grid(column=2, row=2, sticky=(W, E))
ttk.Button(mainframe, text="Calculate", command=self.calculate).grid(column=3, row=3, sticky=W)
ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W)
ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E)
ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W)
for child in mainframe.winfo_children():
child.grid_configure(padx=5, pady=5)
feet_entry.focus()
root.bind("<Return>", self.calculate)
def calculate(self, *args):
try:
value = float(self.feet.get())
self.meters.set(int(0.3048 * value * 10000.0 + 0.5)/10000.0)
except ValueError:
pass
root = Tk()
FeetToMeters(root)
root.mainloop()
Spotted a mistake? Couldn't find what you were looking for? Suggestions? Let me know!
If you've found this tutorial useful, please check out Modern Tkinter.