The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Каталог документации / Раздел "Программирование, языки" / Оглавление документа

Программирование графического интерфейса с помощью Qt 4, Часть 2

Продолжаем знакомство с Interview Framework

Продолжим знакомство с парадигмой модель-вид, реализованной в Qt 4. Пример из предыдущей статьи был, пожалуй, слишком простым для того, чтобы вы могли почувствовать преимущества системы Interview Framework. На этот раз мы усложним нашу базу данных и программы, предназначенные для работы с ней. Теперь вместо одной таблицы у нас будет три.

  

Упрощение структуры БД 
Если вы занимаетесь проектированием баз данных, то можете пропустить этот раздел, для остальных же я кратко поясню, что именно было сделано. Вспомните таблицу из предыдущей статьи. Каждая запись содержала имя автора произведения, название альбома и композиции, а также год выхода альбома. Вся эта информация хранилась в виде строк, а это значит, что строки с одними и теми же значениями (имена авторов и названия альбомов) часто повторялись. Такой подход нельзя назвать эффективным. Повторение одних и тех же данных делает БД громоздкой и трудно управляемой. Кроме того, необходимость вводить всю информацию о музыкальном произведении, включая повторяющиеся элементы, увеличивает вероятность появления ошибок в базе данных. Задача нормализации заключается в том, чтобы свести к минимуму (в идеале исключить) повторение одной и той же информации в таблицах БД. Формальное определение нормализации, включающее определение нескольких нормальных форм, вы найдете в литературе по проектированию баз данных. Здесь объяснение ведется на интуитивном уровне, тем более что наша база данных очень проста, а значит и нормализация, которую, мы выполняем, носит элементарный характер. В нашем музыкальном каталоге представлена информация о нескольких сущностях: авторе произведения, альбоме и самом произведении. Повторение данных возникает из-за того, что у одного автора может быть много альбомов (или отдельных композиций), а каждый альбом состоит из нескольких композиций. При этом (так, по крайней мере, предполагается в нашей упрощенной модели данных) у каждого альбома или композиции есть только один автор, а каждая композиция входит, самое большее, в одни альбом. Учитывая эти особенности модели данных, мы можем создать три таблицы: таблицу авторов произведений, таблицу альбомов и таблицу композиций. Рассмотрим таблицу авторов произведений (artists). Эта таблица содержит имя автора (поле name) и идентификатор записи (поле artist_id типа serial), который является первичным ключом. Для тех, кто незнаком с теорией поясню, что первичный ключ минимальное сочетание столбцов, совокупность значений которых уникальна для каждой записи базы данных. Внимательный читатель может заметить, что на имена авторов произведений в таблице artists наложено ограничение уникальности, а значит, сами имена могли бы быть первичным ключом таблицы. Однако имена авторов являются строками, а использование строк в качестве ключей нежелательно по причинам, которые станут понятны далее. Поэтому в качестве первичного ключа мы используем уникальные числовые значения artist_id, которые не имеют никакого самостоятельного смысла. Перейдем теперь к таблице albums. Информация об альбоме содержится в полях title (название) и release_year (год выхода). Кроме того, в таблице albums есть поле artist_id. Это поле представляет собой внешний ключ, который связывает таблицу albums с таблицей artists таким образом, что каждая запись в таблице albums ссылается на запись в таблице artists, соответствующей автору альбома. Таким образом мы можем установить автора альбома. Записи, соответствующие нескольким альбомам одного автора, ссылаются на одну и ту же запись в таблице artists, так что информация об авторах альбома не дублируется(таким образом выполняется ограничении: у каждого альбома один автор, у каждого автора может быть несколько альбомов). Кроме того, в таблице albums есть поле album_id, которое представляет собой первичный ключ записи (первичным ключом таблицы albums могло бы быть сочетание имени альбома и идентификатора автора альбома, но в этом случае нам пришлось бы использовать строки в качестве ставных полей первичного ключа). Таблица compositions содержит сведения о каждой отдельной композиции. Чтобы понять структуру этой таблицы, следует вспомнить уж упомянутую проблему композиция не обязательно должна быть частью какого-либо альбома. По этой причине в таблице compositions два внешних ключа, один ссылается на записи таблицы artists и его значение не может быть пустым (у композиции должен быть автор), второй на записи таблицы albums, и он допускает пустые значения. То, что композиция может не входить в альбом, создает еще одну проблему. В таблице albums есть поле release_year, в котором указывается год выхода альбома. Если бы каждая композиция входила в какой-либо альбом, при том только один, годом выхода композиции можно было бы считать год выхода альбома, но это не так, поэтому в таблицу compositions приходится добавлять свое поле release_year, в котором хранится год выхода композиции. Мы можем оправдать включение этого поля еще и тем, что в альбомы иногда включают композиции, выпущенные ранее. Название композиции хранится, соответственно, в поле title.
Отношения между таблицами представлены графически на рисунке 1.  

