Page 1 of 1

Tkinter Overview, With a Fixed-Width Grid

#1 andrewsw  Icon User is online

  • bin deployable
  • member icon

Reputation: 6242
  • View blog
  • Posts: 24,943
  • Joined: 12-December 12

Posted 04 March 2015 - 10:14 AM

This tutorial provides an overview of the Tkinter GUI framework. It uses a fixed-width grid and demonstrates the use of a number of Tkinter widgets. That is, it demonstrates some of their main properties and how to bind events to them.

This is at an intermediate level, rather than a tutorial from first steps. Dogstopper has an introductory tutorial sequence:

Basic Tkinter and Python

However, if you are familiar with building GUIs in another language then you should be able to follow this tutorial and get up-to-speed quite quickly.

I emphasize that this is an overview, I am not claiming it to present a perfect model for building a Tkinter GUI.



I'm using Python 3. You can still follow the tutorial if using Python 2, it essentially just requires a few changes to the import statements. In particular, it is from Tkinter import in Python 2, from tkinter import in Python 3. Also, some objects have been moved between tkinter and the ttk sub-module.

(An example of another change is that tkMessageBox is now called messagebox.)



Other Sources:

One of my motiviations to write this tutorial was that I couldn't find a comprehensive and up-to-date tutorial that I liked as a single resource. (Admittedly, I didn't spend hours searching; you might have better luck.)

Some that I found useful, although a little dated, were:

Introduction to Tkinter :zetcode
This was my main resource, particularly because it subclasses Frame.

TkDocs
This is good and quite thorough. It doesn't take a subclassing approach though. Note that there is a drop-down on the right from which you can select the Python language, otherwise you'll have to wade through all the Perl, Tcl, etc., code as well.

An Introduction to Tkinter (Work in Progress)
This is from 2005 and incomplete. Nevertheless, a useful resource.

Python GUI Programming (Tkinter) :tutorialspoint

All of these use Python 2 rather than Python 3.

Why Tkinter?

  • It is part of the core Python modules, no installation required.
  • It is fairly simple.
  • It's cross-platform.
  • It is used with other languages (Tcl, Ruby, Perl).

There are other GUI frameworks though, such as wxPython and JPython, and simplegui is a simplified wrapper around Tkinter.

Tkinter isn't perfect, it has a few quirks! I'll mention some of these as they arise during the tutorial. It also isn't very sophisticated or modern. Nevertheless, it is still used a lot, particularly by schools/colleges, and there are lots of questions asked about it here at dream-in-code.

No Images?

I haven't used any images. To work with images requires the PIL (Python Image Library) for Python 2. In Python 3 this is now the Pillow library (PIL fork). Here is some sample code that isn't used in this tutorial:
from PIL import Image, ImageTk # Python 3 - PIL still means "Pillow"
# ..
        self.img = Image.open("coyote.PNG")
        coyote = ImageTk.PhotoImage(self.img)
        imageLabel = Label(self, image=coyote)
        # store image reference to prevent garbage collection
        imageLabel.image = coyote
        imageLabel.grid(row=4, column=3)


There are other image modules available.




Attached Image

Let's Get Tinkering

Create a file named mytkinter.py. You could call it whatever you like, but NOT tkinter!!

Here is the import statement:
from tkinter import Tk, ttk, Frame, Button, Label, Entry, Text, Checkbutton, \
    Scale, Listbox, Menu, BOTH, RIGHT, RAISED, N, E, S, W, \
    HORIZONTAL, END, FALSE, IntVar, StringVar, messagebox as box


[In Python 2 it is Tkinter, capital T.]

The full code is provided at the end of the tutorial.

As you progress with Tkinter you might end-up just using:
from tkinter import *


For the purpose of this tutorial I like to list all the objects individually; it tells you what will be included in the tutorial and provides a useful summary of the object-names.

tkinter.ttk provides a number of themed widgets and Styles. As mentioned earlier, one of the main differences between Python 2 and 3 is that some widgets have been moved (and possibly slightly renamed) between ttk and tkinter.

The following code occurs at the bottom of the file. (You won't be able to run it yet, because the class Example hasn't been defined.)
def main():
    root = Tk()
    #root.geometry("250x150+300+300")    # width x height + x + y
    # we will use centreWindow instead
    root.resizable(width=FALSE, height=FALSE)
    # .. not resizable
    app = Example(root)
    root.mainloop()

if __name__ == '__main__':
    main()


This is very common code to structure and run a Tkinter application.

