Для відображення даних у віджет із довільною формою використовується клас 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
- кнопка далі
- попередня кнопка
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); }
база даних.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.