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.
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.
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)
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)
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)
_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.
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. """ ...
class TicTacToe (Table): """The famous game as widget. """ def __init__ (self): ... self._signals[SIG_TICTACTOE] = []
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)
class TicTacToe (Table): """The famous game as widget. """ def __init__ (self): ... self._finished = False
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