Evgenii Legotckoi
Evgenii Legotckoi11 ноября 2015 г. 7:25

QML - Урок 015. Разработка QML Data Mapper - Аналог QDataWidgetMapper

В процессе переноса GUI одной софтины с QWidgets на QML обнаружилось отсутствие аналога QDataWidgetMapper для QML . Данный факт был несколько портящим настроение. Но делать было нечего, кроме как искать другие способы реализации или делать полностью свой велосипед для этого, свой Qml Data Mapper.

Тем не менее поиски на просторах сети дали свои результаты в качестве примера 2011 года от разработчиков Nokia по реализации SQL Widget Mapper для BlackBerry на основе Cascades (фреймворка для native разработки на QML Qt под BlackBerry, соответственно). Путём вечерней медитации над данным примером удалось перепилить код для нынешнего QML Qt , который вполне неплохо будет чувствовать себя как при работе под Desktop, так и при работе под Android, (кроссплатформенность как ни как).

Для демонстрации работы QML Data Mapper будет написано приложение, которое работает с базой данных SQLite. При каждом запуске приложение записывает несколько строк в базу данных, из которой данные забираются при помощи модели данных наследованной от QSqlQueryModel и отображаются в QML TableView. В главном окне приложения присутствует кнопка, по нажатию которой вызывается диалоговое окно, в котором отображается информация о первой строке таблицы. При двойном клике по одной из строк таблицы также вызывается диалоговое окно, но с информацией об этой строке. Также в диалоговом окне присутствуют кнопки для пролистывания информации из таблицы.


Изменения для реализации QML Data Mapper

  1. В первую очередь изменения коснулись типа объекта, который передаётся для работы данному Data Mapper . В случае с BlackBerry передавался объект класса Control фреймворка Cascades. В новом варианте передаётся объект класса QObject. Или сразу объект класса QQuickItem, которым являются объекты QML интерфейса. Это является возможным, поскольку QQuickItem наследован от QObject.
  2. Вторым изменением является то, что в качестве параметра секции табличной модели данных, откуда будут браться данные для подстановки в QML объекты, передавались переменные типа QString, тогда как в моём случае удобнее было передавать значение типа int для сопоставления с ролью колонки в модели данных, реализация которой для QML была приведена в предыдущем уроке по передачи данных из QSqlQueryModel в QML TableView . Следовательно тип объекта был изменён на int.
  3. Изменение типа модели данных. В случае с BlackBerry использовалась bb::cascades:DataModel. Тогда как данный вариант работает с объектами QAbstractItemModel, а также наследованными от данного класса.
  4. Следующее изменение - это упрощение кода метода update() в private классе QmlDataMapperPrivate, которое было связано с отличием вызова метода QAbstractItemModel::data() от одноимённого метода bb::cascades:DataModel.
  5. Объявление методов addMapping() в качестве public Q_SLOTS вместо обычного public. Данное изменение было связано с тем, что когда возникла необходимость докопаться до объекта QML из под C++ слоя , с которым должен работать QML Data Mapper , и который является диалоговым окном спрятанным в следующих дебрях иерархии объектов QML : rootObject -> TableView -> Tab -> ItemTab -> Dialog. ... Я просто не осилил такие извращения. Тем более, что Data Mapper всё равно регистрируется в QML и к его слотам идёт обращение из QML, чтобы пролистнуть строку, например, то почему бы тоже самое не проделать и с методом addMapping(). Таким образом, отпадает необходимость с помощью методов findChild искать нужный Нам объект. Мы просто передаём этот объект из QML слоя по свойству id.
  6. Добавлен новый метод updateDate(int index) . При установке параметров в Data Mapper объекты QML сразу не получали обновление данных, пришлось разбавить данным костылём.

Структура проекта для демонстрации работы QML Data Mapper

Проект состоит из уже известных файлов по уроку с QML QSqlQueryModel, а также имеет новый класс QmlDataMapper. И также в проект добавлен файл DialogMapper.qml , который представляет собой диалоговое окно, элементам управления которых будут сопоставляться данные из модели данных базы данных SQLite.

QmlSqlQueryModel.pro

TEMPLATE = app

QT += qml quick widgets sql

SOURCES += main.cpp \
    database.cpp \
    model.cpp \
    qmldatamapper.cpp

RESOURCES += qml.qrc

# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =

# Default rules for deployment.
include(deployment.pri)

HEADERS += \
    database.h \
    model.h \
    qmldatamapper.h

database.h

