Trolltech | Documentation | Qt Quarterly | « Deploying Applications on Windows | Mapping Many Signals to One »

Implementing Model/View/Controller
by Jarek Kobus
Qt 4 uses the MVC pattern for its item view classes (QListView, QTable, etc.). But MVC is more than just a pattern for item views: it can be used generally as a means of keeping different widgets synchronized. In this article, we show how to apply it, taking full advantage of Qt's signal--slot mechanism.

In Qt Quarterly 7's article, A Model/View Table for Large Datasets, we saw how to create a model/view QTable subclass. Here we take a more generic approach that can be applied to any of Qt's widget classes (and to our own widget classes).

A model is a set of data, and a view is a GUI component that can present a visual representation of the model to the user. If the model (data) cannot be changed by the user, having a model and a view is sufficient; but if the model can be modified, then we also need a controller, which is a means by which the user can modify the data shown in a view, and have their changes reflected back into the source data.

Let's imagine that we want to provide color management for an application. We want to provide a palette of colors that the user can make use of throughout the application, for example to specify the color of text, or the color of shapes that they have drawn. We might want to present the palette in different ways in different parts of the application, but we want all the colors displayed to be taken from a single palette.

In this example, the model is a palette of colors, and the view is a widget that can display the colors. The controller could be a separate "editor" widget, or could be built-in to the view widget. Whenever the model's data changes (for example, because a user has edited the data in one of the views), all active views must be informed so that they can update themselves. For the purpose of illustration, we will design just one view widget, but we could design any number of view widgets, each presenting the data in their own way.

Our model uses the Singleton design pattern, since we only want one palette to be used throughout the entire application. Let's look at the definition of our palette model class.

    class PaletteModelManager : public QObject
    {
        Q_OBJECT
    
    public:
         PaletteModelManager();
    
        static PaletteModelManager *getInstance();
    
        QMap<QString, QColor> getPalette() const { return thePalette; }
        QColor getColor(const QString &id) const;
    
    public slots:
        QString addColor(const QString &id, const QColor &color);
        void changeColor(const QString &id, const QColor &newColor);
        void removeColor(const QString &id);
    
    signals:
        void colorAdded(const QString &id);
        void colorChanged(const QString &id, const QColor &color);
        void colorRemoved(const QString &id);
    
    private:
        PaletteModelManager(QObject *parent = 0, const char *name = 0)
            : QObject(parent, name) {}
    
        QMap<QString, QColor> thePalette;
        static PaletteModelManager *theManager;
    };
    

The PaletteModelManager class is unusual in some respects. Firstly it provides a static getInstance() function that returns a pointer to the one and only PaletteModelManager object that can exist in the application. Secondly, it has a private constructor; this is to ensure that users cannot instantiate instances of the class itself. These two features are used to make a Singleton class in C++.

The palette itself is a simple map of string IDs X colors. The addColor() slot is uncommon in that it has a non-void return value to support its use as both a slot and as a function.

Conceptually, the class provides several key interfaces. An access interface which gives us a handle through which we can interact with the model---getInstance(); a read interface through which we can read the current state of the model---getPalette() and getColor(); a change interface through which the model's data can be modified---the slots provide this; and an inform interface that notifies views of changes to the model's state---the signals provide this.

    PaletteModelManager *PaletteModelManager::theManager = 0;
    
    PaletteModelManager *PaletteModelManager::getInstance()
    {
        if (!theManager)
            theManager = new PaletteModelManager();
        return theManager;
    }
    
    PaletteModelManager:: PaletteModelManager()
    {
        if (theManager == this)
            theManager = 0;
    }
    

The global PaletteModelManager pointer is statically initialized to 0. (It might be tempting to use a static object rather than a pointer, but some compilers fail to call the constructor for static objects, especially in libraries, so our approach is more robust.) In getInstance(), we create the single instance if it doesn't exist.

We have omitted the implementation of all the slots except for changeColor():

    void PaletteModelManager::changeColor(const QString &id, const QColor &newColor)
    {
        if (!thePalette.contains(id) || thePalette[id] == newColor)
            return;
        emit colorChanged(id, newColor);
        thePalette[id] = newColor;
    }
    

