23 min read

(For more resources related to this topic, see here.)

Mission Briefing

The topics covered here include:

  • Tracing Tkinter variables
  • Widget traversal
  • Validating user input
  • Formatting widget data
  • More on fonts
  • Working with Unicode characters
  • Tkinter class hierarchy
  • Custom-made mixins
  • Tips for code cleanup and program optimization
  • Distributing the Tkinter application
  • Limitations of Tkinter
  • Tkinter alternatives
  • Getting interactive help
  • Tkinter in Python 3. x

Tracing Tkinter variables

When you specify a Tkinter variable as a textvariable for a widget (textvariable = myvar), the widget automatically gets updated whenever the value of the variable changes. However, there might be times when, in addition to updating the widget, you need to do some extra processing at the time of reading or writing (or modifying) the variable.

Tkinter provides a method to attach a callback method that would be triggered every time the value of a variable is accessed. Thus, the callback acts as a variable observer. The callback method is named trace_variable(self, mode, callback), or simply trace(self, mode, callback).

The mode argument can take any one of ‘r’, ‘w’, ‘u’ values, which stand for read, write, or undefined. Depending upon the mode specifications, the callback method is triggered if the variable is read or written.

The callback method gets three arguments by default. The arguments in order of their position are:

  • Name of the Tkinter variable
  • The index of the variable, if the Tkinter variable is an array, else an empty string
  • The access modes (‘w’, ‘r’, or ‘u’)

Note that the triggered callback function may also modify the value of the variable. This modification does not, however, trigger any additional callbacks.

Let’s see a small example of variable tracing in Tkinter, where writing into the Tkinter variable into an entry widget triggers a callback function (refer to the 8.01 trace variable.py Python file available in the code bundle):

from Tkinter import * root = Tk() myvar = StringVar() def trace_when_myvar_written(var,indx,mode): print"Traced variable %s"%myvar.get() myvar.trace_variable("w", trace_when_myvar_written) Label(root, textvariable=myvar).pack(padx=5, pady=5) Entry(root, textvariable=myvar).pack(padx=5, pady=5) root.mainloop()

The description of the preceding code is as follows:

  • This code creates a trace variable on the Tkinter variable myvar in the write (“w”) mode
  • The trace variable is attached to a callback method named trace_when_myvar_written (this means that every time the value of myvar is changed, the callback method will be triggered)

Now, every time you write into the entry widget, it modifies the value of myvar. Because we have set a trace on myvar, it triggers the callback method, which in our example, simply prints the new value into the console.

The code creates a GUI window similar to the one shown here:

It also produces a console output in IDLE, which shows like the following once you start typing in the GUI window:

Traced variable T Traced variable Tr Traced variable Tra Traced variable Trac Traced variable Traci Traced variable Tracin Traced variable Tracing

The trace on a variable is active until it is explicitly deleted. You can delete a trace using:

trace_vdelete(self, mode, callbacktobedeleted) The trace method returns the name of the callback method. This can
be used to get the name of the callback method
that is to be deleted.

Widget traversal

When a GUI has more than one widget, a given widget can come under focus by an explicit mouse-click on the widget. Alternatively, the focus can be shifted to another given widget by pressing the Tab key on the keyboard in the order the widgets were created in the program.

It is therefore vital to create widgets in the order we want the user to traverse through them, or else the user will have a tough time navigating between the widgets using the keyboard.

Different widgets are designed to behave differently to different keyboard strokes. Let’s therefore spend some time trying to understand the rules of traversing through widgets using the keyboard.

Let’s look at the code of the 8.02 widget traversal.py Python file to understand the keyboard traversal behavior for different widgets. Once you run the mentioned .py file, it shows a window something like the following:

The code is simple. It adds an entry widget, a few buttons, a few radio buttons, a text widget, and a scale widget. However, it also demonstrates some of the most important keyboard traversal behaviors for these widgets.

