Trolltech | Documentation | Qt Quarterly | « Providing Context-Sensitive Help

Signal Multiplexing
by Reginald Stadlbauer
Applications that allow the user to open multiple documents, perhaps of different types, must ensure that the user interface is appropriate for the current document, with the right menu options and toolbar buttons enabled or disabled appropriately. For large applications with many actions, it is easy to forget to update all the relevant actions when the user changes documents. This article presents an abstraction that can solve the problem: a signal multiplexer.

Most substantial GUI applications provide some kind of multiple document interface. Often this is implemented using QWorkspace, which provides an MDI framework, but some applications use a tabbed central widget or a widget stack with a document list, icon view, or tab bar to switch between documents. No matter how the multiple documents are handled, only one document is current at any given time.

Applications usually have a particular set of user interface actions associated with each type of document, so the states of the application's actions (enabled/disabled, visible/hidden, etc.) must be correct for the current document, and must change appropriately whenever the current document changes.

In many cases, actions' signals are directly connected to object slots (the slots of a document object in our case), so that the object can respond to user events. But when we have one set of actions with multiple documents we need to make sure that each action the user invokes is applied to the right document. If we're using a QWorkspace one common idiom is to connect actions to slots in the main form which then forward the actions to the appropriate document:

    void MainForm::save()
    {
        MDIForm *form = (MDIForm*)workspace->activeWindow();
        if (form)
            form->save();
    }
    

This introduces an extra level of indirection and requires a cast. Code like this can quickly grow if there are different kinds of documents that must be checked for in each slot.

Without a Multiplexer

A much better approach would be to call the right slot on the right document directly. This can be achieved by automatically connecting all the actions' signals to the current document and automatically disconnecting them all from the other documents, whenever the current document changes.

The automatic connection and disconnection of signals can be achieved using a signal multiplexer class. We can make use of a signal multiplexer to:

The SignalMultiplexer Class

Qt already contains all the necessary classes and functionality to build a signal multiplexer class. Actions are encapsulated by the QAction class, although in some cases a QToolButton or other widget is used directly. The communication mechanism required for multiplexing is provided by Qt's signals and slots.

Signal Multiplexer

The SignalMultiplexer's definition looks like this:

    class SignalMultiplexer : public QObject
    {
        Q_OBJECT
    
    public:
        SignalMultiplexer(QObject *parent = 0, const char *name = 0);
    
        void connect(QObject *sender, const char *signal, const char *slot);
        bool disconnect(QObject *sender, const char *signal, const char *slot);
        void connect(const char *signal, QObject *receiver, const char *slot);
        bool disconnect(const char *signal, QObject *receiver, const char *slot);
    
        QObject *currentObject() const { return object; }
    
    public slots:
        void setCurrentObject(QObject *newObject);
    
    private:
        struct Connection
        {
            QGuardedPtr<QObject> sender;
            QGuardedPtr<QObject> receiver;
            const char *signal;
            const char *slot;
        };
    
        void connect(const Connection &conn);
        void disconnect(const Connection &conn);
    
        QGuardedPtr<QObject> object;
        QValueList<Connection> connections;
    
    };
    

The most interesting functions are connect() and disconnect(). Instead of calling QObject::connect() when making a connection, for example when connecting an action's activated() signal to a document object's slot, we call the first SignalMultiplexer::connect() function and only specify the sender, which is the QAction, or QToolButton, etc.

And instead of calling QObject::connect() to connect a document object signal, for example QTextEdit::copyAvailable(bool), to the application's "copy" action's setEnabled(bool) slot, we call the SignalMultiplexer's second connect() function, specifying the action as the receiver.

The corresponding disconnect() functions remove the connections.

The setCurrentObject(QObject *) function must be called when the current document object changes, so that the SignalMultiplexer can update all the connections. The currentObject() function returns the current document object.

The implementation of the SignalMultiplexer class is quite simple. The class stores a list of all the connections in the connections private variable. When setCurrentObject() is called, all the active QObject connections are removed and a new set of connections to the new current document object are made. The private connect() and disconnect() functions do the real work to set up and remove QObject connections.

We store all our references to QObjects using guarded pointers. The QGuardedPtr class is a template class which can be instantiated for any QObject subclass. A QGuardedPtr object wraps a QObject pointer and supports the -> and * operators for referencing. The advantage of using a QGuardedPtr<QObject> instead of a plain QObject *, is that QGuardedPtr automatically sets the QObject * it wraps to 0 if the QObject gets deleted, thereby ensuring that we don't have any dangling pointers. It also initializes the QObject pointer to 0.

Whenever you need to store pointers to QObjects that you do not own, it is a good idea to use QGuardedPtr to avoid using an invalid pointer if a QObject is deleted. A QGuardedPtr does use more memory than a plain QObject *, so it may not be suitable in some cases.

