Creating custom widgets

Sometimes it is necessary to create an own, specialized widget type, that matches your exact needs. The following section will give you an rough overview about what you have to keep in mind and about what you have to take care, when creating custom widgets. You are encouraged to browse the existing widget sourcecode in order to see, how the one or other is implemented.

In this section, you will create an own widget, that lets the user play the famous game Tic Tac Toe and is able to deal with a custom SIG_TICTACTOE event.

Designing the basis

First of all you should make yourself a picture about how the widget should look like and what it should be able to do. In our case, this would be an area consisting of 3x3 squares, of which each is clickable exactly one time and should fill - dependant on whose turn it is - the square with either a cross or a circle. When the game is ended, the widget should note that by sending a corresponding event, so that other widgets can update themselves according to that.

After you know about how it should look like and what it should be able to do, its time to collect the things you need. Whenever it is possible, you should use existing code or classes, so that you do not need to reinvent anything.

Luckily our example can be completed by using a Table widget with 3 rows and 3 columns and nine ImageButton widgets, that can display the circle or cross. The only things, that do not exist are the signal and graphics for a circle and cross.

Implementing the widget basics

We start by creating a custom class, that inherits directly from the Table.

from ocempgui.draw import Image
from ocempgui.widgets import Table
from ocempgui.widgets.Constants import *

class TicTacToe (Table):
    """The famous game as widget.
    """
    def __init__ (self):
        Table.__init__ (self, 3, 3)
        
Now we have to place nine blank ImageButton widgets into the table. We will size them to 60x60 px, so that they will not resize later, when we set their images, which have a size of 48x48 px.
class TicTacToe (Table):
    ...
    def __init__ (self):
        Table.__init__ (self, 3, 3)
        for i in xrange (3):
            for j in xrange (3):
                button = ImageButton ("")
                button.minsize = 60, 60
                self.add_child (i, j, button)
        
That was not too complicated, but the buttons also need to react upon clicks and they have to distinct between two players.
class TicTacToe (Table):
    ...
    def __init__ (self):
        Table.__init__ (self, 3, 3)
        self._curplayer = "Player 1"

        for i in xrange (3):
            for j in xrange (3):
                button = ImageButton ("")
                button.minsize = 60, 60
                self.add_child (i, j, button)
                button.connect_signal (SIG_CLICKED, self._clicked, button,
                                       i, j)
        
This will cause them to raise the _clicked() method, which we now implement:
class TicTacToe (Table):
    ...
    def __init__ (self):
        ...

    def _clicked (self, button, i, j):
        """Sets the image of the button, if not already done.
        """
        button = self.grid[(i, j)]
        # Check, if it is not already set.
        if button.picture == None:
            if self._curplayer == "Player 1":
                # Use the cross.
                button.picture = "cross.png"
                self._curplayer = "Player 2"
            else:
                button.picture = "circle.png"
                self._curplayer = "Player 1"
        

The widget is fairly usable now, but still lacks the event mechanism, which will be explained in the next section.

Implementing the event handlers

To allow a better interactivity with the TicTacToe widget, it will invoke its event handlers, whenever a player clicked on a square. Dependant on what happens, the event mechanism should indicate this. Thus it will have to pass additional data to the event handlers. We want it to send the following data:

  • TICTACTOE_VALIDSQUARE, if the player clicked on an unoccupied button,

  • TICTACTOE_INVALIDSQUARE, if the player clicked on an occupied button,

  • TICTACTOE_WIN, if the one of the players wins. Further input should not be allowed afterwards.

Implementing the first and second event is easy and we only have to add four lines of code to our existing class. The first line will define the SIG_TICTACTOE signal, the next three lines our signal data we want to send.

from ocempgui.widgets import Table
from ocempgui.widgets.Constants import *

SIG_TICTACTOE = "tictactoe"
TICTACTOE_WIN = 1
TICTACTOE_VALIDSQUARE = 0
TICTACTOE_INVALIDSQUARE = -1

class TicTacToe (Table):
    """The famous game as widget.
    """
    ...
        
The second line prepares the TicTacToe widget to connect callbacks to the SIG_TICTACTOE signal.
class TicTacToe (Table):
    """The famous game as widget.
    """
    def __init__ (self):
        ...
        self._signals[SIG_TICTACTOE] = []
        
The next two line will cause the widget to run the appropriate signal handlers upon clicks on the buttons.
class TicTacToe (Table):
    ...
    def _clicked (self, button, i, j):
        """Sets the image of the button, if not already done.
        """
        button = self.grid[(i, j)]
        # Check, if it is not already set.
        if button.picture == None:
            if self._curplayer == "Player 1":
                button.picture = "cross.png"
                self._curplayer = "Player 2"
            else:
                button.picture = "circle.png"
                self._curplayer = "Player 1"
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_VALIDSQUARE)

        else:
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_INVALIDSQUARE)
        

The third event handler requires a bit more work as we have to check, if there are three buttons displaying the same picture in a row. The most simple (and unoptimized) algorithm for that would be:

  • Check all columns for the first, second and third line.

  • Check all lines for the first, second and third column.

  • Check diagonal the squares located at (0, 0), (1, 1) and (2, 2).

  • Check diagonal the squares located at (0, 2), (1, 1) and (2, 0).