Here are some important points to note (refer to 8.02 widget traversal.py):

  • The Tab key can be used to traverse forward, and Shift + Tab can be used to traverse backwards.
  • The text widget cannot be traversed using the Tab key. This is because the text widget can contain tab characters as its content. Instead, the text widget can be traversed using Ctrl + Tab.
  • Buttons on the widget can be pressed using the spacebar. Similarly, check buttons and radio buttons can also be toggled using the spacebar.
  • You can go up and down the items in a Listbox widget using the up and down arrows.
  • The Scale widget responds to both the left and right keys or up and down keys. Similarly, the Scrollbar widget responds to both the left/right or up/down keys, depending on their orientation.
  • Most of the widgets (except Frame, Label, and Menus) get an outline by default when they have the focus set on them. This outline normally displays as a thin black border around the widget. You can even set the Frame and Label widgets to show this outline by specifying the highlightthickness option to a non-zero Integer value for these widgets.
  • We change the color of the outline using highlightcolor= ‘red’ in our code.
  • Frame, Label, and Menu are not included in the tab navigation path. However, they can be included in the navigation path by using the takefocus = 1 option. You can explicitly exclude a widget from the tab navigation path by setting the takefocus= 0 option.
  • The Tab key traverses widgets in the order they were created. It visits a parent widget first (unless it is excluded using takefocus = 0) followed by all its children widgets.
  • You can use widget.focus_force() to force the input focus to the widget.

Validating user input

Let’s now discuss input data validation.

Most of the applications we have developed in this article are point and click-based (drum machine, chess, drawing application), where validation of user input is not required.

However, data validation is a must in programs like our phonebook application, where the user enters some data, and we store it in a database.

Ignoring the user input validation can be dangerous in such applications because input data can be misused for SQL injection. In general, any application where an user can enter textual data, is a good candidate for validating user input. In fact, it is almost considered a maxim not to trust user inputs.

A wrong user input may be intentional or accidental. In either case, if you fail to validate or sanitize the data, you may cause unexpected error in your program. In worst cases, user input can be used to inject harmful code that may be capable of crashing a program or wiping out an entire database.

Widgets such as Listbox, Combobox, and Radiobuttons allow limited input options, and hence, cannot normally be misused to input wrong data. On the other hand, widgets such as Entry widget, Spinbox widget, and Text widget allow a large possibility of user inputs, and hence, need to be validated for correctness.

To enable validation on a widget, you need to specify an additional option of the form validate = ‘validationmode’ to the widget.

For example, if you want to enable validation on an entry widget, you begin by specifying the validate option as follows:

Entry( root, validate="all", validatecommand=vcmd)

The validation can occur in one of the following validation modes:

Validation Mode

Explanation

none

This is the default mode. No validation occurs if validate is set to “none”

focus

When validate is set to “focus“, the validate command is called twice; once when the widget receives focus and once when the focus is lost

focusin

The validate command is called when the widget receives focus

focusout

The validate command is called when the widget loses focus

key

The validate command is called when the entry is edited

all

The validate command is called in all the above cases

The code of the 8.03 validation mode demo.py file demonstrates all these validation modes by attaching them to a single validation method. Note the different ways different Entry widgets respond to different events. Some Entry widgets call the validation method on focus events while others call the validation method at the time of entering key strokes into the widget, while still others use a combination of focus and key events.

Although we did set the validation mode to trigger the validate method, we need some sort of data to validate against our rules. This is passed to the validate method using percent substitution. For instance, we passed the mode as an argument to our validate method by performing a percent substitution on the validate command, as shown in the following:

vcmd = (self.root.register(self.validate), '%V')

We followed by passing the value of v as an argument to our validate method:

def validate(self, v)

In addition to %V, Tkinter recognizes the following percent substitutions:

Percent substitutions

Explanation

%d

Type of action that occurred on the widget-1 for insert, 0 for delete, and -1 for focus, forced, or textvariable validation.

%i

Index of char string inserted or deleted, if any, else it will be -1.

%P

The value of the entry if the edit is allowed. If you are configuring the Entry widget to have a new textvariable, this will be the value of that textvariable.

%s

The current value of entry, prior to editing.

%S

The text string being inserted/deleted, if any, {} otherwise.

%v

The type of validation currently set.

%V

The type of validation that triggered the callback method (key, focusin,  focusout, and forced).