Таблицы 

Рисунок 1. Структура тестовой базы данных

Метка PK указывает, что данное поле является первичным ключом таблицы, метка FK обозначает внешние ключи. Жирным шрифтом выделены поля, которые не могут иметь пустые значения. Стрелки указывают связи, созданные между таблицами с помощью внешних ключей.

Теперь для каждого типа объектов в нашей базе данных создана своя таблица, а информация о каждом отдельном объекте встречается в БД только один раз. Помимо прочего, это создает нам еще одно дополнительное преимущество: если вдруг выяснится, что автором всех произведений, приписываемых некоему Моцарту, является на самом деле Сальери, нам достаточно будет изменить одну единственную запись в таблице artists, чтобы привести данные БД в соответствие с новым историческим открытием.

Кроме таблиц мы создаем представление view_all, которое сводит полную информацию о каждой композиции в одну таблицу.

Посмотрим теперь на представление таблицы compositions с помощью модели QSqlQueryModel, как в примере из предыдущей статьи (рис. 2). Данные выглядят примерно так, как они хранятся в таблице БД (только пустое значение поля album_id во второй строке заменено несуществующем индексом 0), однако с точки зрения пользователя такое представление данных нельзя назвать удовлетворительным.

 

Рисунок 2. Отображение таблицы compositions файл pic2.png.

При показе данных пользователю было бы желательно заменить ссылки на записи таблиц albums и artists информацией из самих этих таблиц. Именно эту задачу решает класс QSqlRelationalTableModel. Рассмотрим фрагмент программы relational_model, полный текст которой вы найдете по ссылке в конце страницы.

QSqlRelationalTableModel * compositionsRelation = new QSqlRelationalTableModel(0);
compositionsRelation->setTable("compositions");
compositionsRelation->setRelation(1, QSqlRelation("artists", "artist_id", "name"));
compositionsRelation->setRelation(2, QSqlRelation("albums", "album_id", "title"));
compositionsRelation->select();
compositionsRelation->removeColumn(0);
compositionsRelation->setHeaderData(0, Qt::Horizontal, QObject::trUtf8("Автор"));
compositionsRelation->setHeaderData(1, Qt::Horizontal, QObject::trUtf8("Альбом"));
compositionsRelation->setHeaderData(2, Qt::Horizontal, QObject::trUtf8("Год выхода"));
compositionsRelation->setHeaderData(3, Qt::Horizontal, QObject::trUtf8("Композиция"));    