База данных создаётся и наполняется данными при помощи класса-обёртки, который также использовался и в уроках по работе с QSqlTableModel и QSqlRelationalTableModel .

ВНИМАНИЕ!!! - файл базы данных создается в папке C:/example , поэтому или поправьте метод DataBase::connectToDataBase() или создайте папку example на диске C .

#ifndef DATABASE_H
#define DATABASE_H

#include <QObject>
#include <QSql>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlDatabase>
#include <QFile>
#include <QDate>
#include <QDebug>

/* Директивы имен таблицы, полей таблицы и базы данных */
#define DATABASE_HOSTNAME   "ExampleDataBase"
#define DATABASE_NAME       "DataBase.db"

#define TABLE                   "TableExample"
#define TABLE_DATE              "date"
#define TABLE_TIME              "time"
#define TABLE_MESSAGE           "message"
#define TABLE_RANDOM            "random"

class DataBase : public QObject
{
    Q_OBJECT
public:
    explicit DataBase(QObject *parent = 0);
    ~DataBase();
    /* Методы для непосредственной работы с классом
     * Подключение к базе данных и вставка записей в таблицу
     * */
    void connectToDataBase();
    bool inserIntoTable(const QVariantList &data);

private:
    // Сам объект базы данных, с которым будет производиться работа
    QSqlDatabase    db;

private:
    /* Внутренние методы для работы с базой данных
     * */
    bool openDataBase();
    bool restoreDataBase();
    void closeDataBase();
    bool createTable();
};

#endif // DATABASE_H

database.cpp

#include "database.h"

DataBase::DataBase(QObject *parent) : QObject(parent)
{
    // Подключаемся к базе данных
    this->connectToDataBase();
    /* После чего производим наполнение таблицы базы данных
     * контентом, который будет отображаться в TableView
     * */
    for(int i = 0; i < 4; i++){
        QVariantList data;
        int random = qrand(); // Получаем случайные целые числа для вставки а базу данных
        data.append(QDate::currentDate()); // Получаем текущую дату для вставки в БД
        data.append(QTime::currentTime()); // Получаем текущее время для вставки в БД
        // Подготавливаем полученное случайное число для вставки в БД
        data.append(random);
        // Подготавливаем сообщение для вставки в базу данных
        data.append("Получено сообщение от " + QString::number(random));
        // Вставляем данные в БД
        inserIntoTable(data);
    }
}

DataBase::~DataBase()
{

}

/* Методы для подключения к базе данных
 * */
void DataBase::connectToDataBase()
{
    /* Перед подключением к базе данных производим проверку на её существование.
     * В зависимости от результата производим открытие базы данных или её восстановление
     * */
    if(!QFile("C:/example/" DATABASE_NAME).exists()){
        this->restoreDataBase();
    } else {
        this->openDataBase();
    }
}

/* Методы восстановления базы данных
 * */
bool DataBase::restoreDataBase()
{
    if(this->openDataBase()){
        if(!this->createTable()){
            return false;
        } else {
            return true;
        }
    } else {
        qDebug() << "Не удалось восстановить базу данных";
        return false;
    }
    return false;
}

/* Метод для открытия базы данных
 * */
bool DataBase::openDataBase()
{
    /* База данных открывается по заданному пути
     * и имени базы данных, если она существует
     * */
    db = QSqlDatabase::addDatabase("QSQLITE");
    db.setHostName(DATABASE_HOSTNAME);
    db.setDatabaseName("C:/example/" DATABASE_NAME);
    if(db.open()){
        return true;
    } else {
        return false;
    }
}

/* Методы закрытия базы данных
 * */
void DataBase::closeDataBase()
{
    db.close();
}

/* Метод для создания таблицы в базе данных
 * */
bool DataBase::createTable()
{
    /* В данном случае используется формирование сырого SQL-запроса
     * с последующим его выполнением.
     * */
    QSqlQuery query;
    if(!query.exec( "CREATE TABLE " TABLE " ("
                            "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                            TABLE_DATE      " DATE            NOT NULL,"
                            TABLE_TIME      " TIME            NOT NULL,"
                            TABLE_RANDOM    " INTEGER         NOT NULL,"
                            TABLE_MESSAGE   " VARCHAR(255)    NOT NULL"
                        " )"
                    )){
        qDebug() << "DataBase: error of create " << TABLE;
        qDebug() << query.lastError().text();
        return false;
    } else {
        return true;
    }
    return false;
}

/* Метод для вставки записи в базу данных
 * */
