- 1. Структура проекта
- 2. QmlDataBase.pro
- 3. main.cpp
- 4. database.h
- 5. database.cpp
- 6. listmodel.h
- 7. listmodel.cpp
- 8. main.qml
- 9. Итог
- 10. Видеоурок
Небольшой пример по работе с базой данных в QML Qt . В данном уроке сводится воедино информация о применении сигналов и слотов в QML Qt , о доступе к C++ классам из QML слоя, реализации интерфейса приложения на QML, а также о реализации модели/представлении для таблицы базы данных.
База данных содержит таблицу со списком людей, в которой имеется четыре колонки:
- id (INTEGER) - уникальный номер записи;
- FirstName (VARCHAR (255)) - Имя;
- SurName (VARCHAR (255)) - Фамилия;
- Nik (VARCHAR (255)) - Ник.
Приложение должно реализовывать удаление и добавление записей в базу данных через интерфейс приложения. Для добавления записей в Базу данных будет использоваться три поля для ввода данных и кнопка, которая инициализирует добавление данных в базу данных SQL . Добавление записей в таблицу осуществляется через класс обёртку предназначенным для этого методом.Также в приложении присутствует класс ListModel , который реализует модель представления данных для отображения информации в TableView в слое QML.
Структура проекта
- QmlDataBase.pro - профайл проекта;
- database.h - заголовочный файл класса-обёртки для работы с базой данных;
- database.cpp - файл исходных кодов класса-обёртки для работы с базой данных;
- listmodel.h - заголовочный файл модели данных;
- listmodel.cpp - файл исходных кодов модели данных;
- main.cpp - основной файл исходных кодов приложения;
- main.qml - основной файл qml.
QmlDataBase.pro
Для работы с базой данных необходимо подключить модуль sql, а также модуль widgets для нативного внешнего вида приложения.
TEMPLATE = app QT += qml quick widgets sql SOURCES += main.cpp \ database.cpp \ listmodel.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 \ listmodel.h
main.cpp
В основном файле исходных кодов подключаем класс-обёртку для работы с базой данных и класс модели данных. Объекты этих классов необходимо объявить и инициализировать в данном файле, а также настроить доступ к этим объектам и их свойствам из QML слоя.
После того, как доступ к объектам настроен, из QML слоя становятся доступны следующие свойства и функции объекта, которые объявлены в его классе в качестве:
- Сигналов
- Слотов
- А также функции, которые фигурируют в макросе Q_PROPERTY
#include <QApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "database.h" #include "listmodel.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; // Подключаемся к базе данных DataBase database; database.connectToDataBase(); // Объявляем и инициализируем модель данных ListModel *model = new ListModel(); // Обеспечиваем доступ к модели и классу для работы с базой данных из QML engine.rootContext()->setContextProperty("myModel", model); engine.rootContext()->setContextProperty("database", &database); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
database.h
В данном классе объявлены методы для работы с базой данных:
- методы для подключения к базе данных, её восстановления;
- методы для добавления записи в базу данных;
- методы для удаления записей в таблице данных.
Подключение к базе данных необходимо для правильной работы модели данных, которая наследована от QSqlQueryModel . И соответственно использует SQL запросы к открытой в приложении базы данных.
Данный класс реализует паттерн проектирования Facade, хотя и не полностью, поскольку как я уже сказал, что одна из сущностей QSqlQueryModel используется в модели данных в этом уроке.
ВНИМАНИЕ!!! - файл базы данных создается в папке 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 "NameDataBase" #define DATABASE_NAME "Name.db" #define TABLE "NameTable" // Название таблицы #define TABLE_FNAME "FisrtName" // Вторая колонка #define TABLE_SNAME "SurName" // Третья колонка #define TABLE_NIK "Nik" // Четвертая колонка // Первая колонка содержит Autoincrement ID class DataBase : public QObject { Q_OBJECT public: explicit DataBase(QObject *parent = 0); ~DataBase(); /* Методы для непосредственной работы с классом * Подключение к базе данных и вставка записей в таблицу * */ void connectToDataBase(); private: // Сам объект базы данных, с которым будет производиться работа QSqlDatabase db; private: /* Внутренние методы для работы с базой данных * */ bool openDataBase(); // Открытие базы данных bool restoreDataBase(); // Восстановление базы данных void closeDataBase(); // Закрытие базы данных bool createTable(); // Создание базы таблицы в базе данных public slots: bool inserIntoTable(const QVariantList &data); // Добавление записей в таблицу bool inserIntoTable(const QString &fname, const QString &sname, const QString &nik); bool removeRecord(const int id); // Удаление записи из таблицы по её id }; #endif // DATABASE_H
database.cpp
Инициализация подключения к базе данных производится методом connectToDataBase() . Название таблицы, базы данных, файла, а также колонок в таблице определены в директивах define в заголовочном файле.
#include "database.h" DataBase::DataBase(QObject *parent) : QObject(parent) { } DataBase::~DataBase() { } /* Методы для подключения к базе данных * */ void DataBase::connectToDataBase() { /* Перед подключением к базе данных производим проверку на её существование. * В зависимости от результата производим открытие базы данных или её восстановление * */ if(!QFile("C:/example/" DATABASE_NAME).exists()){ this->restoreDataBase(); } else { this->openDataBase(); } } /* Методы восстановления базы данных * */ bool DataBase::restoreDataBase() { // Если база данных открылась ... if(this->openDataBase()){ // Производим восстановление базы данных return (this->createTable()) ? true : false; } 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_FNAME " VARCHAR(255) NOT NULL," TABLE_SNAME " VARCHAR(255) NOT NULL," TABLE_NIK " 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_FNAME ", " TABLE_SNAME ", " TABLE_NIK " ) " "VALUES (:FName, :SName, :Nik)"); query.bindValue(":FName", data[0].toString()); query.bindValue(":SName", data[1].toString()); query.bindValue(":Nik", data[2].toString()); // После чего выполняется запросом методом exec() if(!query.exec()){ qDebug() << "error insert into " << TABLE; qDebug() << query.lastError().text(); return false; } else { return true; } return false; } /* Второй метод для вставки записи в базу данных * */ bool DataBase::inserIntoTable(const QString &fname, const QString &sname, const QString &nik) { QVariantList data; data.append(fname); data.append(sname); data.append(nik); if(inserIntoTable(data)) return true; else return false; } /* Метод для удаления записи из таблицы * */ bool DataBase::removeRecord(const int id) { // Удаление строки из базы данных будет производитсья с помощью SQL-запроса QSqlQuery query; // Удаление производим по id записи, который передается в качестве аргумента функции query.prepare("DELETE FROM " TABLE " WHERE id= :ID ;"); query.bindValue(":ID", id); // Выполняем удаление if(!query.exec()){ qDebug() << "error delete row " << TABLE; qDebug() << query.lastError().text(); return false; } else { return true; } return false; }
listmodel.h
Модель данных является классом, наследованным от QSqlQueryModel, в котором переопределены методы data() и roleNames(). Также в классе перечислены роли, по которым передается информация в представление в интерфейсе. Для удаления данных необходимо получать уникальный ID записи, который вытягивается из модели по методом getID() по роли и номеру строки, который был передан из представления. Для получения данных из Базы данных используется метод updateModel(), в котором устанавливается SQL-запрос к Базе данных.
#ifndef LISTMODEL_H #define LISTMODEL_H #include <QObject> #include <QSqlQueryModel> class ListModel : public QSqlQueryModel { Q_OBJECT public: /* Перечисляем все роли, которые будут использоваться в TableView * Как видите, они должны лежать в памяти выше параметра Qt::UserRole * Связано с тем, что информация ниже этого адреса не для кастомизаций * */ enum Roles { IdRole = Qt::UserRole + 1, // id FNameRole, // имя SNameRole, // фамилия NikRole // ник }; // объявляем конструктор класса explicit ListModel(QObject *parent = 0); // Переопределяем метод, который будет возвращать данные QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; protected: /* хешированная таблица ролей для колонок. * Метод используется в дебрях базового класса QAbstractItemModel, * от которого наследован класс QSqlQueryModel * */ QHash<int, QByteArray> roleNames() const; signals: public slots: void updateModel(); int getId(int row); }; #endif // LISTMODEL_H
listmodel.cpp
Для реализации работы модели в файл исходных кодов подключаем заголовочный файл database.h, в котором имеются директивы define для названий таблицы и колонок. Возвращение данных производится по предопределенным ролям, которые должны быть идентичными в TableView, который будет использован для отображения данных.
#include "listmodel.h" #include "database.h" ListModel::ListModel(QObject *parent) : QSqlQueryModel(parent) { this->updateModel(); } // Метод для получения данных из модели QVariant ListModel::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> ListModel::roleNames() const { /* То есть сохраняем в хеш-таблицу названия ролей * по их номеру * */ QHash<int, QByteArray> roles; roles[IdRole] = "id"; roles[FNameRole] = "fname"; roles[SNameRole] = "sname"; roles[NikRole] = "nik"; return roles; } // Метод обновления таблицы в модели представления данных void ListModel::updateModel() { // Обновление производится SQL-запросом к базе данных this->setQuery("SELECT id, " TABLE_FNAME ", " TABLE_SNAME ", " TABLE_NIK " FROM " TABLE); } // Получение id из строки в модели представления данных int ListModel::getId(int row) { return this->data(this->index(row, 0), IdRole).toInt(); }
main.qml
В Qt парадигма модели/вида/контроллера изменена на модель/представление. Представление объединяет в себе контроллер и вид. Таким образом, main.qml обрабатывает информацию, которую вводит пользователь и передаёт её в backend в удобоваримом виде, но если учесть, что к объекту класса database дан доступ из QML слоя, а добавление данных производится через функцию слот, то условно можно принять, что контроллер реализован в представлении.
import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.2 ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Hello World") // Слой с TaxtField`ами и Button для занесения записей в базу данных RowLayout { id: rowLayout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 5 spacing: 10 Text {text: qsTr("Имя")} TextField {id: fnameField} Text {text: qsTr("Фамилия")} TextField { id: snameField} Text {text: qsTr("НИК")} TextField {id: nikField} Button { text: qsTr("Добавить") // Вносим новую запись в базу данных onClicked: { database.inserIntoTable(fnameField.text , snameField.text, nikField.text) myModel.updateModel() // И обновляем модель данных с новой записью } } } TableView { id: tableView anchors.top: rowLayout.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 5 TableViewColumn { role: "fname" title: "Имя" } TableViewColumn { role: "sname" title: "Фамилия" } TableViewColumn { role: "nik" title: "НИК" } model: myModel // Настройка строки в TableView для перехавата левого клика мыши 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 switch(mouse.button) { case Qt.RightButton: contextMenu.popup() // Вызываем контексткное меню break default: break } } } } } // Контекстно меню предлагает удаление строки из базы данных Menu { id: contextMenu MenuItem { text: qsTr("Удалить") onTriggered: { /* Вызываем диалоговое окно, * которое уточнит намерение удалить строку из базы данных * */ dialogDelete.open() } } } // Диалог подтверждения удаления строки из базы данных MessageDialog { id: dialogDelete title: qsTr("Удаление записи") text: qsTr("Подтвердите удаление записи из журнала") icon: StandardIcon.Warning standardButtons: StandardButton.Ok | StandardButton.Cancel // При положительном ответе ... onAccepted: { /* ... удаляем строку по id, * который забираем из модели данных * по номеру строки в представлении * */ database.removeRecord(myModel.getId(tableView.currentRow)) myModel.updateModel(); // Обновляем модель данных } } }
Итог
В результате Вы получите приложение, которое будет выглядеть так, как показано на ниже следующем рисунке. Также рекомендую к ознакомлению видеоурок по данной статье, поскольку в нём имеется более подробная информация и дополнения по данному программному коду.
Кроме этого рекомендую почитать подробнее о паттерне проектирования Facade и посмотреть более полный вариант реализации данного паттерна в следующей статье: Шаблон проектирования "Фасад"
Добрый день, пытаюсь передать текст из QML слоя в C++ при нажатии на кнопку. При нажатии на кнопку выводит ошибку.
что-то мне сдаётся, что здесь просто пересобрать проект нужно с удалением build каталога
Добрый день, подскажите пожалуйста как сделать изменение данных в таблице из запущенного приложения
Добрый день. В статье про это как раз и говорится. Там для этого есть метод inserIntoTable и он в сатье используется.
Добрый день, пытаюсь разобраться и подргнать пример под себя. Есть бд с огромным количеством полей. В приложении на виджетах при использовании QTableView все работает и путем простого sql запроса может вывести сразу всю таблицу. В qml же приложении я так понял жизненно необходимо определять роли. от этого никак не уйти? И как поступать когда ролей порядка 40, а таблиц много.
Второй вопрос : как ваш класс listmodel связывается с бд, т.е. каким образом он понимает , что она существует. проштудировал ваш пример и нигде нет ни ссылки ни намека на класс database, кроме подключения хедера. При попытке воспроизвести ваш класч listview в методе update в дебаге выдает сообщение о том что не удалось открыть бд. Заранее спасибо
Добрый день! можно как то обойтись без метода updateModel()? После вызова этого метода происходит перерисовка страницы(если я правильно понимаю), и все элементы, например, CheckBox перерисовываются и это очень заметно пользователю, либо если элемент имеет свойство скрыть/отобразить, то он принимает свое первоначальное состояние, а если в этом элементе работаешь и он после updateModel() скрывается не очень хорошо, опять разворачивай. Пробывал emit dataChanged(index, index, {role}) но ничего не происходит, модель не обновляет данные, только после updateModel(), происходит обновление модели, хотя в qml я меняю значение модели onClicked: {model.flag = checked;}. Может кто что подскажет?
updateModel() используется для выборки данных из базы данных, поэтому естественно, что если не выбирать данные, то ничего не обновится.
Что касается сохранения скрытого или развёрнутого состояния, то там нужно разбираться с временным сохранением состояния и после использования updateModel() восстанавливать состояние таблицы.
Проблема в том, что просто - это не сделать. Потребуется приличное количество дополнительного кода.
Помогите, пожалуйста. У меня похожая задача, но я в qml слой долен передать не чистый запрос, а со сложной обработкой, поэтому у меня в С++ слое есть иерархия классов, которая имитирует бд и заполняется при включении приложения, но после того, как там всё посчитается, мне нужно отобразить это в qml слое и тут я сломался. Никак не могу понять, как я могу из плюсового слоя наплодить элементов list view в qml слое? Выходит, мне из плюсового слоя нужно как-то заполнять делегат qml слоя и отображать его. Вообще не могу вшарить, как это происходит.
Добрый день. Я тоже присоеденяюсь к вопросу:
[s]"Второй вопрос : как ваш класс listmodel связывается с бд, т.е. каким образом он понимает , что она существует. проштудировал ваш пример и нигде нет ни ссылки ни намека на класс database, кроме подключения хедера. При попытке воспроизвести ваш класч listview в методе update в дебаге выдает сообщение о том что не удалось открыть бд. Заранее спасибо"[/s]
Смотрите, QSqlDatabase имеет статический метод addDatabase("QSQLITE"), который создаёт соединение с базой данных и возвращает инстанс базы данных
Поэтому все дальнейшие манипуляции с базой данных, если инстанс всего один, выполняются автоматически. Qt сам внутри себя хранит подключение к базе данных и выполняет все запросы через созданное соединение. Поэтому все SQL модели уже знают куда обращаться.
А сама инициализация этого подключения в данном кода выполняется в файле main.cpp
Здравствуйте, возникает такая проблема (я новичок):
ApplicationWindow неизвестный элемент. (М300)
для TextField и Button аналогично.
Могу предположить, что из-за более новой версии(я использую 6.7.2) в этой части:
Нужно указывать другие версии, но я не могу найти какие, можете подсказать, пожалуйста?