Structuring our program

In this section, we decide on an overall structure for our program.

The development of large applications generally starts with recording the software requirement specifications (SRS). This is generally followed by a graphical representation of constructs, such as the class, composition, inheritance, and the hiding of information using several modeling tools. These tools can be flow charts, Unified Modeling Language (UML) tools, data flow diagrams, Venn diagrams (for database modeling), and so on.

These tools are very useful when the problem domain is not very clear. However, if you have ever played the game of chess, you should be very well acquainted with the problem domain. Furthermore, our chess program can be classified as a medium-sized program spanning a few hundred lines of code. Therefore, let's bypass these visual tools and get to the actual program design.

All of our previous projects have been structured as a single file. However, as programs grow in complexity, we need to break programs into modules and class structures.

One of the key objectives of this chapter is to learn to write programs in the MVC architecture. Some of the central aspects of the MVC architecture are as follows:

  • A model handles backend data and logic
  • A view handles the frontend presentation
  • The model and view never interact directly
  • Whenever the view needs to access backend data, it requests the controller to intervene with the model and fetch the required data

Given these aspects, let's create three files for our chess program: model.py, view.py, and controller.py (see 4.01.py).

Now, let's create an empty Model class, an empty View class, and a Controller class in their respective files, as follows:

class Model(): #in model.py
def __init__(self):
pass

class View(): #in view.py
def __init__(self):
pass

class Controller(): # in controller.py
def __init__(self):
self.init_model()

def init_model(self):
self.model = model.Model()

Note that since the Controller class needs to fetch data from the Model class, we instantiated a new Model class from within the Controller class. This now provides us with a way to fetch data from the Model class as and when needed.

Let's also add a separate file called exceptions.py. This will be our central place for the handling of all errors and exceptions. Within this file, add the following single line of code:

class ChessError(Exception): pass

We created a custom ChessError class that was inherited from the standard Exception class. This simple line of code now allows the ChessError class and all of its children to raise errors, which can then be handled by using the try…except block. All the new error classes that will be defined in our code from now on will derive from this ChessError base class.

With this boilerplate code out of the way, let's create another blank file called configurations.py (4.01). We will use this file to store all the constants and configurable values in one place.

Let's define some constants right away, as follows (see code 4.01configurations.py):

NUMBER_OF_ROWS = 8
NUMBER_OF_COLUMNS = 8
DIMENSION_OF_EACH_SQUARE = 64 # denoting 64 pixels
BOARD_COLOR_1 = "#DDB88C"
BOARD_COLOR_2 = "#A66D4F"

To make these constant values available to all files, let's import them in to the model.py, view.py, and controller.py folders (see 4.01):

from configurations import *

As per the tenets of the MVC architecture, the View class is never supposed to interact directly with the Model class. It should always interact with the Controller class, and the Controller class is then responsible for fetching data from the Model class. Accordingly, let's import the controller in the View class and the model in the Controller class, as follows:

import controller # in view.py
import model # in controller.py

Let's start by editing the view.py file to display a chessboard (see 4.01view.py). Our goal for this iteration is to display the empty chessboard as shown in the following screenshot:

Take a look at the code implementation in view.py (see 4.01).
The __init__ method of the View class calls a method called create_chess_base, which is defined as follows:

def create_chess_base(self):
self.create_top_menu()
self.create_canvas()
self.draw_board()
self.create_bottom_frame()

We will not reproduce the code responsible for the creation of the root window, the menu at the top, or the frame at the bottom. We have implemented similar widgets in previous chapters (see 4.01view.py for a complete reference).
However, we will discuss the code that creates the chessboard:

def create_canvas(self):
canvas_width = NUMBER_OF_COLUMNS * DIMENSION_OF_EACH_SQUARE
canvas_height = NUMBER_OF_ROWS * DIMENSION_OF_EACH_SQUARE
self.canvas = Canvas(self.parent, width=canvas_width, height=canvas_height)
self.canvas.pack(padx=8, pady=8)

Nothing fancy here. Creating a Canvas widget is similar to creating other widgets in Tkinter. The Canvas widget takes the width and height of two configurable options. Next, paint the Canvas widget in alternating shades to form the chessboard (view.py):

