Disconnecting
What can be bound, can be severed, and even
for signals and slots there are divorce courts. You can
disconnect a signal from a slot using
QObject.disconnect().
Why would you want to disconnect signals? Not preparatory to
removing a connected widget, for the connections are severed
automatically when the signal recipient is deleted. I've never
needed disconnect() myself, but with a bit
of imagination, a likely scenario can be found.
Imagine therefore that you are writing a monitoring
application. There are several data sources, but you want only
to look at one at a time. The data keeps flowing in from a host
of objects representing the sources. This is a scenario well
worth writing a small test for...
First, we design the interface using
BlackAdder's designer module or Qt Designer. This is a simple
affair, with a combobox that contains the datasources, a
read-only multi-line edit control that will show the output of
the selected datasource, and a close button. The dialog window
will be the main window, too.
Then, we use Designer to add an extra slot to the form,
switchDataSource, which will be called
whenever a new item is selected in the datasource combobox.
Drawing a simple line from the combobox to the form gives us the
opportunity to connect signal and slot:
This raises an interesting point. If the
activated(const QString&) signal passes a
QString to the slot, shouldn't we define
the slot switchDataSource() in the Designer
as having an argument?
The answer is no— we will subclass the generated
python code, and in the subclass we will override the generated
slot with a function that has the requisite number of
arguments. Python does not know the concept of overloading, so
all functions with the same name are the same function. It is
actually impossible to define the number of arguments a slot has
in the Designer— you can only match signals to slots without
arguments.
Having designed the form, we can generate it with a single
menu-choice and start subclassing it, adding all kinds of
interesting bits. First, we create the actual
datasources.
Example 7-8. datasource.py — connecting and disconnecting signals and
slots
#
# datasource.py — a monitor for different datasources
#
import sys, whrandom
from time import *
from qt import *
from frmdatasource import frmDataSource

- The sys module is needed for
QApplication; whrandom is one of the two random modules
Python provides.

- The time module provides lots of time
related functions.

- This is the form we designed
and generated with BlackAdder.
COOKIES=["""That is for every schoolboy and schoolgirl for the next
four hundred years. Have you any idea how much suffering you are going
to cause. Hours spent at school desks trying to find one joke in A
Midsummer Night's Dream? Years wearing stupid tights in school plays
and saying things like 'What ho, my lord' and 'Oh, look, here comes
Othello, talking total crap as usual' Oh, and that is Ken Branagh's
endless uncut four-hour version of Hamlet.
"", """I've got a cunning plan...""","""A Merry Messy Christmas"? All
right, but the main thing is that it should be messy -- messy cake;
soggy pudding; great big wet kisses under the mistletoe...
"""]
def randomFunction():
return str(whrandom.randrange(0, 100))
def timeFunction():
return ctime(time())
def cookieFunction():
return COOKIES[whrandom.randrange(0, len(COOKIES))]

- A list of pithy quotes — global
to this script, so we can treat it like a kind of constant.

- We will define three functions
that provide some data. Later on, there's a generic
DataSource class that can use one
of these functions to compute some data. This function,
obviously, generates random numbers.

- There is no real, practical
reason to choose the whrandom module over the random
module. The randrange(start, end,
step) function returns a random integer
between start and end. Note that we let this function
return a string, not a number. All
data produced by the datasource should be in the same
format.

- This function will simply produce
the current date and time.

- The time()
gives the the number of seconds elapsed since the
‘epoch' — what that means is OS-dependent. For Unix,
it's January 1, 1970. The ctime()
converts that to nice text.

- This last function will return a
cookie, one of the COOKIES list.

- Note how we use
whrandom.randrange() here to pick
one from a list — the start of the range is 0, the
length is the length of the cookies list.
class DataSource(QObject):
def __init__(self, dataFunction, *args):
apply(QObject.__init__, (self,) + args)
self.timer = self.startTimer(1000)
self.dataFunction = dataFunction
def timerEvent(self, ev):
self.emit(PYSIGNAL("timeSignal"), (self.dataFunction(),))

- The
DataSource class is a generic
datasource. We base it on QObject
so we can emit signals from it.

- The constructor of
DataSource takes a function as
the first parameter. This is the actual dataproducing
function. We saw their definitions above. Remember,
every function is an object in its own right — you can
pass them on as arguments, add them to object
dictionaries, etc.

- Every second (1000
milliseconds) the timer will generate an event that
will be caught by the timerEvent
function.

- By creating a local name that
links to the passed function object, we can call this
function as if it were a plain member function of the
class.

- The
timerEvent is called every second
because of the events generated by the timer
object.

- A Python signal is emitted, of
the name "timeSignal" which passes the result of the
dataFunction on.
class DataWindow(frmDataSource):
def __init__(self, *args):
apply(frmDataSource.__init__, (self,) + args)
self.sources = {
"random" : DataSource(randomFunction),
"time" : DataSource(timeFunction),
"cookies" : DataSource(cookieFunction)
}
self.cmbSource.insertStrList(self.sources.keys())
self.currentSource=self.sources.keys()[0]
self.connect(self.sources[self.currentSource],
PYSIGNAL("timeSignal"),
self.appendData)
def switchDataSource(self, source):
source=str(source)
self.disconnect(self.sources[self.currentSource],
PYSIGNAL("timeSignal"),
self.appendData)
self.connect(self.sources[source],
PYSIGNAL("timeSignal"),
self.appendData)
self.currentSource=source
def appendData(self, value):
self.mleWindow.insertLine(value)
self.mleWindow.setCursorPosition(self.mleWindow.numLines(), 0)

- The DataWindow class is a
subclass of the generated form — class
frmDataSource.

- We create a Python dictionary, which takes
DataSource objects (each
instantiated with a different data generating function)
and maps them to distinct names.

- The self.cmbSource combobox is
defined in the generated form. We fill the combobox with
the set of keys to the dictionary. To do this, we use
InsertStrList and not
InsertStringList. A list of Python
strings is converted automatically to a
QStrList, while a
QStringList object must be constructed
separately.

- self.currentSource is a local
variable where we keep track of what datasource we're
looking at.

- Simply connect the "timeSignal" Python signal from
one of the objects in the dictionary of datasources to the
slot that will display the output.

- The switchDataSource function
is where interesting things happen. This function is
a slot that is called whenever the user selects
something from the combobox. The
clicked() signal of the
combobox was connected to the
switchDataSource slot
of the Designer.

- The variable passed by the signal
connected to this slot is of the
QString type. The index to
the dictionary of data sources is a Python string. This
is one instance where we must
convert a QString to a Python
string.

- Using the cached current datasource, we disconnect
the signals it generates from the
appendData function.

- After the signal is disconnected, we can create a
new connection.

- This is the function that shows the data. It simply
adds every value that is passed on by the signal to the
multi-line edit widget, and then sets the cursor to the
last line. If this is not done, the display will not follow
the added data, and instead stay at the beginning.
def main(args):
a = QApplication(args)
QObject.connect(a,SIGNAL('lastWindowClosed()'),a,SLOT('quit()'))
w = DataWindow()
a.setMainWidget(w)
w.show()
a.exec_loop()
if __name__ == '__main__':
main(sys.argv)
As you can see, connecting and
disconnecting signals and slots is a natural and intuitive
technique. Their use is not limited to connecting GUI widgets,
as signals and slots are also useful for the separation of the
data model of an application from its interface. In Part III, We
will investigate an application model based on the strict
separation of model and interface, using signals and slots to
tie everything together.