• 8264阅读
  • 2回复

MVC Architecture with QT4 [复制链接]

上一主题 下一主题
离线kmax
 
只看楼主 倒序阅读 楼主  发表于: 2008-06-10
— 本帖被 XChinux 执行加亮操作(2008-06-13) —
很多QT4的初学者都对这个框架有些陌生,不知道怎么使用信号和插槽,不知道自己的逻辑放在什么类里面,不知道怎么关联自己的类和GUI交互。

这里有篇文章,讲到如何理解MVC结构,从而真正掌握QT4的基本理念和设计思维。

One of the major new features of Qt4 was said to be the model-view-controller environment. I eagarly waited for the release of the new Qt to see what does it offer in the concept of MVC.

What is the model-view-controller concept?
It is a programming concept which divides an information system (like a computer application) into three loosely-coupled parts. One of them, called the Model, holds the whole logic of the system. It serves data requests to other parts, feeding them data they require. It doesn’t care what do they do with it. The second part is the View — responsible for visualisation of the data obtained from the model. The third one is the controller, which integrates the two other parts, deciding how should the user interface react to user input. Typically the end user interacts with the view taking actions which cause requests to be passed to the model either changing it (read-write model) or demanding different data to be fed.

A fine example of the model-view-controller, sometimes simplified to become the model-view concept is the WebDAV protocol. The user interacts with his client (the view) which causes requests to be sent over the http protocol to the server (the controller), often containing different handlers (models) for different requests (like the SVN handler for SVN repositories) that generate the content demanded by the view. They don’t care what will the view do with the data — they only serve requests.

MVC implementation in Qt4
Qt uses the simplified approach to MVC, integrating the controller functionality in the framework itself (signals and slots, events). The user gains access only to the model and view APIs (through QAbstractItemModel and QAbstractItemView classes).

Qt already implements some usable models: QAbstractListModel, QAbstractTableModel, QDirModel, QStandardItemModel (and their derivatives) that can be used with standard views: QListView, QTableView and QTreeView. Worth noting is that Qt provides abstract models fitted for table and list views, but lacks an appropriate model for QTreeView! If one needs such a model, it can be found as one of Qt examples in the reference.

How are the models implemented?
A model can be seen as a multilevel two dimentional array. Each array contains rows and columns and each cell (model item) can have an array of children, every one of which can have an array of own children, etc. Each cell (item) is addressable by an index, consisting of a parent item, and row and column numbers. The model widely uses the index to address cells and extract information from them. With each index an id or a pointer can be assotiatied, which makes it easy to assign custom data to each item. A very nice thing is that each cell can hold an arbitrary amount of data, which can be addressed by specyfiing a so called Role the data applies to. There are some standard roles defined, like Display (which corresponds to data which is drawn as the item representation), Decoration (an icon or pixmap), ToolTip (a hint for the item) or WhatsThis (a more elaborative description of the item). Of course custom roles can be defined too.

How to implement own model?
It is easy as 1-2-3. Just subclass an appropriate model class and reimplement few simple methods:

data - fetches data of a desired role from a cell
index - returns a model index of a given item
parent - returns an index of a parent of an item
rowCount - returns the number of rows (array height) an item has
columnCount - returns the number of columns (array width) an item has
If one wants more complex models (like an editable one), more methods need to be reimplemented (most commonly setData and flags).

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.



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).
描述:例子代码
附件: qq10-mvc.zip (7 K) 下载次数:93
QT 4.4.0  Win XP
离线sht112
只看该作者 1楼 发表于: 2008-06-11
我看了文章,确实写得不错,还从中学到了Singleton Pattern的写法,那就是定义一个static 类指针,同时类的构造函数为private,这样就确保了只有一个类的实例。

但遗憾的是,所带的源码无法编译通过。
离线zy765

只看该作者 2楼 发表于: 2008-09-11
确实很好,例子在ubuntu下可以编译通过,好文章!!!!!
快速回复
限100 字节
 
上一个 下一个