Part 3: Implemening the Graphical User Interface

Implement the GUI of the Sudoku game with which a user will interact.

Creating the Sudoku Board UI

The GUI (graphical user interface) is the interface with which your user will interact. We’ll be using Tkinter (tee-kay-inter), a GUI framework in Python’s standard library, to build the simple interface.

Here is what we will be working towards:

There are a few libraries we’ll need from the Tkinter module:

from Tkinter import Tk, Canvas, Frame, Button, BOTH, TOP, BOTTOM

We’re going to create a class to represent the Sudoku UI that will inherit from Frame, which we’ve imported from Tkinter:

So for each new Sudoku game (i.e. each time we run python sudoku.py), we will create a new UI, SudokuUI with a game (which we will be passing in our SudokuGame later), as well as a parent.

Here is great explanation from the tkinter mailing list about what a parent attribute is for a tkinter frame:

All widgets belong to a parent or master widget until you get to some kind of root or main window that is the master odf [sic] all its sub widgets.

When you delete a window you delete the master and it deletes
all its children. The children delete their children, and so on until
all the widgets making up the window are deleted. […]
Similarly if you add a widget to a window you must tell the new
widget where within the containment tree it sits, you tell it who
its master is. Usually the widget will register itself with its master.

We’ll see later when we put together the whole script that the parent is actually the main window of the whole program.

Moving on, we’ve set self.row and self.col each to 0. We’re just initalizing the row and columns to use later.

Wrapping up the __init__ function, we call self.__initUI(), which we will now implement.

initUI

This private method of the SudokuUI class is the logic that sets up the actual user interface.

We first set the parent title (which is our main/only window) to "Sudoku". Simple enough.

1
2

def__initUI(self):self.parent.title("Sudoku")

Next, self.pack is a Frame attribute that organizes the frame’s geometry relative to the parent. We’re wanting to fill the entire frame, where fill=BOTH means to fill both horizontally and vertically any extra space that is not used by the parent. Other options include NONE, X, or Y.

Next is the canvas attribute. canvas is a general-purpose widget that we will use to display our board. We will use the earlier-defined global variables from part 1, WIDTH and HEIGHT, to help setup the actual width and height of the puzzle canvas.

Below the canvas for the puzzle is the button to clear answers. We create the button attribute using Button, giving it the text of the button, and the command for the button to call when it is pressed. Here, we set the command to __clear_answers, which we will define later.

Like canvas, we will set pack for the button to fill the space, and sit at the bottom of the window.

The first self.canvas.bind is binding "<Button-1>" to a callback – another method - __cell_clicked. With tkinter, "<Button-1>" is actually a mouse click, and refers to the default left button on a mouse (for right-handed mouse settings). "<Button-2>" would refer to the middle button of a mouse, and "<Button-3>" would be a right-click. This is not to be confused with the clear_button we defined earlier.

So here, when the user clicks on the puzzle with a single left-click of the mouse, our UI will call __cell_clicked function, which we will define in a bit. The bind method will actually pass in the x and y location of the cursor, which in __cell_clicked we will turn into actual cells of the puzzle.

Similarly, on the next line, we bind "<Key>" to the callback function, __key_pressed. This binds the key that a user pressed (e.g. the guessed number) to the __key_pressed method.

Helper Functions

In initUI, we call two methods, __draw_grid and __draw_puzzle. We also bind functions to user events: clicking on a button to clear answers, clicking on a particular cell, and pressing a key to fill in a cell.

Draw Grid method

The __draw_grid private method literally draws a grid to represent the Sudoku layout:

Here we are iterating over a simple range between 1 and 9 (excludes 10). If the current iteration number (i) is divisable by 3 with no remainers (hence, the modulo, i % 3 == 0), then the color of the line should be blue. Otherwise, set it to gray.

The first chunk draws the vertical lines by calling create_line on our canvas object. The second chuck then draws the horizontal lines. Simple enough!

Draw Puzzle method

The __draw_puzzle private method then draws the puzzle by filling in the cells with the pre-filled numbers defined in whatever .sudoku board we pass in.

