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:

1
2
3
4
class SudokuUI(Frame):
    """
    The Tkinter UI, responsible for drawing the board and accepting user input.
    """

Frame is defined as a “rectangular region on the screen”. This is essentially just a widget of our puzzle.

We will create an initialization function, which will take two parameters, parent, and game:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class SudokuUI(Frame):
    """
    The Tkinter UI, responsible for drawing the board and accepting user input.
    """
    def __init__(self, parent, game):
        self.game = game
        self.parent = parent
        Frame.__init__(self, parent)

        self.row, self.col = 0, 0

        self.__initUI()

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class SudokuUI(Frame):
    # <-- snip -->

    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)
        self.canvas = Canvas(self,
                             width=WIDTH,
                             height=HEIGHT)
        self.canvas.pack(fill=BOTH, side=TOP)
        clear_button = Button(self,
                              text="Clear answers",
                              command=self.__clear_answers)
        clear_button.pack(fill=BOTH, side=BOTTOM)

        self.__draw_grid()
        self.__draw_puzzle()

        self.canvas.bind("<Button-1>", self.__cell_clicked)
        self.canvas.bind("<Key>", self.__key_pressed)

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.

1
2
3
    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)

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.

1
2
3
4
5
6
    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)
        self.canvas = Canvas(self,
                             width=WIDTH,
                             height=HEIGHT)

Then within the canvas attribute, we again set pack, where the entire square of the puzzle will fill the space, and will be pulled to the top part of the window.

1
2
3
4
5
6
7
    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)
        self.canvas = Canvas(self,
                             width=WIDTH,
                             height=HEIGHT)
        self.canvas.pack(fill=BOTH, side=TOP)

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)
        self.canvas = Canvas(self,
                             width=WIDTH,
                             height=HEIGHT)
        self.canvas.pack(fill=BOTH, side=TOP)
        clear_button = Button(self,
                              text="Clear answers",
                              command=self.__clear_answers)
        clear_button.pack(fill=BOTH, side=BOTTOM)

Next, we call two helper methods, __draw_grid and __draw_puzzle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)
        self.canvas = Canvas(self,
                             width=WIDTH,
                             height=HEIGHT)
        self.canvas.pack(fill=BOTH, side=TOP)
        clear_button = Button(self,
                              text="Clear answers",
                              command=self.__clear_answers)
        clear_button.pack(fill=BOTH, side=BOTTOM)

        self.__draw_grid()
        self.__draw_puzzle()

We’ll go over those two methods in a second.

Finishing up the __initUI method, we have two calls for bind on our canvas object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    def __initUI(self):
        self.parent.title("Sudoku")
        self.pack(fill=BOTH, expand=1)
        self.canvas = Canvas(self,
                             width=WIDTH,
                             height=HEIGHT)
        self.canvas.pack(fill=BOTH, side=TOP)
        clear_button = Button(self,
                              text="Clear answers",
                              command=self.__clear_answers)
        clear_button.pack(fill=BOTH, side=BOTTOM)

        self.__draw_grid()
        self.__draw_puzzle()

        self.canvas.bind("<Button-1>", self.__cell_clicked)
        self.canvas.bind("<Key>", self.__key_pressed)

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class SudokuUI(Frame):
    # <-- snip -->

    def __draw_grid(self):
        """
        Draws grid divided with blue lines into 3x3 squares
        """
        for i in xrange(10):
            color = "blue" if i % 3 == 0 else "gray"

            x0 = MARGIN + i * SIDE
            y0 = MARGIN
            x1 = MARGIN + i * SIDE
            y1 = HEIGHT - MARGIN
            self.canvas.create_line(x0, y0, x1, y1, fill=color)

            x0 = MARGIN
            y0 = MARGIN + i * SIDE
            x1 = WIDTH - MARGIN
            y1 = MARGIN + i * SIDE
            self.canvas.create_line(x0, y0, x1, y1, fill=color)

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    def __draw_puzzle(self):
        self.canvas.delete("numbers")
        for i in xrange(9):
            for j in xrange(9):
                answer = self.game.puzzle[i][j]
                if answer != 0:
                    x = MARGIN + j * SIDE + SIDE / 2
                    y = MARGIN + i * SIDE + SIDE / 2
                    original = self.game.start_puzzle[i][j]
                    color = "black" if answer == original else "sea green"
                    self.canvas.create_text(
                        x, y, text=answer, tags="numbers", fill=color
                    )

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:

1
2
3
4
    def __clear_answers(self):
        self.game.start()
        self.canvas.delete("victory")
        self.__draw_puzzle()

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):
        if self.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):