bool DataBase::inserIntoTable(const QVariantList &data)
{
    /* Запрос SQL формируется из QVariantList,
     * в который передаются данные для вставки в таблицу.
     * */
    QSqlQuery query;
    /* В начале SQL запрос формируется с ключами,
     * которые потом связываются методом bindValue
     * для подстановки данных из QVariantList
     * */
    query.prepare("INSERT INTO " TABLE " ( " TABLE_DATE ", "
                                             TABLE_TIME ", "
                                             TABLE_RANDOM ", "
                                             TABLE_MESSAGE " ) "
                  "VALUES (:Date, :Time, :Random, :Message )");
    query.bindValue(":Date",        data[0].toDate());
    query.bindValue(":Time",        data[1].toTime());
    query.bindValue(":Random",      data[2].toInt());
    query.bindValue(":Message",     data[3].toString());
    // После чего выполняется запросом методом exec()
    if(!query.exec()){
        qDebug() << "error insert into " << TABLE;
        qDebug() << query.lastError().text();
        return false;
    } else {
        return true;
    }
    return false;
}

model.h

Модель представления данных остаётся неизменной по сравнению с предыдущим уроком. Отмечу, что передавать номер секции в QML слое нужно в соответствии с номерами ролей в этой модели данных. То есть, если колонка, которая будет сопоставляться с элементом управления, имеет номер роли Qt:UserRole + 4 , то в QML слое установка соответствия должна будет производиться следующей комбинацией 0x0100 + 4.

#ifndef MODEL_H
#define MODEL_H

#include <QObject>
#include <QSqlQueryModel>

class Model : public QSqlQueryModel
{
    Q_OBJECT
public:
    /* Перечисляем все роли, которые будут использоваться в TableView
     * Как видите, они должны лежать в памяти выше параметра Qt::UserRole
     * Связано с тем, что информация ниже этого адреса не для кастомизаций
     * */
    enum Roles {
        DateRole = Qt::UserRole + 1,    // дата
        TimeRole,                       // время
        RandomRole,                     // псевдослучаное число
        MessageRole                     // сообщение
    };

    // объявляем конструктор класса
    explicit Model(QObject *parent = 0);

    // Переопределяем метод, который будет возвращать данные
    QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;

protected:
    /* хешированная таблица ролей для колонок.
     * Метод используется в дебрях базового класса QAbstractItemModel,
     * от которого наследован класс QSqlQueryModel
     * */
    QHash<int, QByteArray> roleNames() const;

signals:

public slots:
};

#endif // MODEL_H

model.cpp

#include "model.h"

Model::Model(QObject *parent) :
    QSqlQueryModel(parent)
{
    // Конструктор будет пустой ;-)
}

// Метод для получения данных из модели
QVariant Model::data(const QModelIndex & index, int role) const {

    // Определяем номер колонки, адрес так сказать, по номеру роли
    int columnId = role - Qt::UserRole - 1;
    // Создаём индекс с помощью новоиспечённого ID колонки
    QModelIndex modelIndex = this->index(index.row(), columnId);

    /* И с помощью уже метода data() базового класса
     * вытаскиваем данные для таблицы из модели
     * */
    return QSqlQueryModel::data(modelIndex, Qt::DisplayRole);
}

// Метод для получения имен ролей через хешированную таблицу.
QHash<int, QByteArray> Model::roleNames() const {
    /* То есть сохраняем в хеш-таблицу названия ролей
     * по их номеру
     * */
    QHash<int, QByteArray> roles;
    roles[DateRole] = "date";
    roles[TimeRole] = "time";
    roles[RandomRole] = "random";
    roles[MessageRole] = "message";
    return roles;
}

qmldatamapper.h

А теперь то, что касается самого класса Data Mapper для QML. Данный класс может использоваться без изменений для любых других моделей данных, которые организованы по идентичному принципу, как и выше приведённая модель данных, а также наследованных от QAbstractItemModel и её производных классов.

#ifndef QMLDATAMAPPER_H
#define QMLDATAMAPPER_H

/****************************************************************************
 ** Ниже следующий программный код является результатом доработки программного
 ** кода для платформы BlackBerry 10 из Qt Toolkit и также распростряется
 ** по ниже следующей лицензии, которая была применена к исходному програмному коду
 **
 ** EVILEG - Evgenii Legotckoi - 2015
 ** Contact: http://www.evileg.com
 ** Contact: Evgenii Legotckoi (legotskoy@gmail.com)
 ** All rights reserved.
 ****************************************************************************/