We first call delete on the canvas to clear out any previous numbers. This is helpful for when the user wants to clear out the puzzle and start over.

1
2

def__draw_puzzle(self):self.canvas.delete("numbers")

We then iterate over rows and columns, and create a cell. We then grab the same X & Y location of the cell from the game’s puzzle. If it isn’t zero, then fill it in with the appropriate number, otherwise just leave it blank.

You’ll notice that the color of the number could be either "black" or "sea green". So if the initial puzzle has certain numbers already filled in, it will be set to black. Otherwise, when a user inputs a number, it will be set to sea green. If you’d like to use different colors, check out this list of Tkinter-supported color names.

We also set the text of the canvas to the answer (either the original/prefilled number, or the user’s inputted number). We also set a tag, "numbers", so we can easily refer to it later (i.e. when clearing/deleting the board in the beginning of the method).

Note that __draw_puzzle will end up being called every time a user inputs his or her answer into a particular cell with the updated game.puzzle container the user’s guesses/answers.

Clear Answers

Earlier, we created a button for the user to clear his or her answers, and set the command of the button to the method __clear_answers:

With __clear_answers, we first call the start() method associated with the game (from SudokuGame class). This resets the puzzle to its original state. We also delete the "victory" status/tag if the user previously solved the problem (which we will implement later). Lastly, we re-draw the puzzle with the original puzzle using our __draw_puzzle method.

Cell clicked

In the final steps of our __initUI method, we bounded the left mouse click ("<Button-1>") to the callback, __cell_clicked. Now to implement. This callback takes in an event parameter, which will give us the X & Y coordinates of where exactly the user clicked:

1
2

def__cell_clicked(self,event):pass

First, we want to just return out of the function if the game_over flag is set, because no need to do anything if that is the case:

1
2
3

def__cell_clicked(self,event):ifself.game.game_over:return

Next we’ll grab the x and y location of the click (making sure it is indeed within our puzzle widget):

Then we’ll set the focus of the canvas there with focus_set. If we wanted to, we can elect to have the focus of the canvas be highlighted to help the user. We will actually implement this next after we finish this method.

Next we’ll map the X and Y coordinates to an actual cell:

1
2
3
4
5
6
7
8
9
10

def__cell_clicked(self,event):ifself.game.game_over:returnx,y=event.x,event.yif(MARGIN<x<WIDTH-MARGINandMARGIN<y<HEIGHT-MARGIN):self.canvas.focus_set()# get row and col numbers from x,y coordinatesrow,col=(y-MARGIN)/SIDE,(x-MARGIN)/SIDE

If the cell had already been selected, then we’ll deselect the sell. Otherwise, grab the cell that cooresponds with the puzzle:

Alright, done with the __cell_clicked method! Onto the __draw_cursor method.

Draw Cursor

The __draw_cursor method essentially highlights the particular cell that the user has clicked on. Similar to the __draw_puzzle method we defined earlier, we will first delete the "cursor" element, just to clear out the previously highlighted cell.

1
2

def__draw_cursor(self):self.canvas.delete("cursor")

Next, if self.row and self.col are set (e.g. set/put in focus from the `__cell_clicked earlier), then essentially compute the dimensions of the cell, create a rectangle attached to our canvas with those dimensions, and highlight the outline red:

NOTE: event characters in Tkinter return character codes/ordinal values associated with the pressed key from a keyboard, rather than straight integers. So here, Python will compare the evevnt character value of the key input (ord("0") for instance) to that of each character in the string "1234567890". In Python, ord("0") returns 48, and chr(48) returns "0".

So if the key character is a valid Sudoku number, we’ll set the number of the cell in our puzzle, reset the row & column selection that was set from __cell_clicked, and redraw the puzzle with the new numbers, and redraw the cursor.

We’ll also automatically check if the user has completed the puzzle by calling the check_win method we defined earlier in our SudokuGame class. If the user has won, we’ll call a new helper method, __draw_victory, which we will implement next.