Мы создаем объект compositionsRelation класса QSqlRelationalTableModel. Вместо того чтобы указать объекту-модели текст SQL-запроса, мы, с помощью метода setTable(), указываем имя основной таблицы, с которой будет работать модель. Далее, с помощью вызовов метода setRelation() заменяем столбцы таблицы compositions, содержащие внешние ключи столбцами из соответствующих таблиц. Первым аргументом setRelation() должен быть номер столбца таблицы compositions, содержащего внешний ключ (нумерация столбцов начинается с 0). Вторым параметром метода должна быть ссылка на объект класса QSqlRelation, который мы создаем локально. Первым аргументом конструктора QSqlRelation является имя таблицы, на записи которой ссылается внешний ключ таблицы compositions. Далее следует имя столбца таблицы, на который ссылается внешний ключ, затем имя столбца, которым мы хотим заменить столбец исходной таблицы (compositions), содержащий внешней ключ (я знаю, что все это просто). Единственным неприятным ограничением класса QSqlRelation является то, что мы можем заменить столбец исходной таблицы с внешним ключом только одним столбцом внешней таблицы. В нашем случае это не страшно, так как в таблицах artist и albums полезная информация содержится только в одном столбце. Однако это могло бы быть не так. Например, таблица albums могла бы содержать еще и столбец genre (жанр). В таких случаях нам придется конструировать представления (views) средствами языка SQL. Поскольку у таблицы compositions два внешних ключа, мы вызываем метод setRelation() дважды, для установления связи с таблицами artists и albums соответственно. Сама выборка данных из таблицы производится с помощью метода select() объекта compositionsRelation, которому мы не передаем никаких параметров (этот метод и сам знает, что нужно делать, используя заданные нами настройки). С использованием модели QSqlRelationalTableModel таблица музыкальных композиций становится гораздо более информативной (рис. 3).

 

Рисунок 3. Отображение таблицы compositions с помощью модели QSqlRelationalTableModel

Следует отметить один недостаток отображения сложных систем реляционных таблиц с помощью Interview Framework. В нашей модели данных вешний ключ album_id таблицы compositions может содержать пустые значения. При замещении столбца album_id столбцом с названием альбома с помощью метода setRelation(), строки, содержащие пустые значения в поле album_id, просто не попадут в модель (то же самое происходит при попытке сформировать таблицу с помощью запроса SELECT * FROM albums WHERE...). В представлении view_all, которые вы найдете в файле createtables.sql я обошел эту проблему, комбинируя левые и правые объединения (joins). Но класс QSqlRelationalTableModel так делать не умеет, поэтому если вы хотите отображать таблицы с пустыми внешними ключами целиком, вам придется самостоятельно конструировать SQL-запросы. Можно, конечно, пойти и по другому пути ввести в список альбомов псевдо-альбом single, и добавлять в этот альбом все композиции, не являющиеся частью альбомов. При таком подходе замечательная песня Есть на Волге утес классифицировалась бы как сингл неизвестного автора.

Редактирование данных

До сих пор все наши программы Interview Framework могли только просматривать данные. Пришло время заняться редактированием. Мы напишем редактор albums_editor для редактирования данных описанной выше таблицы albums. Классы моделей QSqlTableModel и QSqlRelationalTableModel позволяют редактировать данные в таблицах, полученных в результате SQL-запросов. Поскольку таблица albums содержит внешний ключ, мы воспользуемся классом QSqlRelationalTableModel. Перейдем сразу к исходному тексту программы

QSqlRelationalTableModel * albumsRelation = new QSqlRelationalTableModel(0);
albumsRelation->setTable("albums");
albumsRelation->setRelation(1, QSqlRelation("artists", "artist_id", "name"));
albumsRelation->select();
albumsRelation->setEditStrategy(QSqlTableModel::OnManualSubmit);
CustomView * view = new CustomView(0);
view->setModel(albumsRelation);
view->setColumnHidden(0, true);
view->setWindowTitle(QObject::trUtf8("Альбомы"));
view->setItemDelegate(new QSqlRelationalDelegate(view));
view->show();   

Это фрагмент функции main() программы albums_editor. Блок команд, устанавливающий соединение с БД, мы не рассматриваем, так он у всех наших программ одинаковый. Модель в нашей программе, это объект albumsRelation класса QSqlRelationalTableModel. Вызов метода setTable() указывает программе, что мы работаем с таблицей albums. С помощью метода setRelation() мы подменяем столбец artist_id в таблице albums столбцом с именем автора произведения из таблицы artists так же, как и в предыдущем примере. Далее следует уже знакомый нам вызов метода select().

