Gui Programming: Our Goal: The Triangle Peg Game!
Gui Programming: Our Goal: The Triangle Peg Game!
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu('File')
file_menu.addAction(exit_action)
self.show()
LAYOUT MANAGEMENT
Before we can add more components to our application, we need to talk about layout
management in PyQt4.
Stretch factors indicate the relative amount of leftover space that should be allocated to a
block.
GRID LAYOUT
The Grid Layout class is the most universal, but we will use both Grid and Box
layouts.
A grid is represented with multiple rows and columns. Widgets can be attached to the
grid by indicating the (row, column) space it should fill.
• Create a grid layout with QtWidgets.QGridLayout().
• Attach widgets to the grid with addWidget(QWidget, row, column).
You can also set the number of grid spaces that it should take up.
BASIC PYQT class PegGameWindow(QtWidgets.QMainWindow):
def __init__(self):
...
def setup(self):
Recall that QMainWindow ...
includes a default layout for self.central_widget = QtWidgets.QWidget(self)
traditional GUI components like a self.new_button =
menu bar, status bar, tool bar, etc. StartNewGameBtn(self.central_widget)
The focal point of the application is self.quit_button = QuitBtn(self.central_widget)
stored in the self.setCentralWidget(self.central_widget)
Central Widget of the layout.
exit_action = QtWidgets.QAction('Exit', self)
exit_action.triggered.connect(QtWidgets.qApp.quit)
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu('File')
file_menu.addAction(exit_action)
self.show()
BASIC PYQT
class PegGameWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setup()
def setup(self):
self.setWindowTitle('Triangle Peg Game')
self.setToolTip("Play the triangle peg game!")
self.peg_game = PegGame(self)
self.setCentralWidget(self.peg_game)
...
Let’s really define what this central widget self.show()
should look like. We’ll create a new class
called PegGame.
GRID LAYOUT
So what do we want PegGame
to look like?
PegGame
GRID LAYOUT
PegGame will contain three important
components (at least, to start):
- PegBoard, which will house the board,
pegs, etc.
- Start New Game Button
- Quit Button
PegGame
PegBoard
GRID LAYOUT
So how do we define PegGame? class PegGame(QtWidgets.QWidget):
def __init__(self, parent):
PegGameWindow has a built-in QtWidgets.QWidget.__init__(self, parent)
layout because it inherits from self.setup()
QMainWindow.
def setup(self):
self.board = PegBoard(self)
Our three components self.new_btn = StartNewGameBtn(self)
self.quit_btn = QuitBtn(self)
self.grid = QtWidgets.QGridLayout()
PegGame, however, is a plain widget. self.setLayout(self.grid)
So we can associate a Grid layout with it. self.grid.addWidget(self.board, 1, 1, 1, 4)
self.grid.addWidget(self.new_btn, 2, 1, 1, 1)
self.grid.addWidget(self.quit_btn, 2, 2, 1, 1)
GRID LAYOUT
So how do we define PegGame? class PegGame(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setup()
def setup(self):
self.board = PegBoard(self)
self.new_btn = StartNewGameBtn(self)
self.quit_btn = QuitBtn(self)
Creating and setting our self.grid = QtWidgets.QGridLayout()
grid layout self.setLayout(self.grid)
self.grid.addWidget(self.board, 1, 1, 1, 4)
self.grid.addWidget(self.new_btn, 2, 1, 1, 1)
self.grid.addWidget(self.quit_btn, 2, 2, 1, 1)
GRID LAYOUT
So how do we define PegGame? class PegGame(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setup()
def setup(self):
self.board = PegBoard(self)
self.new_btn = StartNewGameBtn(self)
self.quit_btn = QuitBtn(self)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
self.grid.addWidget(self.board, 1, 1, 1, 4)
Adding our components
self.grid.addWidget(self.new_btn, 2, 1, 1, 1)
to the grid layout.
self.grid.addWidget(self.quit_btn, 2, 2, 1, 1)
row, column
GRID LAYOUT
So how do we define PegGame? class PegGame(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setup()
def setup(self):
self.board = PegBoard(self)
self.new_btn = StartNewGameBtn(self)
self.quit_btn = QuitBtn(self)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
self.grid.addWidget(self.board, 1, 1, 1, 4)
Adding our components
self.grid.addWidget(self.new_btn, 2, 1, 1, 1)
to the grid layout.
self.grid.addWidget(self.quit_btn, 2, 2, 1, 1)
height, width
GRID LAYOUT
class PegGame(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setup()
def setup(self):
self.board = PegBoard(self)
self.new_btn = StartNewGameBtn(self)
self.quit_btn = QuitBtn(self)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
self.grid.addWidget(self.board, 1, 1, 1, 4)
self.grid.addWidget(self.new_btn, 2, 1, 1, 1)
self.grid.addWidget(self.quit_btn, 2, 2, 1, 1)
PEGBOARD
So, what does the PegBoard look like so far?
class PegBoard(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setFixedSize(700, 460)
The parent we passed in was the PegGame instance that holds the PegBoard.
The most basic usage involves creating a QPainter instance, calling the begin method
with the QPaintDevice to which we will be painting, and then calling the end
method.
QPAINTER qp = QtGui.QPainter()
qp.begin(canvas)
...
So what can we do in those ellipses? qp.end()
• Manipulate settings
• font(), brush(), pen() give you access to the tools used to draw. Return QFont, QBrush,
and QPen objects.
• Also setFont(font), setBrush(brush), setPen(pen).
• Draw
• drawEllipse(), drawPolygon(), drawImage(), drawLine(), drawPath(),
drawPicture(), drawPie(), etc…
• If you can dream it, you can draw it.
QPAINTER
class PegBoard(QtWidgets.QWidget):
def __init__(self, parent):
First thing to notice is the method QtWidgets.QWidget.__init__(self, parent)
name we’re using: paintEvent. self.setFixedSize(700, 460)
The QPalette class contains color groups for each widget state. It describes how
the widget should render itself on the screen for each state.
A palette consists of three color groups: Active (keyboard focus), Disabled (not in
focus), and Inactive (disabled). All widgets in Qt contain a palette and use their
palette to draw themselves.
QWidget’s palette() method returns the currently used QPalette object and
setPalette(palette) allows you to reassign the QPalette object being
used.
QPALETTE
For each state of a widget, there are many roles of which we need to describe the look
and feel. Each role has an assigned color and brush.
• Window
• Background
• WindowText
• Foreground
• Button
• Etc.
QPALETTE
Colors and brushes can be set for particular roles in any of a palette's color groups
with setColor() for color and setBrush()for color, style, and texture.
Start by creating a
vertical box layout
on the PegBoard.
PEG HOLES
Now, add 5
horizontal box
layouts to the
vertical box
layout.
PEG HOLES
In each horizontal
box layout, add:
- a very stretchy
spacer.
- the PegHole
objects.
- another very
stretchy spacer.
PEG HOLES
class PegBoard(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setFixedSize(700, 460)
p = self.palette()
p.setColor(self.backgroundRole(), QtGui.QColor(0, 0, 0, 255))
self.setPalette(p)
self.setAutoFillBackground(True)
self.vbox = QtWidgets.QVBoxLayout() Create a vertical box layout for the PegBoard
self.setLayout(self.vbox) instance. The setSpacing method allows us to
self.vbox.setSpacing(0) manipulate the margins between widgets in the
self.place_holes() layout.
PEG HOLES
def place_holes(self):
for row in range(0,5):
rowLayout = QtWidgets.QHBoxLayout()
self.vbox.addLayout(rowLayout)
rowLayout.addStretch(1)
for col in range(0,row+1):
hole = PegHole(self)
rowLayout.addWidget(hole, 0)
rowLayout.addStretch(1)
PEGS
class PegHole(QtWidgets.QWidget):
So, the next step is to create a def __init__(self, parent):
Peg object. QtWidgets.QWidget.__init__(self, parent)
self.grid = QtWidgets.QGridLayout()
Keep in mind that PegHole self.setLayout(self.grid)
objects are designed to house self.peg = None
Peg objects and control their
manipulation on the board.
Radius = 10
PEGS
90
class Peg(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent) 120
self.resize(parent.size())
class PegHole(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
self.peg = None
...
def addPeg(self):
self.peg = Peg(self)
self.grid.addWidget(self.peg)
PEGS
And finally, we extend our PegBoard.place_holes method to associate Pegs with
PegHoles.
def place_holes(self):
for row in range(0,5):
rowLayout = QtWidgets.QHBoxLayout()
self.vbox.addLayout(rowLayout)
rowLayout.addStretch(1)
for col in range(0, row+1):
hole = PegHole(self)
rowLayout.addWidget(hole, 0)
if (row, col) != (2,1):
hole.addPeg()
rowLayout.addStretch(1)
PEGS
PEG MOVEMENT
Now, the hard part. How do we describe the mechanics of moving the pegs? How
does a user interact with the game board?
• mouseReleaseEvent(self, event)
• Called when the mouse is released.
• mouseMoveEvent(self, event)
• Called when the mouse is pressed down and moved.
DRAG SOURCE
class PegHole(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
self.peg = None
drag = QtGui.QDrag(self)
drag.setMimeData(QtCore.QMimeData())
dropAction = drag.exec_(QtCore.Qt.MoveAction)
DRAG SOURCE
class PegHole(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
self.peg = None
Well, not much. We haven’t defined what it looks like to drag the Peg and we haven’t
defined any drop targets. The only thing we can see for sure is that a drag event starts when
the PegHole has an associated Peg, otherwise nothing happens.
DROP TARGET
Now, let’s define a drop target. Since a PegHole is conceptually both the source of the
Peg as well as the eventual destination of the Peg, we will also use a PegHole as our
drop target. Drop targets have the following properties:
• Sets self.setAcceptDrops(True).
• Implements the dragEnterEvent(self, event) method.
• Called when a drag enters the target’s area. Typically defined to either accept or reject the drag.
return
Hide the peg temporarily
self.peg.hide()
drag = QtGui.QDrag(self)
drag.setMimeData(QtCore.QMimeData())
dropAction = drag.exec_(QtCore.Qt.MoveAction)
When “drop” happens, check
whether drop was accepted or if dropAction:
not. If so, delete Peg. Otherwise, del(self.peg)
show the Peg again. self.peg = None
else:
self.peg.show()
DRAG AND DROP
Ok, once again!
DRAG AND DROP def mousePressEvent(self, event):
if not self.peg:
QtWidgets.QWidget.mousePressEvent(self, event)
But wouldn’t it be so nice to
see the Peg move? return
self.peg.hide()
drag = QtGui.QDrag(self)
We’ll create an icon of the drag.setMimeData(QtCore.QMimeData())
Peg and associate it with drag.setPixmap(self.peg_icon)
the QDrag object. drag.setHotSpot(self.peg_icon.rect().topLeft())
dropAction = drag.exec_(QtCore.Qt.MoveAction)
if dropAction:
del(self.peg)
self.peg = None
else:
self.peg.show()
DRAG AND DROP class PegHole(QtWidgets.QWidget):
def __init__(self, parent):
QtWidgets.QWidget.__init__(self, parent)
self.setAcceptDrops(True)
self.grid = QtWidgets.QGridLayout()
self.setLayout(self.grid)
Now we have an icon that represents what self.peg = None
a Peg looks like visually. self.create_icon()
brush.setColor(QtCore.Qt.red)
qp.setBrush(brush)
qp.drawEllipse(0, 0, 20, 20)
qp.end()
DRAG AND DROP
ENFORCING RULES
So, we have the mechanics of movement. Now, all we need to do is enforce some
rules. This comes down to two checks:
• Make sure destination is valid.
• Make sure “hopped” hole contains a Peg to be removed.
We’re going to use the MIME mechanism to send information about our drag source
to our drop target – this will help us decide whether to accept or not.
CHECKING DESTINATION
class PegBoard(QtWidgets.QWidget):
def __init__(self, parent):
We first extend the PegHole class QtWidgets.QWidget.__init__(self, parent)
definition to accept its position as an self.holes = []
argument. We pass in the row and column ...
where it is placed on the board. self.place_holes()
self.peg_count = len(self.holes)- 1
Furthermore, the PegBoard now maintains
a list of its child widgets and an associated def place_holes(self):
counter for row in range(0,5):
row_list = []
...
for col in range(0,row+1):
hole = PegHole(self, row, col)
...
rowLayout.addStretch(1)
self.holes.append(row_list[:])
CHECKING DESTINATION
def mousePressEvent(self, event):
if not self.peg:
QtWidgets.QWidget.mousePressEvent(self, event)
return
self.peg_icon = self.create_icon()
self.peg.hide()
The QMimeData object associated drag = QtGui.QDrag(self)
with the QDrag object will now data = QtCore.QMimeData()
house some information – a pickled data.setText(pickle.dumps((self.row, self.col)))
string holding the source’s coordinates. drag.setMimeData(data)
drag.setPixmap(self.peg_icon)
drag.setHotSpot(self.peg_icon.rect().topLeft())
dropAction = drag.exec_(QtCore.Qt.MoveAction)
...
CHECKING DESTINATION
if(self.parent.holes[hopped[0]][hopped[1]].peg):
self.parent.holes[hopped[0]][hopped[1]].deletePeg()
self.parent.peg_count -= 1
return True
else:
return False
ENFORCING RULES