Introduction

This is a program that draws a simple 2-d hex map.

There is no one specific concept that it is intended to teach - it combines a number of programming techniques, and some recipes listed on this site (and elsewhere), into one program.

I learned a lot while writing it, and thought it might be a generally useful program example to others. As well, I explain some of the thought processes behind writing various sections of the code, which I hope will be informative to someone else learning wxPython or Python.

My main goal in writing it was to learn Python/wxPython, with a secondary goal to use the MVC design pattern. A very minor goal was that someday I might actually use it, to create a wargame. It is written using Python 2.5 and wxPython 2.8.9.1.

Author Note: some of the code was adapted from the book, “wxPython In Action”, specifically the Color Panel (in hex_panel.py module), and the three menu-creation functions in module hex_main.py. Everything else was written by me, John Crawford

Program Screenshot

hmc_hex_map_screenshot.gif

Download source code

How do I make this directly downloadable? HexMapCode.zip

Features

Visible features of the program

Creates a hex map with selectable number of columns and rows.
Displays a hex map larger than the screen, in a scrollable window.
Selects colors for individual hexes, from a Color Panel.
Selects a single hex via mouse-click, or multiple hexes via mouse-dragging.
Resizes the hexes (small or large), and rotates the hex map.
Flashes which hex the mouse is currently over.
Displays a bitmap background behind hex map (which can be changed).

Invisible (design) features

Uses the ModelViewController pattern to separate game code from display code.
Uses a Memo object from the Memoization pattern.
Uses techniques from DoubleBufferedDrawing (though not the same code).
Demonstrates the use of many wxPython features , such as: creating a menu and sub-menu items, creating subclassed windows, drawing to a window using double-buffering and Device Contexts, and many more…
Demonstrates various Python features, such as: creating custom Events (well, one Event :) , event handling, exception handling, and generators.

What wx.Python Objects are Involved

wx.MemoryDC, wx.PaintDC, wx.BufferedPaintDC
wx.Image, wx.EmptyBitmap, wx.StaticBitmap, wx.ScrolledWindow
dc.Blit(), dc.SetPen(), dc.DrawPolygon(), dc.DrawLines()

For a description of them, see the various drawing recipes, including: DoubleBufferedDrawing

What Custom Classes Objects are Involved

A diagram of the major objects is shown below. The MVC component names are in blue (how to color text blue?), and the messages sent between objects, are in red. The diagram doesn’t show every single message – for instance, once the Controller tells the View to rotate the map, the View actually calls vHexGroup, which in turn calls each individual vHex and tells it to rotate itself. The asterisk (*) means one-to-many, for example the map has many hexes.

hmc_diagram_01.gif

Class/Object Descriptions

topFrame – I consider this the Controller part of the MVC model. It’s a wx.Frame that handles keyboard and menu input, calling the View object as needed.

mapView – this is a single View part of the MVC model. It handles all game-related displaying to the screen, through both its own functions, and two related View classes, vHexGroup and vHex. I say ‘single view’ because the whole point of using MVC is the possibility of using multiple views, and one of my Todo items is to add a second view, such as a mini-map.

vHexGroup – this is a collection of vHexes, and handles changes to global properties of hexes, such as size and direction (across screen). It contains a direct link to the Game/Model object, which it queries for game information.

vHex – short for viewHex, this is a Sprite-like object. It contains hex-specific information about x/y coordinates where a hex will be displayed on-screen, as well as color, and other information needed to display that particular hex, such as bitmaps. Also handles Memoization of bitmaps, see Process Overview for details.(add link here once available)

Game – The Model part of MVC, this is a totally abstract game representation, which does not include any visual display information. It creates a game hex map, rotates it, and currently, doesn’t do much else…

gMap – Builds a 2-d array of game hexes.

gHex – contains x/y coords, and a color, and not much else.

bitmap Memo – this is a storage object. See Process Overview for details. (add link once available)

Color Panel – displays a selection of colors that hexes can be changed to. I didn’t put it on the diagram, it’s more of an input item like a menu, not a major object.

Process Overview