root = Tk() This, essentially, initializes the Tkinter engine and provides a root object (widget).

root.geometry("250x150+300+300") This would specify the dimensions and placement of the form/window; instead, I'll use a method to centre the form on the screen.

The terms form, window and frame tend to be used interchangeably. I prefer the term 'form' to describe the entire object that appears on screen, the GUI.

root.resizable(width=FALSE, height=FALSE) I am creating a fixed-width form so there is no reason for the user to resize it.

app = Example(root) This creates an instance of our Example class, the code for which will follow shortly.

root.mainloop() This is essential, entering the main Tkinter event-loop. If we just built a form then it might display very briefly, but the application will immediately exit.

Show Me The Frame

Above the code just discussed type:
class Example(Frame):

    def __init__(self, parent):
        Frame.__init__(self, parent, background="white")
        self.parent = parent
        self.parent.title("Simple Window")
        self.style = ttk.Style()
        self.style.theme_use("default")
        self.centreWindow()
        self.pack(fill=BOTH, expand=1)


We are subclassing Tkinter's Frame.

The parent parameter is the root widget.

Frame.__init__ We initialize the (parent) Tkinter Frame passing a reference to the root widget.

self.parent = parent We store a reference to the root widget.

self.parent.title("Simple Window") The title is set on the parent widget, not the Frame itself.
        self.style = ttk.Style()
        self.style.theme_use("default")