/****************************************************************************
 **
 ** Portions Copyright (C) 2012 Research In Motion Limited.
 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
 ** All rights reserved.
 ** Contact: Research In Motion Ltd. )
 ** Contact: Nokia Corporation (qt-info@nokia.com)
 **
 ** This file is part of the examples of the BB10 Platform and is derived
 ** from a similar file that is part of the Qt Toolkit.
 **
 ** You may use this file under the terms of the BSD license as follows:
 **
 ** "Redistribution and use in source and binary forms, with or without
 ** modification, are permitted provided that the following conditions are
 ** met:
 **   * Redistributions of source code must retain the above copyright
 **     notice, this list of conditions and the following disclaimer.
 **   * Redistributions in binary form must reproduce the above copyright
 **     notice, this list of conditions and the following disclaimer in
 **     the documentation and/or other materials provided with the
 **     distribution.
 **   * Neither the name of Research In Motion, nor the name of Nokia
 **     Corporation and its Subsidiary(-ies), nor the names of its
 **     contributors may be used to endorse or promote products
 **     derived from this software without specific prior written
 **     permission.
 **
 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
 **
 ****************************************************************************/

#include <QObject>
#include <QPointer>
#include <QAbstractItemModel>
#include <QModelIndex>
#include <QQuickItem>

class QmlDataMapperPrivate;

/**
 * Класс QmlDataMapper предоставляет mapping между строками модели данных и представлением в виджете QML.
 * QmlDataMapper может быть использован для создания элементов управления базой даных путём сопоставления их со строками модели данных.
 * Каждый раз, когда изменяется текущий индекс, каждый элемент управления обновляется данными из модели.
 */

class QmlDataMapper : public QObject
{
    Q_OBJECT

    /**
     * Текущий индекс QmlDataMapper. Если используется SQL модель на основе SQL таблицы, то
     * индекс будет между 0 и числом строк. .
     */
    Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)

    // Количество строк, предодставленных моделью
    Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
    explicit QmlDataMapper(QObject *parent = 0);

    ~QmlDataMapper();

    /**
     * Удаляет все сопоставления, которые были созданы методом addMapping().
     */
    void clearMapping();

    // Удаляет сопоставление по объекту из QML слоя
    void removeMapping(QObject *object);

    // Методы доступа к свойствам
    int currentIndex() const;
    int count() const;

    // Возвращает им свойства, которое используется для обновления значений в элементе управления
    QByteArray mappedPropertyName(QObject *object) const;

    // Returns the section identifier that is mapped to the given control
    int mappedSection(QObject *object) const;

    // Возвращает элемент управления, которой сопоставлено с идентификатором секции в модели
    QObject * mappedControlAt(const int &section) const;

    // Возвращает модель данных, с которой работает QmlDataMapper
    QAbstractItemModel * model() const;

    // Устанавливает модель, с которой будет работать QmlDataMapper
    void setModel(QAbstractItemModel *model);

public Q_SLOTS:
    /**
     * Этот метод создаёт сопоставление между элементов упралвения и идентификатором секции
     * в модели данных.
     * Для SQL модели идентификатором секции является но роли колонки в модели представления данных
     * Данные будут установлены в свойство элемента управления, которые зависят от свойства данного элемента управления
     * Данный метод используется для установки элемента управления и секции без наименования свойства.
     * И в данном случае используется только наименование "text", в которое будут подставляться данные
     */
    void addMapping(QObject *object, const int &section);

    /**
     * Этот метод создаёт сопоставление между элементов упралвения и идентификатором секции
     * в модели данных.
     * Для SQL модели идентификатором секции является но роли колонки в модели представления данных
     * Данные будут подставляться в элемент управление в указанное свойство
     */
    void addMapping(QObject *object, const int &section, const QByteArray &propertyName);

    // Данный метод сбрасывает данные в элементе управления
    void revert();

    // Данный метод устанавливает индекс по заданном значению
    void setCurrentIndex(int index);

    // Данный метод устанавливает индекс первой строки
    void toFirst();

    // Данный метод устанавливает индекс последней строки
    void toLast();

    // Данный метод производит инкремент текущего индекса строки
    void toNext();

    // Данный метод производит декремент текущего индекса строки
    void toPrevious();

    // Обновление данных по заданном индексу
    void updateData(int index);

Q_SIGNALS:
    // Сигналы уведомления об изменении для свойств класса
    void currentIndexChanged(int index);
    void countChanged();

private:
    // Private класс, который скрыт API данного класса
    QmlDataMapperPrivate * const d;
};

#endif // QMLDATAMAPPER_H

qmldatamapper.cpp

/****************************************************************************
 ** Ниже следующий программный код является результатом доработки программного
 ** кода для платформы BlackBerry 10 из Qt Toolkit и также распростряется
 ** по ниже следующей лицензии, которая была применена к исходному програмному коду
 **
 ** EVILEG - Evgenii Legotckoi - 2015
 ** Contact: http://www.evileg.com
 ** Contact: Evgenii Legotckoi (legotskoy@gmail.com)
 ** Contact: Евгений Легоцкой (legotskoy@gmail.com)
 ** All rights reserved.
 ****************************************************************************/

/****************************************************************************
 **
 ** Portions Copyright (C) 2012 Research In Motion Limited.
 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
 ** All rights reserved.
 ** Contact: Research In Motion Ltd. )
 ** Contact: Nokia Corporation (qt-info@nokia.com)
 **
 ** This file is part of the examples of the BB10 Platform and is derived
 ** from a similar file that is part of the Qt Toolkit.
 **
 ** You may use this file under the terms of the BSD license as follows:
 **
 ** "Redistribution and use in source and binary forms, with or without
 ** modification, are permitted provided that the following conditions are
 ** met:
 **   * Redistributions of source code must retain the above copyright
 **     notice, this list of conditions and the following disclaimer.
 **   * Redistributions in binary form must reproduce the above copyright
 **     notice, this list of conditions and the following disclaimer in
 **     the documentation and/or other materials provided with the
 **     distribution.
 **   * Neither the name of Research In Motion, nor the name of Nokia
 **     Corporation and its Subsidiary(-ies), nor the names of its
 **     contributors may be used to endorse or promote products
 **     derived from this software without specific prior written
 **     permission.
 **
 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
 **
 ****************************************************************************/

#include "qmldatamapper.h"
#include <QDataWidgetMapper>

struct Mapping {
    QPointer <QObject> object;
    int section;
    QByteArray propertyName;
};

/**
 * Private класс, который содержит все скрытые методы и переменные.
 * Используется концепция, которая позволяет изменять внутреннее API без воздействия на внешнее API
 * (see 
 */
class QmlDataMapperPrivate
{
public:
    QmlDataMapperPrivate()
        : m_model(0), m_currentIndex(-1)
    {
    }

    // Вспомогательный метод обновления участников по заданным сопоставления с заданными параметрами
    void updateMapping(Mapping &mapping, QObject *object, const int &section, const QByteArray &propertyName);

    // Данный метод устанавливает актуальные сопоставления данных из модели данных в элементы управления
    void update();

    // Модель данных, с которой производится работа
    QAbstractItemModel* m_model;

    // Список сопоставлений, котроые установленны
    QVector<Mapping> m_mappings;

    // Текущий индекс QmlDataMapper
    int m_currentIndex;
};

void QmlDataMapperPrivate::updateMapping(Mapping &mapping, QObject *object, const int &section, const QByteArray &propertyName)
{
    mapping.object = object;
    mapping.section = section;

    // Если свойство имени не задано, то по умолчанию используется свойство "text"
    mapping.propertyName = (propertyName.isEmpty() ? "text" : propertyName);
}

void QmlDataMapperPrivate::update()
{
    // Список проверок перед обновлением данных
    if (!m_model)
        return;

    if (m_mappings.isEmpty())
        return;

    if (m_currentIndex == -1)
        return;

    // Перебор всех доступных сопоставлений
    foreach (const Mapping &mapping, m_mappings) {
        if (mapping.object) {
            // Обновляем данные элементов управления, устанавливая данные в свойства по роли
            mapping.object->setProperty(mapping.propertyName, m_model->data(m_model->index(m_currentIndex,0), mapping.section));
        }
    }
}

QmlDataMapper::QmlDataMapper(QObject *parent)
        : QObject(parent), d(new QmlDataMapperPrivate())
{

}

QmlDataMapper::~QmlDataMapper()
{
    delete d;
}


void QmlDataMapper::addMapping(QObject *object, const int &section)
{
    // Добавление сопоставление с свойство по умолчанию
    addMapping(object, section, "text");
}