We're now ready to look at the SignalMultiplexer implementation. We'll confine ourselves to the most interesting code; the complete source is available online.

    void SignalMultiplexer::connect(QObject *sender, const char *signal, const char *slot)
    {
        Connection conn;
        conn.sender = sender;
        conn.signal = signal;
        conn.slot = slot;
    
        connections << conn;
        connect(conn);
    }
    

The public connect() function stores the sender, signal and slot in a new instance of the Connection struct and adds this to the list of connections. Then it calls the private connect() function to set up the real connection.

    bool SignalMultiplexer::disconnect(QObject *sender, const char *signal, const char *slot)
    {
        QValueList<Connection>::Iterator it =
            connections.begin();
        for (; it != connections.end(); ++it) {
            Connection conn = *it;
            if ((QObject*)conn.sender == sender &&
                 qstrcmp(conn.signal, signal) == 0 && qstrcmp(conn.slot, slot) == 0) {
                disconnect(conn);
                connections.remove(it);
                return true;
            }
        }
        return false;
    }
    

The corresponding disconnect() function iterates over the connections list until it finds the connection matching the given sender, signal and slot. Then it calls the internal disconnect() to remove the QObject connection and removes the connection from the connections list.

The other public connect() and disconnect() functions do just about the same things, except that a receiver is given instead of a sender.

Now we will look at the internal connect() and disconnect() functions:

    void SignalMultiplexer::connect(const Connection &conn)
    {
        if (!object)
            return;
        if (!conn.sender && !conn.receiver)
            return;
    
        if (conn.sender)
            QObject::connect((QObject*)conn.sender, conn.signal, (QObject*)object, conn.slot);
        else
            QObject::connect((QObject*)object, conn.signal, (QObject*)conn.receiver, conn.slot);
    }
    

The code does some sanity checking and then calls QObject::connect() to really set up the connection. If the sender is a valid pointer, the current object is used as the receiver, but if the receiver is given, the current object is used as the sender.

The corresponding disconnect() function is almost the same, except that it calls QObject::disconnect().

The last interesting function is setCurrentObject(), which must be called to set the initial current document, and then every time the current document object changes:

    void SignalMultiplexer::setCurrentObject(
            QObject *newObject)
    {
        if (newObject == object)
            return;
    
        QValueList<Connection>::ConstIterator it;
        for (it = connections.begin(); it != connections.end(); ++it)
            disconnect(*it);
        object = newObject;
        for (it = connections.begin(); it != connections.end(); ++it)
            connect(*it);
    }
    

First we check to see if the specified object is the same as the current document object. If it is, there's nothing to do. Otherwise we first iterate over all the connections and call the private disconnect() so that all the current QObject connections are removed. Then we set the private object to the new document object and iterate over all the connections a second time, this time setting up the QObject connections using the new document object as sender or receiver.

Updating Actions

The SignalMultiplexer class allows us to establish two-way connections between actions and document objects. As long as we use the SignalMultiplexer's setCurrentObject() function whenever the current document object changes, we don't have to worry about disconnecting and reconnecting signals and slots because the SignalMultiplexer does this for us.

But we are still left with one outstanding problem: When the current document changes, the actions should update their states. Unfortunately there is no ready-made solution to this problem. However, we can use the SignalMultiplexer class to handle such situations.

When a document is current and its state changes, what normally happens is that the document object emits a signal with information about the change of state. The signal is connected to the relevant action, which in turn responds by changing its own state to reflect the state of the document. For example, an editor widget might emit an undoAvailable() signal if the user has just deleted a line of text, and this signal would be connected to an "undo" action, which would in turn enable a corresponding menu option and toolbar button.

We want to ensure that all actions are updated whenever the current document's state changes, and whenever a new document becomes the current document.

The first step is to provide a function for each document object that emits all the document's signals with the document's current state. We will call this function emitAllSignals(). Whenever a new current document is set, we will call this function and this will result in all the actions' states being updated.

We need to integrate this functionality into the SignalMultiplexer class, so we define the following interface:

    class DocumentObject
    {
    public:
        virtual  DocumentObject() {}
        virtual void emitAllSignals() = 0;
    };
    

We must make sure that every document object also inherits the DocumentObject class and implements emitAllSignals().

We must also change the SignalMultiplexer::setCurrentObject() function, adding the following lines at the end:

      DocumentObject *document = dynamic_cast<DocumentObject*>(newObject);
        if (document)
            document->emitAllSignals();
    

This means that whenever setCurrentObject() is called, it will automatically update all the actions' states for the current document. This works provided that emitAllSignals() is implemented correctly and that all document objects inherit the DocumentObject interface.