Styles (themes) are in the ttk sub-module. Others are clam, alt and classic (there's probably more). Personally, I didn't notice any difference when trying these (on Windows 8). To be honest, it isn't something that interested me so I'll leave you to investigate other themes.

self.centreWindow() This method will be shown in a moment.

self.pack(fill=BOTH, expand=1) This causes the Frame to occupy the full space available to it.

Geometry Managers

The pack() geometry manager places widgets in either rows or columns, or might take a single widget to fill a container widget, such as a Frame.

The grid() manager is the most flexible, placing widgets within a two-dimensional table. Widgets can span rows and/or columns, and rows/columns can be given weight, a "relative weight used to distribute additional space between rows or columns". This is the manager we will use.

The place() manager organizes widgets by placing them at specific (absolute) positions within their parent widget. This is the most accurate, but also requires the most effort and planning. (In other languages this is sometimes called a canvas.)

self.pack(fill=BOTH, expand=1) It is strongly advised that we do not mix pack() with grid(). This statement packs the frame before we begin to construct our grid. (If you disable this line then you'll see that our grid doesn't appear.) So, don't mix pack() and grid(), but you may need to call pack() once. I'll consider this a quirk of Tkinter.

Fixed Width?

Add the following method within our Frame class (Example):
    def centreWindow(self):
        w = 500
        h = 300
        sw = self.parent.winfo_screenwidth()
        sh = self.parent.winfo_screenheight()
        x = (sw - w)/2
        y = (sh - h)/2
        self.parent.geometry('%dx%d+%d+%d' % (w, h, x, y))


By giving the Frame a fixed width and height, not allowing it to be resized and placing widgets at specific row and column positions we can create a fixed-width form with (more or less..) evenly distributed column-heights and row-widths. As we build the form, if a widget won't fit an (imaginary) cell of the grid we will span it across columns and/or down rows.

The grid won't necessarily be exactly evenly split into rows and columns of the same height and width; it is still down to the Geometry Manager to organize - to manage - the rendering of the form. But, as long as we are sensible when placing widgets, it should behave well.

You can create a flexible (fluid, expandable) grid by, firstly, not specifying a height and width, and using the rowconfigure() and columnconfigure() settings of the grid. I don't explore this approach in this tutorial.

You can probably run the application at this point, although the form won't look very interesting.

What's on the Menu?

Add the following code to the __init__ method:
        menubar = Menu(self.parent)
        self.parent.config(menu=menubar)
        fileMenu = Menu(menubar)
        fileMenu.add_command(label="Exit", command=self.quit)
        menubar.add_cascade(label="File", menu=fileMenu)


This creates a menubar with the single option of File/Exit, which runs an in-built command, quit().

I won't be adding anything else to this Menu. This is just here to demonstrate the essential, initial, steps.

Label, Entry and Text
        firstNameLabel = Label(self, text="First Name")
        firstNameLabel.grid(row=0, column=0, sticky=W+E)
        lastNameLabel = Label(self, text="Last Name")
        lastNameLabel.grid(row=1, column=0, sticky=W+E)
        countryLabel = Label(self, text="Country")
        countryLabel.grid(row=2, column=0, sticky=W+E)
        addressLabel = Label(self, text="Address")
        addressLabel.grid(row=3, column=0, pady=10, sticky=W+E+N)


This is largely self-explanatory: add some Labels and position them on the grid. Notice that we don't specify the number of rows and columns of the grid, this is determined by the geometry manager according to where we position widgets. That is, according to the row and column positions that we use.

sticky=W+E Causes the widget to expand left and right to fill the (imaginary) cell(s) that the widget occupies. Effectively, sticking to the left and right edges of the cell.
        addressLabel.grid(row=3, column=0, pady=10, sticky=W+E+N)


I am stretching (sticky) the Label to touch the left, right and top edges of the cell, but then using vertical (y) padding to push it slightly down from the top edge. This is because the Address textbox (Text widget) that will eventually be added next to it will be much larger then the First and LastName boxes (Entry widgets) (and the Country Combobox), and having it placed at the very top (of the Text) looks a little cramped.



It is possible to add a number of settings to widgets with code similar to this:
for child in mainframe.winfo_children():
    child.grid_configure(padx=5, pady=5)


Alternatively, we could subclass a control such as a Label, customize it, and then add instances of our customized Label. For our tutorial it is more useful to explore padding, sticky, etc., individually for each widget.



        firstNameText = Entry(self, width=20)
        firstNameText.grid(row=0, column=1, padx=5, pady=5, ipady=2, sticky=W+E)
        lastNameText = Entry(self, width=20)
        lastNameText.grid(row=1, column=1, padx=5, pady=5, ipady=2, sticky=W+E)


Entry widgets are more commonly called TextBoxes in other languages/GUI frameworks. The width is the number of characters.

ipady is internal, vertical, padding - within the widget. Try removing this and you'll see that there is no additional space above or below the text that you type in these boxes.

Try adding ipadx. I would expect this to cause there to be a gap on the left of the text that we type in the box. It doesn't. Perhaps there is something that I haven't understood about this behaviour. I've decided to consider it another quirk for the moment. If I discover a reason for it I'll update this tutorial!
        addressText = Text(self, padx=5, pady=5, width=20, height=6)
        addressText.grid(row=3, column=1, padx=5, pady=5, sticky=W)


Text is a multi-line version of the Entry widget, height is the number of lines.

Why do I have to add padding to both the widget itself and the grid? I tried different variations on this but this was the approach that I ended up with.

Combobox
        self.countryVar = StringVar()
        self.countryCombo = ttk.Combobox(self, textvariable=self.countryVar)
        self.countryCombo['values'] = ('United States', 'United Kingdom', 'France')
        self.countryCombo.current(1)
        self.countryCombo.bind("<<ComboboxSelected>>", self.newCountry)
        self.countryCombo.grid(row=2, column=1, padx=5, pady=5, ipady=2, sticky=W)


        self.countryVar = StringVar()


StringVar is a Variable Class. It is how we can communicate values obtained from widgets to our code. What we mustn't do is this:
    self.countryVar = "Doh!"


This will break the connection between the widget, its value, and our code. If we needed to set its value we would use:
    self.countryVar.set("France")


However, we should avoid doing this as much as possible; typically this might be used once, to initialize it with a value. Otherwise, we leave it alone and let the widget set its value.

textvariable=self.countryVar This associates the variable with the widget.
        self.countryCombo.bind("<<ComboboxSelected>>", self.newCountry)


Earlier we used the command parameter to assign the default event for a menu-item (File/Exit). We will do the same to assign a click-event to a Button. For more complex widgets using bind() is how we can associate code with a specific event. Here is the method that is called when the Combobox's selection is changed:
    def newCountry(self, event):
        print(self.countryVar.get())


I'm just printing the name of the selected country to prove that it works.

Some will disagree with my choice of the name newCountry, preferring onselected or perhaps countryCombo_onselected. For this tutorial everything is in a single file, so I'm happy to be a bit more explicit - more obvious - with the name.

Scale (a slider)
        self.salaryVar = StringVar()
        salaryLabel = Label(self, text="Salary:", 
                            textvariable=self.salaryVar)
        salaryLabel.grid(row=0, column=2, columnspan=2, sticky=W+E)
        salaryScale = Scale(self, from_=10000, to=100000, orient=HORIZONTAL,
                            resolution=500, command=self.onSalaryScale)
        salaryScale.grid(row=1, column=2, columnspan=2, sticky=W+E)


A Scale is called a slider in other frameworks. resolution=500 determines how much the value jumps by as the slider is moved.
    def onSalaryScale(self, val):
        self.salaryVar.set("Salary: " + str(val))


Although I've associated the variable with the Label (using textvariable) I want it to say "Salary: 20000" rather than just "20000". [There may be a neater way to achieve this, but I didn't pursue it.]