%W

The name of the Entry widget.

These validations provide us with the necessary data we can use to validate the input.

Let’s now pass all these data and just print them through a dummy validate method just to see the kind of data we can expect to get for carrying out our validations (refer to the code of 8.04 percent substitutions demo.py):

Take particular note of data returned by %P and %s, because they pertain to the actual data entered by the user in the Entry widget.

In most cases, you will be checking either of these two data against your validation rules.

Now that we have a background of rules of data validation, let’s see two practical examples that demonstrate input validation.

Key Validation

Let’s assume that we have a form that asks for a user’s name. We want the user to input only alphabets or space characters in the name. Thus, any number or special character is not to be allowed, as shown in the following screenshot of the widget:

This is clearly a case of ‘key’ validation mode, because we want to check if an entry is valid after every key press. The percent substitution that we need to check is %S, because it yields the text string being inserted or deleted in the Entry widget. Accordingly, the code that validates the entry widget is as follows (refer to 8.05 key validation.py):

import Tkinter as tk class KeyValidationDemo(): def __init__(self): root = tk.Tk() tk.Label(root, text='Enter your name').pack() vcmd = (root.register(self.validate_data), '%S') invcmd = (root.register(self.invalid_name), '%S') tk.Entry(root, validate="key", validatecommand=vcmd,
invalidcommand=invcmd).pack(pady=5, padx=5)
self.errmsg = tk.Label(root, text= '', fg='red') self.errmsg.pack() root.mainloop() def validate_data (self, S): self.errmsg.config(text='') return (S.isalpha() or S =='') # always return True or False def invalid_name (self, S): self.errmsg.config(text='Invalid characters n name canonly have
alphabets'%S) app= KeyValidationDemo()

The description of the preceding code is as follows:

  • We first register two options validatecommand (vcmd) and invalidcommand (invcmd).
  • In our example, validatecommand is registered to call the validate_data method, and the invalidcommand option is registered to call another method named invalid_name.
  • The validatecommand option specifies a method to be evaluated which would validate the input. The validation method must return a Boolean value, where a True signifies that the data entered is valid, and a False return value signifies that data is invalid.
  • If the validate method returns False (invalid data), no data is added to the Entry widget and the script registered for invalidcommand is evaluated. In our case, a False validation would call the invalid_name method. The invalidcommand method is generally responsible for displaying error messages or setting back the focus to the Entry widget.

Let’s look at the code register(self, func, subst=None, needcleanup=1).

The register method returns a newly created Tcl function. If this function is called, the Python function func is executed. If an optional function subst is provided it is executed before func.

Focus Out Validation

The previous example demonstrated validation in ‘key’ mode. This means that the validation method was called after every key press to check if the entry was valid.

However, there are situations when you might want to check the entire string entered into the widget, rather than checking individual key stroke entries.

For example, if an Entry widget accepts a valid e-mail address, we would ideally like to check the validity after the user has entered the entire e-mail address, and not after every key stroke entry. This would qualify as validation in ‘focusout’ mode.

Check out the code of 8.06 focus out validation.py for a demonstration on e-mail validation in the focusout mode:

import Tkinter as tk import re class FocusOutValidationDemo(): def __init__(self): self.master = tk.Tk() self.errormsg = tk.Label(text='', fg='red') self.errormsg.pack() tk.Label(text='Enter Email Address').pack() vcmd = (self.master. register(self.validate_email), '%P' ) invcmd = (self.master. register(self.invalid_email), '%P' ) self.emailentry = tk.Entry(self.master, validate ="focusout", validatecommand=vcmd , invalidcommand=invcmd ) self.emailentry.pack() tk.Button(self.master, text="Login").pack() tk.mainloop() def validate_email(self, P): self.errormsg.config(text='') x = re.match(r"[^@]+@[^@]+.[^@]+", P) return (x != None)# True(valid email)/False(invalid email) def invalid_email(self, P): self.errormsg.config(text='Invalid Email Address') self.emailentry.focus_set() app = FocusOutValidationDemo()

The description of the preceding code is as follows:

The code has a lot of similarities to the previous validation example. However, note the following differences:

  • The validate mode is set to ‘focusout’ in contrast to the ‘key’ mode in the previous example. This means that the validation would be done only when the Entry widget loses focus.
  • This program uses data provided by the %P percentage substitution, in contrast to %S, as used in the previous example. This is understandable as %P provides the value entered in the Entry widget, but %S provides the value of the last key stroke.
  • This program uses regular expressions to check if the entered value corresponds to a valid e-mail format. Validation usually relies on regular expressions and a whole lot of explanation to cover this topic, but it is out of the scope of this project and the article. For more information on regular expression modules, visit the following link:

    http://docs.python.org/2/library/re.html

This concludes our discussion on input validation in Tkinter. Hopefully, you should now be able to implement input validation to suit your custom needs.

Formatting widget data

Several input data such as date, time, phone number, credit card number, website URL, IP number, and so on have an associated display format. For instance, date is better represented in a MM/DD/YYYY format.

Fortunately, it is easy to format the data in the required format as the user enters them in the widget (refer to 8.07 formatting entry widget to display date.py). The mentioned Python file formats the user input automatically to insert forward slashes at the required places to display user-entered date in the MM/DD/YYYY format.

from Tkinter import * class FormatEntryWidgetDemo: def __init__(self, root): Label(root, text='Date(MM/DD/YYYY)').pack() self.entereddata = StringVar() self.dateentrywidget =Entry(textvariable=self.entereddata) self.dateentrywidget.pack(padx=5, pady=5) self.dateentrywidget.focus_set() self.slashpositions = [2, 5] root.bind('<Key>', self.format_date_entry_widget) def format_date_entry_widget(self, event): entrylist = [c for c in self.entereddata.get() if c != '/'] for pos in self.slashpositions: if len(entrylist) > pos: entrylist.insert(pos, '/') self.entereddata.set(''.join(entrylist)) # Controlling cursor cursorpos = self.dateentrywidget.index(INSERT) for pos in self.slashpositions: if cursorpos == (pos + 1): # if cursor is on slash cursorpos += 1 if event.keysym not in ['BackSpace', 'Right', 'Left','Up', 'Down']: self.dateentrywidget.icursor(cursorpos) root = Tk() FormatEntryWidgetDemo(root) root.mainloop()

The description of the preceding code is as follows:

  • The Entry widget is bound to the key press event, where every new key press calls the related callback format_date_entry_widget method.
  • First, the format_date_entry_widget method breaks down the entered text into an equivalent list by the name entrylist, also ignoring any slash ‘/’ symbol if entered by the user.
  • It then iterates through the self.slashpositions list and inserts the slash symbol at all required positions in the entrylist argument. The net result of this is a list that has slash inserted at all the right places.
  • The next line converts this list into an equivalent string using join(), and then sets the value of our Entry widget to this string. This ensures that the Entry widget text is formatted into the aforementioned date format.
  • The remaining pieces of code simply control the cursor to ensure that the cursor advances by one position whenever it encounters a slash symbol. It also ensures that key presses, such as ‘BackSpace’, ‘Right’, ‘Left’, ‘Up’, and ‘Down’ are handled properly.

Note that this method does not validate the date value and the user may add any invalid date. The method defined here will simply format it by adding forward slash at third and sixth positions. Adding date validation to this example is left as an exercise for you to complete.

This concludes our brief discussion on formatting data within widgets. Hopefully, you should now be able to create formatted widgets for a wide variety of input data that can be displayed better in a given format.

More on fonts

Many Tkinter widgets let you specify custom font specifications either at the time of widget creation or later using the configure() option. For most cases, default fonts provide a standard look and feel. However, should you want to change font specifications, Tkinter lets you do so. There is one caveat though.

When you specify your own font, you need to make sure it looks good on all platforms where the program is intended to be deployed. This is because a font might look good and match well on a particular platform, but may look awful on another. Unless you know what you are doing, it is always advisable to stick to Tkinter’s default fonts.

