Pull to refresh

Трюки с моделями в Qt

Reading time5 min
Views40K
Всем привет!
В этой небольшой статье я научу вас, одному интересному трюку с моделями, который можно реализовать с помощью 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 за терпение и незаменимые советы.
Tags:
Hubs:
Total votes 23: ↑17 and ↓6+11
Comments20

Articles