Checkbutton
        self.fullTimeVar = IntVar()
        fullTimeCheck = Checkbutton(self, text="Full-time?", 
                               variable=self.fullTimeVar, command=self.fullChecked)
        fullTimeCheck.grid(row=2, column=2, columnspan=2, sticky=W+E)
        #fullTimeCheck.select()


    def fullChecked(self):
        if self.fullTimeVar.get() == 1:
            self.parent.title("Simple Window (full-time)")
        else:
            self.parent.title("Simple Window")


The checked-state is represented by the values 1 or 0. To prove that the code works I just change the title of the window.

Listbox

Here is the full code applicable to the Listbox:
        self.titleVar = StringVar()
        self.titleVar.set("TBA")
        Label(self, textvariable=self.titleVar).grid(
            row=4, column=1, sticky=W+E
        )   # a reference to the label is not retained
        
        title = ['Programmer', 'Developer', 'Web Developer', 'Designer']
        titleList = Listbox(self, height=5)
        for t in title:
            titleList.insert(END, t)
        titleList.grid(row=3, column=2, columnspan=2, pady=5, sticky=N+E+S+W)
        titleList.bind("<<ListboxSelect>>", self.newTitle)


    def newTitle(self, val):
        sender = val.widget
        idx = sender.curselection()
        value = sender.get(idx)
        self.titleVar.set(value)


        Label(self, textvariable=self.titleVar).grid(
            row=4, column=1, sticky=W+E
        )   # a reference to the label is not retained


This is a slight variation to the earlier Labels; we don't need to retain a reference to the Label as we won't need to refer to it once its added to the grid. We could have done this with the earlier Labels if we wanted to.

The code for newTitle seems a little long-winded. Maybe it could be shortened. It works, so I didn't bother to explore it further.

Buttons and messagebox

All that remains is to add OK and Close buttons:
        okBtn = Button(self, text="OK", width=10, command=self.onConfirm)
        okBtn.grid(row=4, column=2, padx=5, pady=3, sticky=W+E)
        closeBtn = Button(self, text="Close", width=10, command=self.onExit)
        closeBtn.grid(row=4, column=3, padx=5, pady=3, sticky=W+E)


    def onConfirm(self):
        box.showinfo("Information", "Thank you!")

    def onExit(self):
        self.quit()




The End. Good luck! Here is the full code for reference.
from tkinter import Tk, ttk, Frame, Button, Label, Entry, Text, Checkbutton, \
    Scale, Listbox, Menu, BOTH, RIGHT, RAISED, N, E, S, W, \
    HORIZONTAL, END, FALSE, IntVar, StringVar, messagebox as box