We use a dynamic_cast to make sure that we only cast the object to a DocumentObject if it inherits the DocumentObject interface. If it doesn't, the dynamic_cast will return 0 and we do nothing with it. A C-style cast would return a corrupted pointer in the failure case, which would lead to a crash.

A nice additional benefit of this solution is that you don't need to initialize the states of all the actions when you create them, since you initially also call setCurrentObject() to set the initial document object, and this call automatically initializes all the actions' states.

Signal Multiplexing in Practice

So far we have presented the SignalMultiplexer class and discussed how we can adapt it to maintain action states. Now we'll see how to put all this into practice.

By way of example we will implement a very limited text editor which can open multiple documents using an MDI interface. We will use a QWorkspace to provide MDI, and a QMainWindow for our main window. Each document will be held in a QTextEdit MDI child window. We will provide the main window with a QToolBar with "undo" and "redo" actions. When the user clicks an action, the undo (or redo) operation must be performed on the currently active editor, and when the current editor changes, the state of the undo and redo actions must be enabled or disabled depending on the state of the new current editor. In addition the states of the undo and redo actions must also change to reflect the user's interactions with the current editor.

We will begin by implementing a simple QTextEdit subclass:

    class TextEdit : public QTextEdit, public DocumentObject
    {
        Q_OBJECT
    
    public:
        TextEdit(QWidget *parent = 0, const char *name = 0);
        void emitAllSignals();
    };
    

Our subclass inherits QTextEdit and DocumentObject to reimplement the emitAllSignals() function. The implementation looks like this:

    void TextEdit::emitAllSignals()
    {
        emit undoAvailable(isUndoAvailable());
        emit redoAvailable(isRedoAvailable());
        ...
    }
    

The only task this function performs is to emit QTextEdit's signals with the editor's current state.

Here's the implementation of the main window:

    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = 0, const char *name = 0);
        void newDocument();
    private slots:
        void currentWindowChanged(QWidget *widget);
    private:
        SignalMultiplexer multiplexer;
        QWorkspace *workspace;
    };
    

The interesting parts here are the private slot currentWindowChanged() and the private member variable multiplexer. We will connect QWorkspace's windowActivated() signal to our currentWindowChanged() slot and in this slot we'll call setCurrentObject() on the multiplexer. Ideally we would have connected windowActivated() directly to the signal multiplexer's setCurrentObject() slot, but unfortunately the parameter types don't match.

The implementation of the MainWindow class looks like this:

    MainWindow::MainWindow(QWidget *parent, const char *name)
        : QMainWindow(parent, name)
    {
        QToolBar *tools = new QToolBar("Undo/Redo", this);
        QAction *actUndo = new QAction("Undo", QPixmap("undo.png"), "&Undo", CTRL+Key_Z, this);
        actUndo->addTo(tools);
        ...
        multiplexer.connect(actUndo, SIGNAL(activated()), SLOT(undo()));
        multiplexer.connect(SIGNAL(undoAvailable(bool)), actUndo, SLOT(setEnabled(bool)));
        ...
        workspace = new QWorkspace(this);
        connect(workspace, SIGNAL(windowActivated(QWidget *)),
                this, SLOT(currentWindowChanged(QWidget *)));
        setCentralWidget(workspace);
        newDocument();
        ...
    }
    

First we create a toolbar and the actions. We want to connect the undo action's activated() signal to the TextEdit's undo() slot. Instead of a direct QObject connection, we tell the signal multiplexer to set up this connection by calling multiplexer.connect(). Similarly, we want to connect the TextEdit's undoAvailable(bool) signal to the action's setEnabled(bool) slot. Again we set this connection up using the multiplexer.

Then we create and set up the workspace. After this we call newDocument() to open an editor. In a real application we would have a "new document" action so that users could create as many editors as they needed.

    void MainWindow::newDocument()
    {
        TextEdit *editor = new TextEdit(workspace);
        editor->setCaption(QString("Editor #%1").arg(workspace->windowList().count() + 1));
        multiplexer.setCurrentObject(editor);
    }
    

This function creates a new TextEdit and makes it the new current document by calling the signal multiplexer's setCurrentObject() function. The multiplexer will disconnect the original current document if any, and re-establish all the connections using the new current document.

Since our TextEdit class inherits the DocumentObject interface this call will also update the state of all the actions that the multiplexer knows about. This works because setCurrentObject() calls the document's emitAllSignals() function. The emitted signals pass on information about the current editor's state to the actions which respond accordingly, for example by becoming enabled or disabled.

Conclusion

This article shows how to abstract a common GUI programming task using Qt's object model with surprisingly little code. It also demonstrates that investing time in thinking about abstractions can lead to simpler and more maintainable software.

The source code for the SignalMultiplexer class and for the example program is available from froglogic.


This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license.

Copyright © 2003 Trolltech Trademarks Qt Quarterly »