void QmlDataMapper::addMapping(QObject *object, const int &section, const QByteArray &propertyName)
{
    // Проверка на то, что добавляемое сопоставление уже существует ...
    for (int i = 0; i < d->m_mappings.count(); ++i) {
        Mapping &mapping = d->m_mappings[i];
        if (mapping.object == object) {
            // ... в случае существования сопоставление, производим обновление данных о секции и свойстве элемента управления ...
            d->updateMapping(mapping, object, section, propertyName);

            // ... и производим обновление содержимого элемента управления
            d->update();
            return;
        }
    }

    // В противном случае добавляем новое сопоставление
    Mapping mapping;
    d->updateMapping(mapping, object, section, propertyName);
    d->m_mappings.append(mapping);

    // ... и производим обновление содержимого элемента управления
    d->update();
}

void QmlDataMapper::clearMapping()
{
    // Очистка списка сопоставлений
    d->m_mappings.clear();
}

int QmlDataMapper::currentIndex() const
{
    return d->m_currentIndex;
}

int QmlDataMapper::count() const
{
    if (!d->m_model)
        return 0;

    // Возвращаем число строк в модели представления данных
    return d->m_model->rowCount();
}

QByteArray QmlDataMapper::mappedPropertyName(QObject *object) const
{
    foreach(const Mapping &mapping, d->m_mappings) {
        if (mapping.object == object)
        return mapping.propertyName;
    }

    return QByteArray();
}

int QmlDataMapper::mappedSection(QObject *object) const
{
    foreach(const Mapping &mapping, d->m_mappings) {
        if (mapping.object == object)
        return mapping.section;
    }

    return 0;
}

QObject* QmlDataMapper::mappedControlAt(const int &section) const
{
    foreach(const Mapping &mapping, d->m_mappings) {
        if (mapping.section == section)
        return mapping.object;
    }

    return 0;
}

QAbstractItemModel* QmlDataMapper::model() const
{
    return d->m_model;
}

void QmlDataMapper::removeMapping(QObject *object)
{
    for (int i = 0; i < d->m_mappings.count(); ++i) {
        if (d->m_mappings[i].object == object) {
            d->m_mappings.remove(i);
            return;
        }
    }
}

void QmlDataMapper::setModel(QAbstractItemModel *model)
{
    d->m_model = model;

    // Установка первоначального индекса содержимого модели представления данных
    d->m_currentIndex = 0;

    // Производим обновление содержимого элемента управления
    d->update();
    emit countChanged();
}

void QmlDataMapper::revert()
{
    // производим обновление содержимого элемента управления
    d->update();
}

void QmlDataMapper::setCurrentIndex(int index)
{
    // Проверка на наличие модели данных
    if (!d->m_model)
        return;

    // получаем число строк
    const int rowCount = d->m_model->rowCount();
    // ... игнорируем неверные значения индекса
    if (index < 0 || index >= rowCount)
        return;

    d->m_currentIndex = index;
    d->update();
    emit currentIndexChanged(d->m_currentIndex);
}

void QmlDataMapper::toFirst()
{
    setCurrentIndex(0);
}

void QmlDataMapper::toLast()
{
    if (!d->m_model)
        return;

    const int rowCount = d->m_model->rowCount();

    setCurrentIndex(rowCount - 1);
}

void QmlDataMapper::toNext()
{
    setCurrentIndex(d->m_currentIndex + 1);
}

void QmlDataMapper::toPrevious()
{
    setCurrentIndex(d->m_currentIndex - 1);
}

void QmlDataMapper::updateData(int index)
{
    // Устанавливаем требуемый индекс
    d->m_currentIndex = index;

    // и обновляем значения в элементах управления
    d->update();
    emit countChanged();
}

main.cpp

Для работы с Data Mapper в C++ части необходимо лишь объявить его, инициализировать и зарегистрировать доступ к нему из QML слоя с помощью метода setContextProperty("mapper", mapper). Ну и не забыть поместить в него модель представления данных методом setModel().

Вся остальная работа с Data Mapper будет производиться в QML слое. В том числе и настройка сопоставлений объектов с колонками модели данных.

#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>

#include <qmldatamapper.h>
#include <database.h>
#include <model.h>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QQmlApplicationEngine engine;

    /// Инициализируем базу данных
    DataBase database;
    /// Объявляем и инициализируем модель представления данных
    Model *model = new Model();
    /** Поскольку Мы отнаследовались от QSqlQueryModel, то
     * для выборки данных нам необходимо выполнить SQL-запрос,
     * в котором мы выберем все необходимы поля из нужной нам таблицы
     * */
    model->setQuery("SELECT " TABLE_DATE ", " TABLE_TIME ", " TABLE_RANDOM ", " TABLE_MESSAGE
                   " FROM " TABLE);

    // Объявляем и инициализируем объект QmlDataMapper
    QmlDataMapper *mapper = new QmlDataMapper();
    mapper->setModel(model);

    /** А это уже знакомо из уроков по сигналам и слотам в QML
     * Помещаем полученную модель в контекст QML, чтобы была возможность
     * обращаться к модели по имени "myModel"
     * */
    engine.rootContext()->setContextProperty("myModel", model);

    /* А также даём доступ к Mapper в контексте QML
     * */
    engine.rootContext()->setContextProperty("mapper", mapper);

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

main.qml

В данном файле объявляются слой с кнопкой, по нажатию которой будет открываться диалоговое окно, и TableView, в котором будут отображаться данные из модели данных. Также диалоговое окно будет открываться по двойному клику по строке в TableView. Кнопка будет открывать диалоговое окно, в котором будет отображена информация о самой первой строке в таблице. Двойной клик будет открывать диалоговое окно с информацией о той строке, по которой мы кликнули.

import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.1

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    RowLayout {
        id: row

        anchors.top: parent.top
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.margins: 5

        height: 35

        Button {
            id: button
            text: qsTr("Открыть Mapper")
            width: 150

            Layout.fillHeight: true

            // Открываем диалоговое окно с индексом первой строки в TableView
            onClicked: {
                dialog.show()
            }
        }

    }

    TableView {
        id: tableView
        anchors.top: row.bottom
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.margins: 5

        TableViewColumn {
            role: "date"    // Эти роли совпадают с названиями ролей в C++ модели
            title: "Date"
        }

        TableViewColumn {
            role: "time"    // Эти роли совпадают с названиями ролей в C++ модели
            title: "Time"
        }

        TableViewColumn {
            role: "random"  // Эти роли совпадают с названиями ролей в C++ модели
            title: "Random"
        }

        TableViewColumn {
            role: "message" // Эти роли совпадают с названиями ролей в C++ модели
            title: "Message"
        }

        // Устанавливаем модель в TableView
        model: myModel

        // Внешний вид строк настраивается для реализации реакции на даблКлик
        rowDelegate: Rectangle {
            anchors.fill: parent
            color: styleData.selected ? 'skyblue' : (styleData.alternate ? 'whitesmoke' : 'white');
            MouseArea {
                anchors.fill: parent
                acceptedButtons: Qt.RightButton | Qt.LeftButton
                onClicked: {
                    tableView.selection.clear()
                    tableView.selection.select(styleData.row)
                    tableView.currentRow = styleData.row
                    tableView.focus = true
                }

                onDoubleClicked: {
                    /* при даблКлике по строке открываем диалоговое окно
                     * с информацией из соответствующей строки
                     * */
                    dialog.editEntry(styleData.row)
                }
            }
        }
    }

    DialogMapper {
        id: dialog
    }
}

DialogMapper.qml

Диалоговое окно пишется в отдельном файле. Работа с QML Data Mapper сводится лишь к добавлению объектов QML в Data Mapper , перелистыванию строк модели данных, а также установке индекса строки в Data Mapper для получения данных конкретной строки из таблицы, если  диалоговое окно вызывается двойным кликом по строке в TableView.

import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.1

Dialog {
    title: qsTr("Dialog Mapper")
    height: 220
    width: 480

    // функция, которая открывает диалоговое окно с данными из первой строки
    function show() {
        open()
        mapper.updateData(0)
    }

    /* Функция, которая открывает диалоговое окно с данными из строки,
     * по которой был произведён даблКлик
     * */
    function editEntry(row) {
        open()
        mapper.updateData(row)
    }

    contentItem: Rectangle {
        implicitHeight: 220
        implicitWidth: 480

        GridLayout {
            anchors.top: parent.top
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.margins: 5
            rowSpacing: 10
            columnSpacing: 10
            rows: 4
            columns: 2

            Text {
                text: qsTr("Дата")
                Layout.fillWidth: true
            }

            TextField {
                id: dateField  // По данному id передаём объект в Data Mapper
                Layout.preferredWidth: 200
            }

            Text {
                text: qsTr("Время")
                Layout.fillWidth: true
            }

            TextField {
                id: timeField   // По данному id передаём объект в Data Mapper
                Layout.preferredWidth: 200
            }

            Text {
                text: qsTr("Случайное число")
                Layout.fillWidth: true
            }

            TextField {
                id: randomField  // По данному id передаём объект в Data Mapper
                Layout.preferredWidth: 200
            }

            Text {
                text: qsTr("Сообщение")
                Layout.fillWidth: true
            }

            TextField {
                id: messageField  // По данному id передаём объект в Data Mapper
                Layout.preferredWidth: 200
            }
        }

        Rectangle {
            color: "#eeeeee"
            height: 50
            anchors.bottom: parent.bottom
            anchors.left: parent.left
            anchors.right: parent.right

            RowLayout {
                anchors.bottom: parent.bottom
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.margins: 5
                spacing: 10

                // Кнопка пролистывает строки от последней к первой
                Button {
                    id: buttonPrevious
                    text: qsTr("Предыдущий")
                    Layout.preferredWidth: 80

                    onClicked: {
                        mapper.toPrevious()
                    }
                }

                // Кнопка пролистывает строки от первой к последней
                Button {
                    id: buttonNext
                    text: qsTr("Следующий")
                    Layout.preferredWidth: 80

                    onClicked: {
                        mapper.toNext()
                    }
                }

                Rectangle {
                    Layout.fillWidth: true
                    color: "#eeeeee"
                }

                Button {
                    id: buttonOk
                    text: qsTr("Ok")
                    Layout.preferredWidth: 80
                    onClicked: close()
                }

                Button {
                    id: buttonCancel
                    text: qsTr("Cancel")
                    Layout.preferredWidth: 80
                    onClicked: close()
                }
            }
        }
    }

    /* По результату создания диалогового окна добавляем
     * в Data Mapper объекты QML по их id, с указанием номера роли в модели данных
     * и свойством объекта, в которое будут подставляться данные.
     *
     * То есть: dateField  - это id объекта TextField
     *          0x0100 + 1 - это DateRole модели, которая равна Qt::UserRole + 1
     *          "text"     - это свойство объекта TextField, куда будут подставляться данные
     * */
    Component.onCompleted: {
        mapper.addMapping(dateField, (0x0100 + 1), "text")
        mapper.addMapping(timeField, (0x0100 + 2), "text")
        mapper.addMapping(randomField, (0x0100 + 3), "text")
        mapper.addMapping(messageField, (0x0100 + 4), "text")
    }
}

Итог

Подытоживая выше приведённую информацию, можно сказать, что всё новое - это хорошо забытое старое. По сути можно перелопатить уже существующие наработки по BlackBerry и портировать программный код под нынешний QML Qt для Desktop и Android, доработав недостающие или нереализованные объекты и классы для работы с QML. Впрочем, разработчикам Qt виднее, как строить развитие фреймворка.

Данный класс не является полным аналогом QDataWidgetMapper, поскольку не реализует редактирование записей в базе данных. Но при этом он выполняет своё прямое назначение.

Результат работы приложения приведён на ниже следующем рисунке. Также в видеоуроке продемонстрирована работа приложения.

Исходный код проекта можно скачать по следующей ссылке: Исходник проекта с QmlDataMapper

Видеоурок

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
ДА

C++ - Тест 003. Условия и циклы

  • Результат:21баллов,
  • Очки рейтинга-10
АФ

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:30баллов,
  • Очки рейтинга-10
ПЩ

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80баллов,
  • Очки рейтинга4
Последние комментарии
k
kmssr9 февраля 2024 г. 5:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 12:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 21:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 19:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 8:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
P
Pisych27 февраля 2023 г. 15:04
Как получить в массив значения из связанной модели? Спасибо, разобрался:))
AC
Alexandru Codreanu19 января 2024 г. 22:57
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…
BlinCT
BlinCT27 декабря 2023 г. 19:57
Растягивать Image на парент по высоте Ну и само собою дял включения scrollbar надо чтобы был Flickable. Так что выходит как то так Flickable{ id: root anchors.fill: parent clip: true property url linkFile p…
Дмитрий
Дмитрий10 января 2024 г. 15:18
Qt Creator загружает всю оперативную память Проблема решена. Удалось разобраться с помощью утилиты strace. Запустил ее: strace ./qtcreator Начал выводиться весь лог работы креатора. В один момент он начал считывать фай…
Evgenii Legotckoi
Evgenii Legotckoi12 декабря 2023 г. 17:48
Побуквенное сравнение двух строк Добрый день. Там случайно не высылается этот сигнал textChanged ещё и при форматировани текста? Если решиать в лоб, то можно просто отключать сигнал/слотовое соединение внутри слота и …

Следите за нами в социальных сетях