Новшества начинаются со следующей строки программы, в которой мы устанавливаем стратегию редактирования. Методу setEditStrategy() передается одна из констант, которая указывает, каким образом изменения, внесенные в модель, должны фиксироваться в базе данных. Выбор стратегии QSqlTableModel::OnFieldChange приведет к тому, что любое изменение в модели будет тут же фиксироваться в базе данных. Этот вариант удобен, если изменения вносятся в модель автоматически (и нечасто). Однако пользователь, редактирующий базу данных вручную, может ошибиться при заполнении значения поля. При исправлении каждой такой ошибки программе придется обращаться к БД, что создаст слишком много обращений. При выборе константы QSqlTableModel::OnRowChange изменения будут вноситься в БД при переходе пользователя к новой строке. Лично я считаю наиболее подходящим для наших целей третий вариант - QSqlTableModel::OnManualSubmit, при котором, для внесения в БД изменений, сделанных в модели, требуется отдельная команда.

Теперь мы переходим к созданию объекта, отображающего данные. Класс CustomView, который я использую, я написал сам на основе класса QTableView. Зачем нам специальный класс для отображения данных? Класс QTableView располагает всем необходимым для редактирования значений в уже существующих ячейках таблицы. При выборе стратегии QSqlTableModel::OnFieldChange изменения в ячейках автоматически вносятся в БД. Однако класс QTableView (а окно, созданное на основе QTableView, является единственным элементом пользовательского интерфейса нашей программы) не умеет добавлять в таблицу новые строки или генерировать по нашему требованию команду передачи данных в БД, которая требуется при выбранной нами стратегии QSqlTableModel::OnManualSubmit. Класс CutomView дополняет класс QTableView необходимыми нам возможностями. Поскольку окно QTableView не обладает ни панелями, ни строкой состояния, я решил не дополнять его другими визуальными элементами, а ввод дополнительных команд реализовать с помощью специальных сочетаний клавиш. Для добавления в модель новой строки в редакторе albums_editor следует использовать сочетание клавиш Ctrl-I, а для фиксации изменений в таблице сочетание Ctrl-S (вы можете дополнить этот перечень команд командами удаления строк). Кроме того, команда Ctrl-U позволяет отменить все изменения, которые мы не успали зафиксировать в БД. Текст класса CustomView приводится ниже.

class CustomView : public  QTableView
{
public:
	CustomView( QWidget * parent = 0 ):QTableView(parent)
	{
	}
protected:
	virtual void keyPressEvent ( QKeyEvent * e )
	{
		if ((e->key() == Qt::Key_I) && (e->modifiers() == Qt::ControlModifier))
		{
			this->model()->insertRow(this->model()->rowCount());
		}
		if ((e->key() == Qt::Key_S) && (e->modifiers() == Qt::ControlModifier))
		{
			((QSqlTableModel *) model())->submitAll();
		}
		if ((e->key() == Qt::Key_U) && (e->modifiers() == Qt::ControlModifier))
		{
			((QSqlTableModel *) model())->revertAll();
		}

		QTableView::keyPressEvent(e);
	}
};   

Метод insertRow() добавляет в таблицу новую строку, которая располагается после той строки, номер которой передан в качестве аргумента insertRow(). Мы передаем методу номер последней строки (значение model()->rowCount()), так что новая строка всегда добавляется в конец таблицы. Метод submitAll() вносит изменения в БД, а метод revertAll() отменяет все изменения, сделанные во время текущего сеанса редактирования (если они еще не были внесены в БД). Обратите внимание, что метод insertRow() реализован в базовом классе QAbstractItemModel, который в принципе предполагает работу с любыми структурами данных. Объясняется это тем, что в моделях Interview Framework данные хранятся в виде иерархии таблиц, независимо от того, какова их исходная структура.

