WHAT YOU LEARN IN THIS CHAPTER: βββ
WROX.COM DOWNLOADS FOR THIS CHAPTER
For this chapter the wrox.com code downloads are found at www.wrox.com/go/pythonprojects
on the Download Code tab. The code is in the Chapter 4 download, called Chapter4.zip
, and individually named according to the names throughout the chapter.
Python is a general-purpose programming language. That means it can be used for many different types of programs. You have already seen how it can be used as a scripting language to glue applications together, as well as its use in managing data persistence and access. You now look at how it can be used to build complete desktop applications.
Desktop applications are the mainstay of personal computing. They include such standard facilities as word-processing programs, spreadsheets, and even games. They often function entirely on the desktop, with no network access required. At other times they may be inherently network oriented, as is the case with a web browser or a clientβserver database application. The distinguishing feature is that the bulk of the functionality is executed on the local PC.
Desktop applications can have a graphical user interface (GUI) or a command-line interface (CLI). In this chapter you see how an application can be structured in such a way that different user interfaces can be created on top of the same underlying program logic. This means you can start off with a simple text interface and then add a graphical front end on top of the existing code. This idea can be taken even further, and a web user interface can often be added, too, making your desktop application into a network application. You see how to do that in the next chapter.
You start off by looking at the basic structure, or architecture, of an application. You then build a simple command-line application that is then extended using some Python modules to provide a much richer user experience. You then move on to build a GUI front end using Pythonβs standard GUI toolkit: Tkinter. This incorporates all the standard GUI features such as controls, menus, and dialogs. You then incorporate extra GUI features and enhance the appearance of the interface using more Python module magic. Next, you take a look at other GUI frameworks that offer even more powerful capabilities. Finally, you look at how to support local configurations and multiple languages.
The key to building effective, extensible applications is to apply a layered architecture. The most common approach is to split the application into three layers: the user interface, the core logic (also known as the business logic), and the data. There may also be a network layer when the application uses the network extensively.
The user interface should present the application logic to the user, but not implement that logic. Its role is to make navigation of the applicationβs features as simple as possible and to display results or outcomes as clearly as possible. The user interface controls which functions are available at any point in timeβfor example, it should not be possible to close a document if no document is open. If using an object-oriented program (OOP), the objects will typically represent things like menus, buttons, and windows. The user interface accesses the core logic by calling functions or methods provided by the logic layer.
The core logic layer contains all of the algorithms and state management of the data. This is where you write the code that changes the data values, creates new entities, opens and closes files, and so on. The aim here is to provide a set of functions, or services, that can be accessed from the user interface. For this to be effective, the core logic functions should not print results, but should return them as values (that is, strings, numbers, lists, objects and so on) that the user interface can present in the appropriate place and format. The core logic only presents the information; it does not concern itself with how that information is displayed. It is this separation of concerns between logic and display that enables you to build different user interfaces on top of the same core logic. The core logic operates on data provided by the data layer. If using OOP, the objects will represent the conceptual entities of the problem, such as bank accounts, people, network messages and locations, and so on.
The data layer manages data. It stores the data in a safe place and retrieves it on demand. It should not contain sophisticated algorithms or logic specific to the application; it simply delivers raw data to the core logic layer for processing. The data layer may include some basic data-integrity processing to ensure consistency of the data. It may also incorporate security features such as password control or encryption. It should expose the data via a set of objects, functions, or services. If using OOP, your objects will typically represent queries, tables, data connections, and so on. Ideally, you should be able to build multiple applications using the same basic data services. The data layer was discussed in more detail in Chapter 3, βManaging Data.β
The interaction between the user interface, logic, and data layers is often designed using a pattern called Model View Controller (MVC). In general, the model represents the core logic and data layers, while the view represents the display elements in the user interface, and the controller represents the interaction and dependencies between those display elements. You use a simplified version of the MVC pattern in this chapter for the GUI design.
In this section you build a very simple command-line interface application for the well-known game tic-tac-toe. The principles discussed in the earlier sections are applied, but in a very simple form so that you can focus on the program structure rather than the detail of what the code is doing. (The code is included in the Chapter4.zip
file under the folder OXO
.)
You start off creating this game by designing the data layer. For this game you need only a simple text file to hold the state of the game so that it can be saved or resumed. A tic-tac-toe game consists of a board with nine squares. Each square can be empty or have the letter βXβ or βOβ in it. You can represent those three options with a simple list of characters. For storage you convert that list into a simple character string.
The only other piece of data needed is which player is due to move next but, in a computer versus human game, you can assume the human is always next to go. So your data layer interface consists of only two exposed, or published, methods:
β saveGame()
β restoreGame()
Because you want to keep your layers separate, you should put these methods into a module. To create this module, type the following code and save it as oxo_data.py
(or load it from the OXO
folder of the Chapter4.zip
download):
β ''' oxo_data is the data module for a tic-tac-toe (or OXO) game.
β It saves and restores a game board. The functions are:
β saveGame(game) -> None
β restoreGame() -> game
β Note that no limits are placed on the size of the data.
β The game implementation is responsible for validating
β all data in and out.'''
β import os.path
β game_file = ".oxogame.dat"
β def _getPath():
β ''' getPath -> string
β Returns a valid path for data file.
β Tries to use the users home folder, defaults to cwd'''
β try:
β game_path = os.environ['HOMEPATH'] or os.environ['HOME']
β if not os.path.exists(game_path):
β game_path = os.getcwd()
β except (KeyError, TypeError):
β game_path = os.getcwd()
β return game_path
β def saveGame(game):
β ''' saveGame(game) -> None
β saves a game object in the data file in the users home folder.
β No checking is done on the input, which is expected to
β be a list of characters'''
β path = os.path.join(_getPath(), game_file)
β with open(path, 'w') as gf:
β gamestr = ''.join(game)
β gf.write(gamestr)
β def restoreGame():
β ''' restoreGame() -> game
β Restores a game from the data file.
β The game object is a list of characters'''
β path = os.path.join(_getPath(), game_file)
β with open(path) as gf:
β gamestr = gf.read()
β return list(gamestr)
β def test():
β print("Path = ", _getPath())
β saveGame(list("XO XO OX"))
β print(restoreGame())
β if __name__ == "__main__": test()
The first function, _getPath()
, is a helper function that uses the os
module to try to determine the userβs home folder and, if that fails, use the current folder. By convention, functions that are not intended to be called by module users have a leading underscore in front of their name, like _getPath()
. The saveGame()
function uses _getPath()
to create a new file containing the string representation of the game. The final function restoreGame()
also uses _getPath()
to locate the saved file and open it, reading back the stored game data.
Ideally, you would include a more sophisticated test function (or a set of unit tests, like those described in Chapter 6, βPython on Bigger Projectsβ). In the interest of brevity, these are not shown here.
You now create the core logic of the game. For that you need to define a number of functions that are used throughout the course of a game. However, to know what those functions are, you first need to think about how the game will be played. So, before diving into logic code, you need to map out the sequence of play and, for that, you can use a sequence diagram.
The game starts by presenting the user with a menu of options. These will include the options to start a new game or restore a saved game. In either case, once the game has been set up, the board will be displayed and the user prompted to select a cell. The computer then analyzes the move and responds with its own. The board is then shown again until a winner is found. The sequence diagram for this is as shown in Figure 4.1.
The sequence diagram is a simplified version of a UML sequence diagram. Each of the βobjectsβ (modules in this case), along with the user, are represented by a vertical line. The arrows indicate messages flowing between the modules (and user). The messages have descriptive titles. Some messages are optional, and the conditions that cause them are indicated in square brackets (called guards). Some sequences of arrows are enclosed in boxes, and these represent loops or conditional blocks with a description given in the upper-left corner. In this case the main game-play sequence is repeated until there is a win, or draw, or until the user selects quit. If the user selects quit, then the sequence in the box at the bottom is executed. Sequence diagrams are a very powerful analysis and design tool.
From the sequence diagram, you can see that you need to provide functions to handle the userβs menu choices (only new is shown in the diagram), as well as a function to play the game. The latter would need to return different results depending on the outcome, which is usually a bad idea. However, if you create separate functions for the userβs and computerβs moves, you can then use the same analysis helper function for both the user and the computer. With that in mind you need to write the following functions:
β newGame()
β saveGame()
β restoreGame()
β userMove()
β computerMove()
You need helper functions to generate a random move and analyze whether a given move wins the game. The list of helper functions is therefore:
β _generateMove()
β _isWinningMove()
Once again, to keep the separation between the layers, you should put the logic code into a separate module, this time called oxo_logic.py
. It looks like this:
β ''' This is the main logic for a tic-tac-toe game.
β It is not optimised for a quality game it simply
β generates random moves and checks the results of
β a move for a winning line. Exposed functions are:
β newGame()
β saveGame()
β restoreGame()
β userMove()
β computerMove()
β '''
β import os, random
β import oxo_data
β def newGame():
β return list(" " * 9)
β def saveGame(game):
β oxo_data.saveGame(game)
β def restoreGame():
β try:
β game = oxo_data.restoreGame()
β if len(game) == 9:
β return game
β else: return newGame()
β except IOError:
β return newGame()
β def _generateMove(game):
β options = [i for i in range(len(game))
β if game[i] == " "]
β return random.choice(options)
β def _isWinningMove(game):
β pass
The newGame()
function simply returns a new list of nine spaces.
The saveGame()
function calls the oxo_data
function of the same name. The restoreGame()
function is marginally more complex in that it catches any errors arising because the data file cannot be found and validates the length of the restored game. It could do more data validation on the content of the data, but you can add that later if you wish.
The _generateMove()
function looks for the unused cells in the current game and then randomly selects a cell to place the computerβs move. This is not optimal, and a more intelligent algorithm would greatly improve the quality of the game.
The _isWinningMove()
method has been left unfinished because it is the most complex of the logic functions. This is where the real processing takes place. The approach you take is very simple and relies on the fact that there are only eight possible wining lines. They can be listed in terms of the indices of the game cells involved:
β wins = ((0,1,2), (3,4,5), (6,7,8),
β (0,3,6), (1,4,7), (2,5,8),
β (0,4,8),(2,4,6))
To assess whether a move has resulted in a win, you need to check each winning line. You can extract the character in each cell of the candidate line and construct a three-character string. For a win all three characters need to be either βXβ or βOβ. The function looks like this:
β def _isWinningMove(game):
β wins = ((0,1,2), (3,4,5), (6,7,8),
β (0,3,6), (1,4,7), (2,5,8),
β (0,4,8), (2,4,6))
β for a,b,c in wins:
β chars = game[a] + game[b] + game[c]
β if chars == 'XXX' or chars == 'OOO':
β return True
β return False
Finally, you need a pair of functions that can analyze a user move and a computer move. The former takes a cell value input by the user; the latter needs only the game because it uses _generateMove()
internally. They return the outcome of the move as one of four character codes. An empty string means the game is still on, an βXβ or βOβ signifies the victor, and a βDβ means itβs a draw. These functions look like this:
β def userMove(game,cell):
β if game[cell] != ' ':
β raise ValueError('Invalid cell')
β else:
β game[cell] = 'X'
β if _isWinningMove(game):
β return 'X'
β else:
β return ''
β def computerMove(game):
β cell = _generateMove(game)
β if cell == -1:
β return 'D'
β game[cell] = 'O'
β if _isWinningMove(game):
β return 'O'
β else:
β return ''
You could have implemented all of these functions as methods of a Game
class. That would have removed the need to pass the game data into each function.
Finally, you need a test function:
β def test():
β result = ''
β game = newGame()
β while not result:
β print(game)
β try:
β result = userMove(game, _generateMove(game))
β except ValueError:
β print("Oops, that shouldn't happen")
β if not result:
β result = computerMove(game)
β
β if not result: continue
β elif result == 'D':
β print("Its a draw")
β else:
β print("Winner is:", result)
β print(game)
β if __name__ == "__main__":
β test()
The test function keeps on generating moves until either somebody wins or the board fills up (at which point _generateMove()
returns -1
). The moves are generated entirely randomly, so there is no intelligence in the selections. You can run the code, and you should see something like this (the actual results you get are random because you use the random
module to generate the moves):
β [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
β [' ', ' ', ' ', ' ', ' ', 'O', ' ', 'X', ' ']
β [' ', 'X', ' ', 'O', ' ', 'O', ' ', 'X', ' ']
β Winner is: X
β [' ', 'X', ' ', 'O', 'X', 'O', 'O', 'X', ' ']
Itβs not very pretty, but then, itβs not supposed to be. Presentation is in the user interface, and thatβs what you build next.
In the previous section, you started to think about how the user would perceive the application, that is, the user experience. The user experience is the key factor in driving the user interface design. You need to consider the flow of control from entering the application through normal use to exit. In a command-line application, the most common approaches are to offer menus, often nested to several levels, or to accept commands typed at a prompt. In this section you build a set of very simple menus and create a simple prompt-based solution. This process illustrates both techniques while keeping the code size small.
If you walk through the user experience in playing a game of tic-tac-toe, the game starts by offering a menu that allows the user to start a new game, resume a saved game, request help, or quit. (The ability to quit easily is an important, but often overlooked, feature of good user interface design.)
The quit option just exits the program. The help screen displays a page of explanatory text. Both of the other options take the user into a tic-tac-toe game. Once in that game, the user can either select a cell, save the game, or quit. If they choose to quit while the game is still in progress, they should be asked if they want to save the game first. If they choose a cell, then the computer analyzes the move, makes its own move, analyzes that, and presents the result. If either move results in a win or in all cells being used, then a message is displayed to the user and an option to quit or return to the main menu.
The user interface presents the information and manages the flow from screen to screen. It does not perform any of the computational logic; that is provided by the core logic layer. The user interface simply calls the functions that you defined in the oxo_logic
module.
You start by defining a function to display a menu and return a valid user selection. The function could be specific to the menu and include the menu definition within it; however, it is not much harder to write a function that takes a menu as input and is therefore reusable for all menus in the system. You could even extract it to a module for reuse across projects.
The menu code, which you can save in oxo_ui.py
(or load it from the zip file), looks like this:
β ''' CLI User Interface for Tic-Tac-Toie game.
β Use as the main program, no reusable functions'''
β import oxo_logic
β menu = ["Start new game",
β "Resume saved game",
β "Display help",
β "Quit"]
β def getMenuChoice(aMenu):
β ''' getMenuChoice(aMenu) -> int
β takes a list of strings as input,
β displays as a numbered menu and
β loops until user selects a valid number'''
β if not aMenu: raise ValueError('No menu content')
β while True:
β print("
")
β for index, item in enumerate(aMenu, start=1):
β print(index, " ", item)
β try:
β choice = int(input("
Choose a menu option: "))
β if 1 <= choice <= len(aMenu):
β return choice
β else: print("Choose a number between 1 and", len(aMenu))
β except ValueError:
β print("Choose the number of a menu option")
β def main():
β print(getMenuChoice(menu))
β getMenuChoice([]) # raise error
β if __name__ == "__main__": main()
Notice that the enumerate
function is used to generate the menu option numbers and that they start at 1
because most users find 0
a strange option choice. The function keeps repeating until the user selects a valid choice, with prompts to correct invalid choices.
Having presented some choices, you now need to write code to do all of those things. The methods are named after the menu options and are shown as follows:
β def startGame():
β return oxo_logic.newGame()
β def resumeGame():
β return oxo_logic.restoreGame()
β def displayHelp():
β print('''
β Start new game: starts a new game of tic-tac-toe
β Resume saved game: restores the last saved game and commences play
β Display help: shows this page
β Quit: quits the application
β ''')
β def quit():
β print("Goodbye...")
β raise SystemExit
Your next step is to write a function to process the userβs choice. That can be done using an if/elif
chain, but that can become difficult to maintain if there are many options. Although the number of possible choices in this project is small, you use a dispatch table because this is a powerful, efficient, and flexible technique. You also need to change the main()
function to loop over the menu and game code until the user quits. Make the following modifications to your program:
β def executeChoice(choice):
β ''' executeChoice(int) -> None
β Execute whichever option the user selected.
β If the choice produces a valid game then
β play the game until it completes.'''
β dispatch = [startGame, resumeGame, displayHelp, quit]
β game = dispatch[choice-1]()
β if game:
β # play game here
β pass
β def main():
β while True:
β choice = getMenuChoice(menu)
β executeChoice(choice)
That only leaves the task of actually playing the game. You need to write a function that takes a starting game position and interacts with the user until the game completes, either because there are no more moves or a winner is found. In addition, it helps if you have a function that displays the game in the usual grid layout rather than the flat data format that you are using internally. These are shown here:
β def printGame(game):
β display = '''
β 1 | 2 | 3 {} | {} | {}
β ---------- ------------
β 4 | 5 | 6 {} | {} | {}
β ---------- ------------
β 7 | 8 | 9 {} | {} | {}'''
β print(display.format(*game))
β def playGame(game):
β result = ""
β while not result:
β printGame(game)
β choice = input("Cell[1-9 or q to quit]: ")
β if choice.lower()[0] == 'q':
β save = input("Save game before quitting?[y/n] ") if
β save.lower()[0] == 'y':
β oxo_logic.saveGame(game)
β quit()
β else:
β try:
β cell = int(choice)-1
β if not (0 <= cell <= 8): # check range
β raise ValueError
β except ValueError:
β print("Choose a number between 1 and 9 or 'q' to quit ")
β continue
β try:
β result = oxo_logic.userMove(game,cell)
β except ValueError:
β print("Choose an empty cell ")
β continue
β if not result:
β result = oxo_logic.computerMove(game)
β if not result:
β continue
β elif result == 'D':
β printGame(game)
β print("Its a draw")
β else:
β printGame(game)
β print("Winner is", result, "
")
The printGame()
function uses formatting to insert the game values into the display. Note the use of the asterisk (*game
) that expands the list into its individual elements.
The playGame()
function displays the user interface prompt for the game screen. You ask for a number representing the cell that the user wishes to place an βXβ into. You also provide the option to quit, and if it is selected, offer the chance to save the game. If the userβs choice is valid, you then use the oxo_logic
functions to determine the outcome of the selection, and if not, a finishing move to get the computer to take a turn. Notice how all of the rules of the game and the data management are in the lower layers. The user interface layer is dealing with presentation and control flow only. One slight quirk of the design is that the board is passed into both the userMove()
and computerMove()
functions, but the updated board is not returned by the functions. That is because the board object is a mutable list and, as such, can be changed by the function and the original variable will reflect those changes.
Later in this chapter, you revisit this game and see how easily you can create a new GUI-based user interface layer on top of the existing oxo_logic
and oxo_data
modules. Before you do that, there are a couple of interesting options you can use to enhance your command-line applications. You look at them in the next sections.
Python has a module in its standard library called cmd
that is specifically designed for building command-line interfaces. In particular it creates the type of interface that you use for Pythonβs help and debugger systems. It presents a command prompt, and you can type in a command. You can request help, and a help screen is presented with the list of available commands. If you type βhelp <command>β, you get a screen explaining how to use the specified command.
In this section you build a cmd
-based version of the tic-tac-toe game from the previous section. It has the same four options that you displayed in the opening menu. The game play part is exactly the same as before. (The finished code is in the file oxo-cmd.py
in the OXO
folder of the Chapter4.zip
download.)
cmd
is based on an object-oriented framework whereby you define a new subclass of the cmd.Cmd
class. This subclass overrides some key methods to provide your application-specific behavior. You then define a set of methods whose names begin with the string do_
. The class then interprets the part following the underscore as a command word that the user can type.
You can build a skeleton version of your tic-tac-toe game by defining some methods that simply print a message to see how it works. It looks like this:
β import cmd
β class Oxo_cmd(cmd.Cmd):
β intro = "Enter a command: new, resume, quit. Type 'help' or '?' for help"
β prompt = "(oxo) "
β def do_new(self, arg):
β print("Starting new game")
β
β def do_restore(self, arg):
β print("Restoring previous game")
β
β def do_quit(self, arg):
β print("Goodbye...")
β raise SystemExit
β def main():
β game = Oxo_cmd().cmdloop()
β if __name__ == "__main__":
β main()
With the preceding code, you created a new class derived from cmd.Cmd
. To that you added an intro message and a prompt string. You then defined the command-response methods.
Notice that you donβt need a help command because that is built into the class mechanism for free. Also notice that you need to provide a second dummy parameter in the method definitions, even if it is not used in the method.
Finally, in the main function you instantiate the game object and execute its cmdloop()
method.
If you run that, you see that it produces a fully functioning command interpreter application.
To turn it into a working tic-tac-toe game, you need to make only a couple of minor tweaks. You import both the oxo_logic
and oxo_ui
modules. You create a game class variable to hold the game data. Then you call the ui_logic
module functions from your command methods. Finally, you call the oxo_ui.playGame()
method to initiate the original game play.
The final game looks like this:
β import cmd, oxo_ui, oxo_logic
β class Oxo_cmd(cmd.Cmd):
β intro = "Enter a command: new, resume, quit. Type 'help' or '?' for help"
β prompt = "(oxo) "
β game = ""
β def do_new(self, arg):
β self.game = oxo_logic.newGame()
β oxo_ui.playGame(self.game)
β def do_resume(self, arg):
β self.game = oxo_logic.restoreGame()
β oxo_ui.playGame(self.game)
β def do_quit(self, arg):
β print("Goodbye...")
β raise SystemExit
β def main():
β game = Oxo_cmd().cmdloop()
β if __name__ == "__main__":
β main()
There are many other options and features for you to explore in the cmd.Cmd
class, but hopefully this example has shown you how easy it is to build a command interpreter style application using cmd
. It should also have demonstrated how powerful the separation of the presentation from the logic is. For a very small amount of coding, you have created an entirely different version of the tic-tac-toe game, but the logic and data layers are completely identical.
In the next section, you take a look at the command line itself and see how to read those command-line input arguments.
When you start a command-line program, the command line itself is stored as a list of strings in sys.argv
. The first element is the script name, and the following elements are the arguments to the command. Thus, if you had a file copying script you might call it like this:
β $ python mycopy.py originalfile copyfile
And the sys.argv
value would be:
β ["mycopy.py", "originalfile", "copyfile"]
However, itβs often the case that command-line scripts take optional arguments to control the display or functionality. For example, many programs offer a β-h
β or ββ-help
β option that causes the commandβs help information to be displayed. You can process those options by examining the contents of sys.argv
, but itβs a nontrivial task so Python includes the argparse
module to assist in handling these kinds of command options.
You now modify the original tic-tac-toe code to display the help message if either -h
or --help
is specified on the command line. It also goes direct to a new game if -n
or --new
is specified, and it goes straight to a restored game if -r
or --res
or--restore
is given. This enables experienced tic-tac-toe players to bypass the initial menu if they wish.
The first thing you need to do is import the module:
β import argparse as ap
Next, modify the main function as shown:
β def main():
β p = ap.ArgumentParser(description="Play a game of Tic-Tac-Toe")
β grp = p.add_mutually_exclusive_group()
β grp.add_argument("-n","--new", action='store_true',
β help="start new game")
β grp.add_argument("-r","--res", "--restore", action='store_true',
β help="restore old game")
β args = p.parse_args()
β if args.new:
β executeChoice(1)
β elif args.res:
β executeChoice(2)
β else:
β while True:
β choice = getMenuChoice(menu)
β executeChoice(choice)
Now, if you try it out, the -h
(or --help
) option produces a standard help screen format produced by argparse
:
β usage: oxo_args_ui.py [-h] [-n | -r]
β Play a game of Tic-Tac-Toe
β optional arguments:
β -h, --help show this help message and exit
β -n, --new start new game
β -r, --res, --restore restore old game
If you specify -n
(or --new)
, it goes straight into a new game. Specifying -r
, --res
, or --restore
brings back the last saved game.
You should notice a couple of features of the argparse
code. First, you created the options as a mutually exclusive group. That was because it makes no sense to specify both new and restore options in the same command. Also you provided a description of the program in the constructor of ArgumentParser
and this then appears in the help screen. Finally, by specifying an action of store_true
, you made the options into boolean flags that enabled them to be used as truth values in the if/elif
tests.
The argparse
module has many other tricks up its sleeve. It can interpret arguments as different types, it can count options and it can associate multiple options, and so on. There is a tutorial on its use, and the documentation has several examples.
The last thing to point out here is that you have effectively given your code yet another user interface mechanism without any modification to the game logic or the data layer. Separating presentation from logic and data is a very powerful design technique.
In the next section, you take a look at a different way to make a command-line application more professional looking for your users, by adding a few GUI features while not implementing a full GUI application.
It is possible to add a few GUI elements to a command-line interface application without the complexity of building a completely graphical user interface. In particular you can pop up information or warning boxes instead of simply printing a message on the terminal. This often makes the messages stand out more to the userβthey do not get lost in the mass of text on the screen. You can also use the standard file selection dialogs when choosing filenames. In this section you add some GUI message boxes to the tic-tac-toe user interface to highlight error messages and to notify the user of the final outcome of a game.
Before you start modifying the game itself, you can explore how these message boxes work at the command-line prompt. They are defined in submodules of the tkinter
package that you explore more fully later in the chapter. These submodules are:
tkinter.messagebox
tkinter.filedialog
tkinter.simpledialog
tkinter.colorchooser
tkinter.font
You only use the first module in this section, but the principle is the same for all of them. Unfortunately, the official documentation is very sparse, so a bit of experimentation at the Python prompt goes a long way. You can try that now with the tkinter.messagebox
module.
Having seen how the standard dialogs work, you can put them to use in your tic-tac-toe game by presenting the results using showinfo
dialogs. You also ask the user if they want to save an incomplete game before quitting with an askyesno
dialog. Although you could make all of the prompts use dialogs, this actually leads to a very cumbersome user interface with dialogs popping up and disappearing constantly. It is better to use this technique judiciously to highlight important information only.
The modifications you need to make are in the playGame()
function. You could modify any of the previous versions of the game, but in this example you use the original oxo_ui.py
file as the basis and save it as oxo_dialog_ui.py
. (Or simply load it from the Chaper4.zip
file.)
The first thing to do is add the import
statements:
β import tkinter
β import tkinter.messagebox as mb
Then you need to modify main
to get rid of the top level window:
β def main():
β top = tkinter.Tk()
β top.withdraw()
β while True:
β choice = getMenuChoice(menu)
β executeChoice(choice)
Finally, you modify playGame()
as shown here:
β def playGame(game):
β result = ""
β while not result:
β printGame(game)
β choice = input("Cell[1-9 or q to quit]: ")
β if choice.lower()[0] == 'q':
β save = mb.askyesno("Save game","Save game before quitting?")
β if save:
β oxo_logic.saveGame(game)
β quit()
β else:
β try:
β cell = int(choice)-1
β if not (0 <= cell <= 8):
β raise ValueError
β except ValueError:
β print("Choose a number or 'q' to quit")
β continue
β try:
β result = oxo_logic.userMove(game,cell)
β except ValueError:
β mb.showerror("Invalid cell", "Choose an empty cell")
β continue
β if not result:
β result = oxo_logic.computerMove(game)
β if not result:
β continue
β elif result == 'D':
β printGame(game)
β mb.showinfo("Result", "It's a draw")
β else:
β printGame(game)
β mb.showinfo("Result", "Winner is {}".format(result))
Although there is quite a lot of code shown here, there are only a few lines of changes. Once again no changes were needed in the logic or data layers.
The next section takes you into the world of GUIs.
In this section you find out how to create GUIs using Pythonβs standard GUI toolkit, Tkinter. All GUIs are built on top of a toolkit of functions or, more commonly, a class library. You look at some of the other toolkits that you can use with Python later in the chapter, but for now the Tkinter toolkit provides a solid foundation for the basic principles.
You start out by examining some of the basic concepts of GUI design, including how GUI toolkits are structured and used.
Virtually all GUIs are event driven. That means you need to write your code to respond to certain events generated by the GUI toolkit. GUIs come with a whole language of their own in terms of the objects from which a GUI is built. There are windows, frames, controls, and so on. These objects are all connected by something called a containment tree. You see what each of these concepts means and how they fit together to form a GUI in the following sections.
You saw how programs can be event driven back in Chapter 2, βScripting with Python,β when you explored the parsing of XML and HTML files. Essentially, the parsers used an internal loop, and whenever they encountered an item of interest, they sent a message to your code. In effect they called a function that you provided.
GUI programs function in a similar manner. The toolkit has an infinite loop within it and, as the user clicks buttons, moves the mouse, or presses keys, the toolkit generates events that result in functions being called. You write functions and register them with particular events so that when a user selects, say the File-> Save
menu item, your function doFileSave()
gets called.
This means the shape of your program code changes. Instead of you controlling the flow of the program from beginning to end, you instead initialize your data and then hand control over to the toolkit. This can be an unsettling experience for some programmers at first, but once you get used to it, you will find it actually frees you from a lot of mundane control-flow programming and lets you focus on what your program needs to do.
One of the first things you notice when dealing with GUIs is the number of new terms you need to learn. Youβve already met event, and will no doubt be familiar with many more, such as menu, button, scrollbar, and so on. As a programmer you find that, sometimes, the common understanding of terms is not quite what the programming meaning is. In addition, there are a bunch of other terms that are not usually exposed to users. Table 4.1 lists some of the most important GUI terms and their meaning from a programmerβs perspective.
Table 4.1 Explanations of Key GUI Terms
TERM | DESCRIPTION |
Window | An area of the screen controlled by an application. Windows are usually rectangular but some GUI environments permit other shapes. Windows can contain other windows and frequently every single GUI control is treated as a window in its own right. |
Control | A control is a GUI object used for controlling the application. Controls have properties and usually generate events. Normally controls correspond to application level objects and the events are coupled to methods of the corresponding object such that when an event occurs the object executes one of its methods. |
Widget | A visible control. Some controls (such as timers) can be associated with a given window but are not visible. Widgets are that subset of controls that are visible and can be manipulated by the user or programmer. |
Frame | A type of widget used to group other widgets together. Often a frame is used to represent the complete window and further frames are embedded within it. Frames sometimes have visible borders and background colors but at other times are invisible and solely used as a container object. |
Label | A widget containing some text or a simple image. It does not generate any events but can be modified in response to an event elsewhere. |
Button | A widget with text and/or images that can be pressed by the user and emits an event in response. |
Text Entry | A widget that can display and/or receive text. It can be a single line entry on a form or a multiline entry such as a text editor pane. Text widgets can often contain other widgets such as images. |
Menu | A widget representing a menu control. The menu contains menu items and/or sub-menus. Menus provide all the mechanisms for the navigation of the menu widget hierarchy. Menu Items, when selected, emit events that can be processed. |
Canvas | A widget for containing graphical shapes and images. Canvas objects normally contain methods that permit drawing of geometrical shapes, charts, and so on. |
Geometry | Every window and widget has a geometry, or set of coordinates indicating its location and size. Different toolkits represent this information differently. Tkinter uses the format (width, height). Location information if needed is shown as: (x-coordinate, y-coordinate) and is relative to the containing widget. |
Dialog | A special kind of window that is owned by the parent application but can be moved around the screen independently. Dialogs can be modal, that means you must close the dialog before the application responds to any other actions, or modeless where the dialog operates in parallel to the main application window. |
Messagebox | A small dialog box that generally presents very simple prompts or requests simple types of user input. You have already used Tkinterβs message boxes in an earlier section. |
Layout | Controls are laid out within a frame according to a particular set of rules or guidelines. These rules form a layout. The layout may be specified in a number of ways, either using on-screen coordinates specified in pixels, using relative position to other components (left, top etc.) or using a grid or table arrangement. A coordinate system is easy to understand but difficult to manage when, for example, a window is resized. You are advised to use non-resizable windows if working with coordinate-based layouts. Better still use non-coordinate layouts and let the toolkit manage things for you. |
Parent-Child | GUI applications tend to consist of a hierarchy of widgets/controls. The top level frame comprising the application window contains sub frames that in turn contain still more frames or controls. These controls can be visualized as a tree structure with each control having a single parent and a number of children. In fact it is normal for this structure to be stored explicitly by the widgets so that the programmer, or more commonly the GUI environment itself, can often perform some common action to a control and all of its children. For example, closing the topmost widget results in all of the child widgets being closed too. The containing widget is called the parent. |
Focus | When a window gets focus it becomes the active window in that all keystrokes and mouse clicks will go to that window and its child widgets. For example a word processor may have a modeless dialog box for searches. The user can switch focus between the main window and the dialog by clicking with the mouse on whichever window is to receive input. |
Every GUI application is constructed in a tree-like manner with a top-level window containing other windows that in turn contain more windows until you eventually reach the lowest level controls and widgets. This hierarchy can be represented as a tree structure and is known as the containment tree.
Events arrive at a child widget, which, if it is unable to handle it, passes the event to its parent and so on up to the top level. Similarly, if a command is given to draw a widget, it will send the command on down to its children; thus a draw command to the top-level widget redraws the entire application, whereas one sent to a button likely only redraws the button.
This concept of events percolating up the tree and commands being pushed down is fundamental to understanding how GUIs operate at the programmer level. It is also the reason that you always need to specify a widgetβs parent when creating it, so that it knows where it sits in the containment tree. An example containment tree is shown in Figure 4.3.
This illustrates the top-level widget containing a single Frame
that represents the outermost window border. This in turn contains two more Frames
; the first contains a text Entry
widget and a Label
, and the second contains the two Buttons
used to control the application. You should refer back to this diagram when you get ready to build a simple GUI in the next section.
Itβs time to turn this discussion into real code. You start by building the application illustrated in Figure 4.3 in the previous section. The code looks like this (and can be loaded from the file demo1.py
in the Tkinter
folder of the Chapter4.zip
file):
β import tkinter as tk
β # create the top level window/frame
β top = tk.Tk()
β F = tk.Frame(top)
β F.pack(fill="both")
β # Now the frame with text entry
β fEntry = tk.Frame(F, border=1)
β eHello = tk.Entry(fEntry)
β eHello.pack(side="left")
β lHistory = tk.Label(fEntry, text=" ", foreground="steelblue")
β lHistory.pack(side="bottom", fill="x")
β fEntry.pack(side="top")
You start by importing tkinter
and creating a top-level widget. (You already saw this in the earlier section on using message boxes.) Next, you create a Frame
to hold all of the other widgets. The first parameter of all widget creation methods is the parent widget, so in this case you specify the parent as being top
. The next step calls F.pack()
. The pack()
call invokes a simple layout manager that, by default, simply packs components into the containing object starting at the top and working down. You are packing your Frame
object into its parent, top
, that represents the main window. The fill option tells the widget to expand to fill the window in both
vertical and horizontal directions.
You then create a second Frame
to hold the Entry
and Label
widgets. You use named parameters of the constructor to set a border
around the entry widget and set the foreground
color of the label font. Notice also that you are using arguments to tell the packer to place the widgets at the sides of the frame rather than its default vertical stacking arrangement. You can see the usage pattern developingβcreate a widget then pack it. Finally, you pack the new fEntry
frame itself.
The next step is to create the buttons and associate some behavior with them. For that you need to create an event handler that is activated when the user presses a button:
β # create the event handler to clear the text
β def evClear():
β lHistory['text'] = eHello.get()
β eHello.delete(0,tk.END)
The event handler sets the text of the lHistory
label to the contents of the eHello
entry field and then deletes the text from the eHello
field itself. You use a dictionary style access to set the Label
βs text. This technique works for any of the attributes of the widget. The delete()
method takes 0
as the first argument. That indicates the start of the text, and the special value tk.END
, used as the second argument, means the end of the text.
You create the buttons and connect the event handler with the following code:
β # Finally the frame with the buttons.
β # sink this one for emphasis
β fButtons = tk.Frame(F, relief="sunken", border=1)
β bClear = tk.Button(fButtons, text="Clear Text", command=evClear)
β bClear.pack(side="left", padx=5, pady=2)
β bQuit = tk.Button(fButtons, text="Quit", command=F.quit)
β bQuit.pack(side="left", padx=5, pady=2)
β fButtons.pack(side="top", fill="x")
β # Now run the eventloop
β F.mainloop()
Once again, you create a frame to hold the buttons and then pack the buttons using side arguments. You also added some padding to the buttons so that you can create some space around them. The bClear
button is connected to your event handler function by specifying the function name, evClear
, as the command argument. The bQuit
button uses the predefined quit
method on the top-level frame, F
.
Finally, you start the Tkinter mainloop()
running and wait for the user to do something. If you run the code, the resulting window should look like Figure 4.4, which shows before and after versions of the application.
Youβll find that you can resize, iconify, and move the window just like any other window in your desktop. Pressing the Quit
button closes the application (as does the usual close icon on the title bar) and pressing the Clear button calls your evClear
function.
Referring back to the containment tree diagram from the previous section, you see that it describes the layout of this application with three frames, a label and entry box, and two buttons.
Now that youβve seen the basics in action, you can move onto a more realistic example using more widgets and linking the controls to more substantial event handling functions. Itβs time to revisit your tic-tac-toe game.
In this section you build a GUI for your tic-tac-toe game using exactly the same logic and data layers as previously. This GUI is much closer to the kind of GUI you would expect to see on a modern desktop application, complete with menus, buttons, and mouse interaction rather than relying on keyboard input. By using the same tic-tac-toe logic as before, you can focus on the structure of the GUI without thinking too much about how the application itself works.
When building a substantial GUI application, it often helps to sketch out roughly what you want it to look like before jumping into code. For a tic-tac-toe game, you want a menu bar with File
and Help
menus. The File
menu has New
, Resume
, Save
, and Exit
menu items. (You could have called the menu the Game
menu, but File
is the conventional choice for GUI applications and consistency of style is one of the advantages of using GUIs.) The Help
menu has Help
and About
options.
The board itself will be represented by nine buttons laid out in the usual grid style. When a button is clicked, its label will display the playerβs mark.
A status bar on the bottom will display messages to the user and the final results will be presented using message boxes. It should look something like Figure 4.5.
With a clear idea of the layout, you now want to start thinking about the code structure. The GUI essentially consists of three areas: the menu bar, the board, and the status bar. The board needs to be treated as a group and centered in its frame so you could make the board itself another frame inside the central frame. The status bar just displays text so it will consist only of a label widget. That just leaves the menu bar.
Menus in Tkinter are a little more complex than the widgets youβve used so far. The initial menu bar is actually quite easy to create because itβs the default for a new menu. But, how do you create the drop-down menus? The answer is that the menu class has a method called add_cascade()
that applies a submenu to a higher level menu. Tkinter knows about menus so it automatically creates drop-down and pop-out menus as necessary without you having to do anything clever. The final anomaly to menus is that they are not added to the window using the normal layout manager methods, like pack()
. Instead the top-level widgetβs menu attribute is set to the top-level menu object.
Creating menus can become rather tiresome with a lot of repetitive typing so itβs often easier to model the menus as data and then use a loop to process the data structure into a menu hierarchy. You can use that approach here even though your menus are actually quite short.
The code to build the menus looks like this (you can load it from the zip file as oxo_menu.py
in the OXO
folder):
β import tkinter as tk
β import tkinter.messagebox as mb
β import oxo_logic
β top = tk.Tk()
β def buildMenu(parent):
β menus = (
β ("File", (("New", evNew),
β ("Resume", evResume),
β ("Save", evSave),
β ("Exit", evExit))),
β ("Help", (("Help", evHelp),
β ("About", evAbout)))
β )
β menubar = tk.Menu(parent)
β for menu in menus:
β m = tk.Menu(parent)
β for item in menu[1]:
β m.add_command(label=item[0], command=item[1])
β menubar.add_cascade(label=menu[0], menu=m)
β return menubar
β def dummy():
β mb.showinfo("Dummy", "Event to be done")
β evNew = dummy
β evResume = dummy
β evSave = dummy
β evExit = top.quit
β evHelp = dummy
β evAbout = dummy
β mbar = buildMenu(top)
β top["menu"] = mbar
β tk.mainloop()
After the initial imports and creation of the top level widget, you define the menu-building function.
The menu structure is defined as a set of nested tuples. The leaf node menu items consist of name-function pairs. You then create the top-level menu bar object and loop over the data structure building the submenus and inserting them into the menu bar. The complete menu bar object is returned.
The next section of code defines the event handler functionsβat least, it will do so when you are finished. For now you simply define a dummy function to handle all events (except evExit
, which is trivial) and assign it to each of the event handler variables. Soon, you return to these variable assignments and convert them into definitions of the real event handling functions for your game.
Finally, you execute the buildMenu()
function and assign the result to the top
widgetβs menu
attribute. You then run the mainloop()
.
When you run this program, you should get a window with a menu bar and when you select any menu item it calls the dummy function.
Having created the menu structure, you now want to extend the program to create the board. This sits within a Frame
that itself sits centrally inside an outer Frame
. The reason for this design is that it separates out the tasks of laying out the board buttons from the layout of the board as a whole.
In the same way that you did for the menus, you create a function that builds the board. The board is the part of the game that is connected to the logic layer and the basic game play so you also need to define code to convert the logical layerβs data model of a game into the displayed board within your GUI and vice-versa. You can do that in small helper functions. You also need to add an event handler function to set the button label when a button is clicked; the interplay between the GUI and logic layer is also part of the button click event handler code.
You tackle the GUI building part first. The code is as shown here (and can be loaded from oxo_gui_board.py
from the OXO
folder of the zip file):
β def evClick(row,col):
β mb.showinfo("Cell clicked", "row:{}, col:{}".format(row,col))
β def buildBoard(parent):
β outer = tk.Frame(parent, border=2, relief="sunken")
β inner = tk.Frame(outer)
β inner.pack()
β for row in range(3):
β for col in range(3):
β cell = tk.Button(inner, text=" ", width="5", height="2",
β command=lambda r=row, c=col : evClick(r,c) )
β cell.grid(row=row, column=col)
β return outer
β mbar = buildMenu(top)
β top["menu"] = mbar
β board = buildBoard(top)
β board.pack()
β status = tk.Label(top, text="testing", border=0,
β background="lightgrey", foreground="red")
β status.pack(anchor="s", fill="x", expand=True)
β tk.mainloop()
You have started to use some of the more cosmetic features of Tkinter to improve the widgetβs appearance and more clearly separate them on the screen. In this case you used the border
and relief
attributes to make the board frame more clearly distinct from the menu and status bar (that you also defined here because itβs just two extra lines and gets you close to your final GUI structure). You also set the color options for the status bar and βanchorβ it to the bottom of the top-level frame (signified by using a value of s
, for south).
The board construction itself is just a couple of for
loops creating the grid pattern. (The width and height values were determined by trial and error.) The command
argument for the buttons is interesting because it uses the lambda
function mechanism. The reason for this is that the command
argument must be a function that takes no arguments, but you need to pass in the row and column values. You do that by setting up two, default-valued parameters where the values are the row
and col
values at the point of button creation. Each button calls the evClick
function with its own unique set of values. This is a common trick when programming with Tkinter. You also used the grid layout manager rather than the packer because the board layout is a perfect match to the grid style of layout. You simply specify the row and column locations of each control and the grid does the rest.
Having gotten the basic GUI structure in place, you can now turn your attention to writing the game play code and hooking up the various menus to the final event handling functions. You can tackle the game code first as it sits mainly in the evClick
event handler and is aided by a couple of helper functions that you can call cells2game
and game2cells
.
The modifications look like this (and can be loaded from the file oxo_gui_game.py
in OXO
folder of the zip file):
β gameover = False
β def evClick(row,col):
β global gameover
β if gameover:
β mb.showerror("Game over", "Game over!")
β return
β game = cells2game()
β index = (3*row) + col
β result = oxo_logic.userMove(game, index)
β game2cells(game)
β if not result:
β result = oxo_logic.computerMove(game)
β game2cells(game)
β if result == "D":
β mb.showinfo("Result", "It's a Draw!")
β gameover = True
β else:
β if result == "X" or result == "O":
β mb.showinfo("Result", "The winner is: {}".format(result))
β gameover = True
β def game2cells(game):
β table = board.pack_slaves()[0]
β for row in range(3):
β for col in range(3):
β table.grid_slaves(row=row,column=col)[0]['text'] = game[3*row+col]
β def cells2game():
β values = []
β table = board.pack_slaves()[0]
β for row in range(3):
β for col in range(3):
β values.append(table.grid_slaves(row=row, column=col)[0]['text'])
β return values
If you compare the evClick
code to the original playGame()
function, you will see that there are many similarities. The game2cells()
function is analogous to the original printGame()
function. The cells2game()
function uses some widget methods to retrieve the child widgets, in this case the buttons. You could have, as an alternative, stored the lists of buttons in a global data structure that would have given more direct access. The game logic of userMove
and computerMove
is unchanged even though the user interface is vastly different.
The last thing to do is fill in the menu event handlers. These are almost trivial to complete, and the finished program looks like this (it is found as oxo_gui_complete.py
in the OXO
folder of the zip file):
β import tkinter as tk
β import tkinter.messagebox as mb
β import oxo_logic
β top = tk.Tk()
β def buildMenu(parent):
β menus = (
β ("File",( ("New", evNew),
β ("Resume", evResume),
β ("Save", evSave),
β ("Exit", evExit))),
β ("Help",( ("Help", evHelp),
β ("About", evAbout)))
β )
β menubar = tk.Menu(parent)
β for menu in menus:
β m = tk.Menu(parent)
β for item in menu[1]:
β m.add_command(label=item[0], command=item[1])
β menubar.add_cascade(label=menu[0], menu=m)
β return menubar
β def evNew():
β status['text'] = "Playing game"
β game2cells(oxo_logic.newGame())
β def evResume ():
β status['text'] = "Playing game"
β game = oxo_logic.restoreGame()
β game2cells(game)
β def evSave():
β game = cells2game()
β oxo_logic.saveGame(game)
β
β def evExit ():
β if status['text'] == "Playing game":
β if mb.askyesno("Quitting","Do you want to save the game before
β quitting?"):
β evSave()
β top.quit()
β
β def evHelp ():
β mb.showinfo("Help",'''
β File->New: starts a new game of tic-tac-toe
β File->Resume: restores the last saved game and commences play
β File->Save: Saves current game.
β File->Exit: quits, prompts to save active game
β Help->Help: shows this page
β Help->About: Shows information about the program and author''')
β def evAbout():
β mb.showinfo("About","Tic-tac-toe game GUI demo by Alan Gauld")
β def evClick(row,col):
β if status['text'] == "Game over":
β mb.showerror("Game over", "Game over!")
β return
β game = cells2game()
β index = (3*row) + col
β result = oxo_logic.userMove(game, index)
β game2cells(game)
β if not result:
β result = oxo_logic.computerMove(game)
β game2cells(game)
β if result == "D":
β mb.showinfo("Result", "It's a Draw!")
β status['text'] = "Game over"
β else:
β if result =="X" or result == "O":
β mb.showinfo("Result", "The winner is: {}".format(result))
β status['text'] = "Game over"
β def game2cells(game):
β table = board.pack_slaves()[0]
β for row in range(3):
β for col in range(3):
β table.grid_slaves(row=row,column=col)[0]['text'] = game[3*row+col]
β def cells2game():
β values = []
β table = board.pack_slaves()[0]
β for row in range(3):
β for col in range(3):
β values.append(table.grid_slaves(row=row, column=col)[0]['text'])
β return values
β def buildBoard(parent):
β outer = tk.Frame(parent, border=2, relief="sunken")
β inner = tk.Frame(outer)
β inner.pack()
β for row in range(3):
β for col in range(3):
β cell = tk.Button(inner, text=" ", width="5", height="2",
β command=lambda r=row, c=col : evClick(r,c) )
β cell.grid(row=row, column=col)
β return outer
β mbar = buildMenu(top)
β top["menu"] = mbar
β board = buildBoard(top)
β board.pack()
β status = tk.Label(top, text="Playing game", border=0,
β background="lightgrey", foreground="red")
β status.pack(anchor="s", fill="x", expand=True)
β tk.mainloop()
The event functions mirror the original functions in that they mostly just call the oxo_logic
functions and then use the game2cells()
function to display the board. You now use the status text instead of the global gameover
flag. The Help menus simply display text in a showinfo
dialog.
The final working game looks like Figure 4.6.
There is a lot more you could do to add polish to this game, but it provides enough to show what can be done using Tkinter as a user interface toolkit and illustrates once again the power of separating the logic and data layers from the presentation layer. You have now written more than 600 lines of code to play the various versions of tic-tac-toe. Thatβs enough for anyone, so itβs time to move onto new pastures.
Tkinter has many other widgets and tricks for you to play with. Experiment with the simple GUI application we started with and add or modify the different options that affect layout and appearance. You havenβt even looked at how to display formatted text or images or plot graphs or build complex dialogs. All of these things are possible and build on the foundation you saw here. There are many Tkinter tutorials and sample programs around, including IDLE, the default IDE for Python. Reading the code and seeing how these programs control appearance and use widgets is a great way to learn.
Tkinter has a couple of extension modules that are included in the standard library (although some Linux distributions omit them for some reason). In the next section, you see what these extension modules can add to your programs.
The two biggest criticisms leveled at Tkinter are that it doesnβt have enough widgets and it looks ugly. In comparison to the other GUI toolkits, these are valid issues. However, in recent releases, Tkinter has been fighting back with the introduction of two new modules built on top of Tkinter. These are tix
, which adds several new widgets, and ttk
, which enables theming, which basically, just means the GUI can look more like the native OS GUI.
Unfortunately, the documentation for these modules is not as comprehensive as you would want, and at the time of writing, the Python documentation often just contains links to the Tcl/Tk documentation. Once you get used to it, you can usually figure out what options you need from there, but itβs not ideal. However, the power of the Python interactive prompt comes to the rescue once again, because you can play with them and experiment to find out what they have to offer. Both modules support most of the same Tkinter widgets that you have used so far; so you should be able to take your Tkinter program and convert it to using ttk or Tix fairly easily. For Tix it is as simple as changing the tkinter
import line as shown:
β import tkinter.tix as tk
This is one benefit of using the tk
alias when importingβyou donβt need to change all your tkinter
prefixes to tix
; you simply set the import to use the same alias. You can try that on your tic-tac-toe game if you like. It functions identically to the Tkinter version except that the title bar of the window displays tix
instead of Tk
.
For ttk
itβs marginally more complicated because ttk
uses the tkinter
mainloop
and top-level window so you need to import both. You then refer to ttk
when creating widgets and tkinter
when controlling the top window and event loop. You see this in practice later, in the βUsing ttkβ section.
Because tix
is so similar to tkinter
, you can translate all you know about tkinter
into tix
and jump straight into learning about the new widgets. There are more than 40 of them, but some are very poorly documented, even in the Tcl/Tk community. If you stick to the subset listed on the Python documentation page, you should be fine. You only dip a toe in the water here, but hopefully it is enough to demonstrate that tix
is a valuable addition to the Tkinter family.
The widgets that you look at here are the ComboBox
, the ScrolledText
, and the Notebook
.
You can see how a ComboBox
input control can be used to set a labelβs text by typing the following code at the Python interactive prompt:
β >>> import tkinter.tix as tix
β >>> top = tix.Tk()
β >>> lab = tix.Label(top)
β >>> lab.pack()
β >>> cb = tix.ComboBox(top,command=lambda s:lab.config(text=s))
β >>> for s in ["Fred","Ginger","Gene","Debbie"]:
β ... cb.insert("end",s)
β ...
β >>> cb.pick(0)
β >>> lab['text'] = "Pick any item"
β >>> cb.pack()
β >>> top.mainloop()
β >>>
There are several things to note here. First, you used config()
to set the text attribute rather than the dictionary style access you used previously. The advantage of config()
is that you can set multiple attributes at once just by passing them as named arguments. Second, the event handler lambda
function uses a string argument, s
, which is passed in by the widget event. The string holds the currently selected value. Figure 4.7 shows the resulting window in action.
The ScrolledText
widget is an extension of the standard Text
widget. As such it can display images as well as formatted text. The Tix version adds scrollbars automatically, which is a useful addition that involves quite a lot of work using the standard toolkit. In use it is very much like the other Tkinter widgets. You can play with it by typing the following code:
β >>> top = tix.Tk()
β >>> st = tix.ScrolledText(top, width=300, height=100)
β >>> st.pack(side='left')
β >>> top.mainloop()
Figure 4.8 shows the resulting text box with enough text to activate the vertical scrollbar.
You can simply type into the text box manually or insert text programmatically, like so:
β >>> t = st.subwidget('text')
β >>> t
β <tkinter.tix._dummyText object at 0x019186D0>
β >>> t.insert('0.0',"Some inserted text")
β >>> t.insert('end',"
more inserted text")
β >>>
Notice that you used the subwidget()
method to get a reference to the underlying text widget and then used its insert()
method to insert the text. This technique of fetching the underlying standard widget is quite common when working with Tix widgets.
You can also select areas of text in a text widget. Carrying on the previous example, you can change the font and weight of the first line of the previous text like so:
β >>> t.tag_configure('newfont', font=("Roman", 16, "bold"))
β >>> s = t.get('1.0','1.end')
β >>> t.delete('1.0','1.end')
β >>> t.insert('1.0',s,'newfont')
The tag_configure()
method creates a new tag, that is to say a style that you can apply to text. The style is called newfont
and uses a Roman
font, of size 16
points and weight bold
. This triplet font specifier format is standard across Tkinter (and therefore Tix and ttk).
You then used get()
to fetch the text from the first character through to the end
of line 1
. You then deleted the existing text from the widget and replaced it with the same text but using the newfont
tag as a third argument of the insert()
method.
The result is as shown in Figure 4.9. Note that all of this is being displayed in a Tix ScrolledText
widget, but you are actually using the underlying standard Tkinter Text
widget to manipulate the text.
The final widget you look at is an altogether more complex contraption. It is a NoteBook
widget. It consists of a number of pages, each with an associated tab that the user can select to activate the page. The page is just a Tkinter container that can be populated with whatever controls you want to use. Often it is a text window or a form. You create a two-page notebook, the first pane containing a ScrolledText
widget and the other a set of buttons that launch various message box dialogs.
To see the Notebook in action, type the following code into a file and execute it from the command line or your IDE (or load it from the file tix-notebook.py
in the Tkinter
folder of the zip file, if you prefer):
β import tkinter.tix as tix
β import tkinter.messagebox as mb
β top = tix.Tk()
β nb = tix.NoteBook(top, width=300, height=200)
β nb.pack(expand=True, fill='both')
β nb.add('page1', label="Text")
β f1 = tix.Frame(nb.subwidget('page1'))
β st = tix.ScrolledText(f1)
β st.subwidget('text').insert("1.0", "Here is where the text goes...")
β st.pack(expand=True)
β f1.pack()
β nb.add('page2', label="Message Boxes")
β f2 = tix.Frame(nb.subwidget('page2'))
β tix.Button(f2, text="error", bg="lightblue",
β command=lambda t="error", m="This is bad!":
β mb.showerror(t,m) ).pack(fill='x',expand=True)
β tix.Button(f2, text="info", bg='pink',
β command=lambda t="info", m="Information":
β mb.showinfo(t,m) ).pack(fill='x',expand=True)
β tix.Button(f2, text="warning", bg='yellow',
β command=lambda t="warning", m="Don't do it!":
β mb.showwarning(t,m) ).pack(fill='x',expand=True)
β tix.Button(f2, text="question", bg='green',
β command=lambda t="question", m="Will I?":
β mb.askquestion(t,m) ).pack(fill='x',expand=True)
β tix.Button(f2, text="yes-no", bg='lightgrey',
β command=lambda t="yes-no", m="Are you sure?":
β mb.askyesno(t,m) ).pack(fill='x',expand=True)
β tix.Button(f2, text="yes-no-cancel", bg='black', fg='white',
β command=lambda t="yes-no-cancel", m="Last chance...":
β mb.askyesnocancel(t,m) ).pack(fill='x',expand=True)
β f2.pack(side='top', fill='x')
β top.mainloop()
In this example you imported the modules and created the top-level widget as usual. You then created a tix.Notebook
object called nb
. To this you added a page and called it page1
. You then created a frame and made its parent the page1
page that you just created. You added a text widget and some text, and then packed the widget and frame.
Next, you created a second page, called page2
, and added a frame to that page as before. You then created a bunch of buttons and linked them to the various message boxes using lambda
functions as commands. You modified the pack()
options of both the buttons and the frame to make the buttons occupy the full width of the page, and you gave them different colors to make them stand out.
Notice that for the notebook, buttons, and ScrolledText widgets, we specified the expand
option to the packer. expand
tells the layout manager to expand the widget when the window is resized. By using expand
in combination with fill
and anchor
, you can very precisely control how your widgets behave when resizing the window. (It is well worth experimenting with the options to get a feel for them.)
Finally, you started the mainloop()
function. When you ran it, the result should have looked like Figure 4.10, which shows both pages of the notebook in action.
As mentioned earlier, the ttk
module brings the concept of themes to Tkinter and Python. A theme is a graphical style and enables the same GUI structure to take on the look and feel of the native operating system. There are several themes that ship with ttk
, but you can also create bespoke themes of your own.
The predefined themes vary by operating system, and you can find the names for your platform by looking at the output of ttk.Style().theme_names()
. On Windows they include Classic
(the default), winnative
, vista
, and xpnative
.
ttk
comes with its own versions of 11 of the standard Tkinter widgets that are theme aware as well as 6 new widgets of its own, including a ComboBox
and NoteBook
. You can change the look of applications just by changing the theme. Figure 4.11 shows a very simple Tkinter GUI presented first using classic
, then using vista
, and then using classic
again, but this time on the Ubuntu Linux platform. Notice that the top button doesnβt change too much, but the new ttk
button is different in each image. The code looks like this:
β >>> import tkinter as tk
β >>> import tkinter.ttk as ttk
β >>> top = tk.Tk()
β >>> s = ttk.Style()
β >>> s.theme_use('classic')
β >>> tk.Button(top,text="old button").pack()
β >>> ttk.Button(top,text="new button").pack()
β >>>
Obviously, you need to swap vista
for classic
in the style object to get the Vista theme. It should be obvious that you need to do a small amount of extra work to define the style object, but otherwise the use of ttk
mostly looks like normal Tkinter programming. You should notice when you run the code that the differences are more than simple tweaks to the appearance of the button. The behavior is different too. For example, when you mouse over the button, it changes color differently to the old style button object.
Thatβs all you really need to know about ttk
for now. There are lots of options that you can play around with; you can even define your own styles and themes. Mostly, you just use it as shown, and you get small but significant improvements to your Tkinter programβs look and feel.
Youβve now looked at several parts of the Tkinter toolset. It is time to wrap things up by bringing these pieces together in a larger example based on something less frivolous than a tic-tac-toe game. You revisit the lending library database that you created in Chapter 3 and build a GUI front end. This will reinforce much of what you have already done, but also introduces some new elements and techniques:
ScrolledListBox
widgetstate
attributeYouβll build this in the following Try It Out.
Youβve now covered enough Tkinter to see how it can wrap a GUI around your application logic and data. Itβs not the most powerful toolkit around, but it is quick and easy to get started, and it comes out of the box with Python. In the next section, you review some of the more powerful third-party GUI toolkits available.
There are many GUI toolkits around, ranging from the very specific native toolkits for Windows, MacOS X, and X windows, to more generic, multiplatform toolkits. Most of them have a wrapper layer of some kind available for Python. They all have the same core ideas and concepts that you saw in Tkinter, although some require an object-based approach while others, like Tkinter, permit a procedural style of programming, too. If Tkinter is not working for you, or if your main area of interest is GUI development, then these other toolkits may hold the answer. In the following sections, you find out about the strengths and weaknesses of each, and for the platform independent toolkits a very short βhello worldβ style sample program. If you want to run these, you need to install the toolkits because they are all provided by third parties.
That having been said, there are no absolute best or worst toolkits here. Each has its fans, and different programmers prefer different toolkits. It is worth taking some time to try out each toolkit of interest and at least work through their introductory tutorial to see whether it fits your personal style of coding. There are also some useful online videos that introduce their features, too.
This is a long-standing toolkit that is a wrapper around the C++ wxWidgets project. wxWidgets is a C++ toolkit designed to work on all the popular operating systems while maintaining a native look and feel. Version 3.0 of wxPython was released late in 2013.
It has a rich set of widgets and powerful features supporting things like cross platform printing. (Printing from a GUI is one of those functions that sounds like it should be easy, but very rarely is!) There are some graphical GUI building tools that can generate code for you, or you can do everything by hand by crafting the code, as you did for Tkinter. While wxPython is powerful, it is still much simpler than some of the other toolkits discussed.
There are active mailing lists and forums for both wxWidgets and wxPython. There are a couple of books available on wxPython, including one written by the lead developers. The wxPython website is: http://www.wxpython.org
.
A sample wxPython program looks like this:
β import wx
β app = wx.App(False)
β frame = wx.Frame(None, wx.ID_ANY, size=(320,240), "Hello World")
β frame.Show(True)
β app.MainLoop()
The Qt toolkit came to prominence in the development of the KDE desktop environment for Linux although it had been developed some time before that as a commercial product. Over time the licensing arrangements of Qt have been simplified such that it is now widely used in open source projects and supports most operating systems with a native look and feel. Qt is a C++ toolkit, and PyQt is the Python wrapper around that. Version 5.2 was released in early 2014.
To give some idea of the scale, Qt has more than 400 classes available and several thousand functions and methods. The learning curve is considerable, but so is the power. Some advanced features are available only to commercial users (who pay license fees), and this mixed mode of free and licensed software is probably the biggest drawback of Qt and hence PyQt. There is a full-featured graphical GUI building tool for PyQt.
A true open source (LGPL) alternative has been released in the form of PySide that offers similar functionality to PyQt and was developed by Nokia while they owned the Qt toolkit. Version 1.2.1 of PySide was released mid-2013.
There are at least two books available on PyQt programming. The website is: http://wwwβ¨.riverbankcomputing.com/software/pyqt/intro
. The PySide web site is: http://qt-projectβ¨.org/wiki/PySide
.
A sample PyQt program looks like this:
β import sys
β from PyQt4 import QtGui
β app = QtGui.QApplication(sys.argv)
β win = QtGui.QWidget()
β win.resize(320, 240)
β win.setWindowTitle("Hello World!")
β win.show()
β sys.exit(app.exec_())
A sample PySide program looks like this:
β import sys
β from PySide import QtGui
β app = QtGui.QApplication(sys.argv)
β win = QtGui.QWidget()
β win.resize(320, 240)
β win.setWindowTitle("Hello World!")
β win.show()
β sys.exit(app.exec_())
As you can see, they are effectively identical, apart from the import statements.
The Gimp ToolKit, or GTK+ as itβs now known, was originally developed in C as the GUI toolset for the GNU GIMP graphics editor. It then developed into a generic graphical toolkit and has become the toolkit behind the GNOME desktop environment used on many Linux distributions. PyGTK is the name used for the Python wrapper around the GTK+ toolkit. However, the situation has become more complex, and there are several parts to PyGTK matching the various parts of GTK+ itself. PyGObject is now the official module supporting most of the GNOME software platform including the GUI. As part of the GNU stable, it is an open source project so it has no complex license issues to contend with. New versions are released regularly.
There is a graphical design tool called Glade that can be used to create the GUI. In typical GNU fashion, the documentation is comprehensive, but not tailored to beginners. The system is very powerful and multiplatformed. Once installed it is reasonably simple to use.
There are several books on GTK+ programming, but they are focused on the underlying C API, not on the Python bindings. This leaves the online, but excellent, documentation found here: https://live.gnome.org/PyGObject.
A sample GTK program looks like this:
β from gi.repository import Gtk
β win = Gtk.Window(title="Hello World")
β win.resize(320,240)
β win.connect("delete-event", Gtk.main_quit)
β win.show_all()
β Gtk.main()
Cocoa and Win32 are the native GUI toolkits for the Mac OS X and Windows operating systems respectively. Both can be programmed from Python. The PyObjC toolkit for Cocoa is provided by the MacPython project, and the native MacOS X development tools can be used to create and connect the GUI to code. You have already met the Pywin32 package back in Chapter 2, where its ability to expose the Win32 API was discussed. The Win32 API is not only about low-level Windows functions, but also it has all the functions used to build Windows native GUIs. The mechanisms are the same; you just call different functions.
The disadvantages to both these toolkits, especially the Win32 API, is the complexity involved combined with the fact that they are strictly limited to their own operating system. If you know that you will never need to support anything else, that may not be a problem. The Cocoa option does at least provide a useful set of development tools, but the Windows option is not so richly endowed.
A far better option for native Windows development is the use of IronPython. This is a version of Python written in Microsoft .NET and supported on Microsoftβs Visual Studio development environment as a standard .NET language. This gives access to all the .NET functionality, but is still limited to Windows. (There is a .NET clone for other platforms known as Mono, but it is not widely used for desktop applications.)
The MacPython website is http://homepages.cwi.nl/~jack/macpython/
. The IronPython website is http://ironpython.net
/.
Dabo is rather different from the other toolkits described in that it is more than just a GUI toolkit. Dabo is a fully featured application framework and toolset. It specializes in database applications such as those commonly found in businesses. It comes with a set of GUI widgets, currently based on a wxWidgets foundation but modified for Dabo (theoretically, another GUI toolkit could be used but the development team has found more productive ways to spend its time!). On top of the GUI, it provides a set of classes that contains the business logic and bridges the gap between the user interface and data layer. The data layer can be any of half a dozen databases, including SQLite.
It is possible to build complex logic into a Dabo application, although its natural home is in building forms-based tools that provide a view and editing capability to the base data. Version 0.9.12 was released June 2013. The Dabo website is found at http://www.dabodev.com/
.
Youβve now covered a fair amount of ground, especially regarding user interface options. You now look at some other issues you will likely meet in building real-world desktop applications in Python: storing configuration data and localization.
In Chapter 3 you saw various strategies for storing data. Earlier in this chapter, you saw how an application can be structured in layers with a data layer at the bottom of the stack. That data layer is concerned with managing the core entities of your application. Those core entities are not the only kinds of data you need to store. You often need to store data about the application itself. That configuration data is specific to a single user or perhaps to the local computer system and hence is called local data. Typically, it is stored in a configuration file or as environment variables. You saw how to read that data in Chapter 2. In this section you consider the various kinds of local data that an application may need to maintain and the options available for storing it.
Applications have several different types of configuration data. Some of it is concerned with getting the application to work in the first place, for example, the network address of a server or the location of the data files. These values are typically specific to a given installation or computer system rather than to an individual user.
Other types of configuration data are things like user preferences. For example, the user may have some control over the layout of the user interface, the colors used, the location and size of the Windows on screen, and so on. Another common category of user-configured data is the selection of helper applications used, for example, the userβs preferred text or image editing software. Some of these details could be exposed in a preferences dialog that the user edits and explicitly saves. Indeed it may even be possible to store different preferences for different usage scenarios for the same application.
Other settings may be stored by the application itself so that it can restore itself to the last state when it restarts. These settings might include the last opened file, the currently open windows and dialogs, and the screen coordinates of each.
The final data type you might want to store is information about how the application is functioning. In particular, error conditions or unexpected inputs can be recorded. This is generically known as logging and involves storing information in a log file that can be examined later either as part of a debug process or to improve effectiveness of the design.
Application specific data could be stored on the local computer, or it could be stored in a local network location that all instances of the application can reach. This raises the question of how the computer knows where to look. The usual solution to this problem is to set an environment variable or use a local configuration file stored in the startup folder of the application. This can then be set as part of the installation procedure. It can even be provided as a startup parameter.
The advantage of storing this kind of information on the network is that it is shared so that any changes made, for example if the database is moved, can be detected immediately by all of the installed instances on the network without having to manually reconfigure each machine or user configuration. It also allows for a backup configuration to be available in the event of a system failure and by changing one environment variable or configuration setting on the local computers the new central configuration can be accessed with minimal downtime.
The physical storage medium is relatively unimportant because the data are relatively static and normally only read once when the application starts up. A simple configuration file using plaintext, XML, or even Windows INI format will likely suffice. Python provides tools for creating and reading all of these; refer to Chapter 2 for details.
User preferences are nearly always stored in the local computer, and often in a configuration file stored in the userβs home directory. Occasionally, applications store user preferences in the main database, especially if the database contains a significant amount of user data anyway. The disadvantage of using the database is that the application can access the preferences only if the database is accessible, which may not be the case if the user is mobile. It is disconcerting for a user to find that the application appears or functions differently depending on whether they are connected to their network or not. Local storage is definitely the preferred option for this kind of data.
Locating the data should be straightforward if a standard filename is used and the location is the home directory, because the home directory is nearly always obtainable either as an environment setting or as a user database value. (See Chapter 2 for guidance on how to determine user details such as the home directory.)
The format is normally a text file using either Windows INI or XML format. If the number of settings is very large, a small local database using SQLite might be appropriate, but this would be separate from the main application data store.
One other factor to consider when dealing with user preferences is how these are set and modified. If the settings are few and simple in nature (for example boolean or integer values), then it might be acceptable to generate a default preferences file and ask the user to manually edit the file. This does carry a risk if the user is not familiar with text editors and uses a rich format word processor to edit the file. This can render the configuration file unreadable by the application. However, if the users are likely to be experienced in editing text, such as developers or system administrators, then this approach can work well. If the configuration data is not simple or is stored in a structured file using XML or similar, then user editing is much less suitable and error prone, and a preferences dialog needs to be included in the application itself.
Storing application state is the least standardized form of local data storage. The location and format of the storage is down to the developer. Some applications make the choice of whether to save-state a user preference; others do it automatically, while most do not save state (except perhaps for a list of recently accessed files). You need to decide how much state information you want to store, where to store it, and what format to use.
To keep application behavior consistent, you should choose a local storage option so that the application behaves as expected even when disconnected from the network. However, you need to be careful with error handling because, if the application was closed while online but then opened offline, many of the resources previously used may not be accessible. You need to have a working fallback configuration that can be used when things go wrong.
The format of the state data is likely to be quite complex because it may involve multiple windows and even tabs and control settings within windows. This almost inevitably requires a rich storage format such as XML. On the other hand, if you are only saving the open file history, a simple text file may suffice, or you could even append it to the userβs preference data in their configuration file.
You often want to keep a record of unexpected events or inputs so that you can analyze them later. This could be during testing, following a system failure, or even as a continuous improvement activity. The usual way to do this is to record messages in a log file. The log file might be a single file that just grows continuously or, more commonly, a file whose name is based on the date. Housekeeping (archiving or deleting of old files) of old files might be done manually, automatically, or via a shell script.
Python provides the logging
package to assist in this process. It can generate standard information in a standard file with different levels of logging (that is it can flag a message with different category markers: debug, info, warning, error, and critical). The package is very flexible and allows for many different configuration options to control how it works. The basic usage is straightforward and you can extend its functions as you need them.
At the most basic level you import the module and then call one of several logging methods corresponding to the categories mentioned. For example:
β >>> import logging
β >>> logging.basicConfig(level=logging.DEBUG)
β >>> logging.info('Heres some info')
β INFO:root:Heres some info
β >>> logging.error('Oops, thats bad')
β ERROR:root:Oops, thats bad
β >>> logging.critical('AAARGH, Its all gone wrong!')
β CRITICAL:root:AAARGH, Its all gone wrong!
Itβs important that you call basicConfig()
before using any of the logging methods; otherwise, it has no effect, and you use the default values. The level setting indicates the minimum level of message that is displayed; DEBUG
is the lowest level, so everything gets printed. In addition to setting the level, you can also specify an output filename.
You can also specify the format of the log message including things like the date of the message, the file and function where it was generated, and so on. (The options are all documented in the LogRecord
section of the logging documentation.) Because the default format doesnβt include any date or time information, you usually want to set something like this:
β >>> import logging
β >>> logging.basicConfig(format="%(asctime)s %(levelname)s : %(message)s")
β >>> logging.error('Its going wrong')
β 2014-04-24 16:12:44,832 ERROR : Its going wrong
β >>> logging.error('Its going wrong')
β 2014-04-24 16:12:54,415 ERROR : Its going wrong
β >>>
β >>> logging.critical('Told you...')
β 2014-04-24 16:13:08,431 CRITICAL : Told you...
You can also use a datefmt
argument to basicConfig()
to change the date format using the same options as you used in time.strftime()
. Here is a short example:
β >>> import logging
β >>> logging.basicConfig(format="%(levelname)s:%(asctime)s %(message)s",
β datefmt="%Y/%m/%d-%I:%M")
β >>> logging.error("It's happened again")
β ERROR:2014/04/24-04:21 It's happened again
β >>>
There are many other things you can do, but the basic usage described here should be enough for all but the largest projects.
Localization is the name given to the various actions required to make a computer application usable in different localities. That includes such features as time zone differences, date and time formatting, currency symbols, numeric formatting and, of course, language differences. In extreme cases it might require a new UI layout to take account of reading direction, such as right to left. Your computer operating system likely has many of these features controlled by a configuration setting, usually created when the operating system is first installed. Most users never change this setting and therefore take it for granted and never think about it. As a programmer your code may have to run on different computers each with potentially different localization options.
In this section you learn about how Python provides support for localization through the use of locales and the Unicode character set. A locale is simply a code value that indicates a standardized group of localized settings (time zone, date format, currency, and so on). Unicode is an international standard for representing characters in different alphabets, for example, Latin, Arabic, Chinese, and so on, as well as different symbols such as punctuation marks and math.
Unicode is all very well in that it enables you to use different alphabets, but how do you translate the strings used in your applications into different languages? That process is known as internationalization. There is a standard industry process for this using a mechanism called gettext that generates language-specific files containing mappings from your embedded strings to the different language versions. Python supports this mechanism via the gettext
module. Localization includes the ability to select the correct string translation using gettext
.
Python supports different locales via the locale module. The way the module works is quite complex and uses a layered approach; however, mostly you donβt need to know about that. You can use a very small subset and generally you pick up the correct locale for your user.
When your program starts, it is usually set to the βCβ locale by default (although that may not always be the case, and local configuration settings may have changed it). However, you usually want to set the locale that your user has chosen. The way to do that is to call the localeβ¨.setlocale()
function with an empty locale argument. This causes the system locale to be selected. Most of the time, thatβs all you need to do. You are strongly recommended to do this only once in your program and to do so near the start of your code.
You can then use locale.getlocale()
to fetch the local details if you need to find out what has been set up (you see that in action in the next section where you look at how to translate your programβs strings into the local language).
Unfortunately, setting the locale is not the end of the story. If you want that change to take effect, there are some changes you need to make to your code. Specifically, there are some type conversion and comparison operations that are not locale-aware in the standard library and built-in functions. To get around that, the locale module provides alternatives. In other cases the standard functions do understand locales if you give them the right hints. For example, the time.strftime()
function can format times to the local style if you use the appropriate formatting specifications such as %x
for the localized date and %X
for the localized time. Similarly, to print()
numbers in the locale specific format, you need to specify the n
style instead of d
or f
or g
in the string format()
method.
The following interpreter session demonstrates some of these features:
β >>> import locale as loc
β >>> import time
β >>> loc.setlocale(loc.LC_ALL,'')
β 'English_United States.1252'
β >>> loc.currency(350)
β '$350.00'
β >>> time.strftime("%x %X", time.localtime())
β '4/22/2014 7:44:57 PM'
β >>>
Repeating those in a cygwin session set to the en_GB
locale, you can see some differences:
β >>> import locale as loc
β >>> import time
β >>> loc.setlocale(loc.LC_ALL, '')
β 'en_GB.UTF-8'
β >>> loc.currency(350)
β 'Β£350.00'
β >>> time.strftime("%x %X", time.localtime())
β '22/04/2014 19:32:23'
The locale itself is clearly very differently specified. The currency uses the appropriate symbols, and the dates and times are quite different with the UK version using a 24-hour clock format and with the day and month of the date transposed.
You can now try some of the conversion and comparison functions. These work the same in both UK and U.S. English, so there is no point in doing a comparison this time.
β >>> print("{:n}".format(3.14159))
β 3.14159
β >>> print("{:n}".format(42))
β 42
β >>> loc.strcoll("Spanish", "Inquisition")
β 1
β >>> loc.strcoll("Inquisition", "Spanish")
β -1
β >>> loc.strcoll("Spanish","Spanish")
β 0
These examples show how the string formatting n
specifier works for both integers and floating-point numbers. The locale.strcoll()
string comparison examples are useful because they take account of locale specific ideas on character ordering. The return values are 1
if the first string is βhigherβ valued, -1
if it is βlowerβ valued, and 0
if the two arguments are the same.
Locale provides the following conversion functions that are useful in particular situations: atoi()
, atof()
, str()
, format()
, and format_string()
.
Computers store data as binary numbers. Characters are mapped onto these numbers so that when the computer prints a string of characters it maps the numeric data in memory to a set of character representations on screen. Back in the dawn of computing, characters were represented by as few as 5 bits and more commonly using 7 or 8 bits. All of these encodings could fit into a single 8-bit byte of storage, so it was very compact. Unfortunately, a single byte can only cater to 256 different combinations, which is fine for the Latin alphabets, used in the Western nations where modern computing originated, but nowhere near enough for all the alphabets in the world as a whole. Over time each country and corporation invented its own encoding system, and software engineers had to write lots of code to cater to these if their software was to be used in different localities. The solution was the Unicode standard.
Unicode is a 32-bit character catalog that can store a huge number of possible characters. Unicode characters are represented by entities called code points that are the numeric values that map onto characters. Code points are described using the format U+xxxx. The U+ indicates that it is a Unicode code point and the xxxx is the hexadecimal number representing the location of the code point in Unicode (there can be up to 8 hex digits not just 4). Code points are then mapped to characters that also have descriptive names. Thus the character βAβ is listed as βU+0041 LATIN CAPITAL LETTER Aβ. The Python representation of the Unicode encoding is uxxxx
for 4 digits or Uxxxxxxxx
for 8 digits, which is a fairly obvious translation of the Unicode format, simply replacing U+ with u
or U
. The codes and names can all be found on the Unicode website at http://www.unicode.org/Public/UNIDATA/NamesList.txt
. It is important to realize that Unicode defines the character only, not its appearance. What you see as a character on your computer screen, or printed out on paper, is known as a glyph, which is a graphical representation of that character in some font or other. Unicode does not specify the font family, weight, size, or any other details about appearance, only the actual character.
The Unicode data has to be stored on the computer as a set of bytes that, as you recall, can only store values from 0β255. The simplest translation, or encoding, of Unicode is known as UTF-32 and is a one-to-one mapping from the Unicode code point value to a 32-bit number. This is simple to understand, but requires 4 bytes for every character, making it very memory and bandwidth hungry. To conserve space two other encodings are used. UTF-16 uses 16-bit blocks to represent most characters, but with an option to extend that to two 16-bit blocks for some rarely used characters. Microsoft Windows uses UTF-16 by default.
UTF-8 stores the most commonly used characters in a single 8-bit block, but can be extended to use 2, 3, or 4 blocks for less commonly used characters. This makes UTF-8 the most compact format if you are using the right set of charactersβspecifically the Latin alphabet. UTF-8 also has the convenient feature of having the original ASCII character set as its lowest set of bytes, making interworking with older non-Unicode applications much easier. UTF-8 is the default encoding for Python version 3.
That is all pretty complicated, so how does Python help you handle all of this? Python version 3 uses Unicode strings with UTF-8 as its default encoding, although you can change that if you need to. The way to change the encoding is to put a special comment as the very first line of your code, like this,
β # -*- coding: <encoding name> -*-
where the encoding name is whatever encoding you choose to use, typically utf-16
or ascii
.
You can also use the Unicode characters in literal strings. You can even use their long names, for example:
β >>> print('A')
β A
β >>> print('u0041')
β A
β >>> print("N{LATIN CAPITAL LETTER A}")
β A
They all print the same character, A.
You can use the encode()
method of a string to get the raw bytes used to store the data:
β >>> print("u00A1Help!")
β Β‘Help!
β >>> print("u00A1Help!".encode('utf-8'))
β b'xc2xa1Help!'
β >>> b'xc2xa1Help!'.decode('utf-8')
β 'Β‘Help!'
β >>>
Notice that the second version printed the UTF-8 two-byte representation of the inverted exclamation point represented by u00A1
. The third line converted the encoding back into a string using encode()
βs complementary method decode()
.
So far youβve seen how to represent single Unicode characters, how to change the default encoding used by Python, and how to convert a string into its byte representation using encode()
and how to turn bytes into a Unicode string using decode()
. Mostly that is all you need to know about Unicode and Python. The interpreter does most of the work for you. You can go on to define your own encodings, too, but that requires some more detailed knowledge and a study of the codecs
module. You really shouldnβt need to do that in most situations.
To conclude this section, you should be aware of the unicodedata
module that is particularly useful at the interactive prompt as a way of finding out about Unicode characters. Combined with the data published on the Unicode website you should be able to answer most questions that arise during regular coding.
You can tell the Unicode name and category of a given character using the unicodedata
module and that can then be used to look up the website for more details. Assume you have just stored some data that you believe to contain a Unicode string and you want to know about the characters it contains. You might try this:
β >>> data = b'xd9x85xd8xb1xd8xadxd8xa8xd8xa7 xd8xa3xd9x84xd8xa7
β xd9x86'
β >>> print(data.decode('utf-8'))
β >>> for ch in data.decode('utf-8'):
β ... print(ord(ch), ud.name(ch))
β ...
β 1605 ARABIC LETTER MEEM
β 1585 ARABIC LETTER REH
β 1581 ARABIC LETTER HAH
β 1576 ARABIC LETTER BEH
β 1575 ARABIC LETTER ALEF
β 32 SPACE
β 1571 ARABIC LETTER ALEF WITH HAMZA ABOVE
β 1604 ARABIC LETTER LAM
β 1575 ARABIC LETTER ALEF
β 1606 ARABIC LETTER NOON
β >>>
Here you have a byte string that you suspect is UTF-8 characters and you want to find out what kind of data it is. You can try decoding it, to check that it is valid UTF-8, and that works, but you donβt recognize the printed character set. You then import the unicodedata
module and run a for
loop over the data printing out the long Unicode name of each character. From this it is obvious that the data is in Arabic.
To translate your program strings using the gettext
mechanism, there is a standard set of steps you need to take. First, you have to use gettext
functions to identify the strings in your code that you want translated. Second, you run a utility to extract those strings into a template file, typically called messages.po
. After that you have to produce translation files based on messages.po
, ideally by hiring a set of translators or perhaps by trusting Google translate or similar tools! Third, use another tool to convert the translation files to the language specific .mo
format used by getttext
, for example, messages_en.mo
for the English version. Finally, you need to ship the folder with the .mo
files in it along with your translation. There are various different tools available depending on the operating system. For Windows users there are a couple of scripts in the Tools/i18n
folder of your Python distribution. For UNIX-like systems there are operating system utilities available that are Python aware.
You now walk through a simple exampleβthe ubiquitous βHello worldβ script. The first step is to use the gettext
module and its functions to mark your program strings that require translation. You start by creating a new Python code file called gettext_demo.py
(or load it from the gettext
folder of the zip file):
β import gettext
β import locale as loc
β # Set up the locale and translation mechanism
β #############################
β loc.setlocale(loc.LC_ALL,'')
β filename = "res/messages_{}.mo".format(loc.getlocale()[0][0:2])
β trans = gettext.GNUTranslations(open(filename,'rb'))
β trans.install()
β # Now the main program with gettext markers
β #############################
β print(_("Hello World"))
This sets the locale as discussed in the previous section and uses it to dynamically create the name of the translation file to be used. You are only creating an English translation so you could have hard-coded the name, but using getlocale()
demonstrates how it would be done if you had more than one language available.
Next, you instantiate a gettext.GNUTranslations
object and install()
it. This activates the magic function _()
that you then use to surround all the strings you need translated. In this case thatβs the single string "Hello World"
.
Your next step is to generate the messages.po
template file. If you are on a UNIX-like operating system, you can run the tool xgettext
like this:
β $ xgettext gettext_demo.py
You should now find a messages.po
file in the same folder as your Python file. Open this in your text editor and replace the string CHARSET
with UTF-8
and insert a translation of "Hello world
" in the empty string at the bottom. Just to prove that the translation is being picked up, you should use something like "Hello beautiful world
" but normally, for English, you would just repeat the same string. (There are a bunch of other metadata fields that you could fill in, but they are not needed for this demonstration so you can just ignore them.) Now save the file as messages_en.po
. It should look like this:
β # SOME DESCRIPTIVE TITLE.
β # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
β # This file is distributed under the same license as the PACKAGE package.
β # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
β #
β #, fuzzy
β msgid ""
β msgstr ""
β "Project-Id-Version: PACKAGE VERSION
"
β "Report-Msgid-Bugs-To:
"
β "POT-Creation-Date: 2014-04-22 18:05+0100
"
β "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
β "Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
β "Language-Team: LANGUAGE <[email protected]>
"
β "Language:
"
β "MIME-Version: 1.0
"
β "Content-Type: text/plain; charset=UTF-8
"
β "Content-Transfer-Encoding: 8bit
"
β #: gettext-demo.py:14
β msgid "Hello World"
β msgstr "Hello beautiful world"
Next, you want to create the res
(for resource) folder that you specified in the filename in the Python script. The name is not critical, but res
is a reasonable convention. You now run another utility called msgfmt
like this:
β $ msgfmt -o res/messages_en.mo messages_en.po
Note the different file endings! This creates the translation file that you told gettext
to read when running your script. You are now ready to run the demo:
β $ python3 gettext_demo.py
You should find that the translated string "Hello beautiful world"
is printed instead of the original "Hello World"
that you had in your code. This proves that the translation worked. You can go on to make more translation files by repeating the steps after running xgettext
for each new language. To test them you need to change the local language settings.
Localization is a complex and growing area of study. However, if you plan on distributing your applications to multiple language users or in different countries, it is something you cannot afford to ignore.
In this chapter you saw the power of structuring applications in layers to separate out the data processing from the core, or business, logic and the presentation. In particular you saw how you could build multiple user interfaces on top of the same core logic and data layers. In the process you explored several variations of command-line interfaces including different styles of user interaction and powerful command-line options.
You also saw how to build GUI applications using Tkinter, the standard GUI toolkit in Python, along with its ancillary modules that offer more widgets and improved appearance. You concluded this exploration by building a significant user interface on top of an existing data layer using many of the features already explored but using an object-oriented style rather than a procedural approach. Finally, you reviewed some alternative third-party GUI frameworks that offer even more power than Tkinter should you wish to get more serious about GUI applications.
The chapter looked at some wider issues in building applications for other people to use. It covered the various types of non-core data (such as configuration values) that you can store and the options available for each type. You also covered the use of the Python logging
module to record significant events and how you can manage the levels of logging and how it is stored.
You concluded the chapter with a look at the issues around localizing applications for the user. This includes using localized settings for currency and time formats as well as different alphabets. Python supports Unicode character sets, and you used encode()
and decode()
methods to convert strings to and from their raw bytes. Finally, you experimented with the gettext
mechanism for displaying different languages within your application.
Convert the oxo-logic.py
module to reflect OOP design by creating a Game
class.
Explore the Tkinter.filedialog
module to get the name of a text file from a user and then display that file on screen.
Replace the label in the first GUI example program with a Tix ScrolledText
widget so that it displays the history of all the entries from the Entry
widget.
Rewrite the first GUI example to be compatible with gettext and generate a new English version with different text on the controls.
KEY CONCEPT | DESCRIPTION |
Desktop application | A program that runs primarily on the userβs local computer. Generally an interactive program that, for example, modifies data or plays a game. |
Data layer | The part of an application that stores, modifies, and retrieves data. It has little or no knowledge of the application logic or business rules and no knowledge of how the data will be presented. |
Core logic | The part of the application that processes the data. This is where the complex algorithms and business rules will be located. The data will be fetched or written via the data layer. The logic is not concerned with how the results are presented merely that the correct data are created. |
Presentation layer (aka User Interface) | The part of the application with which the user interacts. This is all about presenting the data in a clear manner and enabling the user to access the functions of the application in a logical and obvious manner. The presentation layer should not depend on the correctness of the underlying data to function, it is only concerned with presenting the data it is given. (Some basic data validation could legitimately occur, such as ensuring that a given field contains an integer within a given range.) |
Localization | The process and mechanisms whereby different users can use the same application regardless of their location and still see the output in terms that they recognize. That is the application should comply with local layouts of structured data such as dates or currency. |
Unicode | An internationally agreed set of standards for presenting character sets. Unicode is not concerned with the shape of the characters it represents (their glyphs) but only with the content, or meaning, of the individual characters. |
Internationalization | The process of translating the strings on a display such that they are legible by users of different languages. |