LXF103:Qt4

Материал из Linuxformat.

Перейти к: навигация, поиск
Программирование в стиле Qt Осваиваем технологии, лежащие в основе нашумевшего KDE4

Содержание

MVC по-нормальному

Qt4
ЧАСТЬ 2 В прошлый раз мы едва успели взглянуть на богатство возможностей, предлагаемых системой Interview. За прошедший месяц Андрей Боровский привел данные в нормальную форму и готов продолжить разговор на новом уровне.
Теории должны быть настолько просты, насколько возможно, но не проще.
А.Эйнштейн.

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

Нормализация

Процесс, в результате которого произойдет это «растроение», называется нормализацией. Если вы занимаетесь проектированием баз данных, можете пропустить этот раздел, для остальных же я кратко поясню, что именно было проделано. Вспомните таблицу из предыдущей статьи. Каждая запись в ней содержала имя автора произведения, название альбома и композиции, а также год выхода альбома. Вся эта информация хранилась в виде строк, а это значит, что одни и те же значения (имена авторов и названия альбомов) частенько повторялись. Такой подход нельзя назвать эффективным. Повторение одних и тех же данных делает БД громоздкой и трудно управляемой. Кроме того, необходимость вводить всю информацию о музыкальном произведении, включая повторяющиеся элементы, увеличивает вероятность появления ошибок в базе данных. Задача нормализации как раз и заключается в том, чтобы свести к минимуму (в идеале – исключить) повторение одной и той же информации в таблицах БД. Формальное определение нормализации, включающее введение нескольких нормальных форм, можно найти в литературе по проектированию баз данных. Здесь объяснение будет вестись на интуитивном уровне, тем более что наша база данных очень проста, а значит, и нормализация, которую мы выполняем, носит элементарный характер.

В нашем музыкальном каталоге представлена информация о нескольких сущностях: авторе произведения, альбоме и самом произведении. Повторение данных возникает из-за того, что у одного автора может быть много альбомов (или отдельных песен), а каждый альбом состоит из нескольких композиций. При этом (так, по крайней мере, предполагается в нашей упрощенной модели данных) у каждого альбома или композиции есть только один автор, а каждая композиция входит, самое большее, в один альбом.

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

Учитывая эти особенности модели данных, мы можем создать три таблицы: таблицу авторов произведений, таблицу альбомов и таблицу композиций.

Рассмотрим таблицу авторов произведений (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.

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

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

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

Кстати об отношениях

Рис.2Рис. 2. Отображение таблицы compositions.

Посмотрим теперь на представление таблицы compositions с помощью модели QSqlQueryModel, как в примере из предыдущей статьи (Рис. 2). Данные выглядят примерно так, как они хранятся в таблице БД (только пустое значение поля album_id во второй строке заменено несуществующем индексом 0), однако с точки зрения пользователя такое представление нельзя назвать удовлетворительным. При показе данных пользователю было бы желательно заменить ссылки на записи таблиц 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Рис. 3. Отображение таблицы compositions с помощью модели QsqlRelationalTableModel.

Следует отметить один недостаток отображения сложных систем реляционных таблиц с помощью Interview Framework. В нашей модели данных внешний ключ album_id таблицы compositions может содержать пустые значения. При замещении столбца album_id столбцом с названием альбома с помощью метода setRelation(), строки, содержащие пустые значения в поле album_id, просто не попадут в модель (то же самое происходит при попытке сформировать таблицу с помощью запроса SELECT * FROM albums WHERE...). В представлении view_all, которые вы найдете в файле createtables.sql, я обошел эту проблему, комбинируя левые и правые объединения (join). Но класс 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. Класс CustomView дополняет класс 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().

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

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

Индексы

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

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

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

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию