Всем привет!
В этой небольшой статье я научу вас, одному интересному трюку с моделями, который можно реализовать с помощью MVC фреймворка Qt.
Двухуровневая модель дерева:
Модель списка:
В результате трюка мы получим модель объединяющую две вышеприведенные модели:
И так как же это сделать? Я думаю вы уже догадались что сделать это можно прибегнув к помощи QAbstractProxyModel. А вот и нет! К сожалению стандартный класс QAbstractProxyModel может преобразовать лишь одну исходную модель (что тоже неплохо). Поэтому мы напишем свою ModelJoinerProxy, которая будет компоновать наши две исходные модели в одно целое.
И так приступим:
Наша модель посредник представляет собой модель двухуровневого дерева, чтобы достичь этого мы
переопределяем index(..) и parent(..) так как будто мы строим модель обычного дерева.
Дальше нам нужно чтобы наша модель имела правильное количество строк и столбцов,
(для простоты количество столбцов в исходных моделях и модели посреднике будет = 1)
для этого мы переопределяем rowCount(....) и columnCount(.....).
Теперь самое интересное, нам нужно преобразовать индексы наших моделей чтобы они могли
взаимодействовать друг с другом.
Теперь осталось только переопределить data(...) чтобы наша модель могла отдавать данные представлениям (и всем кому мы захотим).
Так же необходимо подключить все необходимые сигналы моделей источников к соответствующим слотам нашей модели. Это нужно для того чтобы наша модель реагировала на любые изменения в моделях источниках.
Например:
и реализовать сами слоты
Ну вот и все, наша модель посредник готова, осталось только подключить к ней исходные модели, а саму модель посредник подключить к представлению. Можно по желанию сделать ее редактируемой переопределив setData(...)
А зачем это все собственно говоря нужно? Не могу подобрать нужных слов, но думаю что люди, которым
действительно близка затронутая мной тема сами все поймут. Например в моем текущем проекте складывается иерархия из примерно 15 прокси моделей (самописные + стандартные) и всего лишь одной исходной модели данных. Без прокси моделей это бы занимало намного больше кода, и как следствие больше багов, проблемы синхронизации моделей ну итд.
Надеюсь что прочитав эту статью, вы откроете для себя новый взгляд на MVC в Qt, и сможете, сами делать
преобразования ваших структур данных в соответствии с потребностями вашего GUI.
А вообще неплохо было бы иметь под рукой дополнение к MVC Qt состоящее из пары десятков подобных моделей посредников. Например захотели вы сгруппировать свои данные по каким-либо параметрам, воспользовались моделью посредником для группировки ну и т.д.
Большое спасибо yshurik за терпение и незаменимые советы.
В этой небольшой статье я научу вас, одному интересному трюку с моделями, который можно реализовать с помощью MVC фреймворка Qt.
Исходные данные для трюка.
Двухуровневая модель дерева:
|Parent 1
-----Child 1
-----Child N
|Parent N
-----Child 1
-----Child N
Модель списка:
Item1
Item2
Item3
В результате трюка мы получим модель объединяющую две вышеприведенные модели:
|Parent 1
------Child 1
------Child N
|Parent N
------Child 1
------Child N
|Item1
|Item2
|Item3
Приступим к реализации.
И так как же это сделать? Я думаю вы уже догадались что сделать это можно прибегнув к помощи QAbstractProxyModel. А вот и нет! К сожалению стандартный класс QAbstractProxyModel может преобразовать лишь одну исходную модель (что тоже неплохо). Поэтому мы напишем свою ModelJoinerProxy, которая будет компоновать наши две исходные модели в одно целое.
И так приступим:
//наследуемся от QAbstractItemModel чтобы наша модель могли
//использовать стандартные представления Qt
class ModelJoinerProxy : public QAbstractItemModel
{
Q_OBJECT
public:
ModelJoinerProxy(QObject *parent = 0);
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex &child) const;
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
//установить модель дерева в качестве 1 модели источника
virtual void setSourceModel1(QAbstractItemModel *sourceModel1);
//установить модель списка в качестве 2 модели источника
virtual void setSourceModel2(QAbstractItemModel *sourceModel2);
//вернуть индекс исходной модели который соответствует индексу прокси модели
virtual QModelIndex mapToSource(const QModelIndex &) const;
//вернуть индекс прокси модели который соответствует индексу исходной модели
virtual QModelIndex mapFromSource(const QModelIndex &) const;
private slots:
void source_dataChanged(QModelIndex, QModelIndex);
void source_rowsAboutToBeInserted(QModelIndex p, int from, int to);
void source_rowsInserted(QModelIndex p, int, int);
void source_rowsAboutToBeRemoved(QModelIndex, int, int);
void source_rowsRemoved(QModelIndex, int, int);
void source_modelReset();
private:
QAbstractItemModel *m1;
QAbstractItemModel *m2;
};
Наша модель посредник представляет собой модель двухуровневого дерева, чтобы достичь этого мы
переопределяем index(..) и parent(..) так как будто мы строим модель обычного дерева.
Дальше нам нужно чтобы наша модель имела правильное количество строк и столбцов,
(для простоты количество столбцов в исходных моделях и модели посреднике будет = 1)
для этого мы переопределяем rowCount(....) и columnCount(.....).
int ModelJoinerProxy::rowCount(const QModelIndex &parent) const
{
int count = 0;
//1 уровень
if (!parent.isValid())
count = m1->rowCount() + m2->rowCount();
//2 уровень
else if (parent.internalId() == -1)
{
// Если строка верхнего уровня с таким номером есть в модели дерева m1 ,
//то возвращаем количество строк детей этой строки для прокси
if ( parent.row() < m1->rowCount() )
count = m1->rowCount( m1->index(parent.row(),0) );
//если строки верхнего уровня с таким номером нет в модели дерева и она не вызодит за границы
// возвращаем количество строк детей этой строки для прокси из второй модели
else if ( parent.row() > (m1->rowCount()-1) && parent.row() < (m1->rowCount() + m2->rowCount()) )
count = m2->rowCount(m2->index(parent.row()-m1->rowCount(), 0));
}
return count;
}
int ModelJoinerProxy::columnCount(const QModelIndex &parent) const
{
return 1;
}
Теперь самое интересное, нам нужно преобразовать индексы наших моделей чтобы они могли
взаимодействовать друг с другом.
//вернуть индекс исходной модели который соответствует индексу прокси модели
QModelIndex ModelJoinerProxy::mapToSource(const QModelIndex & proxy) const
{
//возвращаем из модели дерева индекс первого уровня
if ( proxy.row() < m1->rowCount() && !proxy.parent().isValid())
{
return m1->index(proxy.row(),0) ;
}
//возвращаем из модели дерева индекс второго уровня
if ( proxy.parent().isValid())
{
return m1->index(proxy.row(),0,
m1->index( proxy.parent().row(),0) );
}
//возвращаем индекс из модели списка
if ( proxy.row() > (m1->rowCount()-1) && proxy.row() < (m1->rowCount() + m2->rowCount()) )
{
int offset = (proxy.row() - m1->rowCount());
return m2->index(offset, 0);
}
return QModelIndex();
}
//вернуть индекс в прокси модели по индексу исходной
QModelIndex ModelJoinerProxy::mapFromSource(const QModelIndex &source) const
{
QModelIndex proxy;
if (source.model() == m1)
{
//верхний уровень модели дерева
if (!source.parent().isValid())
{
proxy = index(source.row(), 0);
}
//нижний уровень модели дерева
else
{
QModelIndex source_parent = index(source.parent().row() ,0);
proxy = index(source.row(), 0, source_parent);
}
}
//модель списка
if (source.model() == m2)
{
int offset = m1->rowCount() + source.row();
proxy = index(offset, 0);
}
return proxy;
}
Теперь осталось только переопределить data(...) чтобы наша модель могла отдавать данные представлениям (и всем кому мы захотим).
QVariant ModelJoinerProxy::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
return mapToSource(index).data(role);
}
Так же необходимо подключить все необходимые сигналы моделей источников к соответствующим слотам нашей модели. Это нужно для того чтобы наша модель реагировала на любые изменения в моделях источниках.
Например:
void ModelJoinerProxy::setSourceModel1(QAbstractItemModel *sourceModel1)
{
m1 = sourceModel1;
connect(m1, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(source_dataChanged(QModelIndex,QModelIndex)));
........
void ModelJoinerProxy::setSourceModel2(QAbstractItemModel *sourceModel2)
{
m2 = sourceModel2;
connect(m2, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(source_dataChanged(QModelIndex,QModelIndex)));
........
и реализовать сами слоты
void ModelJoinerProxy::source_dataChanged(QModelIndex tl, QModelIndex br)
{
QModelIndex p_tl = mapFromSource(tl);
QModelIndex p_br = mapFromSource(br);
emit dataChanged(p_tl, p_br);
}
Ну вот и все, наша модель посредник готова, осталось только подключить к ней исходные модели, а саму модель посредник подключить к представлению. Можно по желанию сделать ее редактируемой переопределив setData(...)
Заключение
А зачем это все собственно говоря нужно? Не могу подобрать нужных слов, но думаю что люди, которым
действительно близка затронутая мной тема сами все поймут. Например в моем текущем проекте складывается иерархия из примерно 15 прокси моделей (самописные + стандартные) и всего лишь одной исходной модели данных. Без прокси моделей это бы занимало намного больше кода, и как следствие больше багов, проблемы синхронизации моделей ну итд.
Надеюсь что прочитав эту статью, вы откроете для себя новый взгляд на MVC в Qt, и сможете, сами делать
преобразования ваших структур данных в соответствии с потребностями вашего GUI.
А вообще неплохо было бы иметь под рукой дополнение к MVC Qt состоящее из пары десятков подобных моделей посредников. Например захотели вы сгруппировать свои данные по каким-либо параметрам, воспользовались моделью посредником для группировки ну и т.д.
Большое спасибо yshurik за терпение и незаменимые советы.