class Example(Frame):

    def __init__(self, parent):
        Frame.__init__(self, parent, background="white")
        self.parent = parent
        self.parent.title("Simple Window")
        self.style = ttk.Style()
        self.style.theme_use("default")
        self.centreWindow()
        self.pack(fill=BOTH, expand=1)
        
        menubar = Menu(self.parent)
        self.parent.config(menu=menubar)
        fileMenu = Menu(menubar)
        fileMenu.add_command(label="Exit", command=self.quit)
        menubar.add_cascade(label="File", menu=fileMenu)

        firstNameLabel = Label(self, text="First Name")
        firstNameLabel.grid(row=0, column=0, sticky=W+E)
        lastNameLabel = Label(self, text="Last Name")
        lastNameLabel.grid(row=1, column=0, sticky=W+E)
        countryLabel = Label(self, text="Country")
        countryLabel.grid(row=2, column=0, sticky=W+E)
        addressLabel = Label(self, text="Address")
        addressLabel.grid(row=3, column=0, pady=10, sticky=W+E+N)
        
        firstNameText = Entry(self, width=20)
        firstNameText.grid(row=0, column=1, padx=5, pady=5, ipady=2, sticky=W+E)
        lastNameText = Entry(self, width=20)
        lastNameText.grid(row=1, column=1, padx=5, pady=5, ipady=2, sticky=W+E)
        
        self.countryVar = StringVar()
        self.countryCombo = ttk.Combobox(self, textvariable=self.countryVar)
        self.countryCombo['values'] = ('United States', 'United Kingdom', 'France')
        self.countryCombo.current(1)
        self.countryCombo.bind("<<ComboboxSelected>>", self.newCountry)
        self.countryCombo.grid(row=2, column=1, padx=5, pady=5, ipady=2, sticky=W)
        
        addressText = Text(self, padx=5, pady=5, width=20, height=6)
        addressText.grid(row=3, column=1, padx=5, pady=5, sticky=W)
        
        self.salaryVar = StringVar()
        salaryLabel = Label(self, text="Salary:", 
                            textvariable=self.salaryVar)
        salaryLabel.grid(row=0, column=2, columnspan=2, sticky=W+E)
        salaryScale = Scale(self, from_=10000, to=100000, orient=HORIZONTAL,
                            resolution=500, command=self.onSalaryScale)
        salaryScale.grid(row=1, column=2, columnspan=2, sticky=W+E)
        
        self.fullTimeVar = IntVar()
        fullTimeCheck = Checkbutton(self, text="Full-time?", 
                               variable=self.fullTimeVar, command=self.fullChecked)
        fullTimeCheck.grid(row=2, column=2, columnspan=2, sticky=W+E)
        #fullTimeCheck.select()
        
        self.titleVar = StringVar()
        self.titleVar.set("TBA")
        Label(self, textvariable=self.titleVar).grid(
            row=4, column=1, sticky=W+E
        )   # a reference to the label is not retained
        
        title = ['Programmer', 'Developer', 'Web Developer', 'Designer']
        titleList = Listbox(self, height=5)
        for t in title:
            titleList.insert(END, t)
        titleList.grid(row=3, column=2, columnspan=2, pady=5, sticky=N+E+S+W)
        titleList.bind("<<ListboxSelect>>", self.newTitle)
        
        okBtn = Button(self, text="OK", width=10, command=self.onConfirm)
        okBtn.grid(row=4, column=2, padx=5, pady=3, sticky=W+E)
        closeBtn = Button(self, text="Close", width=10, command=self.onExit)
        closeBtn.grid(row=4, column=3, padx=5, pady=3, sticky=W+E)
    
    def centreWindow(self):
        w = 500
        h = 300
        sw = self.parent.winfo_screenwidth()
        sh = self.parent.winfo_screenheight()
        x = (sw - w)/2
        y = (sh - h)/2
        self.parent.geometry('%dx%d+%d+%d' % (w, h, x, y))

    def onExit(self):
        self.quit()
    
    def newCountry(self, event):
        print(self.countryVar.get())
    
    def onSalaryScale(self, val):
        self.salaryVar.set("Salary: " + str(val))
    
    def fullChecked(self):
        if self.fullTimeVar.get() == 1:
            self.parent.title("Simple Window (full-time)")
        else:
            self.parent.title("Simple Window")
    
    def newTitle(self, val):
        sender = val.widget
        idx = sender.curselection()
        value = sender.get(idx)
        self.titleVar.set(value)
    
    def onConfirm(self):
        box.showinfo("Information", "Thank you!")

def main():
    root = Tk()
    #root.geometry("250x150+300+300")    # width x height + x + y
    # we will use centreWindow instead
    root.resizable(width=FALSE, height=FALSE)
    # .. not resizable
    app = Example(root)
    root.mainloop()

if __name__ == '__main__':
    main()


This post has been edited by andrewsw: 04 March 2015 - 11:51 AM


Is This A Good Question/Topic? 0
  • +

Replies To: Tkinter Overview, With a Fixed-Width Grid

#2 andrewsw  Icon User is online

  • bin deployable
  • member icon

Reputation: 6242
  • View blog
  • Posts: 24,943
  • Joined: 12-December 12

Posted 05 March 2015 - 11:11 AM

The officially linked docs for Tkinter are:

Tkinter 8.5 reference: a GUI for Python :NEW MEXICO TECH

They contain all the reference material, although it is still Python 2 based.



To change the font in Python 3 add font to the list of named imports and add:
    chosenFont = font.Font(family='Verdana', size=10, weight='normal')

I added the fonts individually using:
    firstNameLabel = Label(self, text="First Name", font=chosenFont)

I don't think we can add this font in one go, to all the widgets, but it should be possible to loop through the widgets applying the font.

In Python 2 I believe it requires:
import tkFont

font = tkFont.Font(option, ...)

Was This Post Helpful? 0
  • +
  • -

Page 1 of 1