12.3. Разработка форм, ориентированных на работу с базами данных.

Qt предлагает инновационный подход к организации взаимодействий форм с базами данных. Вместо того, чтобы создавать отдельные версии встроенных виджетов, для работы с данными, Qt делает любой виджет ориентированным на работу с базами данных, с помощью класов QSqlForm и QSqlPropertyMap, которые выступают посредниками между базой данных и виджетами. Любой виджет, не зависимо от того -- стандартный он или нет, сможет работать с базами данных, при использовании этих классов.

Класс QSqlForm, производный от QObject, облегчает создание форм для просмотра и изменения отдельных записей. В общем случае, порядок создания формы диалога выглядит следующим образом:

  1. Создаются виджеты-редакторы (QLineEdit, QComboBox, QSpinBox и т.п.) для каждого из полей записи.

  2. Создается экземпляр QSqlCursor.

  3. Создается экземпляр QSqlForm.

  4. Выполняется настройка QSqlForm, которая заключается в связывании полей записи с виджетами.

  5. Вызывается метод QSqlForm::readFields(), который заполняет виджеты данными.

  6. Диалог выводится перед пользователем.

  7. По завершении работы диалога, Вызывается метод QSqlForm::writeFields(), чтобы скопировать измененные значения обратно в базу данных.

Все эти действия мы продемонстрируем на примере диалога CdForm. Он предназначен для создания и изменения записи о компакт диске. Пользователь может задать название диска, исполнителя и год выпуска, а так же название и продолжительность каждого произведения на диске.

Рисунок 12.4. Диалог CdForm.