Most platforms have their own set of standard fonts that are used by the platform’s native widgets. So, rather than trying to reinvent the wheel on what looks good on a given platform or what would be available for a given platform, Tkinter assigns these standard platform-specific fonts into its widget, thus providing a native look and feel on every platform.

Tkinter assigns nine fonts to nine different names, which you can therefore use in your programs. The font names are as follows:

  • TkDefaultFont
  • TkTextFont
  • TkFixedFont
  • TkMenuFont
  • TkHeadingFont
  • TkCaptionFont
  • TkSmallCaptionFont
  • TkIconFont
  • TkTooltipFont

Accordingly, you can use them in your programs in the following way:

Label(text="Sale Up to 50% Off !", font="TkHeadingFont 20") Label(text="**Conditions Apply", font="TkSmallCaptionFont 8")

Using these kinds of fonts mark up, you can be assured that your font will look native across all platforms.

Finer Control over Font

In addition to the above method on handling fonts, Tkinter provides a separate Font class implementation. The source code of this class is located at the following link: <Python27_installtion_dir>Liblib-tktkfont.py.

To use this module, you need to import tkFont into your namespace.(refer to 8.08 tkfont demo.py):

from Tkinter import Tk, Label, Pack import tkFont root=Tk() label = Label(root, text="Humpty Dumpty was pushed") label.pack() currentfont = tkFont.Font(font=label['font']) print'Actual :' + str(currentfont. actual ()) print'Family :' + currentfont. cget ("family") print'Weight :' + currentfont.cget("weight") print'Text width of Dumpty : %d' %currentfont. measure ("Dumpty") print'Metrics:' + str(currentfont. metrics ()) currentfont.config(size=14) label.config (font=currentfont) print'New Actual :' + str(currentfont. actual ()) root.mainloop()

The console output of this program is as follows:

Actual :{'family': 'Segoe UI', 'weight': 'normal', 'slant': 'roman',
'overstrike': 0, 'underline': 0, 'size': 9}
Family : Segoe UI Weight : normal Text width of Dumpty : 43 Metrics:{'fixed': 0, 'ascent': 12, 'descent': 3, 'linespace': 15}

As you can see, the tkfont module provides a much better fine-grained control over various aspects of fonts, which are otherwise inaccessible.

Font Selector

Now that we have seen the basic features available in the tkfont module, let’s use it to implement a font selector. The font selector would look like the one shown here:

The code for the font selector is as follows (refer to 8.09 font selector.py):

from Tkinter import * import ttk import tkFont class FontSelectorDemo (): def __init__(self): self.currentfont = tkFont.Font(font=('Times New Roman',12)) self.family = StringVar(value='Times New Roman') self.fontsize = StringVar(value='12') self.fontweight =StringVar(value=tkFont.NORMAL) self.slant = StringVar(value=tkFont.ROMAN) self.underlinevalue = BooleanVar(value=False) self.overstrikevalue= BooleanVar(value=False) self. gui_creator ()

The description of the preceding code is as follows:

  • We import Tkinter (for all widgets), ttk (for the Combobox widget), and tkfont for handling font-related aspects of the program
  • We create a class named FontSelectorDemo and use its __init_ method to initialize al attributes that we intend to track in our program.
  • Finally, the __init__ method calls another method named gui_creator(), which is be responsible for creating all the GUI elements of the program

Creating the GUI

The code represented here is a highly abridged version of the actual code (refer to 8.09 font selector.py). Here, we removed all the code that creates basic widgets, such as Label and Checkbuttons, in order to show only the font-related code:

def gui_creator(self): # create the top labels – code removed fontList = ttk.Combobox(textvariable=self.family) fontList.bind('<<ComboboxSelected>>', self.on_value_change) allfonts = list(tkFont.families()) allfonts.sort() fontList['values'] = allfonts # Font Sizes sizeList = ttk.Combobox(textvariable=self.fontsize) sizeList.bind('<<ComboboxSelected>>', self.on_value_change) allfontsizes = range(6,70) sizeList['values'] = allfontsizes # add four checkbuttons to provide choice for font style # all checkbuttons command attached to self.on_value_change #create text widget sampletext ='The quick brown fox jumps over the lazy dog' self.text.insert(INSERT,'%sn%s'% (sampletext,sampletext.upper()), 'fontspecs' ) self.text.config(state=DISABLED)

