Реализация Model/View/Controller
Материал из Wiki.crossplatform.ru
Qt Quarterly | Выпуск 10 | Документация |
by Jarek Kobus
Qt 4 использует форму MVC для своих классов представления элементов (QListView, QTable, и т.п.). Но MVC - это более чем только форма для представлений элементов: он может использоваться в общем как средство объединения различных компонент widgets. В этой статье, мы показываем, как применить данный метод.
В 7-ой еженедельной статье Qt Quarterly A Model/View Table for Large Datasets мы увидели, как создать модель подкласса QTable. Здесь мы используем более общий подход, который может быть применен к любому из классов Qt's widget (и к нашим собственным классам widget).
Модель - это набор данных, а представление - это компонент GUI, который может представлять визуально модели для просмотра пользователю. Если данные не могут быть изменены пользователем, наличие модели и представления достаточно; но если модель может быть изменена, то нам также нужен диспетчер, который является средством, с помощью которого пользователь может изменить данные, которые визуализируются.
Давайте представим себе, что мы хотим обеспечить управление цветовой палитрой для приложения. Мы хотим обеспечить палитру цветов, чтобы пользователь мог использовать его всюду в приложении, например, чтобы конкретизировать цвет текста, или цвета форм, которые он рисует. Мы, возможно, хотели бы представить палитру в различных способах в различных частях приложения, но мы хотим, чтобы все показанные цвета находились в единой палитре.
В этом примере, модель - это палитра цветов, а представление - это widget, который может показать определенные цвета. Диспетчер мог бы быть отдельным "редактором" представления, или мог бы быть встроен в саму компоненту. Каждый раз, когда исходные данные изменяются (например, потому что пользователь редактировал данные в одной из компонент), все активные представления после этого должны самообновляться. С целью иллюстрации, мы разработаем только одно представление, но мы могли бы разработать любое число представлений, чтобы каждое из них отображало данные уникальным образом.
Наша модель использует метод единого класса, так как мы только хотим, чтобы одна палитра использовалась в нашем главном приложении. Давайте посмотрим на определение нашего главного класса палитры.
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; };
Класс PaletteModelManager необыкновенен в некоторых отношениях. Во-первых, это обеспечивает статистическая функция getInstance(), которая возвращает указатель на объект PaletteModelManager, который должен существовать в приложении. Во-вторых, это закрытый конструктор, что должно гарантировать, что пользователи не могут изменять данных экземпляров класса непосредственно. Эти две особенности используются, чтобы сделать отдельный класс в C++.
Палитра непосредственно - это простая карта цветов. addColor() слот - уникальный, уникальность проявляется в том, что эта функция возвращает непустое значение, что позволяет использовать ее его не только в качестве слота, но и в качестве функции.
Класс обеспечивает несколько ключевых интерфейсов. Интерфейс доступа, который предоставляет нам указатель, через который мы можем взаимодействовать с моделью - getInstance(); интерфейс состояния, через который мы можем прочитать текущее состояние модели - getPalette() и getColor(); интерфейс изменения, через который уникальные данные могут быть изменены - слоты обеспечивают это; и информирующий интерфейс, который извещает дочерние компоненты об изменении данных в главном компоненте - обеспечивают это сигналы
PaletteModelManager *PaletteModelManager::theManager = 0; PaletteModelManager *PaletteModelManager::getInstance() { if (!theManager) theManager = new PaletteModelManager(); return theManager; } PaletteModelManager:: PaletteModelManager() { if (theManager == this) theManager = 0; }
Глобальный указатель PaletteModelManager инициализируется статически в 0. С помощью getInstance(), мы создаем единый экземпляр, если его до сих пор не существовало.
Мы пренебрегли реализацией всех слотов, за исключением 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; }
Здесь мы запустили сигнал colorChanged() перед выполнением изменения. Это должно гарантировать, что какой-нибудь слот, соединенный с colorChanged(), будет находится в оригинальном состоянии, пока ему не будут переданы ID и цвет, которые будут установлены в качестве параметров. Это особенно полезно, если вы хотите отслеживать изменения, например, чтобы поддерживать историю действий пользователя. Иногда, возможно, является более подходящим вариантом запустить сигнал изменения (как мы делаем в PaletteModelManager::addColor()), но в таких случаях предыдущее состояние не может быть доступно непосредственно из менеджера палитры, так что, если требуется, значит, требуемые изменения должны передаваться в параметрах сигнала, который извещает об изменении текущего состояния на новое.
Теперь, когда мы увидели, как нужно определить нашу модель, давайте посмотрим, как этим пользоваться на практике. Мы создадим компонент иконки, которая показывает цвета и их ID; ниже приведен код:
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; };
Наше представление иконки содержит указатель PaletteModelManager. Так как мы имеем только один PaletteModelManager, то мы могли бы просто использовать функцию getInstance() , однако мы выбрали более общий подход, так как большинство моделей не существуют как единые модели. Частные слоты используются внутри, чтобы обновить представление иконки и менеджера палитры. Наш диспетчер встроен непосредственно в представление, в данном случае как контекстное меню.
Сейчас мы рассмотрим главные функции:
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&))); }
Конструктор простой; мы только устанавливаем PaletteModelManager, и присоединяем контекстное меню.
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&))); } }
Когда новый менеджер палитры установлен, подсоединения к старому (если таковой имеется) уже не существует, и устанавливается новое соединение. Мы не описали действие функции clearOld() - данная функция очищает составляющую X цветовых карт с определенным ID элемента, и очищает представление иконки непосредственно.
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; } }
Функция FillNew() заполняет объект с определенным ID цветом из палитры и выдает изменения непосредственно в иконку.
void PaletteIconView::colorAdded(const QString &id) { QIconViewItem *item = new QIconViewItem(this, id, getColorPixmap(theManager->getColor(id))); itemFromColorId[id] = item; colorIdFromItem[item] = id; }
Когда пользователь добавляет новый цвет через контекстное меню, мы создаем новый элемент иконки и обновляем объекты и ID элементов.
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); } }
Контекстное меню простое. Для начала мы проверяем, что у нас имеется менеджер палитры, так как мы не можем обойтись без него. Затем мы создаем пункты меню, но блокируем те, которые только применимы к элементу, если пользователь не выбирал элементы меню (тогда элемент = 0). Если пользователь выбрал пункт меню "добавить", то возникает всплывающее меню для выбора цвета, и если он выбирают цвет, то возникает диалог ввода цвета, если же он выбрал пункт меню "изменение", то возникает диалоговое окно, с помощью которого можно изменить цвет на нужный.
Отметьте, что добавление, изменение и удаление применяются к менеджеру палитры, а не к представлению иконки; это потому, что менеджер палитры ответственен за цветовые данные, он генерирует новые сигналы, касающиеся изменения его состояния, ко всем связанным с ним компонентам, таким образом, они могут обновить себя. Это более безопасный подход, чем обновление состояний представлений непосредственно, так как это гарантирует, что все представления будут обновлены только через модель.
Главная функция main() здесь проста, она создает два представления палитры.
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(); }
Как только мы создаем наши представления, мы добавляем несколько цветов. Пользователь может добавить, изменить, и исключить цвета, используя контекстное меню, которое представляет каждый пункт меню.
Заключение
Благодаря механизму сигналов и слотов, реализация компонентов MVC тривиально. Особое внимание должно уделяться тому, вызываются ли сигналы, которые извещают об изменении модели, после или же перед изменением, применяемым фактически. Проще и безопасней всего обновлять представления объектов косвенно через вызов контроллером модели, вместо того, чтобы делать это непосредственно в ответ контроллеру. Заметьте также, что для действительно надежной имплементации, вы должны убедиться, что попытки обновления модели в ответ на сигнал от модели обрабатываются правильно: например, не следует вызывать removeColor() из слота, присоединенного, к примеру, к colorAdded().
Классы, представленные здесь, могли бы быть расширены несколькими способами, например, созданием плагина PaletteIconView для использования в Qt Designer, или реализацией сигналов и слотов для обновления и извещая идентификатора цвета. PaletteIconView мог бы быть преобразован таким образом, что обеспечивал бы поддержку технологии Drag And Drop, в то время как PaletteModelManager мог бы обеспечить загрузку и сохранение палитр. Дополнительные объекты, которые работают с палитрой, могли бы также быть созданы, например, подклассы combobox и listbox. Полный исходный код для этой статьи доступен здесь: qq10-mvc.zip (8K).