def draw_board(self):
current_color = BOARD_COLOR_2
for row in range(NUMBER_OF_ROWS):
current_color = self.get_alternate_color(current_color)
for col in range(NUMBER_OF_COLUMNS):
x1, y1 = self.get_x_y_coordinate(row, col)
x2, y2 = x1 + DIMENSION_OF_EACH_SQUARE, y1 +DIMENSION_OF_EACH_SQUARE
self.canvas.create_rectangle(x1, y1, x2, y2, fill=current_color)
current_color = self.get_alternate_color(current_color)

def get_x_y_coordinate(self, row, col):
x = (col * DIMENSION_OF_EACH_SQUARE)
y = ((7 - row) * DIMENSION_OF_EACH_SQUARE)
return (x, y)

def get_alternate_color(self, current_color):
if current_color == self.board_color_2:
next_color = self.board_color_1
else:
next_color = self.board_color_2
return next_color

The following is the description of the code:

  • We used the Canvas widget's create_rectangle() method to draw alternating shades of squares to resemble a chessboard.
  • The rectangles are drawn from point x1, y1, and they extend to x2, y2. These values correspond to two diagonally opposite corners of the rectangle (coordinates of the upper-left and lower-right edges).
  • The x and y values are calculated by using a newly defined method called get_x_y_coordinate(), which performs simple mathematics depending on the dimensions of each square that was defined in pixel units earlier.
    The y value is calculated by first subtracting a row from (7-row) because the Canvas widget measures the coordinates starting from the top left. The top-left corner of the canvas has the coordinates (0, 0).
  • The get_alternate_color method is a helper method that, not surprisingly, returns the alternate color.
The Tkinter Canvas widget lets you draw a line, an oval, a rectangle, an arc, and polygon shapes at a given coordinate. You can also specify various configuration options, such as fill, outline, width, and so on for each of these shapes.
The Canvas widget uses a coordinate system to specify the position of objects on the widget. Coordinates are measured in pixels. The top-left corner of the canvas has the coordinates (00).
The objects drawn on the Canvas widget are usually handled by assigning them an ID or a tag. We will see an example of this later in the chapter.
If an object on the Canvas widget is tagged to multiple tags, the options defined for tags at the top of the stack have precedence.
However, you can change the precedence of tags by using tag_raise(name) or tag_lower(name).
For a complete list of Canvas widget-related options, refer to the interactive help for the Canvas widget using help(Tkinter.Canvas) in the command line, as follows:
>>> import tkinter
>>> help(tkinter.Canvas)

Next, let's bind the mouse click to the Canvas widget from the __init__ method of the View class (see 4.01view.py), as follows:

self.canvas.bind("<Button-1>", self.on_square_clicked)

The bound method calls another method called get_clicked_row_column(), and for now it prints the result on the console as follows:

def on_square_clicked(self, event):
clicked_row, clicked_column = self.get_clicked_row_column(event)
print("Hey you clicked on", clicked_row, clicked_column)

The get_clicked_row_column() method is defined as follows:

def get_clicked_row_column(self, event):
col_size = row_size = DIMENSION_OF_EACH_SQUARE
clicked_column = event.x // col_size
clicked_row = 7 - (event.y // row_size)
return (clicked_row, clicked_column)

Now, if you run the code (see 4.01view.py) and click on different squares, it should output a message like this to the console:

Hey you clicked on 0 7
Hey you clicked on 3 3

This completes our first iteration. In this iteration, we determined the broader file structure for the chess program. We created the model, view, and controller classes. We also decided to keep all the constants and configuration values in a separate file called configurations.py.

We have now had a first taste of the Canvas widget. We created a blank canvas and then added square areas using the canvas.create_rectangle method to create a chessboard.

Now, if you run 4.01view.py, you will see an empty chessboard. You will also find out that the File menu and the Edit menu dropdowns are not functional. The About menu should show a standard messagebox widget. 

Before you proceed to the next section, you are encouraged to go and explore the code in the 4.01 folder in its entirety.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.99.71