Для отображения данных в виджет с произвольной формой используется класс QDataMapperWidget . Для работы с этим виджетом по-прежнему требуется модель, для представления данных.
Например, QSqlTableModel или QSqlRelationalTableModel , но данные подставляются уже не в QTableView , а в различные произвольные объекты. Например QLineEdit или QComboBox. Или в диалоговое окно, для добавления записей, с которым поработаем в этой статье.
Итак, задача стоит следующая. Написать программу, которая выводит в таблицу список компьютеров, а каждый компьютер имеет три поля данных: Имя Хоста, IP-адрес и MAC-адрес. Также должна быть кнопка для вызова диалогового окна, через которое Мы можем добавить новое устройство в таблицу. Также у нас должна быть возможность редактирования записей через тоже самое диалоговое окно.
После того, как задача описана, приступаем к её реализации в программном коде, который был написан в QtCreator 3.3.1 на основе Qt 5.4.1.
Структура проекта для QDataWidgetMapper
Проект создается как Приложение Qt Widgets, в котором создаются следующие файлы:
- QDataMapperWidget.pro - профайл;
- mainwindow.h - заголовочный файл основного окна приложения;
- mainwindow.cpp - исходный код окна;
- main.cpp - основной исходный файл, с которого стартует приложение;
- mainwindow.ui - формочка основного окна приложения;
- database.h - заголовочный файл вспомогательного класса, применяющегося для работы с информацией, которая помещена в базу данных;
- database.cpp - исходный файл вспомогательного класса, применяющегося для работы с информацией, которая помещена в базу данных;
- dialogadddevice.h - заголовочный файл диалогового окна для добавления и редактирования записей;
- dialogadddevice.cpp - исходный файл диалогового окна для добавления и редактирования записей;
- dialogadddevice.ui
Примечание. Большую часть интерфейса создаю в дизайнере, чтобы не загромождать логику основного кода лишней информацией. По сути это лишь дело вкуса и привычки.
mainwindow.ui
Форма главного окна проекта QDataWidgetMapper Формочка главного окна будет простенькой. А пользоваться вы будем из этой формочки двумя объектами:
- addDeviceButton
- deviceTableView
dialogadddevice.ui
В формочке диалогового окна используются три поля QLineEdit , две кнопки и один ButtonBox , который является элементом по умолчанию для класса, который наследуется от класса QDialog.
Диалоговое окно QDataWidgetMapper Имеем в работе следующие объекты:
- HostnameLineEdit
- IPAddressLineEdit
- MACLineEdit
- buttonBox
- nextButton
- previousButton
QDataMapperWidget.pro
В Профайл проекта необходимо добавить директиву, которая объявляет об использовании библиотек SQL.
#------------------------------------------------- # # Project created by QtCreator 2015-08-16T23:58:29 # #------------------------------------------------- QT += core QT += gui QT += sql greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = QDataMapperWidget TEMPLATE = app SOURCES += main.cpp\ mainwindow.cpp \ database.cpp \ dialogadddevice.cpp HEADERS += mainwindow.h \ database.h \ dialogadddevice.h FORMS += mainwindow.ui \ dialogadddevice.ui
main.cpp
Файл используется в проекте, будучи созданным по умолчанию.
#include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }
mainwindow.h
В заголовочном файле главного окна определяем слоты для запуска диалога добавления записей, обновления модели и запуска диалога редактирования записей. Также имеются методы для инициализации внешнего вида окна, помимо основных настроек, выполняемых в дизайнере интерфейсов Qt, а также метод инициализации модели представления данных.
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QSqlTableModel> #include <database.h> #include <dialogadddevice.h> namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void on_addDeviceButton_clicked(); void slotUpdateModels(); void slotEditRecord(QModelIndex index); private: Ui::MainWindow *ui; DataBase *db; QSqlTableModel *modelDevice; private: void setupModel(const QString &tableName, const QStringList &headers); void createUI(); }; #endif // MAINWINDOW_H
mainwindow.cpp
В файле исходного кода основного окна производим инициализацию таблицы с данными, как было сделано в одной из прошлых статей, к примеру по работе с QSqlTableModel . А также прописываем логику поведения приложения при нажатии кнопки Добавить, которая вызывает диалог добавления записи в таблицу. Также этот диалог вызывается при двойном нажатии на запись в таблице данных. В этом случае в диалог передается информация о том, какая запись была нажата и её данные подставляются в поля для редактирования.
#include "mainwindow.h" #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); this->setWindowTitle("QDataWidgetMapper Example"); /* Первым делом необходимо создать объект для работы с базой данных * и инициализировать подключение к базе данных * */ db = new DataBase(); db->connectToDataBase(); /* Инициализируем модели для представления данных * с заданием названий колонок * */ this->setupModel(DEVICE, QStringList() << trUtf8("id") << trUtf8("Имя хоста") << trUtf8("IP адрес") << trUtf8("MAC-адрес") ); /* Инициализируем внешний вид таблицы с данными * */ this->createUI(); } MainWindow::~MainWindow() { delete ui; } void MainWindow::setupModel(const QString &tableName, const QStringList &headers) { /* Производим инициализацию модели представления данных * */ modelDevice = new QSqlTableModel(this); modelDevice->setTable(tableName); modelDevice->select(); /* Устанавливаем названия колонок в таблице с сортировкой данных * */ for(int i = 0, j = 0; i < modelDevice->columnCount(); i++, j++){ modelDevice->setHeaderData(i,Qt::Horizontal,headers[j]); } } void MainWindow::createUI() { ui->deviceTableView->setModel(modelDevice); // Устанавливаем модель на TableView ui->deviceTableView->setColumnHidden(0, true); // Скрываем колонку с id записей // Разрешаем выделение строк ui->deviceTableView->setSelectionBehavior(QAbstractItemView::SelectRows); // Устанавливаем режим выделения лишь одно строки в таблице ui->deviceTableView->setSelectionMode(QAbstractItemView::SingleSelection); // Устанавливаем размер колонок по содержимому ui->deviceTableView->resizeColumnsToContents(); ui->deviceTableView->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->deviceTableView->horizontalHeader()->setStretchLastSection(true); connect(ui->deviceTableView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(slotEditRecord(QModelIndex))); } /* Метод для активации диалога добавления записей * */ void MainWindow::on_addDeviceButton_clicked() { /* Создаем диалог и подключаем его сигнал завершения работы * к слоту обновления вида модели представления данных * */ DialogAddDevice *addDeviceDialog = new DialogAddDevice(); connect(addDeviceDialog, SIGNAL(signalReady()), this, SLOT(slotUpdateModels())); /* Выполняем запуск диалогового окна * */ addDeviceDialog->setWindowTitle(trUtf8("Добавить Устройство")); addDeviceDialog->exec(); } /* Слот обновления модели представления данных * */ void MainWindow::slotUpdateModels() { modelDevice->select(); } /* Метод для активации диалога добавления записей в режиме редактирования * с передачей индекса выбранной строки * */ void MainWindow::slotEditRecord(QModelIndex index) { /* Также создаем диалог и подключаем его сигнал завершения работы * к слоту обновления вида модели представления данных, но передаём * в качестве параметров строку записи * */ DialogAddDevice *addDeviceDialog = new DialogAddDevice(index.row()); connect(addDeviceDialog, SIGNAL(signalReady()), this, SLOT(slotUpdateModel())); /* Выполняем запуск диалогового окна * */ addDeviceDialog->setWindowTitle(trUtf8("Редактировать Устройство")); addDeviceDialog->exec(); }
dialogadddevice.h
Заголовочный файл диалога добавления и редактирования записей. Как видно по заголовочному файлу, здесь также используется модель для представления данных, но её данные транслируются не в QTableView , как например в уроке по QSqlRelationalModel или в классе MainWindow этой статьи, а в объект класса QDataWidgetMapper. Также здесь переопределяется метод accept(), поскольку прежде чем, закрывать окно, необходимо удостовериться, что данные заполнены верно. В данном проекте критерием правильности заполнения данных является отсутствие дублирующихся записей.
#ifndef DIALOGADDDEVICE_H #define DIALOGADDDEVICE_H #include <QDialog> #include <QSqlTableModel> #include <QDataWidgetMapper> #include <QMessageBox> #include <database.h> namespace Ui { class DialogAddDevice; } class DialogAddDevice : public QDialog { Q_OBJECT public: explicit DialogAddDevice(int row = -1, QWidget *parent = 0); ~DialogAddDevice(); signals: void signalReady(); private slots: void on_buttonBox_accepted(); void updateButtons(int row); private: Ui::DialogAddDevice *ui; QSqlTableModel *model; QDataWidgetMapper *mapper; private: void setupModel(); void createUI(); void accept(); }; #endif // DIALOGADDDEVICE_H
dialogadddevice.cpp
В данном файле реализована логика работы класса. Проверка на правильность введённых данных осуществляется в момент нажатия кнопки OK. В случае правильно заполненных полей производится вставка записи в таблицу или её редактирование. Валидация данных в полях IP адрес и MAC адрес производится с помощью Валидатора , написание которого было описано в одной из прошлых статей . Также реализована проверка на существование записи с похожими данными.
#include "dialogadddevice.h" #include "ui_dialogadddevice.h" DialogAddDevice::DialogAddDevice(int row, QWidget *parent) : QDialog(parent), ui(new Ui::DialogAddDevice) { ui->setupUi(this); /* Метода для инициализации модели, * из которой будут транслироваться данные * */ setupModel(); /* Если строка не задана, то есть равна -1, * тогда диалог работает по принципу создания новой записи. * А именно, в модель вставляется новая строка и работа ведётся с ней. * */ if(row == -1){ model->insertRow(model->rowCount(QModelIndex())); mapper->toLast(); /* В противном случае диалог настраивается на заданную запись * */ } else { mapper->setCurrentModelIndex(model->index(row,0)); } createUI(); } DialogAddDevice::~DialogAddDevice() { delete ui; } /* Метод настройки модели данных и mapper * */ void DialogAddDevice::setupModel() { /* Инициализируем модель и делаем выборку из неё * */ model = new QSqlTableModel(this); model->setTable(DEVICE); model->setEditStrategy(QSqlTableModel::OnManualSubmit); model->select(); /* Инициализируем mapper и привязываем * поля данных к объектам LineEdit * */ mapper = new QDataWidgetMapper(); mapper->setModel(model); mapper->addMapping(ui->HostnameLineEdit, 1); mapper->addMapping(ui->IPAddressLineEdit, 2); mapper->addMapping(ui->MACLineEdit, 3); /* Ручное подтверждение изменения данных * через mapper * */ mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit); /* Подключаем коннекты от кнопок пролистывания * к прилистыванию модели данных в mapper * */ connect(ui->previousButton, SIGNAL(clicked()), mapper, SLOT(toPrevious())); connect(ui->nextButton, SIGNAL(clicked()), mapper, SLOT(toNext())); /* При изменении индекса в mapper изменяем состояние кнопок * */ connect(mapper, SIGNAL(currentIndexChanged(int)), this, SLOT(updateButtons(int))); } /* Метод для установки валидатора на поле ввода IP и MAC адресов * */ void DialogAddDevice::createUI() { QString ipRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; QRegExp ipRegex ("^" + ipRange + "\\." + ipRange + "\\." + ipRange + "\\." + ipRange + "$"); QRegExpValidator *ipValidator = new QRegExpValidator(ipRegex, this); ui->IPAddressLineEdit->setValidator(ipValidator); QString macRange = "(?:[0-9A-Fa-f][0-9A-Fa-f])"; QRegExp macRegex ("^" + macRange + "\\:" + macRange + "\\:" + macRange + "\\:" + macRange + "\\:" + macRange + "\\:" + macRange + "$"); QRegExpValidator *macValidator = new QRegExpValidator(macRegex, this); ui->MACLineEdit->setValidator(macValidator); } void DialogAddDevice::on_buttonBox_accepted() { /* SQL-запрос для проверки существования записи * с такими же учетными данными. * Если запись не существует или находится лишь индекс * редактируемой в данный момент записи, * то диалог позволяет вставку записи в таблицу данных * */ QSqlQuery query; QString str = QString("SELECT EXISTS (SELECT " DEVICE_HOSTNAME " FROM " DEVICE " WHERE ( " DEVICE_HOSTNAME " = '%1' " " OR " DEVICE_IP " = '%2' )" " AND id NOT LIKE '%3' )") .arg(ui->HostnameLineEdit->text(), ui->IPAddressLineEdit->text(), model->data(model->index(mapper->currentIndex(),0), Qt::DisplayRole).toString()); query.prepare(str); query.exec(); query.next(); /* Если запись существует, то диалог вызывает * предупредительное сообщение * */ if(query.value(0) != 0){ QMessageBox::information(this, trUtf8("Ошибка хоста"), trUtf8("Хост с таким именем или IP-адресом уже существует")); /* В противном случае производится вставка новых данных в таблицу * и диалог завершается с передачей сигнала для обновления * таблицы в главном окне * */ } else { mapper->submit(); model->submitAll(); emit signalReady(); this->close(); } } void DialogAddDevice::accept() { } /* Метод изменения состояния активности кнопок пролистывания * */ void DialogAddDevice::updateButtons(int row) { /* В том случае, если мы достигаем одного из крайних (самый первый или * самый последний) из индексов в таблице данных, * то мы изменяем состояние соответствующей кнопки на * состояние неактивна * */ ui->previousButton->setEnabled(row > 0); ui->nextButton->setEnabled(row < model->rowCount() - 1); }
database.h
Вспомогательный класс для работы с базой данных является видоизменённым вариантом этого же класса из прошлых уроков.
#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 DEVICE "DeviceTable" #define DEVICE_HOSTNAME "Hostname" #define DEVICE_IP "IP" #define DEVICE_MAC "MAC" class DataBase : public QObject { Q_OBJECT public: explicit DataBase(QObject *parent = 0); ~DataBase(); /* Методы для непосредственной работы с классом * Подключение к базе данных и вставка записей в таблицу * */ void connectToDataBase(); bool inserIntoDeviceTable(const QVariantList &data); private: // Сам объект базы данных, с которым будет производиться работа QSqlDatabase db; private: /* Внутренние методы для работы с базой данных * */ bool openDataBase(); bool restoreDataBase(); void closeDataBase(); bool createDeviceTable(); }; #endif // DATABASE_H
database.cpp
#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()){ if(!this->createDeviceTable()){ 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::createDeviceTable() { /* В данном случае используется формирование сырого SQL-запроса * с последующим его выполнением. * */ QSqlQuery query; if(!query.exec( "CREATE TABLE " DEVICE " (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " DEVICE_HOSTNAME " VARCHAR(255) NOT NULL," DEVICE_IP " VARCHAR(16) NOT NULL," DEVICE_MAC " VARCHAR(18) NOT NULL" " )" )){ qDebug() << "DataBase: error of create " << DEVICE; qDebug() << query.lastError().text(); return false; } else { return true; } return false; } /* Метод для вставки записи в таблицу устройств * */ bool DataBase::inserIntoDeviceTable(const QVariantList &data) { /* Запрос SQL формируется из QVariantList, * в который передаются данные для вставки в таблицу. * */ QSqlQuery query; /* В начале SQL запрос формируется с ключами, * которые потом связываются методом bindValue * для подстановки данных из QVariantList * */ query.prepare("INSERT INTO " DEVICE " ( " DEVICE_HOSTNAME ", " DEVICE_IP ", " DEVICE_MAC " ) " "VALUES (:Hostname, :IP, :MAC )"); query.bindValue(":Hostname", data[0].toString()); query.bindValue(":IP", data[1].toString()); query.bindValue(":MAC", data[2].toString()); // После чего выполняется запросом методом exec() if(!query.exec()){ qDebug() << "error insert into " << DEVICE; qDebug() << query.lastError().text(); return false; } else { return true; } return false; }
Итог
Архив с исходниками: QDataWidgetMapper
В результате работающее приложение должно выглядеть следующим образом:
Вопрос: Как записать данные в столбец который не привязан к элементу ввода в форме? Например в таблице
привязаны к lineedit все поля кроме object_id и brend_id. object id это автоинкремент с ним все понятно подставляется значение по умолчанию. Как с используя QDataWidgetMapper записывать определенное значение в brend_id? При редактировании записей никаких вопросов нет а вот при попытке добавления выдает ошибку.Либо добавить в форум соответствующий виджет и сделать его скрытым через setVisible(false) , либо написать собственный маппер, в котором некоторые поля будут обрабатываться внутри маппера по заданному алгоритму. Я изучал вопрос маппера в QML, вот статья на эту тему , но там не для QSqlTableModel , а просто для модели данных.
Если делать со скрытым виджетом, то следует отслеживать сигнал currentIndexChanged и уже по нему обрабатывать требуемые скрытые виджеты так, как требуется. В документации нет и намёка на работу со скрытыми полями каким-либо документированным способом, поэтому вариант со скрытыми виджетами видится мне наиболее адекватным.
Про скрытый виджет идея пришла сразу, но думал что есть другие варианты.
Увы, но других вариантов я не вижу. QDataWidgetMapper явно подходит только для этого, иначе был бы какой-нибудь класс держатель значений. Но тот же самый QVariant, что наиболее близкое по смыслу, здесь явно не подходит. Так что... только скрытый виджет.
Успешно заработало :-)
Ну и отлично :-)
Как сделать так, что бы когда я открываю диалог, я не могу ничего делать в программе, пока я не закрою сам диалог? (Я не заметил(плохо прочел код))
Очень долго искал подобное описание.Спасибо. Только подскажи пожалуйста, как можно реализовать удаление строки с помощью кнопки
Нужно навешать слот на клик кнопки
И еще, как можно из диалогового окна, из QTextEdit , передать данные в таблицу?
Спасибо большое, но немного не пойму, что именно нужно прописать в слоте, что бы получать id и удалять через кнопку
Вставка данных в таблицу базы данных производится через INSERT SQL запрос. Пример такого запроса есть в этой статье
Можете в Mapper`e прикрутить ещё одно поле ввода, которое будет выводить id записи. Поле ввода можете настроить в режим readOnly, чтобы его нельзя было редактировать. Тогда сможете забирать через метод text() или value() данный id записи.
Хорошо, спасибо большое за помощь
Evileg, Здравствуйте. Можете пожалуйста пояснить, вот есть метод:
Добрый день!
Так получилось, что конкретно этот метод не используется в данном примере.
А где находится связь между нажатием на кнопку "ok" и выполнением слота
Спасибо!
Здравствуйте! В этой статье и нескольких Ваших других заметил метод closeDataBase(), но не нашел чтобы он использовался. И вообще во многих источниках видел исходники без закрытия БД. В php обычно (всегда) после запроса(ов) БД закрывается, и выходит здесь (в qt c++) также такое предусмотрено, но не совсем ясно понял когда и где его нужно (целесообразно) использовать? Как быть, если программа обращается к БД редко единичным образом или же когда все время требуется соединение и частые обращения?
В Qt После создания соединения с БД оно хранится на протяжения работы с приложением, но не постоянно. Необходимости закрывать соединение особо нет.
Добрый день!
@EVILEG can you kindly provide a response? Please
If you want to show two columns from your model in combobox, then, I think, you can try to set Model in QComboBox and use custom ItemDelegate for QComboBox. Try to research information about delegates in official documentation.
А как можно добавить картинку в БД через QDataWidgetMapper? Это делается методом addMapping? Статью про изображение в базе данных я читал.
Добрый день. Я не проверял mapping изображений. Тем более, что вопрос в том, на что именно их маппить. В той статье делается на QLabel. А это нередактируемый виджет, как например QLineEdit. Тем более, что такой специфичный функционал как изображения вряд ли подготовливался для маппинга.
Так что здесь скорее при выборе записи нужно или делать дополнительный запрос к базе данных или смотреть, содержит ли текущая запись информацию об изображении и помещать его на QLabel. То есть прямой маппинг не заработает, по моему мнению.
Мне просто надо изображение из QLabel'a, расположеннго на диалоговом окне, вставить в БД. Значит, всё-таки это лучше сделать через запрос?
Лучше через запрос. Видите ли, если вы будете вставлять изображение в базу данных, там Вам придётся преобразовать данные изображения в BLOB. Как в той статье. А в QDataWidgetMapper такая логика просто отсутсвует. Так что здесь или наследоваться от QDataWidgetMapper и писать свой, или просто через запрос.
Через запрос проще и быстрее. Через наследование уже больше мороки, но в крупных проектах это окупается (В ОЧЕНЬ КРУПНЫХ)
Реализовал поиск через QSortFilterProxyModel, но при выборе результата в Mapper передаются значения из tableView по порядку, а не полученные в результате поиска значения. Такая же проблема при сортировке данных в tableView.
Как правильно реализовать поиск?
Для фильтрации QSqlTableModel нужно использовать метод setFilter у этой самой модели QSqlTableModel. Туда передаётся SQL запрос для where
Начал делать что-то похожее, но без sql.
Не очень понимаю, что делает
Вернее не понимаю вот что: откуда он берёт данные, которые затем подставляет в диалог?
Моё собственное решение (делаю через QStandartItemModel) — передать в конструктор диалога все соответствующие поля из моей таблицы, которые затем через setText() прописать в QLineEdit.
Работать — работает (значения, разумеется, подставляются), но правильно ли так делать?
Понял сам так - происходит через setTable() "Sets the database table on which the model operates to tableName. Does not select data from the table, but fetches its field information", а затем через select() "Populates the model with data from the table that was set via setTable(), using the specified filter and sort condition, and returns true if successful; otherwise returns false".
Hi EVILEG,
Thnaks for example.
With this example code i am not getting header data. Any idea? I am new to QT and C++.
Can someone please help.