class TicTacToe (Table):
    ...
    def _check_input (self):
        """Checks for three in a row.
        """
        three = False
        image = None

        # Check the columns
        for i in xrange (3):
            if three:
                break
            image = self.grid[(i, 0)].path
            if image:
                three = (self.grid[(i, 1)].path == image) and \
                        (self.grid[(i, 2)].path == image)

        if not three:
            # Check the rows.
            for i in xrange (3):
                if three:
                    break
                image = self.grid[(0, i)].path
                if image:
                    three = (self.grid[(1, i)].path == image) and \
                            (self.grid[(2, i)].path == image)

        if not three:
            # Diagonal left to right
            image = self.grid[(0, 0)].path
            if image:
                three = (self.grid[(1, 1)].path == image) and \
                        (self.grid[(2, 2)].path == image)
            if not three:
                # Diagonal right to left
                image = self.grid[(2, 0)].path
                if image:
                    three = (self.grid[(1, 1)].path == image) and \
                            (self.grid[(0, 2)].path == image)

        if three:
            self._finished = True
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_WIN)
        
The check has to be performed right after a valid input. The _finished attribute will be used to check, if input is still possible or not. We have to place it in the constructor code
class TicTacToe (Table):
    """The famous game as widget.
    """
    def __init__ (self):
        ...
        self._finished = False
        
as well as to check its value in the button callback, in which we have to add the input check, too.
    def _clicked (self, button, i, j):
        """Sets the image of the button, if not already done.
        """
        if self._finished:
            return
        
        button = self.grid[(i, j)]
        # Check, if it is not already set.
        if button.picture == None:
            if self._curplayer == "Player 1":
                # Use the cross.
                button.picture = "cross.png"
            else:
                button.picture = "circle.png"
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_VALIDSQUARE)
            self._check_input ()

            # Set it after the check, so we can get the correct player name.
            if self._curplayer == "Player 1":
                self._curplayer = "Player 2"
            else:
                self._curplayer = "Player 1"
        else:
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_INVALIDSQUARE)
        

As you see, switching the current player is done right after running signal handler and input check now, so that clicks onto an already occupied buttons do not cause a player change.

So far we are done. You can play a basic Tic Tac Toe game with your newly created widget and the widget can react upon the user input. It is however neither very usable nor nice to look at. All those things will be explained in the next section, but first let us look at the complete code.

Note that an additional curplayer attribute is at the bottom of the code, so that it is possible to find out, who's turn it is.

You can find the code as a python script under examples/tictactoe/TicTacToeSimple.py. There is also a small starting test script called tictactosimple.py as well as the both needed graphics.

from ocempgui.widgets import *
from ocempgui.widgets.Constants import *

SIG_TICTACTOE = "tictactoe"
TICTACTOE_WIN = 1
TICTACTOE_VALIDSQUARE = 0
TICTACTOE_INVALIDSQUARE = -1

class TicTacToe (Table):
    """The famous game as widget.
    """
    def __init__ (self):
        Table.__init__ (self, 3, 3)
        self._curplayer = "Player 1"
        self._finished = False
        
        for i in xrange (3):
            for j in xrange (3):
                button = ImageButton ("")
                button.minsize = 60, 60
                self.add_child (i, j, button)
                button.connect_signal (SIG_CLICKED, self._clicked, button,
                                       i, j)
        self._signals[SIG_TICTACTOE] = []

    def _clicked (self, button, i, j):
        """Sets the image of the button, if not already done.
        """
        if self._finished:
            return
        
        button = self.grid[(i, j)]
        # Check, if it is not already set.
        if button.picture == None:
            if self._curplayer == "Player 1":
                # Use the cross.
                button.picture = "cross.png"
            else:
                button.picture = "circle.png"
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_VALIDSQUARE)
            self._check_input ()

            # Set it after the check, so we can get the correct player name.
            if self._curplayer == "Player 1":
                self._curplayer = "Player 2"
            else:
                self._curplayer = "Player 1"
        else:
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_INVALIDSQUARE)

    def _check_input (self):
        """Checks for three in a row.
        """
        three = False
        image = None

        # Check the columns
        for i in xrange (3):
            if three:
                break
            image = self.grid[(i, 0)].path
            if image:
                three = (self.grid[(i, 1)].path == image) and \
                        (self.grid[(i, 2)].path == image)

        if not three:
            # Check the rows.
            for i in xrange (3):
                if three:
                    break
                image = self.grid[(0, i)].path
                if image:
                    three = (self.grid[(1, i)].path == image) and \
                            (self.grid[(2, i)].path == image)

        if not three:
            # Diagonal left to right
            image = self.grid[(0, 0)].path
            if image:
                three = (self.grid[(1, 1)].path == image) and \
                        (self.grid[(2, 2)].path == image)
            if not three:
                # Diagonal right to left
                image = self.grid[(2, 0)].path
                if image:
                    three = (self.grid[(1, 1)].path == image) and \
                            (self.grid[(0, 2)].path == image)

        if three:
            self._finished = True
            self.run_signal_handlers (SIG_TICTACTOE, TICTACTOE_WIN)

    curplayer = property (lambda self: self._curplayer)

Example 48. Simple TicTacToe widget