Here we've emitted the colorChanged() signal before performing the change. This is to ensure that in any slot connected to the colorChanged() signal, the palette is still in its original state, with the ID and color that are going to be changed passed as parameters. This is especially useful if you want to track the changes, for example to support an undo stack or a history. Sometimes it may be more appropriate to emit the change signal after the change (as we do in PaletteModelManager::addColor()), but in such cases the previous state cannot be accessed directly from the palette manager, so if it is needed it must be passed in the parameters of the signal that notifies the change of state. Another strategy would be to emit notifying signals before and after a change in state.

Now that we've seen how to implement our model, let's see how to make use of it. We'll create a custom icon view that shows the colors and their ID strings; here's the definition:

    class PaletteIconView : public QIconView
    {
        Q_OBJECT
    
    public:
        PaletteIconView(QWidget *parent = 0, const char *name = 0);
        PaletteIconView() {}
    
        void setPaletteModelManager(PaletteModelManager *manager);
    
    private slots:
        void colorAdded(const QString &id);
        void colorChanged(const QString &id, const QColor &newColor);
        void colorRemoved(const QString &id);
    
        void contextMenuRequested(QIconViewItem *item, const QPoint &pos);
    
    private:
        void clearOld();
        void fillNew();
        QPixmap getColorPixmap(const QColor &color) const;
    
        PaletteModelManager *theManager;
        QMap<QString, QIconViewItem *> itemFromColorId;
        QMap<QIconViewItem *, QString> colorIdFromItem;
    };
    

Our custom icon view holds a pointer to the PaletteModelManager. Since we only have a single PaletteModelManager, we could simply have used its static getInstance() function instead, but we've chosen a more general approach, since most models are not implemented as Singletons. The private slots are used internally to update the icon view and the palette manager. Our controller is built-in to our view, in this case as a context menu.

Mvc

Now we'll review the PaletteIconView's main functions.

    PaletteIconView::PaletteIconView(QWidget *parent, const char *name)
        : QIconView(parent, name), theManager(0)
    {
        setPaletteModelManager(PaletteModelManager::getInstance());
        connect(this, SIGNAL(contextMenuRequested(QIconViewItem*, const QPoint&)),
                this, SLOT(contextMenuRequested(QIconViewItem*, const QPoint&)));
    }
    

The constructor is straightforward; we just set the PaletteModelManager, and connect the context menu.

    void PaletteIconView::setPaletteModelManager(PaletteModelManager *manager)
    {
        if (theManager == manager)
            return;
        if (theManager) {
            disconnect(theManager, SIGNAL(colorAdded(const QString&)),
                       this, SLOT(colorAdded(const QString&)));
            disconnect(theManager, SIGNAL(colorChanged(const QString&, const QColor&)),
                       this, SLOT(colorChanged(const QString&, const QColor&)));
            disconnect(theManager, SIGNAL(colorRemoved(const QString&)),
                       this, SLOT(colorRemoved(const QString&)));
            clearOld();
        }
        theManager = manager;
        if (theManager) {
            fillNew();
            connect(theManager, SIGNAL(colorAdded(const QString&)),
                    this, SLOT(colorAdded(const QString&)));
            connect(theManager, SIGNAL(colorChanged(const QString&, const QColor&)),
                    this, SLOT(colorChanged(const QString&, const QColor&)));
            connect(theManager, SIGNAL(colorRemoved(const QString&)),
                    this, SLOT(colorRemoved(const QString&)));
        }
    }
    

When a new palette manager is set, the connections to the old one (if any) are broken, and new ones established. We haven't shown the clearOld() function; it clears the item X color ID maps, and clears the icon view itself.

    void PaletteIconView::fillNew()
    {
        QMap<QString, QColor> palette = theManager->getPalette();
        QMap<QString, QColor>::const_iterator i = palette.constBegin();
        while (i != palette.constEnd()) {
            colorAdded(i.key());
            ++i;
        }
    }
    

