How to Write a Sudoku GUI in Python/wxPython

Python-wxPython Tutorial

Introduction:

Let's face it. I am a dinosaur. I got my computer science degree in the mid 70s. And it won't be long before I am in my mid 70s. So why am I trying to learn Python? Mostly because learning new things keeps my brain active. And besides, nobody on Daniweb seems to be asking questions about vb.Net anymore. So I thought I'd give it a try. After a few weeks of puttering I managed to learn enough Python to rewrite all of my vbscripts (stop laughing, it's an actual language). Now that I was comfortable (but still not fluent) in Python I decided to really torture myself and tackle wxPython. I had gotten used to creating GUIs in vb.Net by just dragging and dropping controls in Visual Studio. I was spoiled. With wxPython I would have to craft my interface by hand with no automatic code generation. It reminded me of the old days writing apps in Presentation Manager for OS/2.

For those of you not familiar with wxPython, it is essentially a wrapper for the wxWidgets toolkit, a free and open source, cross-platform library for creating GUIs. wxWidgets has been around since 1992. wxPython allows access to wxWidgets from Python. While tkinter (which is badly out of date) renders controls in a generic style (no matter the platform), wxWidgets renders controls in the style of the platform on which it is running so wxPython apps will look like native Windows desktop apps.

Learning wxPython is difficult at the start. Imagine a blind man asks you to describe a beach and you reply by describing every grain of sand on it. He would have little sense of what a beach was, or what could be done on it. That's what a lot of the wxPython documentation looks like. There is an online reference (plus a down-loadable version) but it consists of infinitely cross-linked text where it seems that the thing you are looking for is always described elsewhere (where "elsewhere" is never linked to). It reminds me of Zork (you are in a maze of twisty little passages - all alike). Only other dinosaurs are likely to get that reference. The docstrings will tell you what you can do but not how you can do it.

There are a number of books available such as

  1. wxPython in Action by Noel Rappin & Robin Dunn (wxPython author)
  2. wxPython Recipes: A Problem - Solution Approach by Mike Driscoll
  3. wxPython 2.8 Application Development Cookbook by Cody Precord
  4. Creating GUI Applications with wxPython by Mike Driscoll

Only book 4 is for the current release (4.0.3) which is what I am using (versions 4 and above are for Python 3.0 and above). Some of the examples from book 1 will result in "Deprecated" warnings but these should be easy to correct. Current documentation is available at the wxPython web site. You will also find a forum there where you can ask questions.

While I am on the topic of books, if you are just learning Python, I have read around a dozen "teach yourself" books and one is head and shoulders above the rest. Do yourself a huge favour and buy Beginning Python: From Novice to Professional by Magnus Lie Hetland.

Continuing on, a few years ago I wrote a vb.Net GUI that would allow me to solve Sudoku puzzles without the hassle of repeatedly filling in and erasing digits. I wanted a tool where I could enter a puzzle, then eliminate possible digits one by one and have the tool do the grunt work while leaving me to do the brain work. I figured that a port to Python/wxPython would be a suitable challenge.

Developing the app in vb.Net was a different sort of challenge with having to set up a project for nested custom controls, and a test project to run it and more configuration settings to create the library and specifying the platform and on and on and on. It was not pleasant and I was glad to be done. Developing in Python/wxPython, on the other hand, was (except for the infrequent times I hit a wall and repeatedly hit my head against it) actually fun. And in spite of my experience with vb.Net, I highly recommend you use Visual Studio 2019 for developing and debugging Python code. It has excellent Python support and breakpoints are awesome.

As with anything I post, I welcome constructive criticism and suggestions on how to improve. Because I am still in the initial stages I expect there will be a number of things that could have been done better. I also frequently suffer from

2010-04-20.jpg

So please post your comments and suggestions. Remember, none of us is as smart as all of us.

The Basics of Sudoku

A Sudoku puzzle consists of a 3x3 grid of panes where each pane consists of a 3x3 grid of tiles. Each tile can contain a single digit from 1-9. A puzzle will come with some of the tiles already filled in. To solve the puzzle you must fill in the remaining digits with the restrictions

  1. each pane must have 9 unique digits
  2. each row of tiles must have 9 unique digits
  3. each column of tiles must have 9 unique digits

A_Sudoku_Puzzle.jpg

The Application Interface

In my application, a tile initially consists of a 3x3 grid of buttons labeled '1'-'9'. When the app starts (or when it is cleared to start a new puzzle), you specify the initial puzzle values by clicking on the corresponding button in each tile. For example, to set the first tile in pane 1 to the value 6, just click on button 6. This marks that tile as solved. Each button that you click is removed and replaced a larger image of that digit. Once you have entered the initial puzzle you click the Solve button. During setup, when you set the value for a tile, that value (button) is removed from every other tile in that pane.

Once you click Solve you can begin to eliminate digits. Since each row, and column may contain only one of each digit, when you enter Solve mode, the app removes all occurrences of the entered digits from all tiles where they are no longer valid. If this causes any other tiles to be solved (only one possible value remains), those solved values are also removed from the puzzle. As you can see, this removes a great deal of the grunt work. Whereas in Setup mode, clicking a button sets the tile to that value, in Solve mode, clicking a button removes only that button. However, if removal of a button leaves only one button, that last button becomes the solved value (also triggering a possible update of tiles in the same pane, row or column).

Once there are no more buttons to click you may click on the Check button. This will scan the puzzle to ensure that you have solved it within the given constraints.

Let's start with a short intro to wxPython and go from there.

Introduction to wxPython

A simple wxPython program consists of an application object and a frame. It looks like this:

import wx

