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.

Designing the interface

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:

Connecting the activated(const QString&) signal to the switchDataSource() 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                                       (1)
from time import *                                         (2)
from qt import *

from frmdatasource import frmDataSource                    (3)
        
(1)
The sys module is needed for QApplication; whrandom is one of the two random modules Python provides.
(2)
The time module provides lots of time related functions.
(3)
This is the form we designed and generated with BlackAdder.
COOKIES=["""That is for every schoolboy and schoolgirl for the next(1)
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():                                      (2)
    return str(whrandom.randrange(0, 100))                 (3)

def timeFunction():                                        (4)
    return ctime(time())                                   (5)

def cookieFunction():                                      (6)
    return COOKIES[whrandom.randrange(0, len(COOKIES))]    (7)
        
(1)
A list of pithy quotes — global to this script, so we can treat it like a kind of constant.
(2)
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.
(3)
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.
(4)
This function will simply produce the current date and time.
(5)
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.
(6)
This last function will return a cookie, one of the COOKIES list.
(7)
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):                                 (1)

    def __init__(self, dataFunction, *args):               (2)
        apply(QObject.__init__, (self,) + args)
        self.timer = self.startTimer(1000)                 (3)
        self.dataFunction = dataFunction                   (4)

    def timerEvent(self, ev):                              (5)
        self.emit(PYSIGNAL("timeSignal"), (self.dataFunction(),))(6)
        
(1)
The DataSource class is a generic datasource. We base it on QObject so we can emit signals from it.
(2)
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.
(3)
Every second (1000 milliseconds) the timer will generate an event that will be caught by the timerEvent function.
(4)
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.
(5)
The timerEvent is called every second because of the events generated by the timer object.
(6)
A Python signal is emitted, of the name "timeSignal" which passes the result of the dataFunction on.
class DataWindow(frmDataSource):                           (1)

    def __init__(self, *args):
        apply(frmDataSource.__init__, (self,) + args)

        self.sources = {                                   (2)
            "random" : DataSource(randomFunction),
            "time" : DataSource(timeFunction),
            "cookies" : DataSource(cookieFunction)
            }

        self.cmbSource.insertStrList(self.sources.keys())  (3)
        self.currentSource=self.sources.keys()[0]          (4)
        self.connect(self.sources[self.currentSource],     (5)
                     PYSIGNAL("timeSignal"),
                     self.appendData)
                     
    def switchDataSource(self, source):                    (6)
        source=str(source)                                 (7)
        self.disconnect(self.sources[self.currentSource],  (8)
                     PYSIGNAL("timeSignal"),
                     self.appendData)
        self.connect(self.sources[source],                 (9)
                     PYSIGNAL("timeSignal"),
                     self.appendData)
        self.currentSource=source
        
    def appendData(self, value):                           (10)
        self.mleWindow.insertLine(value)
        self.mleWindow.setCursorPosition(self.mleWindow.numLines(), 0)
        
(1)
The DataWindow class is a subclass of the generated form — class frmDataSource.
(2)
We create a Python dictionary, which takes DataSource objects (each instantiated with a different data generating function) and maps them to distinct names.
(3)
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.
(4)
self.currentSource is a local variable where we keep track of what datasource we're looking at.
(5)
Simply connect the "timeSignal" Python signal from one of the objects in the dictionary of datasources to the slot that will display the output.
(6)
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.
(7)
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.
(8)
Using the cached current datasource, we disconnect the signals it generates from the appendData function.
(9)
After the signal is disconnected, we can create a new connection.
(10)
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.