1
2
3
4
5
6
7
    def __cell_clicked(self, event):
        if self.game.game_over:
            return

        x, y = event.x, event.y
        if (MARGIN < x < WIDTH - MARGIN and MARGIN < y < HEIGHT - MARGIN):
            self.canvas.focus_set()

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):
        if self.game.game_over:
            return

        x, y = event.x, event.y
        if (MARGIN < x < WIDTH - MARGIN and MARGIN < y < HEIGHT - MARGIN):
            self.canvas.focus_set()

            # get row and col numbers from x,y coordinates
            row, 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    def __cell_clicked(self, event):
        if self.game.game_over:
            return

        x, y = event.x, event.y
        if (MARGIN < x < WIDTH - MARGIN and MARGIN < y < HEIGHT - MARGIN):
            self.canvas.focus_set()

            # get row and col numbers from x,y coordinates
            row, col = (y - MARGIN) / SIDE, (x - MARGIN) / SIDE

            # if cell was selected already - deselect it
            if (row, col) == (self.row, self.col):
                self.row, self.col = -1, -1
            elif self.game.puzzle[row][col] == 0:
                self.row, self.col = row, col

Finally, outside of the if scope, we will call __draw_cursor, which we will implement next.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    def __cell_clicked(self, event):
        if self.game.game_over:
            return
        x, y = event.x, event.y
        if (MARGIN < x < WIDTH - MARGIN and MARGIN < y < HEIGHT - MARGIN):
            self.canvas.focus_set()

            # get row and col numbers from x,y coordinates
            row, col = (y - MARGIN) / SIDE, (x - MARGIN) / SIDE

            # if cell was selected already - deselect it
            if (row, col) == (self.row, self.col):
                self.row, self.col = -1, -1
            elif self.game.puzzle[row][col] == 0:
                self.row, self.col = row, col

        self.__draw_cursor()

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    def __draw_cursor(self):
        self.canvas.delete("cursor")
        if self.row >= 0 and self.col >= 0:
            x0 = MARGIN + self.col * SIDE + 1
            y0 = MARGIN + self.row * SIDE + 1
            x1 = MARGIN + (self.col + 1) * SIDE - 1
            y1 = MARGIN + (self.row + 1) * SIDE - 1
            self.canvas.create_rectangle(
                x0, y0, x1, y1,
                outline="red", tags="cursor"
            )

It will look like this to the user if he or she clicked on the cell in the 3rd row, 6th column:

Not too hard, I hope! Now to that next callback function, __key_pressed.

Key Pressed

So now that the user has selected a cell, and we’ve appropriately found it and highlighted it, the user can now use the keyboard to enter in his or her guess for a particular cell.

Like in __cell_clicked, in __initUI, we bind the "<Key>" event to __key_pressed, and actually pass in that event (e.g. the actual key character) into the function.

We’ll start off similarly to __cell_clicked by returning out of the function if the game_over flag is set to True:

1
2
3
    def __key_pressed(self, event):
        if self.game.game_over:
            return

Next, if the cell is selected (i.e. both row and column numbers are at/above 0), and the key (event character) is a character in the string "1234567890".

1
2
3
4
    def __key_pressed(self, event):
        if self.game.game_over:
            return
        if self.row >= 0 and self.col >= 0 and event.char in "1234567890":
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    def __key_pressed(self, event):
        if self.game.game_over:
            return
        if self.row >= 0 and self.col >= 0 and event.char in "1234567890":
            self.game.puzzle[self.row][self.col] = int(event.char)
            self.col, self.row = -1, -1
            self.__draw_puzzle()
            self.__draw_cursor()
            if self.game.check_win():
                self.__draw_victory()

Look at that beautiful __key_pressed method! Let’s finish this class with the final method, __draw_victory.

Draw Victory

If the user has successfully completed the puzzle, we want to create an overlay of the UI to let he or she know. Here, we will just create a simple orange circle with some text inside.

First, we’ll calculate the dimensions for a circle, and we’ll attach it to the canvas via the create_oval method with our desired color fill, outline, and tag attribute.

1
2
3
4
5
6
7
8
    def __draw_victory(self):
        # create a oval (which will be a circle)
        x0 = y0 = MARGIN + SIDE * 2
        x1 = y1 = MARGIN + SIDE * 7
        self.canvas.create_oval(
            x0, y0, x1, y1,
            tags="victory", fill="dark orange", outline="orange"
        )

Next, let’s put some text inside the circle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    def __draw_victory(self):
        # create a oval (which will be a circle)
        x0 = y0 = MARGIN + SIDE * 2
        x1 = y1 = MARGIN + SIDE * 7
        self.canvas.create_oval(
            x0, y0, x1, y1,
            tags="victory", fill="dark orange", outline="orange"
        )
        # create text
        x = y = MARGIN + 4 * SIDE + SIDE / 2
        self.canvas.create_text(
            x, y,
            text="You win!", tags="winner",
            fill="white", font=("Arial", 32)
        )

Here, we are calculating the center of the circle, then adding text within the circle with the desired attributes. Here is what it will look like:

Phew! That is it for the SudokuUI class object! Everything we need to draw the board, fill with some numbers, and allow the user to input data.

Next up, putting the sudoku.py script together.