You don't have to rely solely on the signals that are provided by Qt widgets, however; you can create your own. Signals are created using the Signal class. A simple signal definition would be:
[python]
from PySide.QtCore import Signal
tapped = Signal()
[/python]
[python]
from PyQt4.QtCore import pyqtSignal
tapped = pyqtSignal()
[/python]
Then, when the conditions for the object being tapped are satisfied, you call the signal's emit
method, and the signal is emitted, calling any slots to which it is connected:
[python]
thing.tapped.emit()
[/python]
This is good for two reasons; first, it allows users of your objects to interact with them in familiar ways; and second, it allows your objects to be used more flexibly, leaving the definition effects of actions on your object to the code that uses them.
A Simple PySide/PyQt Signal Emitting Example
Let's define a simple PunchingBag
class that does only one thing: when its punch
is called, it emits a punched
signal:
[python]
from PySide.QtCore import QObject, Signal, Slot
class PunchingBag(QObject):
''' Represents a punching bag; when you punch it, it
emits a signal that indicates that it was punched. '''
punched = Signal()
def __init__(self):
# Initialize the PunchingBag as a QObject
QObject.__init__(self)
def punch(self):
''' Punch the bag '''
self.punched.emit()
[/python]
[python]
from PyQt4.QtCore import QObject, pyqtSignal, pyqtSlot
class PunchingBag(QObject):
''' Represents a punching bag; when you punch it, it
emits a signal that indicates that it was punched. '''
punched = pyqtSignal()
def __init__(self):
# Initialize the PunchingBag as a QObject
QObject.__init__(self)
def punch(self):
''' Punch the bag '''
self.punched.emit()
[/python]
You can easily see what we've done. The PunchingBag
inherits from QObject
so it can emit signals; it has a signal called punched
, which carries no data; and it has a punch
method which does nothing but emit the punched
signal.
To make our PunchingBag
useful, we need to connect its punched
signal to a slot that does something. We'll define a simple one that prints, "Bag was punched" to the console, instantiate our PunchingBag
, and connect its punched
signal to the slot:
[python]
@Slot()
def say_punched():
''' Give evidence that a bag was punched. '''
print('Bag was punched.')
bag = PunchingBag()
# Connect the bag's punched signal to the say_punched slot
bag.punched.connect(say_punched)
[/python]
[python]
@pyqtSlot()
def say_punched():
''' Give evidence that a bag was punched. '''
print('Bag was punched.')
bag = PunchingBag()
# Connect the bag's punched signal to the say_punched slot
bag.punched.connect(say_punched)
[/python]
Then, we'll punch the bag and see what happens:
[python]
# Punch the bag 10 times
for i in range(10):
bag.punch()
[/python]
When you put it all in a script and run it, it will print:
[shell]
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
Bag was punched.
[/shell]
Effective, but not particularly impressive. However, you can see the usefulness of it: our punching bag would be a good fit anywhere you need a bag that reacts to punching, because the PunchingBag
leaves implementation of a reaction to punching to the code that uses it.
Data-Carrying PySide/PyQt Signals
One of the most interesting things you can do when creating signals is to make them carry data. For example, you could make a signal carry an integer, thus:
[python]
updated = Signal(int)
[/python]
[python]
updated = pyqtSignal(int)
[/python]
or a string:
[python]
updated = Signal(str)
[/python]
[python]
updated = pyqtSignal(str)
[/python]
The datatype may be any Python type name or a string identifying a C++ datatype. Since this tutorial presupposes no C++ knowledge, we'll stick to Python types.
A PySide/PyQt Signal-Sending Circle
Let's define a Circle with properties x
, y
, and r
, denoting the x
and y
position of the center of the circle, and its radius, respectively. You might want to have one signal that is emitted when the circle is resized, and another that is emitted when it is moved; we'll call them resized
and moved
, respectively.
It would be possible to have the slots to which the resized
and moved
signals are connected check the new position or size of the circle and respond accordingly, but it's more convenient and requires less knowledge of circles by the slot functions if the signal that is sent can include that information.
[python]
from PySide.QtCore import QObject, Signal, Slot
class Circle(QObject):
''' Represents a circle defined by the x and y
coordinates of its center and its radius r. '''
# Signal emitted when the circle is resized,
# carrying its integer radius
resized = Signal(int)
# Signal emitted when the circle is moved, carrying
# the x and y coordinates of its center.
moved = Signal(int, int)
def __init__(self, x, y, r):
# Initialize the Circle as a QObject so it can emit signals
QObject.__init__(self)
# "Hide" the values and expose them via properties
self._x = x
self._y = y
self._r = r
@property
def x(self):
return self._x
@x.setter
def x(self, new_x):
self._x = new_x
# After the center is moved, emit the
# moved signal with the new coordinates
self.moved.emit(new_x, self.y)
@property
def y(self):
return self._y
@y.setter
def y(self, new_y):
self._y = new_y
# After the center is moved, emit the moved
# signal with the new coordinates
self.moved.emit(self.x, new_y)
@property
def r(self):
return self._r
@r.setter
def r(self, new_r):
self._r = new_r
# After the radius is changed, emit the
# resized signal with the new radius
self.resized.emit(new_r)
[/python]
[python]
from PyQt4.QtCore import QObject, pyqtSignal, pyqtSlot
class Circle(QObject):
''' Represents a circle defined by the x and y
coordinates of its center and its radius r. '''
# Signal emitted when the circle is resized,
# carrying its integer radius
resized = pyqtSignal(int)
# Signal emitted when the circle is moved, carrying
# the x and y coordinates of its center.
moved = pyqtSignal(int, int)
def __init__(self, x, y, r):
# Initialize the Circle as a QObject so it can emit signals
QObject.__init__(self)
# "Hide" the values and expose them via properties
self._x = x
self._y = y
self._r = r
@property
def x(self):
return self._x
@x.setter
def x(self, new_x):
self._x = new_x
# After the center is moved, emit the
# moved signal with the new coordinates
self.moved.emit(new_x, self.y)
@property
def y(self):
return self._y
@y.setter
def y(self, new_y):
self._y = new_y
# After the center is moved, emit the moved
# signal with the new coordinates
self.moved.emit(self.x, new_y)
@property
def r(self):
return self._r
@r.setter
def r(self, new_r):
self._r = new_r
# After the radius is changed, emit the
# resized signal with the new radius
self.resized.emit(new_r)
[/python]
Note these salient points:
- The
Circle
inherits fromQObject
so it can emit signals. - The signals are created with the signature of the slot to which they will be connected.
- The same signal can be emitted in multiple places.
Now, let's define some slots that can be connected to the Circle's signals. Remember last time, when we said we'd see more about the @Slot
decorator? We now have signals that carry data, so we'll see how to make slots that can receive it. To make a slot accept data from a signal, we simply define it with the same signature as its signal:
[python]
# A slot for the "moved" signal, accepting the x and y coordinates
@Slot(int, int)
def on_moved(x, y):
print('Circle was moved to (%s, %s).' % (x, y))
# A slot for the "resized" signal, accepting the radius
@Slot(int)
def on_resized(r):
print('Circle was resized to radius %s.' % r)
[/python]
[python]
# A slot for the "moved" signal, accepting the x and y coordinates
@pyqtSlot(int, int)
def on_moved(x, y):
print('Circle was moved to (%s, %s).' % (x, y))
# A slot for the "resized" signal, accepting the radius
@pyqtSlot(int)
def on_resized(r):
print('Circle was resized to radius %s.' % r)
[/python]
Very simple and intuitive. For more information on Python decorators, you might want to checkout the article - Python Decorators Overview to familiarise yourself.
Finally, let's instantiate a Circle, hook up the signals to the slots, and move and resize it:
[python]
c = Circle(5, 5, 4)
# Connect the Circle's signals to our simple slots
c.moved.connect(on_moved)
c.resized.connect(on_resized)
# Move the circle one unit to the right
c.x += 1
# Increase the circle's radius by one unit
c.r += 1
[/python]
When you run the resulting script, your output should be:
[shell]
Circle was moved to (6, 5).
Circle was resized to radius 5.
[/shell]
Now that we've developed a better understanding of signals and slots, we are ready to use some more advanced widgets. In our next instalment, we will begin to discuss the QListWidget
and QListView
, two ways of creating list box controls.