Начнем с определения класса: class CdForm : public QDialog { Q_OBJECT public: CdForm(QWidget *parent = 0, const char *name = 0); CdForm(int id, QWidget *parent = 0, const char *name = 0); ~CdForm(); protected slots: void accept(); void reject(); private slots: void addNewArtist(); void moveTrackUp(); void moveTrackDown(); void beforeInsertTrack(QSqlRecord *buffer); void beforeDeleteTrack(QSqlRecord *buffer); private: void init(); void createNewRecord(); void swapTracks(int trackA, int trackB); QLabel *titleLabel; QLabel *artistLabel; ... QDataTable *trackTable; QSqlForm *sqlForm; QSqlCursor *cdCursor; QSqlCursor *trackCursor; int cdId; bool newCd; }; В классе объявлены два конструктора: один вставляет новую запись в базу данных, другой -- обновляет существующую запись. Слоты accept() и reject() унаследованы от QDialog. CdForm::CdForm(QWidget *parent, const char *name) : QDialog(parent, name) { setCaption(tr("Add a CD")); cdId = -1; init(); } Первый конструктор записывает в заголовок диалога строку "Add a CD" ("добавить диск") и вызывает приватную функцию init(), которая выполняет остальную часть работы. CdForm::CdForm(int id, QWidget *parent, const char *name) : QDialog(parent, name) { setCaption(tr("Edit a CD")); cdId = id; init(); } Второй конструктор записывает в заголовок диалога строку "Edit a CD" ("изменить сведения о диске") и так же вызывает функцию init(). void CdForm::init() { db = QSqlDatabase::database("CD"); db->transaction(); if (cdId == -1) createNewRecord(); В функции init() запускается транзакция для соединения под именем "CD". Для диалогов CdForm и ArtistForm используются различные соединения, поскольку оба они могут отображаться одновременно и при этом нельзя допустить, чтобы операция Cancel в одной форме выполняла откат транзакции, запущенной в другой форме.

Если идентификатор диска не задан, вызывается функция createNewRecord(), которая вставляет пустую запись в таблицу. Это позволит использовать cdId как внешний ключ для QDataTable с дорожками. Если пользователь нажмет на кнопку Cancel, все изменения, произведенные в контексте транзакции, будут отменены, в том числе и операция вставки новой записи.

В этом диалоге используется еще одно соединение с базой данных, отличное от того, что используется в ArtistForm. Сделано это так потому, что в соединении, активной может быть только одна транзакция, а в данном приложении может сложиться ситуация, когда потребуется иметь две активных транзакции одновременно, например, в случае, когда пользователь нажимает кнопку Add New и открывает диалог ArtistForm.

titleLabel = new QLabel(tr("&Title:"), this); artistLabel = new QLabel(tr("&Artist:"), this); yearLabel = new QLabel(tr("&Year:"), this); titleLineEdit = new QLineEdit(this); yearSpinBox = new QSpinBox(this); yearSpinBox->setRange(1900, 2100); yearSpinBox->setValue(QDate::currentDate().year()); artistComboBox = new ArtistComboBox(db, this); artistButton = new QPushButton(tr("Add &New..."), this); ... cancelButton = new QPushButton(tr("Cancel"), this); На форме диалога размещаются текстовые метки, поле ввода, счетчик, выпадающий список и кнопки. Выпадающий список принадлежит к классу ArtistComboBox, о котором мы поговорим немного ниже. trackCursor = new QSqlCursor("track", true, db); trackTable = new QDataTable(trackCursor, false, this); trackTable->setFilter("cdid = " + QString::number(cdId)); trackTable->setSort(trackCursor->index("number")); trackTable->addColumn("title", tr("Track")); trackTable->addColumn("duration", tr("Duration")); trackTable->refresh(); Далее создается и настраивается QDataTable, которая позволит пользователю просматривать и изменять сведения о дорожках на текущем компакт диске. Очень напоминает то, что мы делали с классом ArtistForm, в предыдущем разделе. cdCursor = new QSqlCursor("cd", true, db); cdCursor->select("id = " + QString::number(cdId)); cdCursor->next(); Создается QSqlCursor и текущей, для него, делается запись, содержащая идентификатор требуемого диска. QSqlPropertyMap *propertyMap = new QSqlPropertyMap; propertyMap->insert("ArtistComboBox", "artistId"); sqlForm = new QSqlForm(this); sqlForm->installPropertyMap(propertyMap); sqlForm->setRecord(cdCursor->primeUpdate()); sqlForm->insert(titleLineEdit, "title"); sqlForm->insert(artistComboBox, "artistid"); sqlForm->insert(yearSpinBox, "year"); sqlForm->readFields(); Класс QSqlPropertyMap хранит сведения, благодаря которым QSqlForm "знает" -- значения какого типа, в каком свойстве, может хранить тот или иной виджет-редактор. Класс QSqlForm уже "знает", что QLineEdit запоминает свое значение в свойстве text, а QSpinBox -- в свойстве value. Но он ничего не знает о нестандартных виджетах, коим является ArtistComboBox. Поэтому мы должны вставить название класса и имя свойства класса ("ArtistComboBox", "artistId") в карту свойств и вызвать installPropertyMap(), чтобы указать QSqlForm, что при работе с виджетом класса ArtistComboBox следует использовать свойство artistId.

Кроме того экземпляру класса QSqlForm нужно передать буфер, с которым он будет работать, а так же сообщить о том, какой виджет, какому полю в таблице соответствует. В заключение, вызовом readFields(), данные считываются из базы и переносятся в виджеты.

connect(artistButton, SIGNAL(clicked()), this, SLOT(addNewArtist())); connect(moveUpButton, SIGNAL(clicked()), this, SLOT(moveTrackUp())); connect(moveDownButton, SIGNAL(clicked()), this, SLOT(moveTrackDown())); connect(updateButton, SIGNAL(clicked()), this, SLOT(accept())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); connect(trackTable, SIGNAL(beforeInsert(QSqlRecord *)), this, SLOT(beforeInsertTrack(QSqlRecord *))); ... } На последней стадии выполнения функции производится соединение сигналов и слотов, которые будут описаны несколько ниже. void CdForm::accept() { sqlForm->writeFields(); cdCursor->update(); db->commit(); QDialog::accept(); } Когда пользователь нажимает на кнопку Update, производится запись новых значений полей в буфер редактирования объекта QSqlCursor. Затем выполняется SQL-предложение UPDATE, вызовом функции update(), вызовом commit() подтверждается транзакция и в заключение вызывается метод accept(), унаследованный от QDialog. void CdForm::reject() { db->rollback(); QDialog::reject(); } Когда пользователь нажимает на кнопку Cancel, производится откат произведенных изменений и форма диалога закрывается. void CdForm::addNewArtist() { ArtistForm form(this); if (form.exec()) { artistComboBox->refresh(); updateButton->setEnabled(artistComboBox->count() > 0); } } Когда пользователь нажимает на кнопку Add New, запускается модальный диалог ArtistForm. Этот диалог позволяет добавлять нового исполнителя в базу данных, удалять или изменять сведения об исполнителях. Если пользователь нажмет кнопку Update, будет вызвана функция ArtistComboBox::refresh(), которая обновит список исполнителей в виджете.

Если в списке нет ни одного исполнителя, кнопка Update будет запрещена, поскольку необходимо избежать создания записи о CD, без указания имени исполнителя.

void CdForm::beforeInsertTrack(QSqlRecord *buffer) { buffer->setValue("id", generateId("track", db)); buffer->setValue("number", trackCursor->size() + 1); buffer->setValue("cdid", cdId); } Слот beforeInsertTrack() связан с сигналом beforeInsert(). Он заполняет поля id, number и cdid. void CdForm::beforeDeleteTrack(QSqlRecord *buffer) { QSqlQuery query(db); query.exec("UPDATE track SET number = number - 1 " "WHERE track.number > " + buffer->value("number").toString()); } Слот beforeDeleteTrack() связан с сигналом beforeDelete(). Он выполняет перенумерацию дорожек на диске, чьи номера больше номера удаляемой дорожки, чтобы сохранить неразрывность последовательности номеров дорожек. Например, допустим, что диск содержит 6 дорожек и пользователь удаляет 4-ю, тогда 5-я дорожка получит номер 4, а 6-я -- 5.

