[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: Draw a hexagonal grid that displays textured tiles in Python

[A tiled hexagonal grid drawn with Python]

My previous post Draw a hexagonal grid that responds to mouse clicks in Python shows how you can draw a hexagonal grid and let the user click on grid cells. This post draws cells by filling them with textures. There are quite a few changes between this program and the previous one, but most of them are minor. Here I'll described the basic ideas and show you some key. You can download the example to see more details.

Hex.load_tile

The first big change is in the Hex class, which now includes the following load_tile method.

@staticmethod def load_tile(tile_name, points): '''Load a tile into the Hex.tiles dictionary.''' # If we have already loaded the tile, do nothing. if tile_name is None or tile_name in Hex.tiles: return # Calculate tile dimensions. xmin, ymin, xmax, ymax = get_hex_bounds(points) wid = round(xmax - xmin) hgt = round(ymax - ymin) # Load the tile image and crop to size. tile_image = Image.open(tile_name) tile_image = tile_image.crop((0, 0, wid, hgt)) # Make a hexagonal mask. if Hex.tile_mask is None: Hex.tile_mask = Image.new('L', (wid, hgt), 'black') tile_mask_dr = ImageDraw.Draw(Hex.tile_mask) new_points = [(point[0]-xmin, point[1]-ymin) for point in points] tile_mask_dr.polygon(new_points, fill='white') # Make the new tile. # Note: The tile and mask images must be the same size to use paste. tile = Image.new('RGBA', (wid, hgt), (0, 0, 0, 0)) tile.paste(tile_image, (0, 0), Hex.tile_mask) Hex.tiles[tile_name] = ImageTk.PhotoImage(tile)

This is a static method that the main app class uses to load tiles. The method first checks whether the tile's file name is already in the Hex.tiles class dictionary. If the tile is already loaded, the method exist.

If the tile isn't already loaded, the method finds the hex's bounding coordinates and uses them to calculate its hex's width and height. It loads the image file and crops it to the hex's width and height. Note that the code doesn't resize the tile to fit the hex, but you could modify the code to do that if you like.

Next, the code checks whether it has previously created the tile mask stored in the cleverly named class variable Hex.tile_mask. If it has not yet created the mask, the code creates a new black image with the hex's width and height. It generates points to define the hexagon shifted to the origin and then uses them to fill the hexagonal area on the mask with white.

The code then creates a new RGBA image to fit the hex. RGBA means the image has red, green, blue, and alpha components. The alpha component is basically opacity and is needed to draw with a transparency mask. Initially the new image is filled with color (0, 0, 0, 0). That value has zero opacity, so the image is initially completely transparent.

The method draws the tile's image onto the new RGBA image using Hex.tile_mask as a mask so the image fills the hexagon and the rest of the image remains transparent.

Finally, the code saves the tile image in Hex.tiles using the tile's file name as the key.

Hex.is_highlighted

The Hex class's is_highlighted property setter contains a few changes just to make the selected hex stand out better. The previous version made the selected hex pink with red text. That won't work if we're filling cells with tiles.

Here's how the new version works.

@is_highlighted.setter def is_highlighted(self, value): # If the value isn't changing, do nothing. if self._is_highlighted == value: return # Save the value. self._is_highlighted = value # Update the Hex's appearance. if self._is_highlighted: self.canvas.itemconfig(self.polygon_id, outline=Hex.highlight_outline, width=Hex.highlight_line_width) self.canvas.itemconfig(self.label_id, fill=Hex.highlight_text_fill, font=Hex.highlight_font, text=f'<{self.row}, {self.col}>') self.canvas.tag_raise(self.polygon_id) else: self.canvas.itemconfig(self.polygon_id, outline=Hex.normal_outline, width=Hex.normal_line_width) self.canvas.itemconfig(self.label_id, fill=Hex.normal_text_fill, font=Hex.normal_font, text=f'({self.row}, {self.col})') self.canvas.tag_raise(self.polygon_id)

This code first checks whether the hex's is_highlighted property is changing and, if it is not, returns.

Next, if the cell is highlighted, the code sets the hexagon's outline color and line thickness. It also makes the cell's text yellow and larger.

The code then raises the hexagon to the top of the canvas's stacking order. That way the hexagons of adjacent cells won't appear on top of the highlighted cell. (If you like, comment out the call to tag_raise to see what happens.)

If the cell should not be highlighted, the property setter resets the hexagon's color and line thickness, and the hex's text.

Hex Constructor

The last significant addition in this version of the program is in the Hex class's constructor. The following code shows the key addition highlighted in blue.

def __init__(self, row, col, canvas, points, tile_name=None): self.row = row self.col = col self.canvas = canvas self.points = points self._is_highlighted = False # See if we have a tile name. if tile_name is not None: # Load the tile if necessary. Hex.load_tile(tile_name, points) # Draw the tile. xmin, ymin, xmax, ymax = get_hex_bounds(points) self.tile_id = canvas.create_image(xmin, ymin, image=Hex.tiles[tile_name], anchor=tk.NW) # Put the tile on the bottom of the stacking order. canvas.tag_lower(self.tile_id) # Create the hexagon. self.polygon_id = canvas.create_polygon(points, fill='', outline=Hex.normal_outline, width=Hex.normal_line_width) # Get the hex's center. self.cx = (points[0][0] + points[3][0]) / 2 self.cy = (points[1][1] + points[4][1]) / 2 # Make a label for this hex. text = f'({row}, {col})' self.label_id = canvas.create_text(self.cx, self.cy, text=text, fill=Hex.normal_text_fill, font=Hex.normal_font, anchor=tk.CENTER)

The blue code calls the Hex class's load_tile method to load the hex's tile. It then uses the tile to create the hex's image and pushes it to the bottom of the stacking order so it doesn't cover any adjacent cell outlines. The rest of the constructor is as before.

Conclusion

The rest of the program is pretty similar to the previous version.

Note that the Hex class and the app's self.hexes property give you a good place to store information about the hexes. For example, you can make a Hex object store variables representing objects, units, monsters, players, or just about anything else in the hex. You'll have to figure out how to draw them.

Download the example to experiment with it and to see additional details.

© 2024 - 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.