Everything starts with the Controller object (topFrame). It creates a View (mapView, which initially only displays a bitmap background), a Color Panel, and handles menu functions. Once the user chooses the menu ‘New Game’ and selects the number of columns/rows, it creates a Model object (Game) and passes it to the View. The View grabs information from the Model, and builds up a display, consisting of a hex map with the chosen columns/rows.

At that point, the View handles most user input. You can use the mouse to select an active color from the Color Panel, to select a hex to recolor, or to mouse-drag and select a group of hexes. Also from the menus, you can rotate the map, resize the hexes, or clear them.

And although program itself doesn’t tell the user, some keyboard letters do the same, such as ‘r’ for rotate. I originally started with keyboard-only input, and then later added the menus, which are obviously more clear to a user. If this wasn’t just a training program, I’d pop up a window explaining to the user what keyboard shortcuts they could use :)

A View to a... Model

Being a Model is all about networking and connections…

In an MVC pattern, the View has to get information about the Model, so it can display the information. In this case, I simply gave the View a direct link to the Game Model object:

   1 def BuildNewMap(self, game=None): # here, self is mapView
   2     if game:
   3         self.gameModel = game

and the View calls various game functions, or just reads the instance variables directly, to query it, such as:

   1 for hex in self.gameModel.List_Hexes()
   2 ## or
   3 self.dir = self.gameModel.hex_dir

In addition, the Game Model object has a link back to the Controller (topFrame), but only for a specific function, and event handler.

The design of wxPython is that most classes are derived from wx.EvtHandler. Any class so derived, can handle events, and more importantly, generate events, with a call to

   1 self.GetEventHandler().ProcessEvent(new_evt)

Any classes that are not part of wxPython, such as my Game class, do not have access to its event handling. Yet, I need the Game Model to somehow communicate when it changes, so that the View can update. I didn’t want to write a whole new event handling class (which I did in the Pygame version), plus it would conflict with wxPython’s event handling. I decided to just give the Game class access to an event handler

So when topFrame creates a new game, it does:

   1 self.gameModel = Game(cols, rows, self.GetEventHandler(), self.GetId())

and passes its own Event Handler function, and its own id. Thus, the Game can create new events and put them into the event queue, and by using topFrame’s id, the events appear to have originated from topFrame:

   1 evt = MapRotatedEvent(EVT_MAP_ROT, self._eventId)
   2 self._eventMgr.ProcessEvent(evt) # where self is the Game class

In the Beginning, was the Bitmap

Behold the basic Bitmap: hmc_hex_raw.gif

It consists of a rectangle, which first I fill with the color ‘black’:

   1 bmp = wx.EmptyBitmap(self.width, self.height)
   2 dc = wx.MemoryDC(bmp)
   3 dc.SetBackground(wx.Brush('Black'))
   4 dc.Clear()

And then I draw a filled polygon with the color ‘light grey’:

   1 dc.SetPen(wx.Pen(fill_color, 0))
   2 dc.DrawPolygon(self.point_list, 0, 0)
   3 del dc

And then the black background is set transparent:

   1 bmp.SetMaskColour('Black')

Giving us this: hmc_hex_clear.gif

A vHex builds every possible bitmap type, some of which are shown here: hmc_bitmap_types.gif

The complete list of possibilities is:

Plain grey or with a selected color.
Across the map or down the map.
Flashed (mouse is over) with a red outline.
Flashed (mouse is over) with a solid red hex.
Small or large hex (large hexes not shown, since, well they look just like small hexes, only larger…)

A vHex takes its current attributes, which include direction (across or down the map), and size (large or small), and creates a bitmap representation of each possibility – plain, outline-flashed, and solid-flashed.

When there is a state change in the hex, it draws the relevant bitmap to the window to show that change. For instance – when the mouse moves over a hex, it is Flashed, meaning that a large red border goes around the hex to visually indicate to the user, that this hex would be selected if they clicked the mouse. (See diagram above.) In order to change the screen, the vHex picks the bitmap which is the correct color (grey, unless changed) and correct border (thick red line), and draws that bitmap to the window.

When the mouse is moved away from that hex – the hex redraws itself to the window, using a bitmap of a plain hex (grey) and no border, thus erasing the border on-screen.

There are currently two methods of Flashing a hex: Outline-flashed is basically a red outline; Solid-flash is an entire red hex. I realize it’s redundant to have two different ways of flashing a hex, and makes the code more complex.

Why did I write two different ways of doing the same thing?

One of the things that I did at first, to indicate which hex the mouse was over, was draw a border outlining the hex. The border was based on the same hex.point_list that I used to define the hex polygon. This had the effect of putting a single-pixel thick red line around the hex. It was visible, but not greatly so.

So I decided to make it more visible by adding another flash type, a solid hex, and made which one was in effect, a menu option. The solid hex was extremely visible. However, I left in the original code for a single-pixel line border. Eventually I realized that I could make the line thicker, by changing my hex-point function, which I did.

This was a purely a learning exercise, so I left all of the code in. If this was a production project, I’d certainly pick one way and delete the other, but it’s not – so removing the redundant code is left as an exercise for the reader :)

Take a Memo

How a Memo solved a major performance problem.

If you read the Bitmap section above, you know that the vHex has a lotta bitmaps it’s creating – over a dozen types. They’re small, so memory isn’t a major problem. But when I first rewrote the code to have so many bitmaps, I saw a big slowdown in drawing the map. Any time I changed a parameter, such as hex size, or direction, especially if it was a 30x30 hex map, it took 3-5 seconds to recreate the map and draw the screen.

Now, a few seconds may not be much in the greater Scheme of things, but as any programmer knows (or will find out the hard way), users want instant gratification. The idea of waiting three whole seconds for a response is totally unacceptable…

Basically, I had violated one of the big principles of programming, which is: Don't Repeat Yourself, or DRY

In this case, I was repeating the same operation over and over. With a hex map of 30x30 hexes, a vHex was creating the same plain grey hex - 900 times! Plus it also created a Flashed version, with two types of flash bitmaps, for a total of 2700 bitmaps created on a large map. No wonder it was slow to rebuild the map.

For a while, I lived with it, just making small maps while I wrote and tested the code. Eventually, I remembered reading an article about Memoization, which was exactly what I needed.

The short version is: for functions that do a lot of repeated computations – if they store the results of each computation with a key, the next time they have to do the same computation, they can just retrieve the stored results with the key. So they only have to do a slow operation once, and can reuse it later, saving time. Here is the code:

   1 def MakeBitmap(self, fill_color, border_color):
   2     size_k = self.parent.hex_d
   3     dir_k = self.parent.dir 
   4     full_key = '%s_%s_%s_%s' % (size_k, dir_k, fill_color, border_color)
   5     if full_key in vHex.bmp_dict: # bitmap already exists, use it.
   6         return vHex.bmp_dict[full_key]
   7 
   8     ## bitmap creation code here, then:
   9 
  10     vHex.bmp_dict[full_key] = bmp # add new bitmap to Memo
  11     return bmp

This code builds a unique key – every factor that identifies a specific bitmap: size, direction across map, flash border fill, and color. With that key, it checks if the key is already in a bitmap dictionary, which means the bitmap has already been created. If that is true, it grabs the bitmap and returns a copy of it. If not, the code creates a new bitmap and adds it to the dictionary.

So now, rather than creating 900 identical bitmaps – the function will create the first bitmap, and when it is asked to create a second identical bitmap, will use a copy of the first, for a huge time savings.

Note that vHex.bmp_dict is a Class variable, not an instance variable. There is only one copy of it, and it’s referenced via the Class name of vHex. Otherwise every single hex would create its own copy, making the whole thing pointless…

Evolution of the Design

Originally I was learning Python, with an interest in writing games, and had just heard of Pygame. So as part of learning Pygame, I wrote a hex-map drawing program. It worked well – I was able to draw a collection of hexes on-screen, and select them individually. However, my next goal was to make a map larger than the screen – which required scrollbars. Unfortunately, Pygame doesn’t have those… It’s a nice library, but oriented more towards arcade games than wargames. I really didn’t want to write my own scrollbar code, so I started looking at the available GUIs for Python, of which wxPython seemed the best choice.

Some code worked essentially unchanged, such as the hex-point computation functions. However, mostly it turned into a total rewrite. With Pygame, I had written my own event-loop managing code, and event generators. However, wxPython has app.MainLoop(), and its own event manager, so that code wasn’t applicable. Nor were any of the drawing routines re-usable. Oh well, so it goes...

Lessons Learned about wxPython

The biggest major problem I encountered in wxPython, had to do with the scrolling window. My first version had only the topFrame and a subclassed wx.ScrollingWindow called mapView. I bound (binded?) keyboard events to an OnKeyPress() function of mapView, and all worked well. But eventually, I wanted to add a second object, the Color Panel. This did not go well… I soon discovered that Frames do not make the best container, and that it is highly recommended to use a Panel inside the Frame, as a top-level container for other objects.

So I added a topPanel just underneath the topFrame, to hold the children objects, ColorPanel and mapView. It seemed to be working, until I realized that my keyboard events were no longer being received. I couldn’t bind them either to the topFrame, topPanel, or mapView, and have them fire. I had no idea what was going wrong. And honestly, I am still not sure what’s happening, but I at least have a partial fix. It seems to have something to do with what widget has the Focus. In particular, there are issues with container widgets, such as topPanel, handling focus between it and any children. And I didn’t realize it, but after looking up the chain of object – wx.ScrolledWindow is a subclass of wx.Panel. So every item I had on my topPanel, was also a panel and container class. In fact, when I changed it from a wx.ScrolledWindow to regular wx.Window, the keyboard binding worked fine. This wasn’t a solution, since I wanted a scrolling hex map, but it confirmed to me that having all wx.Panels/ was causing problems.

My current solution is to bind the LostFocus event, so any time the mapView loses focus, it grabs it back. Kludgy, but effective. I really need to figure out how focus works – one of these days…

Lessons Learned about Python

I like to think my code is at least semi-Pythonic, but it certainly didn’t start out that way…

Dictionaries Good

One of the first functions I had written was read the keyboard for arrow-key presses, so I could scroll the map, like this:

   1 def OnKeyPress(self, event):
   2     if event.GetKeyCode() == wx.WXK_LEFT:
   3         self.mapPanel.ScrollWindow(10, 0)
   4     elif event.GetKeyCode() == wx.WXK_RIGHT:
   5         self. mapPanel.ScrollWindow (-10, 0)
   6     elif event.GetKeyCode() == wx.WXK_UP:
   7         self. mapPanel.ScrollWindow (0, 10)
   8     elif event.GetKeyCode() == wx.WXK_DOWN:
   9         self. mapPanel.ScrollWindow (0, -10)

where the parameters to ScrollWindow() are x, y coordinate changes. This code certainly worked, and yet… The more I thought about it, the more it kept bothering me… Eventually I had the minor insight that a dictionary would work here, and changed the code to:

   1 arrowkey_list = (wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN)
   2 
   3 def OnKeyPress(self, event):
   4     if event.GetKeyCode() in arrowkey_list:
   5         self.MoveMap(event.GetKeyCode())
   6 
   7 def MoveMap(self, key):
   8     move_list = {wx.WXK_LEFT : (10, 0), \
   9                  wx.WXK_RIGHT : (-10, 0), \
  10                  wx.WXK_UP : (0, 10), \
  11                  wx.WXK_DOWN : (0, -10) }
  12     self.MapPanel.ScrollWindow(*move_list[key])

This uses a dictionary to associate a given keypress code to an x/y coordinate change Tuple. So rather than a long block of if/elif statements, I do a single dictionary access using the actual key pressed, as the index. From what I have read about Python, the dictionary access routines are highly optimized, so it should be as fast or faster, than if/elif statements. Even if it isn’t, frankly, I think the code is more clear, and easier to change. Later on I added four more keys to the list, with no problems (also to move_list, not shown):

   1 arrowkey_list = (wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN,\
   2     wx.WXK_PAGEUP, wx.WXK_PAGEDOWN, wx.WXK_HOME, wx.WXK_END)

And surprise! While I was writing this article – I realized I had duplicated the arrow-key code list. A given key such as wx.WXK_UP, appeared in two places, arrowkey_list and move_list. Make it DRY!

So I changed the code, deleted arrowkey_list, and wrote this:

   1 move_list = {wx.WXK_LEFT : (10, 0), \
   2              wx.WXK_RIGHT : (-10, 0), \
   3              wx.WXK_UP : (0, 10), \
   4              wx.WXK_DOWN : (0, -10) }
   5 
   6 def OnKeyPress(self, event):
   7     if event.GetKeyCode() in move_list:
   8         self.MoveMap(event.GetKeyCode())
   9 
  10 def MoveMap(self, key):
  11     self.MapPanel.ScrollWindow(*move_list[key])

This is much cleaner. The arrow-key codes only appear one time. Adding more keys, like the page_up/down keys mentioned above, would only need to be added in one place, not two – which could have caused a small bug if I had forgotten to repeat it. And another nice feature, is that the function MoveMap() is guaranteed to be called with valid input. It will only be called with keys from the dictionary, which it accesses with those same keys, so it will never get an invalid key error. Now that’s Pythonic.

Special Concerns

Minor Glitch: The code for determining which hex is mouse-clicked is slightly glitchy – if the mouse is close enough to the border, it can pick the wrong hex.

A Known Bug: Involving Focus, wx.Panels, and the related kludge. Under some conditions, such as minimizing the game window, or switching to other windows (not sure exactly), the game window seems to permanently lose focus, and I can’t read the keyboard at all. The mouse and menus still work, just not the keyboard.

A Fixed Bug (I hope): I was getting errors after three minutes of using the program. Specifically, the code:

   1 dc = wx.MemoryDC(self.mapBuffer)
   2 dc2 = wx.MemoryDC(hex.currentBitmap)
   3 dc.Blit(hex.x_offset, hex.y_offset, hex.width, hex.height,\
   4     dc2, 0, 0, useMask=True)

was crashing the Python interpreter, and generating an exception:

I added a try/except block:

   1 try:
   2     dc = wx.MemoryDC(self.mapBuffer)
   3     dc2 = wx.MemoryDC(hex.currentBitmap)
   4     dc.Blit(hex.x_offset, hex.y_offset, hex.width, hex.height,\
   5         dc2, 0, 0, useMask=True)
   6 except AssertionError:
   7     print 'Problems opening DC. Should not happen...'

And sure enough, the exception fired. This didn’t really tell me more than before, but by handling the exception, at least my program wasn’t crashing - it was printing a warning message. Better than having Python crash with an exception.

Anyway, something was obviously wrong with creating a Device Context. One of the warnings that I had read about DCs, is that they are a finite system resource, and can be used up if a program doesn’t release them. I suspected that might be happening, so added two lines to delete the DCs:

   1 try:
   2     dc = wx.MemoryDC(self.mapBuffer)
   3     dc2 = wx.MemoryDC(hex.currentBitmap)
   4     dc.Blit(hex.x_offset, hex.y_offset, hex.width, hex.height,\
   5     dc2, 0, 0, useMask=True)
   6     del dc
   7     del dc2
   8 except AssertionError:
   9     print 'Problems opening DC. Should not happen...'

According to the documentation, when you create a Device Context, and let it go out of scope, it’s supposed to be destroyed. I don’t know if there is a delay in Python doing garbage-collection, or what is going on under the hood. But, by explicitly destroying the DCs, it seems to work. Or at least, the program ran for six minutes without crashing, and hasn’t crashed since.

Comments

By: John Crawford

Feedback welcome, please put comments here.


Doesn't work in linux - Ubuntu 10.04 LTS, Linux kernel 2.6.32-34, Gnome 2.30.2, wxPython version 2.8.10.1 (gtk2-unicode)! Have been struggling with problem with wx.MemoryDC for a week now and found this example that produces the same error I got in my program. The mapView looks all distorted. Searching for solutions I found a link on the net suggesting that the problem is .Blit function on wx.MemoryDC. In my application that was not the issue. For me it was that I was creating the MemoryDC object inside OnPaint that was binded? To wx.EVT_PAINT. The picture I then created became all messed up. When moving it out to init for the controll it worked. It worked from the start on both MS Windows XP and MS Windows 7.

By: Strixx

I think you're close. Most likely the problem comes from the fact that it is creating and using a paint DC (a wx.BufferedPaintDC in this case) for one window while in the EVT_PAINT event for another. Feel free to refactor that and upload a fixed version of the zip file.

--RobinDunn

HexMapCode (last edited 2011-10-06 15:48:56 by c-98-246-90-205)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.