The description of the preceding code is as follows:

  • We have highlighted the code that creates two Combobox widgets; one for the Font Family, and the other for the Font Size selection.
  • We use tkfont.families() to fetch the list of all the fonts installed on a computer. This is converted into a list format and sorted before it is inserted into the fontList Combobox widget.
  • Similarly, we add a font size range of values from 6 to 70 in the Font Size combobox.
  • We also add four Checkbutton widgets to keep track of font styles bold, italics, underline , and overstrike. The code for this has not been shown previously, because we have created similar check buttons in some of our previous programs.
  • We then add a Text widget and insert a sample text into it. More importantly, we add a tag to the text named fontspec.
  • Finally, all our widgets have a command callback method connecting back to a common method named on_value_change. This method will be responsible for updating the display of the sample text at the time of changes in the values of any of the widgets.

Updating Sample Text

def on_value_change(self, event=None): try: self.currentfont.config(family=self.family.get(), size=
self.fontsize.get(), weight=self.fontweight.get(), slant=self.slant.get(),
underline=self.underlinevalue.get(), overstrike=self.overstrikevalue.get()) self.text.tag_config('fontspecs', font=self.currentfont) except ValueError: pass ### invalid entry - ignored for now. You can use a
tkMessageBox dialog to show an error

The description of the preceding code is as follows:

  • This method is called at the time of a state change for any of the widgets
  • This method simply fetches all font data and configures our currentfont attribute with the updated font values
  • Finally, it updates the text content tagged as fontspec with the values of the current font

Working with Unicode characters

Computers only understand binary numbers. Therefore, all that you see on your computer, for example, texts, images, audio, video, and so on need to be expressed in terms of binary numbers.

This is where encoding comes into play. An encoding is a set of standard rules that assign unique numeral values to each text character.

Python 2.x default encoding is ASCII (American Standard Code for Information Interchange). The ASCII character encoding is a 7-bit encoding that can encode 2 ^7 (128) characters.

Because ASCII encoding was developed in America, it encodes characters from the English alphabet, namely, the numbers 0-9, the letters a-z and A-Z, some common punctuation symbols, some teletype machine control codes, and a blank space.

It is here that Unicode encoding comes to our rescue. The following are the key features of Unicode encoding:

  • It is a way to represent text without bytes
  • It provides unique code point for each character of every language
  • It defines more than a million code points, representing characters of all major scripts on the earth
  • Within Unicode, there are several Unicode Transformation Formats (UTF)
  • UTF-8 is one of the most commonly used encodings, where 8 means that 8-bit numbers are used in the encoding
  • Python also supports UTF-16 encoding, but it’s less frequently used, and UTF-32 is not supported by Python 2. x

Say you want to display a Hindi character on a Tkinter Label widget. You would intuitively try to run a code like the following:

from Tkinter import * root = Tk() Label( root, text = " भारतमेंआपकास्वागतहै " ).pack() root.mainloop()

If you try to run the previous code, you will get an error message as follows:

SyntaxError: Non-ASCII character 'xe0' in file 8.07.py on line 4, but no
encoding declared; see http://www.Python.org/peps/pep-0263.html for details.

This means that Python 2.x, by default, cannot handle non-ASCII characters. Python standard library supports over 100 encodings, but if you are trying to use anything other than ASCII encoding you have to explicitly declare the encoding.

Fortunately, handling other encodings is very simple in Python. There are two ways in which you can deal with non-ASCII characters.

Declaring line encoding

The first way is to mark a string containing Unicode characters with the prefix u explicitly, as shown in the following code snippet (refer to 8.10 line encoding.py):

from Tkinter import * root = Tk() Label(root, text = u"भारतमेंआपकास्वागतहै").pack() root.mainloop()

When you try to run this program from IDLE, you get a warning message similar to the following one:

Simply click on Ok to save this file as UTF-8 and run this program to display the Unicode label.

Summary

In this article, we discussed some vital aspects of GUI programming form a common theme in many GUI programs.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here