app = wx.App(redirect=False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show()
app.MainLoop()

You first create an instance of a basic application. Until you do this you cannot create any other wx objects. wx.App takes one optional argument (the default is False). If False then any print output will go to the console (if one is available). If True then an output window is created as needed and print output will go there.

Every application has to have at least one frame. Since the first one is the top level frame it has no parent object (so the first parameter, the parent, is None). The second parameter is an object id. In most cases you can let the system assign one so you specify wx.ID_ANY (or its integer value of -1). The third parameter is the titlebar text. The next line makes the frame visible. The last line enters the event loop. From this point until you close the window, events will be handled within wxPython.

wxPython objects support inheritance. A lot of the objects you build will be based on existing objects. In fact, everything in Python inherits from a basic object class. You'll see how this works as we build our application. Typically you'll see the applications taking this form:

import wx

class App(wx.App):

    def OnInit(self):
        self.frame = Frame("Sample App")
        self.frame.Show()
        return True

class Frame(wx.Frame):

    def __init__(self, title):

        super(Frame, self).__init__(None, title=title)

        self.panel = wx.Panel(self, size=(400,300)) 

if __name__ == "__main__":
    app = App()
    app.MainLoop()

If you run this you will see a blank window with a title bar and the usual titlebar gadgets.

You'll create an application object subclassed from wx.App and build everything from within that leaving the mainline as a minimal front end. In this case app creates a frame and frame in turn creates a panel. A subclassed wx.App must have an OnInit method and it must return True or the application will exit.

You are going to see a lot of lines like

super(Frame, self).__init__(...

If you subclass an object and you need to initialize it you will have to provide an __init__ method. But if you do this you will override the parent object's __init__ method so you must call it explicitly.

A very useful features is the ability to inspect your objects at run time. You do this by adding two lines as follows:

import wx
import wx.lib.mixins.inspection

class App(wx.App):

    def OnInit(self):
        self.frame = Frame("Sample App")
        self.frame.Show()
        return True

class Frame(wx.Frame):

    def __init__(self, title):

        super(Frame, self).__init__(None, title=title)

        self.panel = wx.Panel(self, size=(400,300)) 

if __name__ == "__main__":
    app = App()
    wx.lib.inspection.InspectionTool().Show()
    app.MainLoop()

If you run the above code you will see two windows. One is the blank app window. The other is the object inspection tool. In the left pane you can expand the object tree to inspect any object (including objects in the PyCrust extension that provides the inspection tool). As you select objects in the left pane you can inspect them in the right pane. The bottom pane allows you to enter ad hoc Python commands where the object obj always refers to the object with focus in the tree. It's worth noting that the display shows only the standard properties. It will not include properties that you have defined. For example, if you select a pane in the Sudoku app, you will not see pane.index in the list. You can still see what the value is, however, by typing obj.index in the bottom panel.

Now is a good time to introduce sizers. Sizers are pseudo-containers that are used to automatically size and position other controls. Typically you will create a sizer then add controls, specifying parameters such as alignment, allocation of available space, borders, etc. A box sizer can arrange controls in either a single row or a single column. A grid sizer can arrange controls in an equally spaced grid. There are more complex sizers, but these are the ones I will be using here (and the only ones I have bothered to learn).

The inspection tool does not show sizers by default but you can enable them from the toolbar. They will appear on the display as if they were containers but you do not need to refer to them when addressing objects. For example, I use a sizer to arrange the buttons in a tile, the tiles in a pane, and the panes in a puzzle but you do not need to do

puzzle.sizer.pane.sizer.tile.sizer.button.

A final note about sizers - you will see that the only controls that I specify a size for are the buttons inside each tile. Everything else is automatically sized to fit (accounting for the padding I specify when I add the controls to each sizer). If you change the button size from (24,24) to (30,30), the entire puzzle will expand accordingly.

Let's start building up our application.

The Application

First of all, decide where you want to work on the project and unzip sudoku.zip there. It will create the following tree:

.\Sudoku
     images
     Pane.py
     Presets.txt
     Puzzle.py
     Sudoku.pyw
     Sudoku-minimal.py
     Tile.py

.\Sudoku\images
     1.jpg
     2.jpg
     3.jpg
     4.jpg
     5.jpg
     6.jpg
     7.jpg
     8.jpg
     9.jpg

Each major object (tile, pane, puzzle) file comes with test code so that you can run the simple case. The final application objects will be more complicated but the simple case files will get across the basic ideas. With any luck you won't have to do much typing. The first draft application is in Sudoku-minimal.py and does not include the bells and whistles. The completed application is in Sudoku.pyw.

A comment on comments - in order to squeeze as much code onto a screen as possible (while still including white space to improve readability), I have eliminated most comments. I felt that with the explanations they were redundant. However, the final post of the complete application will include comments. If you don't comment your code while you are writing it then shame on you ;-P

The Tile Object

The first control we are going to build is the Tile object. We are going to base our Tile on (inherit, or sub-class) a wx.Panel.

Tile.jpg

We are going to define the following methods:

SetSetup

SetSetup will be called to switch the tile between the two possible states of setup and solve. In setup mode, clicking on a button will cause the tile to be solved with that button as the solved value. In solve mode, clicking on a button will remove that button from the list of possible tile values.

Clear

Clear will be called to reset the tile to its initial state. All buttons will be made visible and any background image of a digit will be removed.

IsSolved

This will return True if this tile has been solved, or False if it has not.

OnClick

This is the event handler that will be called when any button in this tile is clicked. In Setup mode it will remove all buttons and show the clicked button value as the solved digit.

Remove

Remove will be called with a button as a parameter. It will remove that button from the tile. If that leaves only one remaining button then it will mark that tile as solved.

ShowSolved

ShowSolved will be called with a button as a parameter. It will call Remove to remove all remaining visible buttons and will show the tile as being solved.

ShowDigit

Loads a bitmap image into the tile and displays it. If no digit is given it will remove an image if one is there.

We will also define a few custom properties to make the housekeeping a little cleaner.

setup

This will be True if we are in setup mode and False if we are in solve mode.

buttons

This is a list of all the buttons in the tile, visible or not.

shown

This is a list of all the visible buttons in the tile. Note that this list must be a copy (self.shown = self.buttons.copy()) of self.buttons because doing self.shown = self.buttons will just produce two references to the same list. Later changes to self.shown would alter self.buttons. This is a common "gotcha" for novice Python programmers. I'm enough of a novice that I made that mistake twice while writing this app.

name

This is the name ('1'-'9') that is shown on the button.

index

This is the 0-relative index of the tile in the tiles list of the parent object.

solved

This will be '' if the tile is unsolved, or '1'-'9' representing the displayed value if the tile is solved.

row

This is the row number (0-2) for this tile.

col

This is the column number (0-2) for this tile.

value

This is a list (str) of all of the digits currently visible in the tile.

bitmap

This is a wx.StaticBitmap that will remain hidden until the tile is solved. For now we will load each image as required from disk. Later we will create a list of bitmaps to speed things up a bit.

We will be adding a few more methods and properties as we build our application but these will do to start.

Here is the first run at the tile object. Note that instead of the default window style I am specifying wx.CLOSE_BOX and wx.CAPTION. Because all sizes will be fixed I don't want the window to be sizable. I'm also eliminating the minimize control but you can certainly add it if you want to.

import wx
import wx.lib.mixins.inspection

class App(wx.App):

    def OnInit(self):
        self.frame = Frame("Tile Object")
        self.frame.SetPosition((10,10))
        self.frame.Show()
        wx.lib.inspection.InspectionTool().Show()
        return True

class Frame(wx.Frame):

    def __init__(self, title):

        super(Frame, self).__init__(None, title=title, style=wx.CLOSE_BOX | wx.CAPTION)

        self.tile = Tile(self, '1', 0)
        self.tile.SetSetup(True)
        self.Fit()

class Tile(wx.Panel):

    def __init__(self, parent, name, index):

        super(Tile, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup   = True                 # True if entering puzzle           
        self.buttons = []                   # all buttons                       
        self.shown   = []                   # visible buttons                   
        self.name    = name                 # Tile name is '1' to '9'           
        self.index   = index                # Tile index is 0 to 8              
        self.solved  = ''                   # set to solved digit when solved   
        self.row     = index // 3           # row number (0-2) for this tile    
        self.col     = index  % 3           # col number (0-2) for this tile    
        self.value   = list("123456789")    # visible digits                    

        # The bitmap control is used to display the solved value

        self.bitmap  = wx.StaticBitmap(self, -1)
        self.bitmap.Hide()

        # Create a 3x3 grid of buttons with an OnClick handler.

        gsizer = wx.GridSizer(cols=3, vgap=0, hgap=0) 

        for i in range(0,9): 
            button = wx.Button(self, label=str(i+1), size=(40,40))
            button.name = str(i+1)
            button.index = i
            button.Bind(wx.EVT_BUTTON, self.OnClick)
            self.buttons.append(button)
            self.shown.append(button)
            flags = wx.EXPAND | wx.ALIGN_CENTRE
            gsizer.Add(button,0, flags , border=0)      
        self.SetSizerAndFit(gsizer)  

    def SetSetup(self, setup):

        if setup: self.Clear()
        self.setup = setup

    def Clear(self):

        self.value  = list("123456789")
        self.solved = ''
        self.shown  = self.buttons.copy()
        self.ShowDigit('') 

        for button in self.buttons: button.Show()   

    def IsSolved(self):

        return self.solved != ''

    def OnClick(self, event):

        button = event.GetEventObject()

        if self.setup:
            self.ShowSolved(button)            
        else:
            self.Remove(button)

    def Remove(self, button):

        button.Hide()

        try:
            self.shown.remove(button)
            self.value.remove(button.name)
        except: pass

        if len(self.shown) == 1: 
            self.ShowSolved(self.shown[0]) 

    def ShowSolved(self, button):

        for b in self.shown: b.Hide()

        self.shown  = []
        self.value  = []
        self.solved = button.name

        self.ShowDigit(button.name)

    def ShowDigit(self, digit):

        if digit == '':
            self.bitmap.Hide()
        else:
            bmp = wx.Image('images/' + digit + '.jpg', wx.BITMAP_TYPE_ANY).ConvertToBitmap()
            self.bitmap.Bitmap = bmp
            self.bitmap.Centre()
            self.bitmap.Show()

# Main ###############################################################

if __name__ == '__main__':

    app = App(True) 
    app.MainLoop()

Note that because we called self.tile.SetSetup(True) it will run in Setup mode so clicking on a button will solve the tile. If you want to see how it works in Solve mode then call it with self.tile.SetSetup(False). For the stand-alone version I used size = (40,40) for the buttons to make things a little easier to see.

Next we will build a pane.

The Pane Object

Like the tile object, the pane will contain a 3x3 grid (using the GridSizer again), but of tiles instead of buttons.

Pane.jpg

Our only methods for now will be

SetSetup

All this will do is set the background colour and then set each of the contained tiles into the same mode.

IsSolved

This will return two values. The first will indicate with True or False whether the pane has been solved. If solved then the second value will be a null string. If not solved the string will indicate the reason.

Its custom properties are

setup

This will be True if we are in setup mode and False if we are in solve mode.

tiles

This is a list of all the tiles in the pane.

name

This is the name ('1'-'9') of the pane.

index

This is the 0-relative index of the pane in the panes list of the parent object.

row

This is the row number (0-2) for this pane.

col

This is the column number (0-2) for this pane.

You'll notice when you run Pane.py that you can solve tiles by clicking on them. But clicking on a tile button removes the button from only that tile. What we would like is for the clicked button to be removed from every other tile in that pane. To do that we have to have some way for the tile object to signal the pane that something has happened.

Here's where I introduce custom events. What we will do is create a new type of event, then add an event handler to the pane to process it. We create both a new event type object, and a binder to be used later to associate the event with a handler. Because an event handler requires an event object as a parameter, we will also create a custom event and populate it with information that the handler will find useful. Some of the information could be derived from the event object but it is clearer to just do this when we create the event.

myEVT_TILE_SOLVED = wx.NewEventType()
EVT_TILE_SOLVED   = wx.PyEventBinder(myEVT_TILE_SOLVED)

class TileSolvedEvent(wx.PyCommandEvent):
    """ Raised by a Tile object when a tile has been marked as solved """

    def __init__(self, evtType, tile, button):

        wx.PyCommandEvent.__init__(self, evtType)

        self.solved = button.name           # string value of the solved tile   
        self.number = int(self.solved)      # integer value of the solved tile  
        self.button = button                # button for the solved value       
        self.tile   = tile                  # the solved tile                   
        self.pane   = tile.Parent           # pane containing the solved tile   

We'll trigger the event in our Tile.ShowSolved() method which will now become

def ShowSolved(self, button):

    for b in self.shown: b.Hide()
    self.shown  = []
    self.value  = []

    # Display the bitmap image for the solved value.

    self.ShowDigit(button.name)

    # Raise the TileSolved event to allow app to scan for eliminations

    evt = TileSolvedEvent(myEVT_TILE_SOLVED, self, button)
    self.GetEventHandler().ProcessEvent(evt)

First we create an event object and load it with information, then we get the event handler and execute it. It doesn't help to trigger an event unless you have written an event handler, so before we start generating events we'd better create the handler in the pane object and bind it to the custom event. The handler will look like:

def Tile_OnSolved(self, event):

    bIndex = event.button.index

    for tile in self.tiles:
        if not tile.solved:
            tile.Remove(tile.buttons[bIndex])

and we bind it to the handler in __init__ by

self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)

Now when you run this with self.pane.SetSetup(True) you will see that the clicked button is now removed from all other tiles in the pane. One more thing to mention, once an event has been handled it disappears. However, actions in one pane can affect other panes so the event must also be passed up to the parent object to manage those effects. We do this by adding

    event.Skip()

to the end of our handler. But there remains one problem. We don't want to do this elimination during setup. We want the user to be able to setup the puzzle to match a given (printed) puzzle. So we we'll eliminate tiles in the same pane during setup, but not pass it up for further processing.

    if not self.setup:
        event.Skip()

The file Pane-event.py contains the code for tile & pane with the event handler.

You may have noticed the lists RPAD and CPAD at the top of the listing. These will be used when populating the grid sizers. we want extra padding on outside edges of panes and tiles in the grid and indexing RPAD and CPAD by the appropriate row and col index adds the appropriate padding.

I'm going to add a few more constants to make the code a little clearer. Except for BITMAPS they should be self-explanatory.

IMAGES  = 'images/'
PRESETS = 'presets.txt'
BITMAPS = {}

SETUP_COLOUR = 'yellow'
SOLVE_COLOUR = 'light grey'
PANE_COLOUR  = 'white'

PANEBORDER = 15 
TILEBORDER = 10

the images folder contains bitmaps of the nine digits. Instead of hitting on the disk every time we want to show a digit, we can load them into a dictionary at startup.

Now it's time to create the Puzzle object.

The Puzzle Object

By now you should be seeing a familiar pattern. The puzzle object is going to be a 3x3 grid of panes.

Puzzle.jpg

The puzzle object has to do a lot more housekeeping so we'll have a few more methods.

SetSetup

Like pane, this handles transitions between the states and ensures that the desired state propagates downward to all panes.

Tile_OnSolved

This is the handler that will respond, at the puzzle level, to TileSolved events. It calls Eliminate to remove the solved value from related tiles.

Eliminate

Eliminates the solved value from all tiles in all panels in the same row or column.

ScanAndEliminate

Called when moving from setup to solve mode. Because we don't eliminate tiles in other panes during setup (to ensure that the entered puzzle matches a printed puzzle) we have to scan the entire puzzle for solved tiles, then call Eliminate to remove from related tiles.

IsSolved

This will return two values. The first will indicate with True or False whether the puzzle has been solved. If solved then the second value will be a null string. If not solved the string will indicate the reason.

And now the properties

setup

This will be True if we are in setup mode and False if we are in solve mode.

panes

This is a list of all the panes in the puzzle.

rows

This is a list, indexed by a 0-relative pane number that identifies which panes are in the same row as a given pane. For example, for pane number 4, panes [3, 4, 5] are in the same row.

cols

Same as rows but for columns.

If you don't understand how the elimination is done I suggest you take an example and work through the numbers by stepping through the code.

Here is the code from puzzle.py.

import wx
import wx.lib.mixins.inspection

IMAGES  = 'images/'
PRESETS = 'presets.txt'
BITMAPS = {}

SETUP_COLOUR = 'yellow'
SOLVE_COLOUR = 'light grey'
PANE_COLOUR  = 'white'

PANEBORDER = 15 
TILEBORDER = 10

RPAD = (wx.TOP , 0, wx.BOTTOM)
CPAD = (wx.LEFT, 0, wx.RIGHT)

myEVT_TILE_SOLVED = wx.NewEventType()
EVT_TILE_SOLVED   = wx.PyEventBinder(myEVT_TILE_SOLVED)

class TileSolvedEvent(wx.PyCommandEvent):

    def __init__(self, evtType, tile, button):

        wx.PyCommandEvent.__init__(self, evtType)

        self.solved = button.name       # string value of the solved tile       
        self.number = int(self.solved)  # integer value of the solved tile      
        self.button = button            # button for the solved value           
        self.tile   = tile              # the solved tile                       
        self.pane   = tile.Parent       # pane containing the solved tile       

class App(wx.App):

    def OnInit(self):
        self.frame = Frame("Puzzle Object")
        self.frame.SetPosition((10,10))
        self.frame.Show()
        wx.lib.inspection.InspectionTool().Show()
        return True

class Frame(wx.Frame):

    def __init__(self, title):

        super(Frame, self).__init__(None, title=title, style=wx.CLOSE_BOX | wx.CAPTION)

        self.puzzle = Puzzle(self)
        self.puzzle.SetSetup(True)
        self.Fit()

class Puzzle(wx.Panel):

    def __init__(self, parent):

        super(Puzzle, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup = True               # true if in initial setup              
        self.panes = []                 # all panes within this puzzle          

        self.rows = [[0,1,2], [0,1,2], [0,1,2], 
                     [3,4,5], [3,4,5], [3,4,5], 
                     [6,7,8], [6,7,8], [6,7,8]]

        self.cols = [[0,3,6], [1,4,7], [2,5,8], 
                     [0,3,6], [1,4,7], [2,5,8], 
                     [0,3,6], [1,4,7], [2,5,8]]

        gsizer = wx.GridSizer(cols=3, vgap=5, hgap=5)

        for i in range(0,9):
            pane = Pane(self, name=str(i+1), index=i)
            self.panes.append(pane)
            flags = wx.ALIGN_CENTER | RPAD[pane.row] | CPAD[pane.col]
            gsizer.Add(pane, 0, flags, border=PANEBORDER)

        self.SetSizerAndFit(gsizer)

        self.SetBackgroundColour(SETUP_COLOUR) 
        self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)

    def SetSetup(self, setup):

        for pane in self.panes: pane.SetSetup(setup)

        if setup:
            self.state = []
        else:
            self.ScanAndEliminate()

        self.SetBackgroundColour(SETUP_COLOUR if setup else SOLVE_COLOUR)
        self.Refresh()

        self.setup = setup

    def Tile_OnSolved(self, event):

        self.Eliminate(event.pane.index, event.tile.index, event.button.index)

    def Eliminate(self, pIndex, tIndex, bIndex):

        # Eliminate the solved value from all tiles in all panels in the same row

        for p in self.rows[pIndex]:
            pane = self.panes[p]
            for t in self.rows[tIndex]:
                tile = pane.tiles[t]
                tile.Remove(tile.buttons[bIndex])

        # Eliminate the solved value from all tiles in all panels in the same column

        for p in self.cols[pIndex]:
            pane = self.panes[p]
            for t in self.cols[tIndex]:
                tile = pane.tiles[t]
                tile.Remove(tile.buttons[bIndex])

    def ScanAndEliminate(self):

        for pane in self.panes:
            for tile in pane.tiles:
                if tile.solved:
                    bIndex = int(tile.solved) - 1
                    self.Eliminate(pane.index, tile.index, bIndex)

    def IsSolved(self):

        # Check that every pane has been solved

        for p, pane in enumerate(self.panes):
            solved, reason = pane.IsSolved()
            if not solved: return (False, 'Pane ' + pane.name + ' ' + reason)

        # Check that each row has 9 unique digits

        for row in range(0,9):
            digits = set()
            for col in range(0,9):
                p = 3 * (row // 3) + col // 3
                t = col % 3 + 3 * (row % 3)
                digits.add(self.panes[p].tiles[t].solved)
            if len(digits) != 9:
                return (False, 'Row ' + str(r) + ' contains a repeated digit')

        # Check that each column has 9 unique digits

        for col in range(0,9):
            digits = set()
            for row in range(0,9):
                p = 3 * (row // 3) + col // 3
                t = col % 3 + 3 * (row % 3)
                digits.add(self.panes[p].tiles[t].solved)
            if len(digits) != 9:
                return False, 'Column ' + str(r) + ' contains a repeated digit'

            return (True, 'Congratulations')

class Pane(wx.Panel):

    def __init__(self, parent, name, index):

        super(Pane, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup = True               # True if in setup mode, False otherwise
        self.tiles = []                 # All tiles in this pane                
        self.name  = name               # Pane name is '1' to '9'               
        self.index = index              # Pane index is 0 to 8                  
        self.row   = index // 3         # row number (0-2) for this pane        
        self.col   = index  % 3         # col number (0-2) for this pane        

        # Make a 3x3 grid of tiles

        gsizer = wx.GridSizer(cols=3, vgap=5, hgap=5)

        for i in range(0,9):
            tile = Tile(self, name=str(i+1), index=i)
            self.tiles.append(tile)
            flags = wx.ALIGN_CENTER | RPAD[tile.row] | CPAD[tile.col]
            gsizer.Add(tile, 0, flags, border=TILEBORDER)

        self.SetSizerAndFit(gsizer)

        self.SetBackgroundColour(PANE_COLOUR)     
        self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)

    def SetSetup(self, setup):

        for tile in self.tiles: tile.SetSetup(setup)

        self.setup = setup

    def IsSolved(self):

        digits = set()

        # Check that every tile has been solved and has all 9 digits

        for t,tile in enumerate(self.tiles):
            if not tile.IsSolved():
                return (False, 'Tile ' + tile.name + ' has not been solved')
            digits.add(tile.solved)

        if len(digits) != 9:
            return (False, 'Pane ' + self.name + ' has repeated digits')

        return (True, '')

    def Tile_OnSolved(self, event):

        b = event.button.index

        for tile in self.tiles:
            if not tile.IsSolved():
                tile.Remove(tile.buttons[b])

        if not self.setup:
            event.Skip()

class Tile(wx.Panel):

    def __init__(self, parent, name, index):

        super(Tile, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup   = True                 # True if entering puzzle           
        self.buttons = []                   # all buttons                       
        self.shown   = []                   # visible buttons                   
        self.name    = name                 # Tile name is '1' to '9'           
        self.index   = index                # Tile index is 0 to 8              
        self.solved  = ''                   # set to solved digit when solved   
        self.row     = index // 3           # row number (0-2) for this tile    
        self.col     = index  % 3           # col number (0-2) for this tile    
        self.value   = list("123456789")    # visible digits                    

        # The bitmap control is used to display the solved value

        self.bitmap  = wx.StaticBitmap(self, -1)
        self.bitmap.Hide()

        # Create a 3x3 grid of buttons with an OnClick handler.

        gsizer = wx.GridSizer(cols=3, vgap=0, hgap=0) 

        for i in range(0,9): 
            button = wx.Button(self, label=str(i+1), size=(24,24))
            button.name = str(i+1)
            button.index = i
            button.Bind(wx.EVT_BUTTON, self.OnClick)
            self.buttons.append(button)
            self.shown.append(button)
            flags = wx.EXPAND | wx.ALIGN_CENTRE
            gsizer.Add(button,0, flags , border=0)      
        self.SetSizerAndFit(gsizer)  

    def SetSetup(self, setup):

        if setup: self.Clear()
        self.setup = setup

    def Clear(self):

        self.value  = list("123456789")
        self.solved = ''
        self.shown  = self.buttons.copy()
        self.ShowDigit('') 

        for button in self.buttons: button.Show()   

    def IsSolved(self):

        return self.solved != ''

    def OnClick(self, event):

        button = event.GetEventObject()

        if self.setup:
            self.ShowSolved(button)            
        else:
            self.Remove(button)

    def Remove(self, button):

        button.Hide()

        try:
            self.shown.remove(button)
            self.value.remove(button.name)
        except: pass

        if len(self.shown) == 1: 
            self.ShowSolved(self.shown[0]) 

    def ShowSolved(self, button):

        for b in self.shown: b.Hide()

        self.shown  = []
        self.value  = []
        self.solved = button.name

        self.ShowDigit(button.name)

        evt = TileSolvedEvent(myEVT_TILE_SOLVED, self, button)
        self.GetEventHandler().ProcessEvent(evt)

    def ShowDigit(self, digit):

        if digit == '':
            self.bitmap.Hide()
        else:
            bmp = wx.Image('images/' + digit + '.jpg', wx.BITMAP_TYPE_ANY).ConvertToBitmap()
            self.bitmap.Bitmap = bmp
            self.bitmap.Centre()
            self.bitmap.Show()

# Main ###############################################################

if __name__ == '__main__':

    app = App()

    # Load the digit background images into bitmaps

    for i in ('123456789'):
        BITMAPS[i] = wx.Image(IMAGES + i + '.jpg', wx.BITMAP_TYPE_ANY).ConvertToBitmap()

    app.MainLoop()

The Controls Object

I figured that the number of options for the application was small enough, and the operations were simple enough that a menu bar was unnecessary. As such, all program functions, other than clicking of tiles, are performed through buttons.

Because the controls aren't really functional on their own I didn't provide a stand-alone test module for them.

The controls we'll need (at least for now) are:

Solve

This button will be enabled at startup, and when the user decides to enter a new puzzle by clicking Clear. The user will select solved tiles by clicking on them. Once the puzzle has been entered, clicking Solve will move the puzzle from Setup mode into Solve mode and disable the Solve button.

Clear

Clicking this button at any time will cause the puzzle to be reset to the start state. The Solve button will be enabled and the user will be able to enter a new puzzle.

Check

Just because all of the tiles have a digit doesn't mean that the puzzle has been successfully solved. Clicking this button will cause the puzzle to be scanned. The user will be notified at the first occurrence of

  1. a tile that has not been solved
  2. a non-unique digit in any pane
  3. a non-unique digit in any row
  4. a non-unique digit in any column

For the time being if the solution is incorrect, the user is pretty much screwed and would have to clear the puzzle, re-enter it and try again. Later on we will add in two features which are surprisingly easy to implement in Python (not so much in vb.Net). They are

  1. reset to start (re-display the originally entered values)
  2. unlimited undo

Inspect

While this is something you wouldn't see in the finished application, it's only a couple of extra lines of code. Clicking this will bring up the inspection window allowing you to browse the complete object hierarchy. Note again that sizers are not included by default in the object tree, but if you click on the Sizers button in the inspection toolbar you will be able to examine these as well.

NewButton

This method creates a new wx.Button object, binds it to a handler, and saves a reference to it in a dictionary (see properties below)

The controls object has the following properties:

buttons

Typically you would create a property for each button with a unique name like

self.solve = wx.Button...

but because the same setup is required for each button (create, bind, save reference) I find it easier to have a generic NewButton method. Each button reference is saved in a dictionary with the button name as the key, and the button reference (address) as the value. That way when I need to do something like enable or disable a button I can just do

self.buttons['Solve'].Enabled = False

puzzle

This is a direct reference through the parent object, to the puzzle object. It's normally not recommended to cross reference objects like this. I could have arranged the objects like

Sudoku
    Controls
        Puzzle
        etc.

But with my limited knowledge of sizers at the time it would have made the layout more difficult. I could change it but that would mean rewriting this tutorial (again) and debugging (again).

The buttons are all in a single column so instead of the more complicated GridSizer, I use a BoxSizer with its orientation set to wx.VERTICAL. A BoxSizer can arrange controls in a single row (wx.HORIZONTAL), or a single column (wx.VERTICAL).

Here is the Controls object.

class Controls(wx.Panel):

    def __init__(self, parent, puzzle):

        super(Controls, self).__init__(parent, -1)

        self.buttons = {}               # references to command buttons         
        self.puzzle  = puzzle           # direct path to puzzle object          

        bsizer = wx.BoxSizer(wx.VERTICAL)
        bsizer.Add(self.NewButton(name='Solve'  , handler=self.Solve_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Clear'  , handler=self.Clear_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Check'  , handler=self.Check_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Inspect', handler=self.Inspect_OnClick),0,wx.ALL,15)
        self.SetSizerAndFit(bsizer)

        self.buttons['Check'].Enabled = False

        self.SetBackgroundColour('grey')

    def NewButton(self, name, handler):

        button = wx.Button(self,name=name, label=name)
        button.Bind(wx.EVT_BUTTON, handler)
        self.buttons[name] = button
        return button

    def Solve_OnClick(self, event):

        self.puzzle.SetSetup(False)

        self.buttons['Solve' ].Enabled = False
        self.buttons['Check' ].Enabled = True

    def Inspect_OnClick(self, event):

        wx.lib.inspection.InspectionTool().Show()

    def Clear_OnClick(self, event):

        self.puzzle.SetSetup(True)

        self.buttons['Solve' ].Enabled = True
        self.buttons['Check' ].Enabled = False

    def Check_OnClick(self, event):

        result, text = self.puzzle.IsSolved()
        wx.MessageBox(text,"Sudoku", wx.OK | wx.CENTRE | wx.ICON_INFORMATION, self)

Notice that the Check_OnClick doesn't really have to do any checking. It just asks the puzzle object "have you been solved". I think the code, for the most part, is self-explanatory.

That just leaves us with the task of creating a top level window (frame) to contain everything else.

The Sudoku Object

This object will have no methods. It will act only as a container. It looks like this

Sudoku-minimal.jpg

class Sudoku(wx.Frame):

    def __init__(self, title):

        super(Sudoku, self).__init__(None, title=title, style=wx.CLOSE_BOX | wx.CAPTION)

        self.setup    = True
        self.puzzle   = Puzzle(self)
        self.controls = Controls(self, self.puzzle)

        bsizer = wx.BoxSizer(wx.HORIZONTAL)
        bsizer.Add(self.puzzle, 0, wx.ALL, 10)
        bsizer.Add(self.controls, 0, wx.EXPAND, 0)
        self.SetSizerAndFit(bsizer)

        self.SetBackgroundColour('white')
        self.Raise()

The only new thing here is self.Raise(). This causes the application window to initially appear on top of all other windows. This class contains only two things, a puzzle object and a controls object. Because they are arranged side-by-side, we again use a BoxSizer, but this time with wx.HORIZONTAL. Once again I will post the complete code for the project so far. Following that I will explain how to add the Reset and Undo features. This is the code from sudoku-minimal.py. If you run it you will have a functional, but still limited, Sudoku tool.

import wx
import wx.lib.mixins.inspection

IMAGES  = 'images/'
PRESETS = 'presets.txt'
BITMAPS = {}

SETUP_COLOUR = 'yellow'
SOLVE_COLOUR = 'light grey'
PANE_COLOUR  = 'white'

PANEBORDER = 15 
TILEBORDER = 10

RPAD = (wx.TOP , 0, wx.BOTTOM)
CPAD = (wx.LEFT, 0, wx.RIGHT)

myEVT_TILE_SOLVED = wx.NewEventType()
EVT_TILE_SOLVED   = wx.PyEventBinder(myEVT_TILE_SOLVED)

class TileSolvedEvent(wx.PyCommandEvent):

    def __init__(self, evtType, tile, button):

        wx.PyCommandEvent.__init__(self, evtType)

        self.solved = button.name       # string value of the solved tile       
        self.number = int(self.solved)  # integer value of the solved tile      
        self.button = button            # button for the solved value           
        self.tile   = tile              # the solved tile                       
        self.pane   = tile.Parent       # pane containing the solved tile       

class App(wx.App):

    def OnInit(self):
        self.sudoku = Sudoku("Sudoku")
        self.sudoku.SetPosition((10,10))
        self.sudoku.Show()
        return True

class Sudoku(wx.Frame):

    def __init__(self, title):

        super(Sudoku, self).__init__(None, title=title, style=wx.CLOSE_BOX | wx.CAPTION)

        self.setup    = True
        self.puzzle   = Puzzle(self)
        self.controls = Controls(self, self.puzzle)

        bsizer = wx.BoxSizer(wx.HORIZONTAL)
        bsizer.Add(self.puzzle, 0, wx.ALL, 10)
        bsizer.Add(self.controls, 0, wx.EXPAND, 0)
        self.SetSizerAndFit(bsizer)

        self.SetBackgroundColour('white')
        self.Raise()

class Controls(wx.Panel):

    def __init__(self, parent, puzzle):

        super(Controls, self).__init__(parent, -1)

        self.buttons = {}               # references to command buttons         
        self.puzzle  = puzzle           # direct path to puzzle object          

        bsizer = wx.BoxSizer(wx.VERTICAL)
        bsizer.Add(self.NewButton(name='Solve'  , handler=self.Solve_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Clear'  , handler=self.Clear_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Check'  , handler=self.Check_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Inspect', handler=self.Inspect_OnClick),0,wx.ALL,15)
        self.SetSizerAndFit(bsizer)

        self.buttons['Check'].Enabled = False

        self.SetBackgroundColour('grey')

    def NewButton(self, name, handler):

        button = wx.Button(self,name=name, label=name)
        button.Bind(wx.EVT_BUTTON, handler)
        self.buttons[name] = button
        return button

    def Solve_OnClick(self, event):

        self.puzzle.SetSetup(False)

        self.buttons['Solve' ].Enabled = False
        self.buttons['Check' ].Enabled = True

    def Inspect_OnClick(self, event):

        wx.lib.inspection.InspectionTool().Show()

    def Clear_OnClick(self, event):

        self.puzzle.SetSetup(True)

        self.buttons['Solve' ].Enabled = True
        self.buttons['Check' ].Enabled = False

    def Check_OnClick(self, event):

        result, text = self.puzzle.IsSolved()
        wx.MessageBox(text,"Sudoku", wx.OK | wx.CENTRE | wx.ICON_INFORMATION, self)

class Puzzle(wx.Panel):

    def __init__(self, parent):

        super(Puzzle, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup = True               # true if in initial setup              
        self.panes = []                 # all panes within this puzzle          

        self.rows = [[0,1,2], [0,1,2], [0,1,2], 
                     [3,4,5], [3,4,5], [3,4,5], 
                     [6,7,8], [6,7,8], [6,7,8]]

        self.cols = [[0,3,6], [1,4,7], [2,5,8], 
                     [0,3,6], [1,4,7], [2,5,8], 
                     [0,3,6], [1,4,7], [2,5,8]]

        gsizer = wx.GridSizer(cols=3, vgap=5, hgap=5)

        for i in range(0,9):
            pane = Pane(self, name=str(i+1), index=i)
            self.panes.append(pane)
            flags = wx.ALIGN_CENTER | RPAD[pane.row] | CPAD[pane.col]
            gsizer.Add(pane, 0, flags, border=PANEBORDER)

        self.SetSizerAndFit(gsizer)

        self.SetBackgroundColour(SETUP_COLOUR) 
        self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)

    def SetSetup(self, setup):

        for pane in self.panes: pane.SetSetup(setup)

        if setup:
            self.state = []
        else:
            self.ScanAndEliminate()

        self.SetBackgroundColour(SETUP_COLOUR if setup else SOLVE_COLOUR)
        self.Refresh()

        self.setup = setup

    def Tile_OnSolved(self, event):

        self.Eliminate(event.pane.index, event.tile.index, event.button.index)

    def Eliminate(self, pIndex, tIndex, bIndex):

        # Eliminate the solved value from all tiles in all panels in the same row

        for p in self.rows[pIndex]:
            pane = self.panes[p]
            for t in self.rows[tIndex]:
                tile = pane.tiles[t]
                tile.Remove(tile.buttons[bIndex])

        # Eliminate the solved value from all tiles in all panels in the same column

        for p in self.cols[pIndex]:
            pane = self.panes[p]
            for t in self.cols[tIndex]:
                tile = pane.tiles[t]
                tile.Remove(tile.buttons[bIndex])

    def ScanAndEliminate(self):

        for pane in self.panes:
            for tile in pane.tiles:
                if tile.solved:
                    bIndex = int(tile.solved) - 1
                    self.Eliminate(pane.index, tile.index, bIndex)

    def IsSolved(self):

        # Check that every pane has been solved

        for p, pane in enumerate(self.panes):
            solved, reason = pane.IsSolved()
            if not solved: return (False, 'Pane ' + pane.name + ' ' + reason)

        # Check that each row has 9 unique digits

        for row in range(0,9):
            digits = set()
            for col in range(0,9):
                p = 3 * (row // 3) + col // 3
                t = col % 3 + 3 * (row % 3)
                digits.add(self.panes[p].tiles[t].solved)
            if len(digits) != 9:
                return (False, 'Row ' + str(r) + ' contains a repeated digit')

        # Check that each column has 9 unique digits

        for col in range(0,9):
            digits = set()
            for row in range(0,9):
                p = 3 * (row // 3) + col // 3
                t = col % 3 + 3 * (row % 3)
                digits.add(self.panes[p].tiles[t].solved)
            if len(digits) != 9:
                return False, 'Column ' + str(r) + ' contains a repeated digit'

            return (True, 'Congratulations')

class Pane(wx.Panel):

    def __init__(self, parent, name, index):

        super(Pane, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup = True               # True if in setup mode, False otherwise
        self.tiles = []                 # All tiles in this pane                
        self.name  = name               # Pane name is '1' to '9'               
        self.index = index              # Pane index is 0 to 8                  
        self.row   = index // 3         # row number (0-2) for this pane        
        self.col   = index  % 3         # col number (0-2) for this pane        

        # Make a 3x3 grid of tiles

        gsizer = wx.GridSizer(cols=3, vgap=5, hgap=5)

        for i in range(0,9):
            tile = Tile(self, name=str(i+1), index=i)
            self.tiles.append(tile)
            flags = wx.ALIGN_CENTER | RPAD[tile.row] | CPAD[tile.col]
            gsizer.Add(tile, 0, flags, border=TILEBORDER)

        self.SetSizerAndFit(gsizer)

        self.SetBackgroundColour(PANE_COLOUR)     
        self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)

    def SetSetup(self, setup):

        for tile in self.tiles: tile.SetSetup(setup)

        self.setup = setup

    def IsSolved(self):

        digits = set()

        # Check that every tile has been solved and has all 9 digits

        for t,tile in enumerate(self.tiles):
            if not tile.IsSolved():
                return (False, 'Tile ' + tile.name + ' has not been solved')
            digits.add(tile.solved)

        if len(digits) != 9:
            return (False, 'Pane ' + self.name + ' has repeated digits')

        return (True, '')

    def Tile_OnSolved(self, event):

        b = event.button.index

        for tile in self.tiles:
            if not tile.IsSolved():
                tile.Remove(tile.buttons[b])

        if not self.setup:
            event.Skip()

class Tile(wx.Panel):

    def __init__(self, parent, name, index):

        super(Tile, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup   = True                 # True if entering puzzle           
        self.buttons = []                   # all buttons                       
        self.shown   = []                   # visible buttons                   
        self.name    = name                 # Tile name is '1' to '9'           
        self.index   = index                # Tile index is 0 to 8              
        self.solved  = ''                   # set to solved digit when solved   
        self.row     = index // 3           # row number (0-2) for this tile    
        self.col     = index  % 3           # col number (0-2) for this tile    
        self.value   = list("123456789")    # visible digits                    

        # The bitmap control is used to display the solved value

        self.bitmap  = wx.StaticBitmap(self, -1)
        self.bitmap.Hide()

        # Create a 3x3 grid of buttons with an OnClick handler.

        gsizer = wx.GridSizer(cols=3, vgap=0, hgap=0) 

        for i in range(0,9): 
            button = wx.Button(self, label=str(i+1), size=(24,24))
            button.name = str(i+1)
            button.index = i
            button.Bind(wx.EVT_BUTTON, self.OnClick)
            self.buttons.append(button)
            self.shown.append(button)
            flags = wx.EXPAND | wx.ALIGN_CENTRE
            gsizer.Add(button,0, flags , border=0)      
        self.SetSizerAndFit(gsizer)  

    def SetSetup(self, setup):

        if setup: self.Clear()
        self.setup = setup

    def Clear(self):

        self.value  = list("123456789")
        self.solved = ''
        self.shown  = self.buttons.copy()
        self.ShowDigit('') 

        for button in self.buttons: button.Show()   

    def IsSolved(self):

        return self.solved != ''

    def OnClick(self, event):

        button = event.GetEventObject()

        if self.setup:
            self.ShowSolved(button)            
        else:
            self.Remove(button)

    def Remove(self, button):

        button.Hide()

        try:
            self.shown.remove(button)
            self.value.remove(button.name)
        except: pass

        if len(self.shown) == 1: 
            self.ShowSolved(self.shown[0]) 

    def ShowSolved(self, button):

        for b in self.shown: b.Hide()

        self.shown  = []
        self.value  = []
        self.solved = button.name

        self.ShowDigit(button.name)

        evt = TileSolvedEvent(myEVT_TILE_SOLVED, self, button)
        self.GetEventHandler().ProcessEvent(evt)

    def ShowDigit(self, digit):

        if digit == '':
            self.bitmap.Hide()
        else:
            bmp = wx.Image('images/' + digit + '.jpg', wx.BITMAP_TYPE_ANY).ConvertToBitmap()
            self.bitmap.Bitmap = bmp
            self.bitmap.Centre()
            self.bitmap.Show()

# Main ###############################################################

if __name__ == '__main__':

    app = App()

    # Load the digit background images into bitmaps

    for i in ('123456789'):
        BITMAPS[i] = wx.Image(IMAGES + i + '.jpg', wx.BITMAP_TYPE_ANY).ConvertToBitmap()

    app.MainLoop()

Finally, we are going to add our bells and whistles. Everyone makes mistakes. And it would be nice if all mistakes were correctable. Fortunately we can easily implement that. vb.Net requires the use of complicated data structures. Python (thank you Guido) has lists. What we would like to do is save the complete state of the puzzle at every change. Obviously, a good place to start is with the originally entered puzzle, then at every tile button click thereafter. We need two things

  1. some mechanism whereby a tile can signal to the puzzle that a state save is needed
  2. some sort of state stack where we can push (save) or pop (restore) a state

We have already seen what to do for the first item. All we have to do is create another custom event and trigger it in the tile button click handler. As long as we do that at the top of the handler, the new event will be handled before anything else changes state. The second item is also quite trivial. We create a new method for a pane that returns the state of that pane. Similarly, we create the same method for a tile. The state list will contain one list for each pane, where each pane entry is a list containing the state for each tile. The state of a tile is simply the list of remaining digits for that tile. When we want to restore a previous state we can

  1. clear the puzzle
  2. restore the values for all tiles

But let's say the user has really messed things up and would like to start over. In this case, rather than have the user click undo many times, it would be more convenient to have a Reset button. This would throw away everything in the state stack except for state[0] (the initial state), then call ScanAndEliminate to resolve all puzzle tiles.

So we are actually going to have a couple of new methods for Pane and Tile. These will be

GetState

Which will return the current state of that object, and

SetState

Which will take a state entry and restore it to that object.

For Pane they look like

def GetState(self):
    state = []
    for tile in self.tiles:
        state.append(tile.GetState())
    return state

def SetState(self, state):
    for t,tile in enumerate(state):
        self.tiles[t].SetState(tile)

and for Tile they are

def GetState(self):

    return [self.solved] if self.solved else self.value.copy()

def SetState(self, state):

    self.Clear()

    if len(state) == 1:
        button = self.buttons[int(state[0])-1]
        self.ShowSolved(button)
    else:
        for index,digit in enumerate(list("123456789")):
            if digit not in state:
                self.Remove(self.buttons[index])

In GetState (tile) note that everything in state is a list so for a single value (when the tile is solved) we must return

[self.solved]

rather than

self.solved

There is still one problem. When restoring states at the tile level, we don't want to start triggering TileSolved events so we are going to have to suppress them somehow. We'll introduce another tile property, self.events which we will normally have set to True, but we will set it to False while restoring a tile state. Our tile SetState will then look like

def SetState(self, state):

    self.events = False

    self.Clear()

    if len(state) == 1:
        button = self.buttons[int(state[0])-1]
        self.ShowSolved(button)
    else:
        for index,digit in enumerate(list("123456789")):
            if digit not in state:
                self.Remove(self.buttons[index])

    self.events = True

and in our button ShowSolved method we will now do

    if self.events:
        evt = TileSolvedEvent(myEVT_TILE_SOLVED, self, button)
        self.GetEventHandler().ProcessEvent(evt)

We'll add an Undo button in our Controls object and a little housekeeping code in the Puzzle object to create and manage the stack. These methods are

GetState

This returns one list which is the complete puzzle state. All it has to do it build a list containing the state of each pane.

PushState

Calls GetState to get the current state and pushes it onto the stack.

PopState

Pops the previous state off the stack and restores it. Note that it will always preserve the initial state.

One more nice feature would be the ability to load a puzzle from a file. Over the months of solving Sudoku puzzles with my vb.Net app I have come across a number of puzzles which were challenging but not entirely evil. I had a button in that app which would append the initial puzzle to a file of puzzle presets that had the form

--98-2--- 6-7-5---- 13-----9- --478---- --354-9-- ----297-- -9-----65 ----9-1-8 ---5-1---
-1--27--- --7--86-3 -------12 --9-8-1-- ----4---- --6-3-5-- 34------- 7-91--5-- ---69--8-
-1--5-3-4 --57----- 2---81--9 -----1--- -6-3-4-5- ---8----- 9--42---3 -----85-- 1-6-5--2-
-135--9-- -----7-6- 7-93---8- ---6---8- --1---5-- -4---3--- -2---68-9 -7-9----- --8--427-
-15-----4 7-4---8-- -----6-3- -79-6-3-- ----5---- --2-4-16- -3-1----- --8---4-3 6-----89-
-182--3-- 7--8----1 ---9---7- 1-573---6 --------- 7---962-3 -7---1--- 5----8--2 --2--938-
-2------7 --8--79-- 37--4--62 ----5-9-- --5---3-- --9-3---- 18--9--32 --36--5-- 7------1-

Each line represents one puzzle. Each set of 9 values represents the initial values of a pane. For example, the first line,

--98-2--- 6-7-5---- 13-----9- --478---- --354-9-- ----297-- -9-----65 ----9-1-8 ---5-1---

would resolve into the puzzle

- - 9   6 - 7   1 3 -
8 - 2   - 5 -   - - -
- - -   - - -   - 9 -

- - 4   - - 3   - - -
7 8 -   5 4 -   - 2 9
- - -   9 - -   7 - -

- 9 -   - - -   - - -
- - -   - 9 -   5 - 1
- 6 5   1 - 8   - - -

So it was easy to add a Preset button to load a random line from the preset file into the app. As a future feature I might add a hint button which could tell the user if a particular digit appeared only once in a pane, row or column.

So our final (except for the many improvements I hope to make with some helpful feedback) application follows. You can find the code in the file Sudoku.py.

"""                                                                             
    Name:                                                                       

        Sudoku.py                                                               

    Description:                                                                

        This is a GUI tool to be used as an aid in solving Sudoku puzzles. While
        not eliminating any of the brain work, it does eliminate the tedious    
        business of filling in and erasing values as they are solved. It        
        features multiple undo as well as the ability to load puzzles from a    
        file of presets.                                                        

    Notes:                                                                      

        This is a port of a program I originally wrote in vb.Net. I wanted a    
        project that I could use to learn both Python and wxPython. The Python  
        code is much cleaner than the corresponding vb.Net code and the         
        development time was considerably less. Because this is a learning      
        project in progress I expect that there are a number of things that can 
        be improved. As with any code I post, constructive criticism and        
        suggestions for improvement are always welcome.                         

        None of us is as smart as all of us.                                    

    Audit:                                                                      

        2019-08-02  rj  Original code                                           

"""

import wx
import random
import wx.lib.mixins.inspection

TITLE   = 'Sudoku Assistant - Version 1.0 - 2019-08-02 by Reverend Jim'

# IMAGES        name of the folder containing 76x76 bitmaps of the digits 1-9   
# PRESETS       name of the folder containing sample puzzles                    
# BITMAPS       dictionary of bitmaps built from the files in IMAGES            
# SETUP_COLOUR  puzzle colour during setup mode                                 
# SOLVE_COLOUR  puzzle coulor during solution mode                              
# PANEBORDER    border around the outside of the 3x3 (outer) grid of panes      
# TILEBORDER    border around the outside of the 3x3 (inner) grid of tiles      

IMAGES  = 'images/'
PRESETS = 'presets.txt'
BITMAPS = {}

# Background colours for the two modes

SETUP_COLOUR = 'yellow'
SOLVE_COLOUR = 'light grey'
PANE_COLOUR  = 'white'

# Configuration layout parameters

PANEBORDER = 15 
TILEBORDER = 10

# RPAD & CPAD are used to set different padding depending on where in the 3x3   
# grid the object is located. For example, the pane in row 0 and col 2 (the top 
# right of the grid) will have padding RPAD[0] | CPAD[2] or wx.TOP | wx.RIGHT.  

RPAD = (wx.TOP , 0, wx.BOTTOM)
CPAD = (wx.LEFT, 0, wx.RIGHT)

# Custom events ----------------------------------------------------------------

# When a Tile object is marked as solved (either during setup when the tile is  
# clicked, or during solution when all buttons/digits but one have been         
# elimintated) this event is raised to allow the pane and puzzle level objects  
# to eliminate the solved value from the rest of the puzzle.                    

myEVT_TILE_SOLVED = wx.NewEventType()
EVT_TILE_SOLVED   = wx.PyEventBinder(myEVT_TILE_SOLVED)

class TileSolvedEvent(wx.PyCommandEvent):
    """ 
    Raised by a Tile object when a tile has been marked as solved. The handler
    for this event will scan all panes and eliminate the solved value if it
    appears in the same pane, or in other panes in the same row or column.
    """

    def __init__(self, evtType, tile, button):

        wx.PyCommandEvent.__init__(self, evtType)

        self.solved = button.name       # string value of the solved tile       
        self.number = int(self.solved)  # integer value of the solved tile      
        self.button = button            # button for the solved value           
        self.tile   = tile              # the solved tile                       
        self.pane   = tile.Parent       # pane containing the solved tile       

# When a button in a tile is clicked we want to save the state of the puzzle so 
# we can undo it. We need to do this before the button changes state so when a  
# button is clicked we will raise the ButtonClickedEvent.                       

myEVT_BUTTON_CLICKED = wx.NewEventType()
EVT_BUTTON_CLICKED = wx.PyEventBinder(myEVT_BUTTON_CLICKED)

class ButtonClickedEvent(wx.PyCommandEvent):
    """ Raised by a Tile object when any button in a tile has been clicked """

    def __init__(self, evtType):

        wx.PyCommandEvent.__init__(self, evtType)

# End of Custom events ---------------------------------------------------------

class App(wx.App):

    def OnInit(self):
        self.sudoku = Sudoku(TITLE)
        self.sudoku.Centre()
        self.sudoku.Show()
        return True

class Sudoku(wx.Frame):
    """ 
    Contains the puzzle window and the controls window. 
    """

    def __init__(self, title):

        super(Sudoku, self).__init__(None, title=title, style=wx.CLOSE_BOX | wx.CAPTION)

        self.setup    = True
        self.puzzle   = Puzzle(self)
        self.controls = Controls(self, self.puzzle)

        bsizer = wx.BoxSizer(wx.HORIZONTAL)
        bsizer.Add(self.puzzle, 0, wx.ALL, 10)
        bsizer.Add(self.controls, 0, wx.EXPAND, 0)
        self.SetSizerAndFit(bsizer)

        self.SetBackgroundColour('white')
        self.Raise()

class Controls(wx.Panel):
    """
    Contains all of the button controls other than the tile buttons.
    """

    def __init__(self, parent, puzzle):

        super(Controls, self).__init__(parent, -1)

        self.buttons = {}               # references to command buttons         
        self.puzzle  = puzzle           # direct path to puzzle object          

        bsizer = wx.BoxSizer(wx.VERTICAL)
        bsizer.Add(self.NewButton(name='Solve'  , handler=self.Solve_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Undo'   , handler=self.Undo_OnClick)   ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Reset'  , handler=self.Reset_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Clear'  , handler=self.Clear_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Check'  , handler=self.Check_OnClick)  ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Preset' , handler=self.Preset_OnClick) ,0,wx.ALL,15)
        bsizer.Add(self.NewButton(name='Inspect', handler=self.Inspect_OnClick),0,wx.ALL,15)
        self.SetSizerAndFit(bsizer)

        self.buttons['Reset'].Enabled = False
        self.buttons['Check'].Enabled = False

        self.SetBackgroundColour('grey')

    def NewButton(self, name, handler):
        """
        Create a new control button and bind it to a handler. Save a reference
        to the button in a dictionary so we can enable/disable by name.
        """
        button = wx.Button(self,name=name, label=name)
        button.Bind(wx.EVT_BUTTON, handler)
        self.buttons[name] = button
        return button

    def Solve_OnClick(self, event):
        """
        Move the puzzle from setup mode into solution mode
        """
        self.puzzle.SetSetup(False)

        self.buttons['Solve' ].Enabled = False
        self.buttons['Reset' ].Enabled = True
        self.buttons['Preset'].Enabled = False
        self.buttons['Check' ].Enabled = True

    def Inspect_OnClick(self, event):
        """
        Display the PyCrust object inspection tool 
        """
        wx.lib.inspection.InspectionTool().Show()

    def Undo_OnClick(self, event):
        """ 
        Undo the last move 
        """        
        self.puzzle.PopState()

    def Reset_OnClick(self, event):
        """ 
        Reset the puzzle to the state when Solve was first clicked.
        """               
        self.puzzle.PopState(reset=True)

    def Clear_OnClick(self, event):
        """ 
        Clear the puzzle so a new puzzle can be entered 
        """
        self.puzzle.SetSetup(True)

        self.buttons['Solve' ].Enabled = True
        self.buttons['Reset' ].Enabled = False
        self.buttons['Check' ].Enabled = False
        self.buttons['Preset'].Enabled = True

    def Check_OnClick(self, event):
        """ 
        Check the puzzle for a correct solution 
        """
        result, text = self.puzzle.IsSolved()
        wx.MessageBox(text,"Sudoku", wx.OK | wx.CENTRE | wx.ICON_INFORMATION, self)

    def Preset_OnClick(self, event):
        """ 
        Load a preset puzzle 
        """
        self.puzzle.LoadPreset(random.choice(open(PRESETS).read().splitlines()))

class Puzzle(wx.Panel):
    """                                                                         
    A puzzle consists of a 3x3 grid of panes where each pane (when solved) will 
    consist of a 3x3 grid of digits from 1-9. 
    """

    def __init__(self, parent):

        super(Puzzle, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup = True               # true if in initial setup              
        self.panes = []                 # all panes within this puzzle          
        self.state = []                 # stack for Undo operations             

        # The following two lists group the panes (indexed 0-8) that are in the 
        # same row and column

        self.rows = [[0,1,2], [0,1,2], [0,1,2], 
                     [3,4,5], [3,4,5], [3,4,5], 
                     [6,7,8], [6,7,8], [6,7,8]]

        self.cols = [[0,3,6], [1,4,7], [2,5,8], 
                     [0,3,6], [1,4,7], [2,5,8], 
                     [0,3,6], [1,4,7], [2,5,8]]

        # Make a 3x3 grid of panes

        gsizer = wx.GridSizer(cols=3, vgap=5, hgap=5)

        for i in range(0,9):
            pane = Pane(self, name=str(i+1), index=i)
            self.panes.append(pane)
            flags = wx.ALIGN_CENTER | RPAD[pane.row] | CPAD[pane.col]
            gsizer.Add(pane, 0, flags, border=PANEBORDER)

        self.SetSizerAndFit(gsizer)

        self.SetBackgroundColour(SETUP_COLOUR)

        # Add handlers to allow processing for eliminations on tile solved event
        # and Undo. Note that there are no buttons to click in this object, but 
        # it will still process button clicks passed up from the tile object so 
        # that  eliminations can be done over the entire puzzle grid.           

        self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)
        self.Bind(EVT_BUTTON_CLICKED, self.Button_OnClick)

    def SetSetup(self, setup):
        """ 
        Change state to either Setup mode (True) or Solve (False) mode.
        """
        for pane in self.panes: pane.SetSetup(setup)

        if setup:

            # Moving from solve mode to setup mode. Clear the undo stack

            self.state = []

        else:

            # Moving from setup mode to solve mode. Eliminate all digits
            # that have been entered from the rest of the puzzle and    
            # Save the resulting state as state[0]. This will be used if
            # the user does a Reset.                                    

            self.ScanAndEliminate()
            self.PushState()

        self.SetBackgroundColour(SETUP_COLOUR if setup else SOLVE_COLOUR)
        self.Refresh()

        self.setup = setup

    def Button_OnClick(self, event):
        """
        A tile button has been clicked. Save the current state on the stack
        before any eliminations are done.
        """
        self.PushState()

    def PushState(self):
        """ 
        Push the current state of the puzzle onto the Undo stack.
        """
        self.state.append(self.GetState())

    def PopState(self, reset=False):
        """ 
        Restore the puzzle to a prior state. If Reset=True then restore the
        puzzle to the initial state.
        """
        if reset or len(self.state) == 1:
            # If there is only the initial state available then use it
            state = self.state[0]
        else:
            # Remove the last state and restore it
            state = self.state.pop()

        for p,pane in enumerate(state):
            self.panes[p].SetState(pane)

    def GetState(self):
        """ 
        Get the current state of all panes 
        """
        state = []
        for pane in self.panes:
            state.append(pane.GetState())
        return state

    def LoadPreset(self, preset):
        """
        Load a preset into the puzzle
        """
        for p,values in enumerate(preset.split()):
            self.panes[p].LoadPreset(values)

    def DumpState(self, text):
        """
        Debugging method. I used this while trying to hunt down a bug in the
        undo/reset logic. It prints out the entire state stack in an easy to
        read format.
        """
        print('\n' +10*'- ' + text + 10*' -' + '\n')
        print('Numstates = %d\n' % len(self.state))

        for s,state in enumerate(self.state):
            print('\nstate ' + str(s) + '\n')
            for p,pane in enumerate(state):
                out = []
                for t in pane:
                    out.append(''.join(t))
                print(out)
            print("")

    def Tile_OnSolved(self, event):
        """
        A tile has been solved. Eliminate the solved value from other parts of
        the puzzle. The actual work is fobbed off to another method because the
        same logic must be executed when scanning after "Solve" is clicked.
        """
        self.Eliminate(event.pane.index, event.tile.index, event.button.index)

    def Eliminate(self, pIndex, tIndex, bIndex):
        """
        Take the solved value from panes[p].tiles[t].buttons[b] and eliminate
        it from other parts of the puzzle.
        """
        # Eliminate the solved value from all tiles in all panels in the same row

        for p in self.rows[pIndex]:
            pane = self.panes[p]
            for t in self.rows[tIndex]:
                tile = pane.tiles[t]
                tile.Remove(tile.buttons[bIndex])

        # Eliminate the solved value from all tiles in all panels in the same column

        for p in self.cols[pIndex]:
            pane = self.panes[p]
            for t in self.cols[tIndex]:
                tile = pane.tiles[t]
                tile.Remove(tile.buttons[bIndex])

    def ScanAndEliminate(self):
        """ 
        Scan the entire puzzle for solved tiles and eliminate those values from
        all tiles in the same row or column.
        """

        for pane in self.panes:
            for tile in pane.tiles:
                if tile.solved:
                    bIndex = int(tile.solved) - 1
                    self.Eliminate(pane.index, tile.index, bIndex)

    def IsSolved(self):
        """ 
        Returns (True, '') if the puzzle has been solved or (False, 'reason')
        if it has not been solved. 
        """
        # Check that every pane has been solved

        for p, pane in enumerate(self.panes):
            solved, reason = pane.IsSolved()
            if not solved: return (False, 'Pane ' + pane.name + ' ' + reason)

        # Check that each row has 9 unique digits. We do this using a set. We
        # add each digit as found then check at the end that the set has nine
        # values in it.

        for row in range(0,9):
            digits = set()
            for col in range(0,9):
                p = 3 * (row // 3) + col // 3
                t = col % 3 + 3 * (row % 3)
                digits.add(self.panes[p].tiles[t].solved)
            if len(digits) != 9:
                return (False, 'Row ' + str(r) + ' contains a repeated digit')

        # Check that each column has 9 unique digits

        for col in range(0,9):
            digits = set()
            for row in range(0,9):
                p = 3 * (row // 3) + col // 3
                t = col % 3 + 3 * (row % 3)
                digits.add(self.panes[p].tiles[t].solved)
            if len(digits) != 9:
                return False, 'Column ' + str(r) + ' contains a repeated digit'

            return (True, 'Congratulations')

class Pane(wx.Panel):
    """                                                                         
    A pane consists of a 3x3 grid of tiles where a tile, when solved, consists  
    of a single digit from 1-9.              
    """

    def __init__(self, parent, name, index):

        super(Pane, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup = True               # True if in setup mode, False otherwise
        self.tiles = []                 # All tiles in this pane                
        self.name  = name               # Pane name is '1' to '9'               
        self.index = index              # Pane index is 0 to 8                  
        self.row   = index // 3         # row number (0-2) for this pane        
        self.col   = index  % 3         # col number (0-2) for this pane        

        # Make a 3x3 grid of tiles

        gsizer = wx.GridSizer(cols=3, vgap=5, hgap=5)

        for i in range(0,9):
            tile = Tile(self, name=str(i+1), index=i)
            self.tiles.append(tile)
            flags = wx.ALIGN_CENTER | RPAD[tile.row] | CPAD[tile.col]
            gsizer.Add(tile, 0, flags, border=TILEBORDER)

        self.SetSizerAndFit(gsizer)

        self.SetBackgroundColour(PANE_COLOUR)        
        self.Bind(EVT_TILE_SOLVED, self.Tile_OnSolved)

    def SetSetup(self, setup):
        """ 
        Change state to either Setup mode (True) or Solve (False) mode.
        """
        for tile in self.tiles: tile.SetSetup(setup)

        self.setup = setup

    def GetState(self):
        """ 
        Get the current state of all tiles 
        """
        state = []
        for tile in self.tiles:
            state.append(tile.GetState())
        return state

    def SetState(self, state):
        """
        Set all tiles in this pane according to the given state
        """
        for t,tile in enumerate(state):
            self.tiles[t].SetState(tile)

    def LoadPreset(self, preset):
        """
        Load a preset into this pane
        """
        for t,value in enumerate(list(preset)):
            self.tiles[t].LoadPreset(value)

    def IsSolved(self):
        """ 
        Return (True, '') if this pane has been solved or (False, 'reason')
        if it has not been solved.
        """        
        digits = set()

        # Check that every tile has been solved and has all 9 digits

        for t,tile in enumerate(self.tiles):
            if not tile.IsSolved():
                return (False, 'Tile ' + tile.name + ' has not been solved')
            digits.add(tile.solved)

        if len(digits) != 9:
            return (False, 'Pane ' + self.name + ' has repeated digits')

        return (True, '')

    def Tile_OnSolved(self, event):
        """                                                                     
        A tile in this pane has been solved. Eliminate the solved value from    
        all other tiles in this pane.                                           
        """
        b = event.button.index

        for tile in self.tiles:
            if not tile.IsSolved():
                tile.Remove(tile.buttons[b])

        # Do not pass the solved event up if we are in setup. This will allow   
        # the entered puzzle to be displayed as given. Elimination through other
        # panes will be done when we enter solution mode.                       

        if not self.setup:
            event.Skip()

class Tile(wx.Panel):
    """                                                                         
    A tile represents one Sudoku square which can contain a single digit from   
    1-9. Initially the tile will contain one button for each digit in a 3x3     
    grid. In setup mode, clicking a digit will cause all all buttons to be      
    removed and the clicked digit displayed as the "solved" digit. In solve     
    mode, clicking a digit will cause that digit to be removed. When only one   
    digit remains, that digit will be displayed as the "solved" value.          
    """

    def __init__(self, parent, name, index):

        super(Tile, self).__init__(parent, -1, style=wx.BORDER_DOUBLE)

        self.setup   = True                 # True if entering puzzle           
        self.buttons = []                   # all buttons                       
        self.shown   = []                   # visible buttons                   
        self.name    = name                 # Tile name is '1' to '9'           
        self.index   = index                # Tile index is 0 to 8              
        self.events  = True                 # True to enable event generation   
        self.solved  = ''                   # set to solved digit when solved   
        self.row     = index // 3           # row number (0-2) for this tile    
        self.col     = index  % 3           # col number (0-2) for this tile    
        self.value   = list("123456789")    # visible digits                    

        # The bitmap control is used to display the solved value

        self.bitmap  = wx.StaticBitmap(self, -1)
        self.bitmap.Hide()

        # Create a 3x3 grid of buttons with an OnClick handler.

        gsizer = wx.GridSizer(cols=3, vgap=0, hgap=0) 

        for i in range(0,9): 
            button = wx.Button(self, label=str(i+1), size=(24,24))
            button.name = str(i+1)
            button.index = i
            button.Bind(wx.EVT_BUTTON, self.OnClick)
            self.buttons.append(button)
            self.shown.append(button)
            flags = wx.EXPAND | wx.ALIGN_CENTRE
            gsizer.Add(button,0, flags , border=0)      
        self.SetSizerAndFit(gsizer)  

    def SetSetup(self, setup):
        """ 
        Change state to either Setup mode (True) or Solve (False) mode.
        """
        if setup: self.Clear()
        self.setup = setup

    def SetState(self, state):
        """ Set the tile to the values in the given state. We have to disable
        the triggering of events while we do this to avoid ending up in an
        infinite loop. 
        """
        self.events = False

        self.Clear()

        if len(state) == 1:
            button = self.buttons[int(state[0])-1]
            self.ShowSolved(button)
        else:
            for index,digit in enumerate(list("123456789")):
                if digit not in state:
                    self.Remove(self.buttons[index])

        self.events = True

    def GetState(self):
        """ Return a list of all digits being displayed. If the tile is
        solved then the list has only the solved value. Note that we have
        to return a copy of self.value. If we return the actual self.value
        then we get a reference, and any changes to self.value will affect
        all undo stack entries.
        """
        return [self.solved] if self.solved else self.value.copy()

    def LoadPreset(self, digit):
        """
        Load a preset into this tile. Note that the digit from the preset file
        will be either an actual digit (for a solved value) or "-" to indicate
        no value.
        """
        if digit in '123456789':
            self.ShowSolved(self.buttons[int(digit)-1])        

    def Clear(self):
        """ 
        Clear the tile (reset to "new") 
        """
        self.value  = list("123456789")
        self.solved = ''
        self.shown  = self.buttons.copy()
        self.ShowDigit('') 

        for button in self.buttons: button.Show()   

    def IsSolved(self):
        """
        Return True if this tile is solved, otherwise False
        """
        return self.solved != ''

    def OnClick(self, event):
        """
        Handler for a button click event
        """  
        # Trigger an event to save the current state for a possible undo

        evt = ButtonClickedEvent(myEVT_BUTTON_CLICKED)
        self.GetEventHandler().ProcessEvent(evt)

        # If in setup mode then display the clicked button as solved, otherwise
        # remove the clicked button.

        if self.setup:
            self.ShowSolved(event.GetEventObject())            
        else:
            self.Remove(event.GetEventObject())

    def Remove(self, button):
        """                                                                     
        Remove the given button from the tile.        
        """        
        # Remove the given button. Do this in a try/except so that we don't have
        # to test before calling if the button is actually visible at the time. 

        button.Hide()

        try:
            self.shown.remove(button)
            self.value.remove(button.name)
        except: pass

        if len(self.shown) == 1: 
            self.ShowSolved(self.shown[0]) 

    def ShowSolved(self, button):
        """ 
        Remove any remaining buttons and display the digit of the last button  
        as the solved value for the tile.                   
        """
        for b in self.shown: b.Hide()

        self.shown  = []
        self.value  = []
        self.solved = button.name

        # Display the bitmap image for the solved value.

        self.ShowDigit(button.name)

        # Raise the TileSolved event to allow app to scan for eliminations

        evt = TileSolvedEvent(myEVT_TILE_SOLVED, self, button)
        self.GetEventHandler().ProcessEvent(evt)

    def ShowDigit(self, digit):
        """ 
        Set the background image for this tile to the given digit 
        """

        if digit == '':
            self.bitmap.Hide()
        else:
            self.bitmap.Bitmap = BITMAPS[digit]
            self.bitmap.Centre()
            self.bitmap.Show()

# Main ###############################################################

if __name__ == '__main__':

    app = App()

    # Load the digit background images into bitmaps

    for i in ('123456789'):
        BITMAPS[i] = wx.Image(IMAGES + i + '.jpg', wx.BITMAP_TYPE_ANY).ConvertToBitmap()

    app.MainLoop()

Sudoku.jpg

How to Add a Click-to-Call Button in WordPress (Step by Step)

Do you want to add a click-to-call button in WordPress? As more people visit your website using their smart phones, adding a tap-to-call button makes it easy for them to contact you. In this article, we will show you how to easily add a click-to-call button in your WordPress website.

Adding a click to call button in WordPress

Why Add Click-to-Call Button in WordPress

Many small business websites rely on customers to contact them for quotes, pre-sale information, or support.

That’s why we recommend adding a contact form to all website owners.

However, depending on the industry some users may want to speak with a person right away.

As mobile web traffic increases around the world, often users find it more convenient to just tap-to-call and get the information they need.

If you’re just starting a business, then we recommend looking into a professional business phone service, so you can do call forwarding, share numbers with employees, and answer calls using your cell phones from anywhere (instead of a landline).

That being said, let’s take a look at how to easily add a click-to-call button in WordPress.

Method 1. Adding a Click-to-Call Now Button in WordPress Using Plugin

This method is easier and recommended for all users. It allows you to easily create a click to call button in Gutenberg, Classic editor, WordPress sidebar widget, and even a sticky floating call button.

First thing you need to do is install and activate the WP Call Button plugin on your site. For more details, see our step by step guide on how to install a WordPress plugin.

Upon activation, you need to visit Settings » WP Call Button page to configure plugin settings.

WordPress Call Button

The first settings page is for the most commonly requested feature, Sticky Call Button, which scrolls with the user as they browse your website.

This highly conversion optimized feature helps you get more calls and close more deals faster.

You need to start by entering your business phone number that you want people to call. WP Call Button plugin uses the smart phone field which means you can simply select your country, and it will automatically add the country code in the proper format.

Note: If you don’t have a business phone number, then you can get one from Nextiva. This is the company we use for our business VoIP phone.

Next, you have the option to add the call button text, select the positioning, and choose the button color.

By default the plugin will add a click to call button all pages and all devices. However you do have the option to show it only on certain pages or show only mobile devices.

Once you’re done, click on the activate toggle at the top and then click on the save changes button to store your settings.

After that, you can visit your website to see the call button in action.

WordPress Click to Call Button

If you want to add a static call button in your WordPress contact page, about page, or any other post / page, then you have two options.

If you’re using the new WordPress block editor, then simply create or edit an existing page. Next, you need to add the WP Call Button block by clicking the “+” icon.

Call Button Gutenberg

WordPress Call Button block comes many customization options such as the ability to customize button color, button text, font size, and the ability to show or hide the phone icon.

Once you add the button, it will allow users to click and call your phone number that you specified in the WP Call Button settings page.

Now, if you’re using the WordPress classic editor, then you need to use the Static Call Button generator to get a shortcode.

Simply go to Settings » WP Call Button and click on the Static Call Button menu on the top.

WordPress Call Button Shortcode

Next, you can customize the call button text, color, and choose whether you want to show or hide the phone icon. After that simply copy the shortcode below and paste it inside the post or page where you want to add it.

WP Call Button plugin also allows you to add the call button in your website sidebar using the sidebar widget.

To do this, you need to go Appearance » Widgets and drag the WP Call Button widget to your widget-ready area.

WordPress Call Button Sidebar Widget

This widget lets you add the Widget title and description. You can click on the Advanced Settings link to customize call button text and call button text.

After reading this, you can probably see why WP Call button is by far the easiest and most comprehensive WordPress click to call button plugin the market.

The best part is that it’s 100% free, and it has a seamless integration with Google Analytics.

All you need to do is use the MonsterInsights plugin, and it will automatically track how many people click on your call button and which pages are getting you the most leads, so you can focus on what’s working to grow your business, faster!

Method 2. Manually Add Click to Call Button in WordPress

This method allows you to manually add click to call buttons and links anywhere on your WordPress website.

The easiest way to add a clickable phone number to your website is by adding them like this:

<!--This link will work on Android and iPhone. It will be visible to desktop users but will result in an error when clicked -->

<a href="tel:+15555551212">+1 (555) 555-1212</a> 
 
<a href="tel:15555551212">Call Me</a>

These links will open the phone app on mobile devices. On desktop computers, Mac users will see an option to open the link in Facetime, whereas Windows 10 users will see the option to open it in Skype.

You can also use HTML to add an image or an icon next to the click-to-call text link.

Let’s take a look at how to do this.

First, you need to visit the Media » Add New page and upload the image you want to use. After uploading the image, you need to click on the ‘Edit’ link to proceed.

Upload phone icon

This will bring you to edit media screen where you need to copy the file URL. You will need it in the next step.

You can now edit the post, page, or custom HTML widget where you want to display the click-to-call phone icon. In the edit area, you can now add the phone icon image wrapped inside the tel: link.

<a href="tel:+15555551212"><img src="https://example.com/wp-content/uploads/2018/10/phoneicon2.png" alt="Call us" /></a> 

Once done, you can visit your website to see the click-to-call link with the image you uploaded.

Click-to-call button with image icon

You can further customize this by adding more text before or after the link, and use custom CSS to style it.

Click to call button with more text and CSS style

Adding a Click-to-Call Link to WordPress Navigation Menus

You can also add the click-to-call link to your WordPress navigation menu.

Simply go to Appearance » Menus page and click on the custom links tab from the right column. Now you can add the phone number in the link field in the following format:

tel:+155555555

After that, add the text you want to display and then click on ‘Add to menu’ button.

Add click-to-call link to navigation menu

Next, visit your website to see your click to call link in action.

Call link in navigation

Want to add an image icon next to your click-to-call link in the navigation menu? See our article on how to add image icons with navigation menus in WordPress.

We hope this article helped you learn how to add a click-to-call button in WordPress. You may also want to see our guide on how to create a free business email address.

If you liked this article, then please subscribe to our YouTube Channel for WordPress video tutorials. You can also find us on Twitter and Facebook.

The post How to Add a Click-to-Call Button in WordPress (Step by Step) appeared first on WPBeginner.