Вернемся к функции main(). При редактировании таблиц БД следует учесть один важный момент: в программе relational_model мы удалили из модели данных первый столбец таблицы compositions с помощью метода removeColumn(), так как он не содержит полезной для пользователя информации. В приложении albums_editor, которое вносит изменения в таблицу albums, мы не можем удалять столбцы из модели albumsRelation (тем более первичные ключи) поскольку в этом случае все SQL-команды, редактирующие БД, окажутся сформированными неправильно. Тем не менее, нам вовсе не требуется показывать пользователю первый столбец таблицы albums (при добавлении строк в таблицу уникальные числовые значения для этого столбца все равно генерируются автоматически). Мы скрываем от пользователя неинтересный ему столбец, но не на уровне модели данных, а на уровне представления (объект view), с помощью метода setColumnHidden().

При помощи метода setItemDelegate() мы устанавливаем объект-делегат, выступающий в роли посредника в процессе редактирования данных. Мы используем объект класса QSqlRelationalDelegate. У этого объекта много полезных возможностей, и некоторые из них мы рассмотрим ниже. Сейчас нас интересует одна функция, являющаяся специфической именно для объектов QSqlRelationalDelegate. Если в окне просмотра таблицы albums мы щелкнем по одному из полей столбца name (позаимствованного из таблицы artists), откроется раскрывающийся список с именами авторов (рис. 4). Таким образом, с помощью делегата QSqlRelationalDelegate мы можем редактировать таблицы, содержащие внешние ключи самым естественным способом с помощью выбора значения столбца внешней таблицы из списка. Излишне говорить, что после выбора из списка подходящего значения в поле artist_id таблицы albums будет добавлен соответствующий внешний ключ (а не само значение).

 

Рисунок 4. Окно таблицы с раскрывающимся списком допустимых значении ячейки.

Индексы

Настала пора поближе познакомиться с системой Interview Framework. Один из основополагающих принципов Interview Framework заключается в приведении самых разных данных, независимо от их исходной структуры и метода их получения, к единому внутреннему представлению. Именно этот принцип обеспечивает универсализм Interview Framework при котором разные объекты-виды и объекты-модели могут свободно взаимодействовать между собой. Для доступа к данным Interview Framework применяет индексы. Индексы Interview Framework это специальные объекты, которые позволяют получить доступ к отдельным элементам данных. Одна из задач индекса заключается в том, чтобы изолировать данные от непосредственного доступа, поэтому при работе с индексами требуется соблюдать определенные ограничения. Индекс представляет нам доступ к элементу данных исходя из состояния модели данных на момент получения индекса. Если после получения индекса состояние модели изменится, индекс может утратить валидность. Это означает, что обычные индексы следует использовать для элементарных операций редактирования данных, причем для каждой операции следует получать новый индекс (даже если мы работаем с тем же самым элементом данных). В более сложных случаях можно воспользоваться постоянными (persistent) индексами.

В программе albums_editor делегат QSqlRelationalDelegate позволил реализовать очень полезную функцию раскрывающийся список значений внешней таблицы. Однако, помимо этого, делегат не привнес в нашу программу ничего существенного. Класс QTableView (и его производные) позволяют редактировать значения без использования делегатов. Все это вовсе не означает, что делегаты бесполезны. Рассмотрим метод createEditor(), который реализован в базовом классе QItemDelegate. Помимо прочих аргументов этому методу передается индекс, представляющий элемент данных, который мы хотим редактировать. Метод возвращает значение типа QWidget *, которо представляет собой указатель на объект-виджет, предназначенный для редактирования элемента данных. Фактически, по нашему требованию метод createEditor() создает редактор данных! В случае объекта QSqlRelationalDelegate метод createEditor() создаст объект-редактор, похожий на редактируемую ячейку таблицы (в том числе, с раскрывающимся списком значений, если выбрана ячейка соответствующего столбца). Поскольку редактировать значения ячеек можно прямо в таблице, толку от этого редактора не очень много. Но в других случаях возможность создавать редакторы данных с помощью делегатов может оказаться очень полезной.

На этом мы завершаем увлекательное путешествие в мир Interview Framework. Следующая статья будет посвящена визуальным компонентам Qt 4, рисованию и каллиграфии.

Исходные тексты примеров


Статья впервые опубликована в журнале Linux Format

© 2008  Андрей Боровский <anb @ symmetrica.net>



Партнёры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2024 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру