Evgenii Legotckoi
Evgenii Legotckoi11 листопада 2015 р. 08: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

база даних.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 хостинг.

Вам це подобається? Поділіться в соціальних мережах!

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
Дмитрий

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

  • Результат:60бали,
  • Рейтинг балів-1
Дмитрий

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

  • Результат:92бали,
  • Рейтинг балів8
d
  • dsfs
  • 26 квітня 2024 р. 11:56

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

  • Результат:80бали,
  • Рейтинг балів4
Останні коментарі
k
kmssr09 лютого 2024 р. 02:43
Qt Linux - Урок 001. Автозапуск програми Qt під Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко05 лютого 2024 р. 09:50
Qt WinAPI - Урок 007. Робота з ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 грудня 2023 р. 18:30
Boost - статичне зв&#39;язування в проекті CMake під Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 грудня 2023 р. 16:38
Boost - статичне зв&#39;язування в проекті CMake під Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 грудня 2023 р. 05:01
Qt/C++ - Урок 056. Підключення бібліотеки Boost в Qt для компіляторів MinGW і MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Тепер обговоріть на форумі
G
George1307 травня 2024 р. 07:27
добавить qlineseries в функции в функции: "GPlotter::addSeries(QString title, QVector &arr)" я вызываю метод setChart(...), я в конструктор передал адрес на QChartView элемент
BlinCT
BlinCT05 травня 2024 р. 12:46
Написать свой GraphsView Всем привет. В Qt есть давольно старый обьект дял работы с графиками ChartsView и есть в 6.7 новый но очень сырой и со слабым функционалом GraphsView. По этой причине я хочу написать х…
PS
Peter Son04 травня 2024 р. 00:57
Best Indian Food Restaurant In Cincinnati OH Ready to embark on a gastronomic journey like no other? Join us at App india restaurant and discover why we're renowned as the Best Indian Food Restaurant In Cincinnati OH . Whether y…
Evgenii Legotckoi
Evgenii Legotckoi02 травня 2024 р. 21:07
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.
IscanderChe
IscanderChe30 квітня 2024 р. 11:22
Во Flask рендер шаблона не передаётся в браузер Доброе утро! Имеется вот такой шаблон: <!doctype html><html> <head> <title>{{ title }}</title> <link rel="stylesheet" href="{{ url_…

Слідкуйте за нами в соціальних мережах