Имеется еще 4 функции, описание которых мы не привели: moveTrackUp(), moveTrackDown(), swapTracks() и createNewRecord(). Они совершенно необходимы, чтобы сделать приложение более-менее удобным, но их реализация не содержит ничего нового для вас, поэтому мы не будем их рассматривать. Исходные тексты функций вы найдете на CD, сопровождающем книгу.

Теперь, после того как мы рассмотрели все классы диалогов в приложении, можно перейти к описанию нестандартного класса ArtistComboBox. Как обычно, начнем с определения класса:

class ArtistComboBox : public QComboBox { Q_OBJECT Q_PROPERTY(int artistId READ artistId WRITE setArtistId) public: ArtistComboBox(QSqlDatabase *database, QWidget *parent = 0, const char *name = 0); void refresh(); int artistId() const; void setArtistId(int id); private: void populate(); QSqlDatabase *db; QMap<int, int> idFromIndex; QMap<int, int> indexFromId; }; Класс ArtistComboBox порожден от класса QComboBox. В него добавлено свойство artistId и несколько функций.

В приватной секции объявлены две переменные-члены типа QMap<int, int>. Первая отвечает за соответствие идентификатора исполнителя индексу в списке виджета. Вторая отвечает за соответствие индекса в списке -- идентификатору исполнителя.

ArtistComboBox::ArtistComboBox(QSqlDatabase *database, QWidget *parent, const char *name) : QComboBox(parent, name) { db = database; populate(); } В конструкторе вызывается функция populate(), которая заполняет список виджета именами и идентификаторами из таблицы artist. void ArtistComboBox::refresh() { int oldArtistId = artistId(); clear(); idFromIndex.clear(); indexFromId.clear(); populate(); setArtistId(oldArtistId); } Функция refresh() очищает и повторно заполняет список виджета самыми свежими данными из базы. При этом, после обновления списка выбранным остается тот же исполнитель, который был выбран до обновления. void ArtistComboBox::populate() { QSqlCursor cursor("artist", true, db); cursor.select(cursor.index("name")); int index = 0; while (cursor.next()) { int id = cursor.value("id").toInt(); insertItem(cursor.value("name").toString(), index); idFromIndex[index] = id; indexFromId[id] = index; ++index; } } Функция populate() переносит список исполнителей из базы данных в список виджета, попутно обновляя словари idFromIndex и indexFromId. int ArtistComboBox::artistId() const { return idFromIndex[currentItem()]; } Функция artistId() возвращает идентификатор текущего исполнителя. void ArtistComboBox::setArtistId(int id) { if (indexFromId.contains(id)) setCurrentItem(indexFromId[id]); } Функция setArtistId() делает текущим исполнителя с заданным идентификатором.

В приложении, часто использующем виджет выпадающего списка, который предназначен для отображения внешнего ключа, вероятно было бы более удобным создать универсальный DatabaseComboBox, в конструктор которого можно было бы передать имя таблицы, имя поля, которое должно отображаться в списке и имя поля, которое должно использоваться в качестве идентификатора.

Закончим обзор приложения "CD Collection" рассмотрением реализации функций createConnections() и main().

inline bool createOneConnection(const QString &name) { QSqlDatabase *db; if (name.isEmpty()) db = QSqlDatabase::addDatabase("QSQLITEX"); else db = QSqlDatabase::addDatabase("QSQLITEX", name); db->setDatabaseName("cdcollection.dat"); if (!db->open()) { db->lastError().showMessage(); return false; } return true; } inline bool createConnections() { return createOneConnection("") && createOneConnection("ARTIST") && createOneConnection("CD"); } Функция createConnections() создает три идентичных соединения с базой данных. Первое соединение создается безымянным, оно будет использоваться по-умолчанию, когда имя соединения не задано явно. Два других соединения создаются с именами "ARTIST" и "CD" -- они используются диалогами ArtistForm и CdForm, соответственно. int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!createConnections()) return 1; MainForm mainForm; app.setMainWidget(&mainForm); mainForm.resize(480, 320); mainForm.show(); return app.exec(); } Функция main() практически ничем не отличается от аналогичных функций, которые мы до сих пор видели, за одним маленьким исключением -- она вызывает createConnections().

Как мы уже говорили в конце предыдущего раздела, приложение можно было бы несколько улучшить, если показывать длительность звучания дорожки не в секундах, а в минутах и секундах. Но, кроме перекрытия метода QSqlCursor::calculateField(), это повлекло бы за собой еще и создание класса -- редактора времени звучания дорожки, производного от QSqlEditorFactory. А затем необходимо было бы с помощью QSqlPropertyMap сообщить QDataTable о том, как получить измененное значение от класса-редактора. За дополнительной информацией обращайтесь к сопроводительной документации по функциям installEditorFactory() и installPropertyMap(), класса QDataTable.

Еще одно из возможных улучшений приложения -- добавить возможность хранения в базе данных изображения обложки диска и отображения его в CdForm. Реализовать это можно за счет добавления в базу данных поля типа BLOB, в котором можно хранить изображения. Получать изображения из базы данных можно в виде QByteArray и затем передавать их в конструктор QImage.