The fillNew() function populates the maps with the IDs and colors from the palette, and adds each color into the icon view.

    void PaletteIconView::colorAdded(const QString &id)
    {
        QIconViewItem *item = new QIconViewItem(this, id,
                                    getColorPixmap(theManager->getColor(id)));
        itemFromColorId[id] = item;
        colorIdFromItem[item] = id;
    }
    

When the user adds a new color via the context menu we create a new icon view item and update the item X ID maps.

    void PaletteIconView::contextMenuRequested(QIconViewItem *item, const QPoint &pos)
    {
        if (!theManager)
            return;
        QPopupMenu menu(this);
        int idAdd = menu.insertItem(tr("Add Color"));
        int idChange = menu.insertItem(tr("Change Color"));
        int idRemove = menu.insertItem(tr("Remove Color"));
        if (!item) {
            menu.setItemEnabled(idChange, false);
            menu.setItemEnabled(idRemove, false);
        }
        int result = menu.exec(pos);
        if (result == idAdd) {
            QColor newColor = QColorDialog::getColor();
            if (newColor.isValid()) {
                QString name = QInputDialog::getText(tr("MVC Palette"), tr("Color Name"));
                if (!name.isEmpty())
                    theManager->addColor(name, newColor);
            }
        }
        else if (result == idChange) {
            QString colorId = colorIdFromItem[item];
            QColor old = theManager->getColor(colorId);
            QColor newColor = QColorDialog::getColor(old);
            if (newColor.isValid())
                theManager->changeColor(colorId, newColor);
        }
        else if (result == idRemove) {
            QString colorId = colorIdFromItem[item];
            theManager->removeColor(colorId);
        }
    }
    

The context menu is straightforward. First we check that we have a palette manager, since we can't do anything without one. Then we create the menu items, but disable those that only apply to an item if the user didn't invoke the menu on an item (i.e. if item == 0). If the user chose Add, we pop up a color dialog, and if they choose a color, we pop up an input dialog to get the color's ID. If they chose Change, we pop up a color dialog so that they can choose a new color, and if they chose Remove we remove the color.

Note that the adding, changing, and removing are all applied to the palette manager, not to the icon view; this is because the palette manager is responsible for the color data, and it will emit signals concerning its change of state to all associated views, including this one, so that they can update themselves. This is a much cleaner and safer approach than updating the view directly, since it ensures that all views are updated only via the model, and using the same code.

Here's a simple main() function that creates two views of a palette.

    int main(int argc, char **argv)
    {
        QApplication app(argc, argv);
        QSplitter splitter;
        splitter.setCaption(splitter.tr("MVC Palette"));
        PaletteIconView view1(&splitter);
        PaletteIconView view2(&splitter);
        PaletteModelManager *manager = PaletteModelManager::getInstance();
        manager->addColor(splitter.tr("Red"), Qt::red);
        manager->addColor(splitter.tr("Green"), Qt::green);
        manager->addColor(splitter.tr("Blue"), Qt::blue);
        app.setMainWidget(&splitter);
        splitter.show();
        return app.exec();
    }
    

Once we create our views, we add a few colors. The user can add, change, and remove colors using the context menu that each view provides, and whatever the user does to one view is applied to both.

Conclusion

Thanks to Qt's signal--slot mechanism, implementing Model/View/Controller components is straightforward. Careful consideration must be given to whether the signals that notify changes to the model are emitted before or after the changes are actually applied. It is simplest and safest to update views indirectly by having their controller call the model rather than directly in response to their controller. Note also that for a really robust implementation, you need to ensure that attempts to update the model in response to a signal from the model are handled sensibly: for example, we wouldn't want removeColor() called from a slot connected to say, colorAdded().

The classes presented here could be enhanced in several ways, for example, by creating a PaletteIconView plugin for use with Qt Designer, or by providing signals and slots for updating and notifying changing a color's ID. The PaletteIconView could be enhanced by providing drag and drop support, while the PaletteModelManager could provide loading and saving of palettes. Additional views that operate on a palette could also be created, for example, combobox and listbox subclasses. A more ambitious extension would be the implementation of an undo/redo mechanism. The full source code for this article is here: qq10-mvc.zip (8K).


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

Copyright © 2004 Trolltech Trademarks Mapping Many Signals to One »