Implement the GUI of the Sudoku game with which a user will interact.
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.
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.
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.
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!
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.
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.
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.
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
.
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":
|
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
.
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.