[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Build a jigsaw puzzle program in Python, Part 1

[A jigsaw puzzle program written in Python]
This is a pretty long example, so I'm going to describe it in two pieces. As usual, the posts don't cover every detail so you should download the example to see additional details.

This post describes helper classes and methods that the main program uses to manage the puzzle. Part 2 explains how the main program uses the helper pieces to create the program.

The pieces described in this post include general setup, the SettingsDialog class, and the PuzzlePiece class.

To use the program, use the File menu's Open command (or press Ctrl+O) to open an image file. The program divides the puzzle into rectangular pieces and scatters them. You can then drag and drop them into position to recreate the original image.

[The jigsaw puzzle program's configuration dialog] To make more or fewer pieces, use the Configuration menu's Settings command to display the Settings dialog shown on the right. Enter the approximate size you would like the pieces to have and click OK. If you have an image loaded, the program breaks it into pieces and randomizes their positions.

The following sections describe the program's main pieces.

Setup

One of the trickier parts of this kind of program is deciding exactly what you want it to do. I've written a few jigsaw puzzle programs before, so I decided to do this one a little differently.

Instead of making the image exactly fit the window, I decided to make the window bigger so you have a place to put unpositioned pieces. I'm calling the full available area the "kitchen table" because that's where people often assemble puzzles. For this example, I decided to make the table twice as wide and 100 pixels taller than the puzzle image. In the picture at the top of this post, the window's surface is the kitchen table. The area inside the red box shows where the puzzle should be assembled.

The program automatically sizes itself to fit the loaded image and the window is non-resizable. It also will not let you drag a puzzle piece out of the window so you don't need to worry about "dropping a piece on the floor." For example, if you dragged a piece completely off of the window's left side, you could never get it back so you couldn't solve the puzzle.

Here's the app class's constructor.

class JigsawApp: def __init__(self): self.window = tk.Tk() self.window.title('jigsaw') self.window.protocol('WM_DELETE_WINDOW', self.kill_callback) self.window.geometry('300x300') self.window.resizable(False, False) # Set initial data. self.puzzle_image = None self.piece_size = 100 self.selected_piece = None # Build the menu. self.build_menus() # Build other stuff... self.canvas = tk.Canvas(self.window) self.canvas.pack(side=tk.TOP, padx=5, pady=5, fill=tk.BOTH, expand=True) # Track Button-1 down, Button-1 motion, and Button-1 up. self.canvas.bind("<Button-1>", self.mouse_down) self.canvas.bind("<B1-Motion>", self.mouse_move) self.canvas.bind("<ButtonRelease-1>", self.mouse_up) # Display the window. self.window.focus_force() self.window.mainloop()

The code creates a tkinter window as usual. It then uses the blue code to make its window non-resizable.

The program initializes a few variables, builds its menus, and binds mouse events to various event handlers (described later). It finishes by setting the focus on the window and entering the tkinter main loop.

For information about how the program builds its menus, download the example and see the post Build standard menus in Python and tkinter.

SettingsDialog

The Settings dialog lets you decide how big the puzzle pieces should be in pixels. The program then gives the pieces a width and height that are reasonably close to the value you set.

To build this dialog, the program follows the technique described in the post Make a custom dialog with Python and tkinter. It's not hard, although there is a fair amount of code.

Here's the constructor with some less interesting pieces omitted.

import tkinter as tk class SettingsDialog(tk.Toplevel): def __init__(self, master, piece_size=50, min_size=20): super().__init__(master) self.transient(master) # Keep dialog above master. self.minsize(300, 200) self.title('Settings') self.min_size = min_size self.piece_size = None # Make a frame to hold the dialog's main body. ... self.piece_size_entry.focus_set() # Grab events for this dialog. self.grab_set()

The SettingsDialog class inherits from tkinter.TopLevel. The constructor first calls the base class's constructor to initialize the window. It then calls transient to indicate that the dialog is a transient window that should stay above the master window.

The code sets the window's minimum size so you don't have a tiny little dialog and sets the dialog's title.

The statement self.min_size = min_size saves the minimum puzzle piece size passed into the constructor. The program uses that value to prevent you from giving the pieces unreasonable sizes like one pixel by one pixel. (You can allow tiny values it if you like, but I don't recommend it.)

The statement self.piece_size = None saves the dialog's default return value.

The program then builds the dialog's user interface, creating labels, a text box, and the OK and Cancel buttons. It binds the Enter character to trigger the OK button and binds the Escape key to trigger the Cancel button.

Finally, the code puts focus on the Size Entry widget and calls grab_set to make future events go to this dialog instead of the main window. That way the dialog receives key presses, mouse clicks, and other events until it is closed so it acts like a modal dialog.

The rest of the dialog's code follows the pattern described in the earlier post. The most interesting piece is the following validate method, which makes sure the user entered a reasonable value for the puzzle piece size and didn't type something like "one hundred," "-1," or "yes please."

def validate(self): ''' Validate the user's entries. Display an error message if appropriate. Return True if the entries are valid, False otherwise. ''' try: piece_size = int(self.piece_size_entry.get()) except: piece_size = 0 if piece_size < self.min_size: messagebox.showerror('Error', f'The size should be at least {self.min_size} pixels.') self.piece_size_entry.focus_set() return False # Save the validated values. self.piece_size = piece_size return True

This code tries to convert whatever the user typed into an integer. If there's an error (for example, if the user types "twenty"), it sets the piece_size variable to 0.

After getting the piece_size value, the method checks whether that value is less than the saved ,in_size value. If it is, the code displays an error message and returns False to tell the dialog that we did not get a valid value so it should not close.

If the value is valid, the method saves the entered value in self.piece_size and returns True to tell the dialog to close.

Download the example to see other dialog-related code.

PuzzlePiece

The PuzzlePiece class represents a puzzle piece. A PuzzlePiece object is in charge of knowing where its piece is, drawing the piece, and repositioning its piece when the user drags and drops it.

Constructor

The following code shows the class's constructor and __repr__ dunder method.

class PuzzlePiece: def __init__(self, puzzle_image, x, y, width, height): '''Save this piece's part of the puzzle.''' self.x0 = x # The piece's home location. self.y0 = y self.x = x # The piece's current location. self.y = y self.width = width self.height = height self.image = puzzle_image.crop((x, y, x + width, y + height)) self.is_home = False def __repr__(self): '''Return a string representation for the piece.''' return(f'PuzzlePiece({self.x}, {self.y}, {self.width}, {self.height})')

The constructor takes as parameters the complete puzzle image (which is a PIL Image object), and the piece's location and size within that image.

The code saves the piece's location and size. It then uses crop to save the piece's part of the puzzle image in the object's image variable.

The __repr__ method returns a textual representation of the piece. In this program, it returns the piece's location and dimensions. Python uses __repr__ when it needs to print a PuzzlePiece object. That means if you print the program's list if pieces, you see something like this:

[PuzzlePiece(709, 44, 113, 112), PuzzlePiece(751, 247, 113, 112), PuzzlePiece(53, 98, 112, 112), PuzzlePiece(768, 76, 112, 112), PuzzlePiece(30, 251, 113, 112), PuzzlePiece(31, 265, 113, 112), PuzzlePiece(749, 18, 112, 112), PuzzlePiece(16, 83, 112, 112), PuzzlePiece(87, 8, 113, 111), PuzzlePiece(697, 69, 113, 111), PuzzlePiece(59, 215, 112, 111), PuzzlePiece(41, 78, 112, 111)]

instead of something like this:

[<__main__.PuzzlePiece object at 0x000001FB82B03E60>, <__main__.PuzzlePiece object at 0x000001FB82DF33B0>, <__main__.PuzzlePiece object at 0x000001FB82DF3260>, <__main__.PuzzlePiece object at 0x000001FB82DF1C70>, <__main__.PuzzlePiece object at 0x000001FB82E056A0>, <__main__.PuzzlePiece object at 0x000001FB82E04CB0>, <__main__.PuzzlePiece object at 0x000001FB82E059D0>, <__main__.PuzzlePiece object at 0x000001FB82E05400>, <__main__.PuzzlePiece object at 0x000001FB82E05A60>, <__main__.PuzzlePiece object at 0x000001FB82E06E40>, <__main__.PuzzlePiece object at 0x000001FB82E071A0>, <__main__.PuzzlePiece object at 0x000001FB82E050D0>]

draw

The PuzzlePiece class provides the following draw method to draw the piece.

def draw(self, dr, table_image): '''Draw this piece at its current position on the table image.''' table_image.paste(self.image, (self.x, self.y)) # If the piece is not home, outline it. if not self.is_home: THICKNESS = 3 x1 = self.x + self.width y1 = self.y + self.height dr.rectangle(((self.x, self.y), (x1, y1)), outline='yellow', width=THICKNESS)

This method takes as a parameter the image that will be displayed on the kitchen table. (Remember that this image includes the puzzle area plus extra space on the sides where you can put unplaced pieces.)

The code first uses the table image's paste method to draw the piece at its current location.

Next, if the piece is not locked in its final location, the code draws a yellow box around the piece.

is_at

When you press the mouse down on the kitchen table, the program needs to know which piece, if any, is at that position. The following is_at method returns True if the piece is at a particular position.

def is_at(self, x, y): '''Return True if the piece is at this position.''' # Once we're locked, it cannot move. if self.is_home: return False if x < self.x: return False if y < self.y: return False if x > self.x + self.width - 1: return False if y > self.y + self.height - 1: return False return True

If the piece is locked at its home position, the method returns False so the main program won't let you drag it.

If the piece is not locked at home, the code checks whether the point is left, above, to the right of, or below the piece. The code returns False in each of those situations because the mouse is not over the piece.

If none of the precious conditions apply, the code returns True to indicate that the position is over this piece.

drop

The last piece of the PuzzlePiece class is the following drop method. The program calls this method when the user drops a piece.

def drop(self): '''Drop this piece at its current position.''' CLOSE_ENOUGH = 10 # Distance in pixels. # See if x and y are within CLOSE of our home location. if math.isclose(self.x0, self.x, abs_tol=CLOSE_ENOUGH) and \ math.isclose(self.y0, self.y, abs_tol=CLOSE_ENOUGH): # Go home. self.x, self.y = self.x0, self.y0 # Lock the piece. self.is_home = True # Return True to indcate that the piece is locked. return True

This method uses math.isclose to see if the piece's X and Y coordinates are within CLOSE_ENUOGH pixels of the piece's home location. If the piece is within that distance, the code moves it to its exact home location, sets is_home = True, and returns True to tell the main program that the piece is in its home location.

To Be Continued...

That's enough for this post. In the next post, I'll explain how the main program uses the pieces described here to implement the jigsaw puzzle program.
© 2024 Rocky Mountain Computer Consulting, Inc. All rights reserved.