Версия для печати

Архив документации на OpenNet.ru / Раздел "Программирование, языки" (Многостраничная версия)

Программирование в Qt 4


Автор: Андрей Боровский <anb @ symmetrica.net>
Оригинал: http://symmetrica.net/qt4/
Статья впервые опубликована в журнале Linux Format


Серия статей дя тех, кто переходит с Qt 3.x на Qt 4.x

Знакомство

Interview Framework

Qt Designer, Arthur и Scribe

Graphics View Framework

Сценарии Qt 4


Программирование графического интерфейса с помощью 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>

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

Qt Designer, Arthur и Scribe

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

Qt Designer старый новый друг

Вы наверняка обратили внимание, что в Qt версии 3 Qt Designer выступал в роли своего рода полу-IDE. Помимо самого визуального редактора в нем присутствовал генератор проектов и собственный редактор кода, который позволял, например, редактировать текст методов-слотов. В Qt 4 разработчики решительно пересмотрели функции Qt Designer. Теперь этот инструмент предназначен исключительно для визуального редактирования. Редактор текста и генератор проектов из нового Qt Designer удалены. Объясняется это тем, что, по мнению разработчиков, генерация проектов и редактирование текстов программ должны целиком переместиться в IDE, такие, например, как KDevelop или Microsoft Visual Studio, а Qt Designer (который интегрируется c указанными IDE) будет выполнять функцию вспомогательного инструмента. Впрочем, версия KDevelop, поддерживающая Qt Designer 4, есть пока далеко не у всех, а продукт Microsoft нас и вовсе не касается, поэтому мы рассмотрим работу с Qt Designer 4, как с самостоятельным средством. Лично мне в новом дизайнере не понравилось то, что он представляет собой набор независимых друг от друга окон (наподобие Glade). Возможно, такая структура упрощает встраивание Qt Designer в другие программные среды, но при работе с самим дизайнером постоянно переключаться между окнами не очень удобно.

При запуске Qt Designer 4 нас встречает диалоговое окно выбора заготовки для формы, которую мы будем редактировать. Можно создать новую форму или открыть уже существующую. В списке заготовок форм всего 4 пункта, из которых мы выбираем пункт Main Window (рис. 1).

Qt Designer 

Рисунок 1. Окно выбора формы Qt Designer

Само главное окно Qt-приложения (класс QMainWindow) также претерпело немало изменений. Теперь панели инструментов (объекты класса QToolBar) и стыкуемые окна (объекты класса QDockWidget) реализованы независимо друг от друга. Принципы работы с главным меню тоже несколько изменились (мы рассмотрим все это ниже). Пока что выберем в приветственном окне Qt Designer форму главного окна и щелкнем кнопку Create. Появится форма с заготовкой меню и строки состояния. Мы добавим в эту форму компонент QFrame, сделаем так, чтобы соответствующий объект заполнял всю форму (команда Lay Out Vertically контекстного меню) и сохраним форму в файле mainform.ui.

Для нашей первой программы с полноценным главным окном нам, разумеется, понадобится функция main(). Вот она:

#include <QApplication>
#include <QMainWindow>
#include "ui_mainform.h"

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
	Ui::MainWindow mainWindow;
	QMainWindow * window = new QMainWindow;
	mainWindow.setupUi(window);
	window->show();
	return app.exec();
}

Эта простенькая функция демонстрирует много не очень заметных, но существенных отличий Qt 4 от Qt 3. Мы начнем разбирать ее с заголовочных файлов. Включение заголовочных файлов QApplication и QMainWindow не должно вызывать вопросов. А вот откуда взялся файл ui_mainform.h? Этот файл должен описывать форму, созданную нами в визуальном редакторе, на языке C++. Иначе говоря, файл ui_mainform.h генерируется на основе данных, содержащихся в файле mainform.ui. Но чем его генерировать? В Qt3 нечто подобное создавалось автоматически программой Qt Designer (в Qt 3 этот файл назывался бы mainform.ui.h), но Qt Designer 4 не генерирует сам никаких исходных текстов. Мы можем возложить эту задачу на утилиту qmake. Тут возникает одна неуклюжесть. Обычно мы сначала пишем функцию main(), а затем вызываем такие инструменты как qmake. Но функция main() нуждается в файле ui_mainform.h, который только будет создан qmake. Я включил ссылку на файл ui_mainform.h в текст файла main.cpp, поскольку знал, что такой файл у нас появится (схему построения имен файлов с объявлением класса формы разгадать нетрудно имя состоит из префикса ui_, имени файла, в котором вы сохранили форму, и расширения .h). Если вы не уверены в том, как будет называться созданный автоматически заголовочный файл, можете сначала запустить qmake, а потом редактировать файл main.cpp. После этого qmake, конечно, придется запускать еще раз.

В файле ui_mainform.h определено пространство имен Ui, а в нем класс MainWindow (так по умолчанию назван класс, соответствующий форме главного окна). В функции main() мы создаем (статически) объект этого класса. Если вы думаете, что класс MainWindow происходит от QMainWindow, то ошибаетесь. На самом деле этот класс, описывающий интерфейс нашей программы, вообще не является потомком QWidget. Можно сказать, что класс MainWindow содержит инструкции по построению интерфейса, предназначенные для объекта класса QMainWindow. Объект этого класса мы и создаем далее в нашей программе. Метод mainWindow.setupUi() настраивает внешний вид объекта QMainWindow, в том числе, создает и настраивает дочерние элементы главного окна, которые мы определили в процессе редактирования формы.

Выполнив команды qmake project и qmake, мы можем собрать нашу программу с помощью make. В результате у нас получается главное окно с пустой формой в клиентской области. Конечно, на этом этапе наша программа выглядит не очень привлекательно, но она послужит холстом, на котором мы будем творить шедевры.

Графическая система Arthur

Хотя изменения графической системы Qt 4 по сравнению с Qt 3 не очень заметны на первый взгляд, новая система предоставляет весьма широкие возможности. Напомню, что основные функции работы с графикой в Qt 3 были реализованы в классе QPainter, который изначально предназначался для вывода графики в растровые массивы. Для поддержки подсистем вывода, не основанных на растровых массивах (например, принтеров PostScript) специальные объекты-потомки класса QPaintDevice эмулировали растровое устройство. В Qt 4 разработчики пошли более простым и логичным путем. Классы QPainter и QPaintDevice остались, но к ним добавился абстрактный класс QPaintEngine. Теперь все функции, реализующие специфику графического вывода на разных устройствах, собраны в классах-потомках QPaintEngine, соответствующих этим устройствам. Классы QPainter и QPaintDevice используют методы QPaintEngine для доступа к графическим устройствам, а не обращаются к этим устройствам напрямую, как раньше. Вам же, наоборот, не придется иметь дела с потомками QPaintEngine, если только вы не захотите расширить функциональность Qt 4, реализовав графический вывод на каком-нибудь неподдерживаемом устройстве. На практике это означает, что сладкая парочка QPainter и QPaintDevice теперь может рисовать практически на всех графических устройствах, доступных на данной платформе, причем работа с разными устройствами, будь то принтер или окно OpenGL, в значительной степени унифицирована. Еще одно преимущество новой системы заключается в том, что многие графические операции, которые раньше были реализованы чисто программными средствами, теперь могут использовать аппаратное ускорение и другие функции, поддерживаемые железом (раньше это было невозможно потому, что между QPainter и устройством лежала прослойка эмулятора растрового массива).

Посмотрим, как все это работает на практике. В качестве примера рассмотрим визуальный компонент QGLWidget. Благодаря новой архитектуре мы можем создавать изображения средствами QPainter в рабочем окне QGLWidget точно так же, как и на поверхности любого другого виджета. Не могу не отметить некоторую диалектичность процесса: когда-то я демонстрировал, как выводить графику OpenGL на поверхности объекта-потомка QWidget. Теперь мы воспользуемся QGLWidget для вывода обычных изображений, которые не являются частью трехмерных сцен. Такое использование OpenGL не по назначению отражает популярную в последнее время тенденцию задействовать мощь 3D-ускорителей в традиционно не-трехмерных задачах, например при отрисовке окон или работе с растровыми картинками.

Вернемся к программе с главным окном. Для того чтобы заставить наше главное окно что-то делать, нам следует создать его потомка. Посмотрим на объявление класса OGLWindow, который представляет собой главное окно программы arthur-demo (полный текст программы вы, как всегда, найдетепо ссылке в коце страницы).

#include <QMainWindow>
#include "ui_mainform.h" 

class GLWidget;

class OGLWindow : public QMainWindow, public Ui::MainWindow
{
public:
	OGLWindow(QWidget *parent = 0);
private:
	GLWidget * glWidget;
};

Класс OGLWindow наследует сразу двум классам QMainWindow и Ui::MainWindow (второй из этих классов объявлен в файле ui_mainform.h. Что дает нам двойное наследование? Вернемся к функции main(), приведенной выше. Нам пришлось создать объект класса Ui::MainWindow для того, чтобы настроить внешний вид объекта QMainWindow. Для объекта класса OGLWindow дополнительных объектов создавать не придется, так как этот объект уже знает все, что нужно для построения его графического интерфейса. Ниже приводится текст конструктора OGLWindow:

OGLWindow::OGLWindow(QWidget *parent):QMainWindow(parent)
{
	setupUi(this);
	glWidget = new GLWidget(frame);
	frame->setLayout(new QHBoxLayout);
	frame->layout()->addWidget(glWidget);
}

Поскольку класс OGLWindow наследует классу Ui::MainWindow, метод setupUi() становится доступен в конструкторе OGLWindow. Мы вызываем этот метод и передаем ему в качестве параметра указатель this. Таким образом, объект класса OGLWindow сам настраивает свой интерфейс, а программисту, который захочет работать с нашим классом, не придется беспокоиться о вызове setupUi(). С классом GLWidget, производным от QGLWidget, мы познакомимся ниже. Объект frame представляет собой панель QFrame, которую мы добавили в форму главного окна в процессе визуального редактирования. Мы делаем объект класса GLWidget дочерним объектом этой панели.

Как вы, конечно, догадались, объект glWidget это визуальный компонент, предназначенный для вывода графики средствами OpenGL. Мы хотим, чтобы он занимал все пространство панели frame. Для этого мы создаем новый объект-менеджер компоновки класса QHBoxLayout, назначаем его в качестве текущего менеджера компоновки объекту frame (с помощью метода setLayout()) и добавляем в коллекцию менеджера объект glWidget. Самый простой способ заставить виджет QGLWidget выводить графическое изображение заключается в том, чтобы перекрыть метод paintEvent() в классе потомке QGLWidget. Именно для этого нам и нужен класс GLWidget. Наш вариант метода paintEvent() приводится ниже:

void GLWidget::paintEvent(QPaintEvent *event)
{
	QPainter painter;
	QPen pen;
	painter.begin(this);
	painter.eraseRect(QRect(0, 0, width(), height()));
	pen.setColor(QColor(0, 127, 0));
	pen.setWidth(4);
	painter.setPen(pen);
	painter.drawLine(0, 0, width(), height());
	painter.setRenderHint(QPainter::Antialiasing);
	pen.setColor(QColor(255, 0, 0));
	painter.setPen(pen);
	painter.drawLine(0, height(), width(), 0);
	painter.setBrush(QColor(255, 0, 0, 127));
	painter.drawRect(0, 0, width()/2, height());
	painter.setBrush(QColor(0, 0, 255, 127));
	painter.drawRect(0, 0, width(), height()/2);
	painter.setBrush(QColor(0, 255, 0, 127));
	painter.drawRect(width()/2, 0, width(), height());
	painter.end();
}

Рисование начинается с вызова метода begin() объекта painter() класса QPainter. Аргументом этого метода должен быть указатель на объект QPaintDevice, к каковому типу теперь приводится и объект класса QGLWidget (и, естественно, его потомки). Останавливаться на каждой инструкции вывода графики мы не будем. Обращу внимание читателей на поддержку сглаживания контуров, которую мы включаем с помощью вызова метода setRenderHint() и смешивания цветов alpha blending (обратите внимание на четвертый аргумент конструктора QColor()). Сглаживание и смешивание являются новыми возможностями QPainter и могут выполняться с использованием аппаратной поддержки (например, поддержки 3D-ускорителя), если аппаратная поддержка включена в вашей системе.

В приведенном выше примере я специально не использовал функции OpenGL (например, воспользовался методом eraseRect() вместо glClearColor()), чтобы показать, что в графической системе Arthur можно задействовать возможности OpenGL, не используя сами команды OpenGL. В результате один и тот же код может использоваться для вывода графики в окне с помощью 3D-ускорителя, для записи графики в растровое изображения или для вывода изображений на принтер PostScript.

Рассмотрим теперь функцию main() нашей программы. Наследование класса главного окна сразу от двух предков (QMainWindow и UI::MainWindow) позволило упростить код и этой функции:

#include <QApplication>
#include "oglform.h"

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
	OGLWindow * window = new OGLWindow;
	window->show();
	return app.exec();
}

Для того чтобы собрать программу успешно, в файле pro нужно включить поддержку модуля QtOpenGL:

QT += opengl

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

 

Рисунок 2. Arthur: не-трехмерная графика с помощью OpenGL

В заключение обзора этой программы (первой нашей программы, использующей главное окно) позволю себе дать вам один совет. Готовьтесь к тому, что при работе с главным окном (и другими окнами, спроектированными в Qt Designer 4) вам все время придется создавать их потомков. В Qt Designer 3 мы могли редактировать код обработчиков сигналов в процессе визуального редактирования. Qt Designer 4 тоже позволяет нам связывать сигналы и слоты и даже определять новые слоты, но, поскольку редактировать код в дизайнере все равно нельзя, лучше определять все новые слоты и связывать их с сигналами в классе-потомке того класса, который сгенерирован Qt Designer 4.

Пишем красиво

Система вывода форматированного текста претерпела в Qt 4 не меньше, а, пожалуй, даже больше изменений, чем система вывода графики. Основой для работы с текстовыми документами в новой системе вывода текста, получившей название Scribe, служит класс QTextDocument. Объект этого класса хранит всю информацию о структуре форматированного документа, а также предоставляет функции для его редактирования. Вы можете использовать класс QTextEdit для ручного редактирования текста, содержащегося в QTextDocument, и класс QTextBrowser для просмотра. Любопытно отметить, что хотя в документации к Qt 4 разработчики советуют использовать повсеместно объекты QTextDocument (а не QTextString) для хранения текста, у класса QTextEdit нет конструктора, которому можно было бы передать ссылку на объект QTextDocument, а вот конструктор со ссылкой на объект QString - есть. Для того чтобы назначить объекту класса QTextEdit объект класса QTextDocument, необходимо вызвать метод setDocument().

Важную роль в редактировании содержимого QTextDocument играет класс QTextCursor. Объекты QTextCursor используются как для указания текущей позиции в документе, так и для обозначения выбранных фрагментов текста. Кроме этого объекты QTextCursor предоставляют в распоряжение программиста ряд методов, предназначенных для редактирования текста и изменения форматирования, начиная с выбранной позиции.

Высшей единицей логической структуры документа QTextDocument является фрейм, представленный классом QTextFrame. Весь документ содержится в корневом фрейме (получить доступ к корневому фрейму можно с помощью метода rootFrame()). Перейти от корневого фрейма к дочерним фреймам можно с помощью метода childFrames() объекта QTextFrame (собственные фреймы положены таким элементам документа как таблица или изображение). На более низком уровне элементы документа представлены текстовыми блоками (объекты класса QTextBlock). Текстовым блоком в форматированном документе является любой массив текста, к символам которого применены одинаковые элементы форматирования (это может быть абзац, фраза или отдельное слово). Обычно программа получает доступ к текстовым блокам тогда, когда пользователь выделяет фрагмент текст или когда сама программа выделяет текстовый фрагмент по какому-либо признаку.

Работа со сложными документами не единственный козырь Scribe. Помимо прочего эта система позволяет выполнять фигурный вывод текста. Если вы хотите, чтобы вместо ровных полей слева и права выводимый вами текст был выровнен по контуру какой-нибудь сложной фигуры, воспользуйтесь классом QTextLayout. Класс QTextLayout управляет компоновкой неформатированного текста, то есть, текста, который выводится одним шрифтом. Объекты класса QTextLayout позволяют нам сделать две вещи: разбить текст на строки с учетом параметров выбранного шрифта и ширины каждой строки и задать расположение каждой строки относительно левого края виртуального листа. После этого вывести фигурно расположенный текст на экран (на принтер, или на другое устройство) можно одной командой.

В качестве демонстрации сложного расположения текста мы рассмотрим программу scribe-demo, полный исходный текст которой вы найдете на диске. Как и программа arthur-demo, наша новая программа добавляет в главное окно свой собственный виджет. На этот раз наш фирменный виджет реализуется классом Label. Класс Label происходит не от класса QLabel, а непосредственно от класса QWidget, ведь мы собираемся выводить текст нашими собственными средствами и функциональность класса QLabel нам ни к чему. Объявление класса Label выглядит просто:

class Label : public QWidget
{
public:
    Label(QWidget *parent);
private:
    QTextLayout * textLayout;
    void makeLayout();	
protected:
    void paintEvent(QPaintEvent *event);
};

В классе Label, как и в классе GLWidget, мы перекрываем метод paintEvent() класса-предка. Кроме того мы водим вспомогательный метод makeLayout(). Рассмотрим определения всех трех методов класса Label (конструктора, makeLayout() и paintEvent()).

Label::Label(QWidget *parent) : QWidget(parent)
{	
	QFont font("Times", 22, -1, true);
	QString text = QObject::trUtf8("Этот фрагмент текста выведен на экран с помощью системы Scribe...");		
	textLayout = new QTextLayout(text, font);
}

void Label::makeLayout()
{
	int indent = 20;
	qreal vertPos = 10;
	QTextLine line;	
	textLayout->beginLayout();
	line = textLayout->createLine();
	while (line.isValid())
	{
		line.setLineWidth(width() - 2 * indent);
		line.setPosition(QPointF(indent, vertPos));
		vertPos += line.height();
		indent += 20;
		line = textLayout->createLine();
	}
	textLayout->endLayout();
}

void Label::paintEvent(QPaintEvent *event)
{
	QPainter painter;
	QPen pen;
	makeLayout();			
	painter.begin(this);
	painter.eraseRect(QRect(0, 0, width(), height()));
	pen.setColor(QColor(0, 0, 127));
	painter.setPen(pen);
	textLayout->draw(&painter, QPoint(0,0));
	painter.end();
}

В конструкторе Label мы создаем объект класса QTextLayout. Конструктору объекта textLayout передаются два аргумента ссылка на строку теста и ссылка на объект QFont, который определяет используемый шрифт. Все самое интересное сосредоточено в методе makeLayout(). Мы начинаем работу с компоновщиком текста с вызова метода beginLayout(). Для каждой строки выводимого текста мы создаем объект класса QTextLine с помощью метода createLine() объекта textLayout. Этот метод будет возвращать объекты QTextLine со значением isValid(), равным true, до тех пор, пока весь текст не будет распределен на строки (общее количество строк, разумеется, зависит от размеров шрифта и ширины каждой строки). Ширина строки устанавливается с помощью метода setLineWidth(), а ее позиция методом setPosition(). Для того, чтобы строки не наезжали друг на друга, мы смещаем отступ очередной строки от верхнего края на значение, равное высоте строки. В этом нам помогает метод height() объекта класса QTextLine. После того как создание и расположение строк закончены, мы вызываем метод endLayout().

Метод makeLayout создает своего рода шаблон, содержащий текст. Для вывода этого текста в виджет достаточно вызвать метод draw() объекта textLayout. Первым аргументом метода draw() должна быть ссылка на объект класса QPainter, вторым аргументом ссылка на объект QPoint, определяющий расположение левого верхнего угла той области, в которой выводится текст (фактическое расположение строк, заданное в методе makeLayout(), при выводе текста будет отсчитываться относительно этой точки). 

 

Рисунок 3. Текст с полями в форме треугольника.

Обратите внимание на важную особенность QTextLayout. Каждый раз, когда мы вызываем метод beginLayout() информация о предыдущей компоновке текста (и всех созданных объектах QTextLine) теряется. Эта особенность может стать источником труднообъяснимых ошибок для новичка, но нам на позволяет создать объект класса QTextLayout один раз (в конструкторе Label), а затем использовать его многократно для генерации компоновок текста, зависящих от ширины виджета, в методе makeLayout(). В связи с этим любопытно отметь, что в релизе Qt 4.4 (на момент написания статьи он находился на стадии бета) у класса QTextLayout появился метод clearLayout (), который очищает список строк компоновщика текста. Лично я большой пользы от этого метода не вижу (разве что кому-то понадобится обнулить список строк между вызовами beginLayout() и endLayout()), а, учитывая то, что этот метод поддерживается не всеми релизами Qt 4, пользоваться им не советую.

На этом наше знакомство с изобразительными средствами Qt 4 не закончилось. В следующий раз мы познакомимся с системой Graphics View, которая появилась в  Qt 4.2 и была серьезно дополнена и переработана в Qt 4.3 и Qt 4.4.

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

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


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

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

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

Graphics View Framework

А что у них за игра? Шахматы? Или какой-нибудь стартрек?
Нет, здесь игра для профессионалов...
Садишься за штурвал воображаемого космолета
и определяешь гравитацию незнакомой тебе планеты.
Ее автомат подбирает случайным образом.
М. Пухов, Путь к Земле (Кон-Тики).

Система Graphics View Framework, появившаяся в Qt, начиная с версии 4.2, пришла на смену графической системе, основанной на классе QCanvas. Graphics View Framework это не только система вывода графики с широкими возможностями, но и готовая реализация парадигмы модель-контроллер-вид для программ, работающих с двухмерной графикой. Мы уже встречались с парадигмой модель-контроллер-вид, когда изучали систему Interview Framework, предназначенную для работы с данными, хранящимися в форме таблиц. Graphics View Framework распространяет те же идеи на двухмерную графику. Для объяснения преимуществ Interview Framework мы пользовались программой, работающей с базой данных. Возможности Graphics View Framework проще всего продемонстрировать на примере компьютерной аркады. Предположим, вы решили написать двухмерную компьютерную игру. Применение подхода модель-контроллер-вид может существенно упростить процесс создания такой игры. Описание игрового мира представляет собой модель данных программы. Визуализацию игрового мира выполняет объект отображения (вид). Контроллер транслирует действия пользователя в события модели. Система Graphics View Framework предоставляет вам заготовки для создания модели, контроллера и объекта отображения, изначально наделенные широкой функциональностью. Кроме того Graphics View Framework берет на себя решение таких задач как обнаружение столкновений (collision detection) и геометрические преобразования изображений.

Разумеется, Graphics View Framework может найти применение не только в играх, но и в любых программах, которым приходится отображать интерактивные графические модели, состоящие из большого числа элементов. Основу Graphics View Framework составляют три класса Qt Library, представленные на схеме (рис. 1).

 

Рисунок 1. Схема Graphics View Framework.

Модель данных реализована с помощью объекта класса QGraphicsScene. Элементами модели данных являются графические примитивы (геометрические фигуры и растровые изображения). Все графические примитивы реализованы с помощью классов-потомков класса QGraphicsItem. Таким образом, объект класса QGraphicsScene можно рассматривать как контейнер для набора объектов классов-потомков QGraphicsItem. Для отображения модели, созданной в QGraphicsScene, служит объект класса QGraphicsView. Работая в системе Graphics View Framework, вы не рисуете изображение непосредственно в окне QGraphicsView (хотя в принципе можете это делать). Вместо этого вы управляете объектами, хранящимися в модели QGraphicsScene. Все изменения объектов модели автоматически отображаются в окне QGraphicsView. При этом вам не нужно заботиться о таких вещах как перерисовка изображения при изменении размеров окна. Поскольку объект класса QGraphicsView связан с моделью, он знает, что нужно отображать в окне, и обновляет содержимое окна автоматически. Вторая важная задача, которую решает связка объектов QGraphicsView и QGraphicsScene преобразование действий пользователя (таких, как щелчок мышью, перемещение курсора мыши над объектом или нажатие клавиши) в события модели. События модели могут быть переданы далее отдельным примитивам, формирующим модель. Эта система передачи событий между разными уровнями Graphics View Framework именуется в документации Qt термином event propagation.

Упомянутые выше функции обнаружения столкновений и геометрических преобразований реализованы в классах QGraphicsScene и QGraphicsItem. Все эти операции выполняются независимо от уровня отображения (на уровень отображения передается только конечный результат оперций). Так же как и в системе Interview Framework, с одной моделью Graphics View Framework может быть связано несколько объектов отображения.

Первая проба

Рассмотрим работу простейшей программы Graphics View Framework, выводящей на экран статическое изображение. Эта программа должна выполнить минимальную последовательность операций, необходимых для работы с Graphics View Framework: создать объекты QGraphicsScene и QGraphicsView и связать их между собой, затем заполнить объект QGraphicsScene графическими примитивами и сделать объект QGraphicsView видимым. Написание программы мы начнем с редактирования визуальной части. Виджет QGraphicsView расположен на панели виджетов Qt Designer в разделе Display Widgets. Класс QGraphicsView является потомком QFrame и его удобно сделать центральным визуальным элементом главного окна. Далее в программе следует создать объект класса QGraphicsScene (это можно сделать, например, в конструкторе главного окна). С помощью метода setScene() объекта QGraphicsView мы связываем объект QGraphicsScene с объектом QGraphicsView.

QGraphicsScene * scene = new QGraphicsScene;
graphicsView->setScene(scene);

Добавлять графические примитивы в объект QGraphicsScene можно разными способами, в том числе с помощью методов группы Add* класса QGraphicsScene. Например, для того чтобы добавить в сцену эллипс, вызываем:

scene->addEllipse(QRectF(-100.0, -100.0, 100.0, 100.0));

где scene объект QGraphicsScene. Обратите внимание на то, что координаты эллипса (точнее, координаты прямоугольника, в который он вписан) задаются числами с плавающей точкой, а не целыми числами, как обычно принято в растровой графике. Ниже мы увидим, что встроенная в Graphics View Framework система геометрических преобразований, а также наличие нескольких систем координат, делают использование чисел с плавающей точкой совершенно необходимым. Координаты, которые мы указали при добавлении эллипса, являются координатами модели, а не графического окна. При отображении модели объектом QGraphicsView они будут автоматически переведены в координаты окна QGraphicsView. Как соотносятся точки начала координат модели и начала координат окна? Ответ на этот вопрос может показаться неожиданным: соотношение систем координат зависит от размеров изображения и размеров окна. По умолчанию графическая система располагает изображение, созданное в QGraphicsView, таким образом, чтобы его геометрический центр совпадал с центром окна QGraphicsView. Если размеры изображения превышают размеры окна, в окне появляются полосы прокрутки. Все это означает, что не существует простой формулы для перевода координат окна в координаты модели и обратно. Если вас не увлекают занятия аналитической геометрией, для пересчета координат лучше воспользоваться специальными функциями, предоставляемыми системой.

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

В процессе передачи событий от одного уровня к другому система Graphics View Framework выполняет преобразования координат. Например, если ваша модель обрабатывает щелчки мыши, координаты курсора мыши в окне QGraphicsView в момент щелчка будут автоматически переведены в координаты модели. Если событие мыши связано с одним из графических примитивов, то координаты курсора будут отображены также в систему координат примитива. Таким образом, в Graphics View Framework зачастую приходится иметь дело с тремя наборами координат одной и той же точки (правда, не все эти координаты будут нам нужны).

Теперь мы должны сделать объект QGraphicsView видимым с помощью метода show(). Далее можно скомпилировать программу. Система Graphics View Framework является частью ядра Qt, поэтому подключать дополнительные модули нам не требуется. В результате работы нашей программы мы получаем окно, в котором на белом фоне изображена черная окружность. Рисунок этот, конечно, не особенно впечатляет, но наше знакомство с Graphics View Framework состоялось.

Пишем свою игру

Для более подробного знакомства с возможностями Graphics View Framework мы напишем обещанную игру, подобие всем известного Сокобана (рис. 2). Напомню правила этой древней и мудрой игры: по лабиринту ходит грузчик, задача которого заключается в том, чтобы перенести хаотично разбросанные ящики в заранее определенное место. Грузчик может только толкать ящик перед собой (тащить за собой не может), причем в каждый момент времени он может толкать только один ящик. Полный исходный текст программы вы найдете по ссылке в конце станицы.

 

Рисунок 2. Наш Сокобан

Для реализации игры нам понадобится создать потомка класса QGraphicsScene:

class MvScene : public QGraphicsScene
{
public:
    MvScene(QObject *parent = 0);
protected:
    virtual void mousePressEvent(QGraphicsSceneMouseEvent * mouseEvent);
    virtual void keyPressEvent(QKeyEvent * keyEvent);
private:
    QGraphicsPixmapItem * worker;
    void makeWalls();
    QGraphicsItem * itemCollidesWith(QGraphicsItem * item);
    void placeBox(float x, float y);
    void setBoxes();
};

В отличие от обычной картинки сцена из игрового мира должна реагировать на действия пользователя. В нашем классе MvScene мы перекрываем функции-обработчики событий mousePressEvent() и keyPressEvent() (для этого, собственно говоря, мы и создаем новый класс). Кроме того в нашем классе реализовано несколько вспомогательных функций. Метод makeWalls() создает стены лабиринта, метод setBoxes() размещает ящики, метод placeBox() нужен для добавления одного ящика в лабиринт, а метод itemCollidesWith() используется для обнаружения столкновений. Метод makeWalls() добавляет в объект-сцену прямоугольники, заполненные рисунком текстуры стены.

void MvScene::makeWalls()
{
	float walls[11][4] = {{0, 0, 25, 245}, {25, 0, 425, 25}, {425, 0, 25, 245}, ...};
	QBrush brush(QColor(255, 255, 255), QPixmap("wall.jpg"));
	QPen pen(Qt::NoPen);
	for (int i = 0; i < 11; i++) {
		QGraphicsItem * item =
		addRect(QRectF(walls[i][0], walls[i][1], walls[i][2], walls[i][3]), pen, brush);
		item->setData(0, "Wall");
	}
}

Прямоугольники добавляются в сцену с помощью метода addRect(). В Qt 4.3 и 4.4 этот метод доступен в нескольких перегруженных вариантах. Мы используем вариант, который доступен во всех версиях Qt, начиная с 4.2. Первым аргументом метода addRect() является объект QRectF, который содержит координаты верхнего левого угла прямоугольника, его ширину и высоту. Второй и третий аргументы соответственно перо и кисть, с помощью которых рисуется прямоугольник. Метод addRect() возвращает указатель на объект класса QGraphicsRectItem, являющегося потомком QGraphicsItem. Рассмотрим подробнее метод setData() класса QGraphicsItem. Помимо графических свойств, таких как координаты и параметры кисти и пера, примитивы Graphics View Framework могут быть наделены дополнительными свойствами, определяющими их поведение в модели данных. Мы можем наделить объекты дополнительными свойствами, создавая новые классы на базе классов графических примитивов, но Graphics View Framework предлагает нам и более простой путь. Каждый объект класса-потомка QGrapihcsItem является контейнером, в который можно добавлять произвольные данные. Именно это и делает метод setData(). Первым аргументом метода является численный идентификатор элемента данных (ключ), вторым аргументом сами данные, представленные в виде значения типа QVariant. В нашей программе мы добавляем в каждый графический примитив один дополнительный элемент данных с ключом 0 и строковым значением. В строке записывается название предмета, которому соответствует данный примитив стена (Wall) или ящик (Box). Эта информация понадобится нам для ответа на вопрос, как грузчик (объект worker) должен реагировать на столкновение с соответствующим примитивом. Изображение грузчика добавляется в графическую сцену с помощью метода addPixmap():

worker = addPixmap(QPixmap("Worker.gif"));

Рассмотрим теперь метод keyPressEvent(), который является движущей силой всей нашей игры:

void MvScene::keyPressEvent(QKeyEvent * keyEvent)
{
	QPointF np;
	np.setX(0);
	np.setY(0);
	switch (keyEvent->key()) {
		case Qt::Key_Left:
			np.setX(-10);
			break;
		case Qt::Key_Right:
			np.setX(10);
			break;
		case Qt::Key_Up:
			np.setY(-10);
			break;
		case Qt::Key_Down:
			np.setY(10);
			break;
	}
	worker->translate(np.x(), np.y());
	QGraphicsItem * obstacle = itemCollidesWith(worker);
	if (obstacle) {
		if (obstacle->data(0) == "Wall") {	
			worker->translate(-np.x(), -np.y());
			printf("Hello wall!\n");
		}
		else
		if (obstacle->data(0) == "Box") {
			obstacle->translate(np.x(), np.y());
			if (itemCollidesWith(obstacle) || itemCollidesWith(worker)) {
				obstacle->translate(-np.x(), -np.y());
				worker->translate(-np.x(), -np.y());
				printf("Cannot move!\n");
			}
		}
	}
}

В этом методе мы решаем несколько задач: перемещаем грузчика по игровому полю в направлении, заданном нажатой клавишей (для управления грузчиком используются клавиши со стрелками), выявляем столкновения грузчика с предметами игрового мира и обрабатываем эти столкновения согласно правилам игры. Перемещение грузчика по сцене выполняется с помощью метода translate() класса QGraphicsItem. Этот метод, наряду с методами rotate() и scale(), входит в базовый интерфейс геометрических преобразований Graphics View Framework. Для того чтобы понять, как работают эти методы, нужно вернуться к описанию различных систем координат, которые используются в графической системе Qt 4. Методы, выполняющие геометрические преобразования примитива, работают в системе координат примитива. Особенность этой системы координат заключается в том, что координаты примитива в ней никогда не меняются. Иначе горя, при переносе, вращении и масштабировании примитива его система координат также подвергается переносу, вращению и масштабированию относительно других систем координат. Например, после поворота примитива на 60 градусов, оси его системы координат так же будут повернуты на 60 градусов и, в результате, перенос примитива вдоль одной из осей будет выполняться под углом к границе экрана. Начиная с Qt 4.3, у класса QGraphicsItem появились методы, позволяющие напрямую манипулировать матрицей преобразований (мы рассмотрим эти методы далее, в разделе, посвященном встраиваемым виджетам). При таких сложных отношениях между системами координат функции, предназначенные для перевода значений из одной системы координат в другую, играют особую роль. Метод mapToScene() класса QGraphicsItem выполняет перевод значений из системы координат примитива в систему координат сцены, а метод mapToItem() перевод из системы координат сцены в систему координат примитива.

Вернемся к нашей программе. Мы перемещаем грузчика в новую позицию и с помощью вспомогательной функции itemCollidesWith() проверяем, не столкнулся ли он с другим примитивом. Если грузчик натолкнулся на стену, мы просто возвращаем его в исходную позицию. Если препятствием оказался ящик, мы перемещаем ящик в новую позицию, и проверяем, не натолкнулся ли ящик на препятствие. После перемещения ящика мы также проверяем, не сталкивается ли грузчик еще с каким либо препятствием. В классическом Сокобане грузчик, ящик и сегмент стены имеют одинаковые размеры, поэтому в каждый момент времени грузчик может столкнуться только с одним препятствием. Наш вариант сложнее, поскольку все объекты имеют разные размеры, и грузчик может натолкнуться на ящик и стену одновременно. Если хотя бы одно из перечисленных выше условий не выполнено, и грузчик, и ящик, который он сдвинул, возвращаются в исходные позиции. Поскольку перерисовка сцены выполняется только после выхода из метода, пользователь не увидит всех этих пробных перемещений. Обнаружение столкновений в нашей игре выполняет вспомогательная функция itemCollidesWith()

QGraphicsItem * MvScene::itemCollidesWith(QGraphicsItem * item)
{
QList<QGraphicsItem *> collisions = collidingItems(item);
foreach (QGraphicsItem * it, collisions) {
        if (it == item)
             continue;
return it;
}
return NULL;
}

Функция возвращает первый примитив, с которым столкнулся проверяемый примитив, или NULL, если проверяемый примитив ни с чем не столкнулся. В основе нашей функции лежит метод collidingItems() класса QGraphicsScene. Этот метод возвращает список примитивов, находящихся в состоянии столкновения с примитивом, переданным методу в качестве параметра (под столкновением понимается частичное или полное перекрытие примитивов в системе координат сцены). Список, возвращаемый методом collidingItems(), никогда не бывает пустым. В нем всегда содержится как минимум один примитив тот, который мы проверяем на столкновения. С точки зрения графической системы примитив всегда сталкивается с самим собой. Любители философской диалектики могут увидеть в этом глубокий смысл, нам же при обнаружении столкновения просто приходится пропускать один из элементов списка. Обратите внимание на конструкцию foreach(). Это не новый оператор языка C++, а макрос Qt 4, упрощающий перебор элементов списка, созданного на основе шаблона.

Наша программа обрабатывает также щелчки мыши. Вообще-то в игре Сокобан мыши делать нечего, но в нашем варианте щелчок левой кнопкой мыши позволяет добавить ящик в лабиринт, а щелчок правой кнопкой удалить уже существующий ящик. С помощью метода itemAt() класса QGraphicsScene можно проверить, попал ли указатель в какой-нибудь графический примитив (в этом случае метод itemAt() возвращает указатель на соответствующий объект). В качестве аргумента методу itemAt() передаются координаты указателя мыши в системе сцены. Координаты указателя мыши в системе координат сцены мы можем получить с помощью метода scenePos() объекта mouseEvent (указатель на этот объект передается методу-обработчику события мыши mousePressEvent()). Помимо метода itemAt() у нас есть еще один способ заставить сцену реагировать на события мыши. Мы можем назначать собственные обработчики событий мыши графическим примитивам (объектам QGraphicsItem). Благодаря системе event propagation обработчик будет вызываться только в том случае, если указатель мыши попал в соответствующий примитив, однако подробное описание этого способа выходит за рамки статьи.

У двухмерных примитивов Graphics View Framework есть и третья координата z. Эта координата определяет, какой из примитивов будет виден, если несколько примитивов частично или полностью перекрываются. Кроме того от значения третей координаты зависит порядок, в котором располагаются примитивы в списке, возвращаемом методом collidingItems() (первым в этом наборе располагается примитив с наименьшим значением z). Если данный примитив полностью скрыт другим примитивом с более высоким значением z, метод isObscured() возвращает значение true. Изменить значение координаты z графического примитива можно с помощью метода setZValue() класса QGraphicsItem.

Встраивание виджетов

Начиная с Qt 4.4, система Graphics View Framework обогатилась еще одной весьма интересной возможностью. Речь идет о встраивании виджетов в графическую сцену. В Qt 4.4 у класса QGraphicsScene появился метод addWidget(), который позволяет добавлять в сцену виджеты как обычные графические примитивы. Виджеты, встроенные в графическую сцену, не теряют своей функциональности. Благодаря механизму передачи событий Graphics View Framework встроенные виджеты реагируют на действия пользователя точно так же, как и их обычные собратья. Впрочем, некоторые отличия в поведении встроенных виджетов все таки присутствуют. Например, диалоговое окно, встроенное в графическую сцену, будет вести себя не совсем так, как независимое диалоговое окно. Одновременно с этим встроенные виджеты обладают свойствами графических примитивов Graphics View Framework. Со встроенными виджетами можно выполнять те же геометрические преобразования, что и с остальными примитивами, для них так же работает обнаружение столкновений и другие функции графической системы. Встраивание виджетов является логическим развитием одной из основных идей Graphics View Framework использования возможностей двухмерной графики для построения сложных пользовательских интерфейсов. В то же время с помощью встраивания виджетов можно создать интерфейсы, которые будут выглядеть, мягко говоря, необычно. На диске вы найдете программу crasyiface, демонстрирующую некоторые возможности встраивания виджетов (рис. 3).

 

Рисунок 3.  Психоделический интерфейс пользователя средствами Qt 4.4.

Рассмотрим фрагмент конструктора объекта-сцены программы crasyiface:

QPushButton * button = new QPushButton(trUtf8("Кнопочка"), 0);
QGraphicsProxyWidget * item = addWidget(button);
button->show();
button = new QPushButton(trUtf8("Кнопочка"), 0);
item = addWidget(button);
button->show();
QTransform transform = item->transform();
transform.translate(50., 30.);
transform.rotate(60.0);
item->setTransform(transform);
button = new QPushButton(trUtf8("Еще кнопочка"), 0);
item = addWidget(button);
button->show();
transform = item->transform();
transform.rotate(80.0, Qt::YAxis);
transform.translate(-10., 90.);
transform.scale(5., 2.);
item->setTransform(transform);
QProgressDialog * dialog = new QProgressDialog(trUtf8("Прогресс"), trUtf8("Отмена"), 0, 100);
dialog->setWindowTitle(trUtf8("Progress Dialog"));
item = addWidget(dialog);
dialog->show();
dialog->setValue(66); 
transform = item->transform();
transform.translate(200., 75.);
transform.rotate(-45.0, Qt::YAxis);
transform.scale(2.5, 2.);
item->setTransform(transform);

Для того чтобы добавить виджет в графическую сцену, мы сначала создаем объект соответствующего виджету класса, а потом вызываем метод addWidget(). Метод addWidget() класса QGraphicsScene возвращает указатель на объект класса QGraphicsProxyWidget. Этот класс является отдаленным потомком класса QGraphicsItem и представляет встроенный виджет в графической сцене. По умолчанию виджеты создаются невидимыми и вызов addWidget() не изменяет их состояния, поэтому мы вызываем метод show(). Для выполнения геометрических преобразований виджета мы воспользуемся матрицей преобразований, которая, напомню, появилась в Qt 4.3. Матрица может быть создана многими способами (да, Нео, это так). Мы получаем ссылку на объект, инкапсулирующий матрицу (объект класса QTransform) с помощью метода transform() объекта класса QGraphicsProxyWidget. У класса QTransform есть методы translate(), rotate() и scale(), которые работают не совсем так, как одноименные методы класса QGraphicsItem. При вызове метода rotate() мы, помимо угла поворота, можем указать ось, вокруг которой должно выполняться вращение. Вращать примитивы можно не только вокруг оси z (что соответствует вращению в плоскости x-y) но и вокруг осей x и y. В результате графической сцене можно придать трехмерный вид. Разумеется, это не настоящая трехмерность, так как координата z не является по-настоящему независимой, но если в качестве основы графического вывода используется портал OpenGL (в документации Qt описано, как можно задействовать OpenGL при работе с виджетом QGraphicsView), то для визуализации примитивов будут задействованы наличные возможности 3D-ускорителя. После того как мы внесли изменения в матрицу преобразований, мы снова назначаем эту матрицу примитиву с помощью метода setTransform(). Обратите внимание на то, что виджеты, встроенные в окно программы crasyiface, сохраняют свою функциональность. Кнопки реагируют на щелчки мыши, а встроенное диалоговое окно можно даже закрыть, щелкнув соответствующую кнопку в его заголовке.

Мы много занимались графикой в Qt 4, однако новая версия Qt может пригодиться и тем, кто пишет консольные программы. Следующая статья начнется с описания системы Qt Console.

Исходные тексты примеров (Сокобан)

Исходные тексты примеров (Встраиваемые виджеты)


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

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

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

Механизм сценариев Qt 4

Довольно картинок! Сегодня речь пойдет о том, что может порадовать взгляд разве что прожженных программистов и заядлых линуксоидов о системе скриптов Qt 4.3 (и выше). На протяжении многих лет библиотека Qt пополнялась различными классами, не имеющими прямого отношения к GUI. Сейчас есть даже специальная версия Qt Console для создания неграфических программ. Пользователям открытой редакции Qt доступны все модули Qt Console, в том числе, QtScript, пришедший на замену Qt Script for Applications (QSA) для Qt3.

Изучение системы скриптов похоже на путешествие по неизведанной стране. Нам придется часто останавливаться и сверяться с картой то есть с общими представлениями о том, как работают встроенные интерпретируемые языки.

Для чего в приложении могут понадобиться скрипты? Они широко используются в офисных пакетах (для автоматизации рутинных задач и передачи вирусов), почтовиках (для создания оригинальных заголовков к письмам и передачи вирусов), web-браузерах (для поддержки динамических web-страниц и передачи вирусов). Теперь все эти возможности стали доступны и разработчикам открытых Qt-программ.

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

Часики 

В качестве примера напишем программу-часы (рис. 1), без кукушки, но зато с мощной системой настройки. Исходные тексты вы найдете по ссылке в конце страницы.

 

Рисунок 1. Часы-будильник ничего необычного

Модуль QtScript использует язык ECMAScript, на котором основаны JScript и JavaScript. Вдаваться в детали синтаксиса ECMAScript мы не станем: если вы имели дело с JavaScript, то уже все знаете, остальных же отсылаем к doc.trolltech.com. С точки зрения Qt-приложения, центральным элементом системы скриптов является класс QScriptEngine, который инкапсулирует скриптовый движок. Чтобы программа поддерживала сценарии, ее необходимо собрать с подключением модуля QtScript и создать хотя бы один объект класса QScriptEngine. Рабочая лошадка QScriptEngine метод evaluate(): именно он выполняет скрипты.

Желая убедиться, что система работает, можно просто дописать в программу две строки:

engine = new QScriptEngine();
    
qint32 result = engine->evaluate("2 + 2").toInt32();

В pro-файл следует добавить директиву

QT += script

В результате выполнения программы в переменную result будет записано число 4. Главным аргументом метода evaluate() (есть и другие, со значениями по умолчанию) является строка, содержащая тело скрипта.

 Для того чтобы оптимально использовать сценарии в вашей программе, необходимо понимать некоторые ключевые особенности встроенного скриптового языка. Какими бы широкими возможностями он ни обладал, для выполнения полезной работы ему потребуется доступ к объектам вашей программы. Каким же образом выполняется передача данных между приложением и скриптом? Вспомним, что переменные в интерпретируемых языках (ECMAScript не исключение) обычно полиморфны, то есть, например, могут рассматриваться и как число, и как строка в зависимости от контекста. Это обстоятельство играет роль не только при написании сценариев: значение любой переменной скрипта QtScript может быть представлено в виде объекта класса QScriptValue. В зависимости от контекста, вы можете привести его к тому или иному простому (составному) типу. Объекты QScriptValue своего рода посредники между скриптом и приложением. Например, метод evaluate() возвращает результат выполнения сценария как раз в виде объекта QScriptValue. Он может содержать величину простого типа (как в примере выше), сведения об ошибке (если таковая произошла) или пустое значение (аналог типа void). Вот так, например, можно вывести на консоль ругательное сообщение:


QScriptValue result = engine->evaluate(script);
if (result.isError())
    qDebug() << "Script error:" << result.toString();

Создаем среду окружения

Таким образом, чтобы передать объект приложения в среду выполнения скрипта, надо предварительно упаковать его в QScriptValue (а также задать имя, под которым объект будет известен сценарию). Для потомков QObject это делается легко. Например, передадим скрипту объект, реализующий главное окно нашей программы (это фрагмент конструктора):

QScriptValue objectWnd = engine->newQObject(this);
engine->globalObject().setProperty("MainWindow", objectWnd);

Методу newQObject() передается указатель на объект, который нужно упаковать в QScriptValue (и другие аргументы со значениями по умолчанию). Во второй строке мы задаем имя объекта в среде скрипта и назначаем ему глобальную область видимости. В результате наше главное окно будет доступно сценариям как MainWindow.

Глобальный объект

 Для тех, кто хочет добраться до сути вещей, приведем более строгое объяснение. Язык ECMAScript является чистым объектно-ориентированным. Иначе говоря, весь код скрипта выполняется в контексте некоего глобального объекта. Последний создается автоматически движком скрипта и доступен в Qt-приложении посредством свойства globalObject() объекта QScriptEngine (как и все, что связано со скриптами, глобальный объект среды представлен в Qt-приложении объектом класса QScriptValue). Метод setProperty() просто создает новое свойство глобального объекта.

Передача объекта в среду окружения скрипта предполагает, что последний сможет получить доступ к его элементам. Возможно, это покажется вам странным, но не все методы переданного объекта доступны в контексте скрипта. Причина в том, что сценарий может видеть только те сущности, описания которых доступны во время выполнения программы, а в языке C++ информация о полях и методах объекта доступна лишь на этапе компиляции. У этой проблемы существует несколько решений. Самое простое задействовать сведения о типах времени выполнения (RTTI), предоставляемые Qt: они реализованы для классов, производных от QObject, благодаря чему вы можете получить информацию о сигналах, слотах и свойствах объектов Qt во время работы программы. Обычные поля и методы объектов будут, увы, недоступны. Создавая новый класс, необходимо позаботиться о том, чтобы нужные элементы были видны скрипту. Рассмотрим объявление класса Clock, который реализует главное окно программы-часов.

class Clock : public QDialog, public Ui::MainForm
{
    Q_OBJECT
public:
	Clock(QWidget *parent = 0);
	virtual ~Clock();
	Q_PROPERTY(QString timeFormat READ getTimeFormat WRITE setTimeFormat)
	Q_PROPERTY(QString dateFormat READ getDateFormat WRITE setDateFormat)
	Q_PROPERTY(QString currentTime READ getCurrentTime)
protected slots:
	void onTimeout();
	void execute(const QString & command);
private:
	...
};

Здесь перечислены три свойства: timeFormat, dateFormat и currentTime. Первые два устанавливают формат отображения даты и времени на наших часах. Свойство currentTime, доступное только для чтения, содержит текущее значение времени. Обратите внимание на слот execute() он выполняет команду Linux, переданную в качестве параметра. Второй способ сделать метод объекта видимым в скрипте использовать в его объявлении макрос Q_INVOKABLE:

Q_INVOKABLE void visibleMethod ();

Теперь, когда мы знаем, как передать объект, рассмотрим простейший сценарий (программа Clock читает его из файла clock.config):

MainWindow.timeFormat = "hh:mm:ss";
    
MainWindow.dateFormat = "MM/dd/yy (dddd)";
MainWindow.windowTitle = "Configurable Clock";

Первые две строки не должны вызывать вопросов. Мы используем определенные нами свойства timeFormat и dateFormat для задания форматов даты и времени с помощью принятых в Qt символов-спецификаторов. Но взгляните на последнюю строку мы не определяли свойство windowTitle! Оно является унаследованным, а мы получили дополнительную возможность настройки, о которой даже не думали. Как уже отмечалось выше, свобода, которую предоставляют скрипты, несет в себе потенциальный риск. Если добавить в сценарий строку

MainWindow.close();

конфигурационный файл завершит работу всей программы. Метод close() класса Clock является слотом, а значит, будет доступен из скрипта как метод объекта MainWindow. Но и это еще не все. Посмотрите на следующую конструкцию:

MainWindow.dateText.text = "Date:";

Что есть свойство dateText объекта MainWindow? Имя dateText в нашей программе присвоено объекту QLabel, который выводит пояснение к строке с текущей датой (под именем здесь понимается не идентификатор переменной-указателя, а значение свойства objectName()).name()). В приведенной выше строке мы присваиваем новое значение свойству text объекта dateText. В результате внешний вид окна программы изменится (рис. 2).

 

Рисунок 2. Cценарий может изменить внешний вид программы до неузнаваемости. Этого ли вы хотели?

Вот как выглядит фрагмент скрипта, изменивший внешний вид часов (ни одной строчки кода в самой программе мы не изменили):

MainWindow.dateLabel.hide();
MainWindow.dateText.hide();
MainWindow.setStyleSheet("QDialog { background: yellow }");
MainWindow.timeLabel.setStyleSheet("QLabel {border: 2px solid green;}");
MainWindow.timeText.setStyleSheet("QLabel {border: 2px solid red;}");

С помощью слота hide() мы убираем строки, связанные с отображением даты. Далее, при помощи слота setStyleSheet() мы изменяем цвета и внешний вид элементов окна (при этом используются описания стилей style sheets).

Когда речь идет о свойствах в контексте Qt, мы ставим скобки после имени, (например, objectName() ), что соответствует синтаксису Qt. Когда же мы говорим о свойствах в контексте скрипта, их имена пишутся без конечных скобок.

Иными словами, скрипт может получить доступ не только ко всем свойствам, сигналам и слотам переданного ему объекта, но и ко всем его поименованным дочерним объектам. Иногда это бывает полезно, иногда не очень. Например, если программа открывает диалоговое окно, а скрипт имеет доступ к соответствующему объекту, он может щелкнуть по кнопке (вызвав сигнал clicked()) вместо пользователя. Итоговый вывод: для взаимодействия со скриптом лучше создавать специальный объект, предоставляющий только те свойства, сигналы и слоты, которые необходимы для целей автоматизации. Доступ скрипта к элементам объекта можно ограничить и по-другому посредством третьего аргумента метода newQObject().

Например, вызов:

QScriptValue objectWnd = engine->newQObject(this,
    QScriptEngine::QtOwnership, QScriptEngine::ExcludeSuperClassMethods | QScriptEngine::ExcludeSuperClassProperties);

приведет к тому, что скрипт не сможет обращаться к свойствам и методам, определенным в классах-предках передаваемого объекта.

Тогда

MainWindow.close();

вызовет ошибку

ReferenceError: close is not defined

Добавление в третий аргумент константы QScriptEngine::ExcludeChildObjects запрещает доступ к дочерним объектам.

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

Делегация полномочий

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

function onTimer() 
{
	if (MainWindow.currentTime == "00:00:00")
		MainWindow.execute("konsole");
}  

Функция onTimer() должна стать обработчиком сигнала timeout(), который периодически генерируется объектом-таймером в нашей программе. Она проверяет текущее значение времени (свойство currentTime) и ровно в полночь запускает программу konsole с помощью слота execute(). После того, как функция-обработчик будет объявлена, ее следует связать с сигналом таймера:

MainWindow.timer.timeout.connect(onTimer);

timer это имя объекта-таймера в Qt-приложении. Сигнал timeout() объекта timer представляется в скрипте как свойство-объект, у которого есть методы connect() и disconnect(). Первый выполняет связывание обработчика и сигнала, второй разрывает эту связь. Необходимо понимать разницу между обращениями

MainWindow.timer.timeout() 

и

MainWindow.timer.timeout 

В первом случае мы вызываем сигнал timeout() таймера, что приводит к вызову связанных с ним слотов. Во втором получаем доступ к свойству-объекту, которое позволяет нам управлять параметрами обработки сигнала. Благодаря возможности назначать функции скрипта в качестве обработчиков сигнала таймера, мы можем превратить нашу программу-часы в будильник с весьма широкими возможностями настройки. В случае с обработкой сигналов в скрипте функция-обработчик вызывается (что вполне логично) и после выхода из метода evaluate(). Но здесь возникает еще один интересный вопрос а сколько времени существует скрипт? Ответ на него вполне логичен столько же, сколько существует его контекст, в том числе, глобальный объект. По умолчанию время жизни контекста скрипта совпадает со временем существования объекта QScriptEngine.

В обратную сторону

Qt-приложение может передавать в контекст скрипта ссылки не только на объекты, но и на функции, не являющиеся методами. Для этого необходимо выполнить те же самые действия: упаковать указатель на функцию в объект QScriptValue и назначить ей имя и область видимости в контексте скрипта. Но с точки зрения сценария и приложения, одна и та же функция выглядит по-разному. В скрипте у нее нет прототипа, так что она может принять любое число аргументов; в программе же нужно обработать два параметра: указатели на объекты QScriptContext и QscriptEngine.

В качестве примера напишем функцию toUnicode(), преобразующую текст из кодировки скрипта в UTF-16, которой пользуется Qt. То же самое можно сделать, используя язык ECMAScript, но для нас важен процесс, а не результат:

QScriptValue toUnicode(QScriptContext *context, QScriptEngine *engine) 
{
	QString s = context->argument(0).toString();
	return QScriptValue(engine, QString::fromUtf8(s.toAscii().data()));
}  

Кода скрипт вызовет нашу функцию, все переданные ей аргументы будут сохранены в специальной коллекции объекта context (их общее число есть context.argumentCount()). Все аргументы, равно как и возвращаемое значение, являются, естественно, объектами QScriptValue. Для присвоения функции имени в контексте скрипта нужно проделать следующее:

QScriptValue func = engine->newFunction(toUnicode);
engine->globalObject().setProperty("toUnicode", func);

Метод newFunction() класса QScriptEngine создает объект QScriptValue, содержащий указатель на функцию. Вторая строка нам уже знакома это задние свойства глобального объекта скрипта. Теперь мы можем использовать в скрипте функцию toUnicode(), например:

MainWindow.windowTitle = toUnicode("Часы с настройкой");

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

Я тебя породил...

Скрипты Qt могут не только пользоваться объектами, предоставленными им приложением, но и создавать свои собственные. Это можно делать на основании хранимых в сценариях описаний типов, но мы пойдем более простым путем переложим ношу на плечи Qt. В нашем демонстрационном приложении Clock определен класс TextInputDialog, который создает диалоговое окно со строкой для ввода текста, пояснением к ней и кнопками OK и Отмена. Его можно использовать в настроечных скриптах программы clock для запроса данных у пользователя (рис.3). Чтобы сценарий мог создать объект класса TextInputDialog тогда, когда ему это нужно, в приложении clock следует объявить функцию

QScriptValue newTextInputDialog(QScriptContext *context, QScriptEngine *engine) 
{
	return engine->newQObject(new TextInputDialog(), QScriptEngine::ScriptOwnership);
}    

Список параметров стандартный. В теле функции мы создаем QScriptValue, инкапсулирующий объект класса TextInputDialog. Он и возвращается в качестве результата функции. Обратите внимание на второй аргумент метода newQObject(). Константа QScriptEngine::ScriptOwnership указывает, что создаваемый объект должен принадлежать скрипту (еще один важный момент: объект, создаваемый специально для сценария, не должен иметь родительского объекта в приложении, то есть parent() должно равняться NULL). Мы делам функцию newTextInputDialog() доступной скрипту так же, как и в случае ToUnicode(). Теперь в сценарии можно писать следующее:

var Dlg = newTextInputDialog();
var Text;
function onAccept()
{
	Text = Dlg.lineEdit.text;
	print(Text);
}
Dlg.accepted.connect(onAccept);
Dlg.modal = true;
Dlg.show();    

Здесь создается объект TextInputDialog, ссылка на который сохраняется в переменной Dlg (поскольку адресное пространство приложения скрипту недоступно, уместно говорить именно о ссылках, а не об указателях). Сигнал accepted() диалогового окна (генерируемый при щелчке на OK) связывается с обработчиком onAccept. В нем мы считываем строку, введенную пользователем (свойство text объекта lineEdit), и распечатываем ее в окне консоли с помощью встроенной функции print().

 

Рисунок 3. Наши часы окончательно обнаглели может быть, во всем виноват вирус?

В чем разница между передачей объектов в скрипт и созданием объектов в скрипте? В первом случае, он является некой данностью. Во втором скрипт сам решает, когда нужно создать объект. В связи с этим у читателя может возникнуть вопрос: создавать объекты мы научились, но почему не предусмотрен метод для их удаления? Отвечаем: такой метод просто не нужен. Среда выполнения скрипта использует механизм управления памятью, известный как сборка мусора. Динамически созданный объект будет удален автоматически, когда все видимые переменные перестанут ссылаться на него. Жаль, что его нельзя применить в реальном мире...

 

JavaScript основан на ECMAScript, так что конфигурационные файлы Clock можно править в KWrite со всеми удобствами.

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


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

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

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

Знакомство с Qt 4, Interview Framework

А вы уже пишете программы под Qt 4.x? Хотя на Linux-десктопах все еще господствует KDE 3.x, а следовательно и Qt 3.x, Все новые версии дистрибутивов Linux поставляются так же и с Qt 4.x, причем во многих дистрибутивах по умолчанию используются именно инструменты Qt четвертой версии. К этому следует добавить, что и переход на KDE 4 дело ближайшего будущего, а значит, если вы еще не портировали свою программу, использующую Qt 3, на новую версию, самое время заняться этим сейчас. Если же вы только собираетесь создавать Qt-проект, Qt 3.x не стоит даже рассматривать. 

В общем и целом можно сказать, что по сравнению с предыдущей версией система Qt 4 улучшилась, обогатилась возможностями и усложнилась. Давно прошли те времена, когда вся Qt умещалась в одной единственной разделяемой библиотеке. Что касается системы Qt 4, то ее основные классы вольготно расположились в 12 библиотеках (тринадцатая библиотека, libQt3Support.so, предназначена для поддержки кода Qt 3.x в тех случаях, когда перенос кода на новую версию представляется совсем уж трудным делом). Мы надеемся, что программисты Linux, привыкшие управлять демонами и убивать зомби, люди не суеверные, и число 13 их не испугает. Заголовочные файлы Qt 4 также умножились в числе и распределены теперь по нескольким поддиректориям. Впрочем, как мы увидим дальше, управлять всем этим зверинцем не так уж и трудно. 

Знакомство с Qt 4 мы начнем с вопросов лицензирования, которые многим, возможно, покажутся слишком скучными. В Qt 4 наконец-то реализована полноценная схема двойного лицензирования для всех платформ (Windows, X11 и Mac), причем открытый вариант Qt распространяется на условиях GPL 2.0. Сами представители Qt именуют свой подход Quid Pro Quo, что в вольном переводе с древней латыни означает баш на баш. Если вы хотите пользоваться средствами Qt бесплатно, взамен вы должны предоставить сообществу свой код (открытая модель). Если вы не желаете делиться кодом, вы должны заплатить деньги (своего рода выкуп, который, естественно, будет потрачен на дальнейшее совершенствование Qt). Интересно отметить, что лицензионная политика Qt не позволяет лицензиатам применять тот же принцип двойного лицензирования к своим продуктам. Если вы ведете разработку с помощью Qt, ваш проект должен быть либо открытым на условиях GPL, либо коммерческим. То есть, вам, конечно, никто не мешает раздавать ваш код бесплатно, но если это делается не на основе GPL, то для разработки этого кода необходимо использовать (и, естественно, оплатить) коммерческую версию Qt. У открытой версии Qt для Windows есть одна неприятная особенность согласно намерениям разработчиков ее можно использовать только совместно со средой компиляции MinGW, которая нравится не всем (мне, например, не нравится). Для того чтобы работать с MS Visual Studio (а это, все-таки, лучшая платформа разработки для Windows), придется заплатить за коммерческую версию Qt. Причины такого ограничения непонятны и упомянутый выше принцип quid pro quo здесь не срабатывает, поскольку сама Microsoft распространяет бесплатный вариант Visual Studio Express Edition. Кроме того, по сведениям, полученным с различных форумов, после небольшой обработки напильником открытую Qt 4 можно заставить работать с компиляторами С++ Microsoft и CodeGear, предназначенными для командной строки. По моему мнению, ограничив поддержку компиляторов для открытой версии Qt, разработчики из TrollTech сделали глупость, которая, я надеюсь, будет исправлена в дальнейших релизах Qt 4.x. 

Перейдем теперь к техническим новшествам Qt 4. Похоже, старым добрым контейнерам STL так и не суждено стать настоящим стандартом. У каждого набора визуальных компонентов, основанного на C++, есть свой набор контейнеров (что, вообще говоря, не очень хорошо, так как делает невизуальный код, который особенно часто использует абстрактные типы данных, труднопереносимым между разными платформами разработки). В Qt 4 появилась система контейнеров Tulip (тюльпан), призванная заменить старые контейнеры Qt 3. Tulip предоставляет в наше распоряжение стандартный список контейнеров последовательного доступа (список, связный список, очередь, вектор, стек) и несколько ассоциативных контейнеров: отображения (классы QMap и QMultiMap), хеш-таблицы (классы QHash и QMultiHash) и набор (класс QSet). Теоретически разница между классами QMap и QMultiMap и классами QHash и QMultiHash заключается в том, что первый класс из каждой пары позволяет связать с ключом только одно значение, тогда как второй класс позволяет назначать по несколько значений одному ключу. Напомню, что в ассоциативных контейнерах хранимым значениям сопоставляются ключи, которые позволяют организовать произвольный доступ к данным контейнера. Разделение классов QHash и QMultiHash вызывает некоторые вопросы. При использовании хеш-таблиц нередко возникают коллизии (когда двум хранимым значениям соответствует один и тот же ключ). Придумать хеш-функцию, которая бы гарантированно не вызывала коллизий, очень трудно (а иногда и нереально). Фактически, возможность связывать несколько значений с одним ключом является неотъемлемым свойством хеш-таблиц. Разработчики контейнеров Qt, разумеется, об этом знают, и в классе QHash реализована возможность добавления нескольких значений с одним и тем же ключом. В результате различие между контейнерами QHash и QMultiHash (второй, кстати, является потомком первого) выглядит скорее косметическим. 

Еще одно новшество Qt 4 - Interview Framework. Система Interview Framework представляет собой вариант реализации парадигмы модель-контроллер-вид. В основе парадигмы модель-контроллер-вид лежит старая и плодотворная идея разделения движка программы и интерфейса. В рамках парадигмы модель-контроллер-вид (подробно описанной в многочисленной литературе по правильному программированию) модель представляет собой, по сути, движок приложения. Именно модель определяет, что и как программа может делать. Термином вид (представление) фактически описывается все, что имеет непосредственное отношение к интерфейсу пользователя. Вид позволяет пользователю получать информацию о состоянии модели и передавать программе команды. Команды пользователя обрабатывает контроллер, который вносит соответствующие изменения в состояние модели или вида и, в частности, не позволяет пользователю нарушить целостность модели в результате введения неправильных команд. Как и многие другие парадигмы, призванные формализовать процесс создания программ, парадигма модель-контроллер-вид редко применяется на практике в чистом виде. В частности отдельные элементы парадигмы нередко объединяются друг с другом. Interview Framework превращает парадигму модель-контроллер-вид в парадигму модель-вид, объединяя контроллер и вид в одно целое. Хотя парадигма модель-контроллер-вид (а, следовательно, и Interview Framework) может применяться при написании множества типов программ, разработчики Interview Framework, судя по всему, ориентировались в основном на создание клиентских приложений для работы с базами данных. Именно на примере клиентского приложения БД проще всего понять, как работает Interview Framework. В клиентском приложении БД, использующем Interview Framework, модель выполняет роль посредника между БД и интерфейсом пользователя, представляющим данные БД. Именно модель определяет логику представления данных. Когда пользователь хочет получить информацию о текущем состоянии БД, пользовательский интерфейс (компонент вид) обращается к модели напрямую. Для работы с отдельными элементами данных служат делегаты, которые предают команды пользователя модели. 

Переходя от абстрактного изложения принципов Interview Framework к изложению более конкретному, мы должны познакомиться (простите за каламбур) с тремя абстрактными классами QAbstractItemModel, QAbstractItemView и QAbstractItemDelegate. Эти классы являются предками всех классов, реализующих, соответственно, модели, представления (виды) и делегаты. Классы QTableView, QTreeView и QListView реализуют три наиболее популярные формы представления данных: таблицу, дерево и простой список. Если вам требуется более сложный компонент отображения модели, вам придется создавать собственный класс, основанный на QAbstractItemView. 

Класс QStandardItemModel, являющийся потомком QAbstractItemModel, представляет собой реализацию модели в самом общем смысле. Помимо прочего, этот класс реализует ряд методов, предназначенных для работы с индексами. Индексы используются в Interview Framework для указания элементов данных, с которыми работает модель. Класс QDirModel реализует модель для работы с директориями. Этот класс пригодится вам, если вы надумаете писать собственный файл-менеджер или свою версию диалоговых окон открытия и сохранения файла. Следует отметить, что один и тот же объект, реализующий модель, может взаимодействовать (в том числе, одновременно) с объектами нескольких разных классов, отвечающими за представление данных. Например, уже упомянутый класс QDirModel может использовать для представления информации о директориях классы QTableView, QTreeView, и QListView. Класс QStringListModel, как можно догадаться по его названию, реализует модель, основой которой является список строк. 

Классы QAbstractTableModel и QAbstractListModel могут служить основой для ваших собственных классов-моделей, предполагающих представление данных в виде таблиц и списков соответственно. Такие классы как QTreeView и QListView предназначены для работы с моделями, но использовать их в качестве самостоятельных виджетов затруднительно. Для решения этой проблемы на базе классов QTableView, QTreeView и QListView созданы классы QTableWidget, QTreeWidget и QListWidget. Объекты этих классов представляют собой обычные визуальные компоненты, при работе с которыми пользователь может добавлять и удалять данные, не заботясь о моделях и делегатах. На самом деле, эти классы просто реализуют свои собственные модели данных, незаметные для пользователя. Как видим, система Interview Framework играет в Qt большую роль, чем может показаться на первый взгляд!

Как отмечалось выше, наиболее удобное средство демонстрации Interview Framework клиентские приложения баз данных. Такое приложение мы и напишем. Прежде всего, рассмотрим арсенал специальных классов, которые Interview Framework предоставляет нам для работы с базами данных. Для создания модели приложения БД к нашим услугам три класса: QSqlQueryModel, QSqlTableModel и QSqlRelationalTableModel. Из этих трех классов QSqlQueryModel самый простой. Его возможности, фактически, ограничиваются передачей результата запроса к базе данных. При этом, правда, стоит отметить, что класс QSqlQueryModel обладает определенными возможностями, позволяющими изменить структуру отображения данных перед передачей ее на уровень представления. Класс QSqlTableModel гораздо функциональнее. Этот класс логически организует результаты SQL-запросов как таблицы и включает в себя функции редактирования данных. Наконец класс QSqlRelationalTableModel позволяет нам задействовать в приложении основные возможности реляционной модели баз данных работу с данными из нескольких таблиц, связанных внешними ключами. Для представления данных моделей SQL наиболее логично использовать объекты класса QTableView (хотя унифицированная структура Interview Framework позволяет использовать совместно с SQL-моделями и другие стандартные виды, они, как правило, менее удобны и информативны при работе с данными БД).

Для нашего первого приложения Interview Framework (и Qt 4!) мы воспользуемся самой простой SQL-моделью QSqlQueryModel. Мы создадим программу, позволяющую просматривать некий каталог музыкальных произведений (на самом деле крошечный фрагмент моего музыкального каталога). Для нашего приложения БД нам понадобится база данных, содержащая хотя бы одну таблицу. Я воспользовался для написания программы примера СУБД PostrgeSQL, которая присутствует в любом дистрибутиве Linux. Вы сможете, если захотите, адаптировать этот пример к любой другой СУБД.

Для тех, кто любит точные инструкции, привожу последовательность действий по созданию базы данных testdb. Настройте и запустите сервер PostrgeSQL а вашем компьютере (распространяться о том, как это делать, я не буду, так как статья посвящена все-таки не PostrgeSQL, да и журнал не так давно писал о настройке сервера). Далее командуйте

createdb testdb

теперь войдите в консольный клиент PostrgeSQL:

psql testdb

и скомандуйте

\i createtable.sql

Файл createtable.sql вы найдете по ссылке в конце статьи. Теперь можете выйти из клиента с помощью команды \q. Перейдем теперь, собственно, к самой программе. Ее исходный текст невелик и состоит всего лишь из одного файла, поэтому я публикую его полностью (вы, конечно, можете найти ее и на диске, в файле main.cpp).

   
#include <QtDebug>
#include <QtGui>
#include <QSqlDatabase>
#include <QSqlQueryModel>
#include <QSqlError>
#include <QTableView>

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
    	QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL");	
	db.setHostName("localhost");
	db.setDatabaseName("testdb");
	db.setUserName("user");
	db.setPassword("password");
	if (!db.open()) {
		qDebug() << QObject::trUtf8("Не смогла я открыть базу данных") << db.lastError().text();	
		return -1;
	}
	QSqlQueryModel * model = new QSqlQueryModel(0);
	model->setQuery("SELECT * FROM music");
	model->setHeaderData(0, Qt::Horizontal, QObject::trUtf8("Автор"));
	model->setHeaderData(1, Qt::Horizontal, QObject::trUtf8("Альбом"));
	model->setHeaderData(2, Qt::Horizontal, QObject::trUtf8("Произведение"));
	model->setHeaderData(3, Qt::Horizontal, QObject::trUtf8("Год выхода"));
	QTableView * view = new QTableView(0);
	view->setModel(model);
	view->setWindowTitle(QObject::trUtf8("Музыкальный каталог"));
	view->show();
	return app.exec();
}

Исходный текст программы начинается, естественно, с заголовочных файлов. Первым следует файл <QtDebug>, который содержит полезные средства для вывода отладочных сообщений. Вследствие некоторых особенностей структуры заголовочных файлов Qt 4.x, если вы решили включить в текст программы этот файл, вы должны включить его прежде всех других заголовочных файлов. Нарушение этого правила приводит к тому, что при определенном сочетании версий Qt 4.x, набора используемых заголовочных файлов и фазы Луны компилятор начинает выдавать довольно странные сообщения об ошибках. Далее следует заголовочный файл <QtGui>. Этот файл содержит объявления классов и функций, реализованных в двух базовых модулях Qt QtCore и QtGui. Включив этот файл в текст нашей программы, мы избавляем себя от необходимости добавлять по отдельности заголовочные файлы для таких классов как QApplication. Файл <QSqlDatabase> содержит объявление класса QSqlDatabase, который мы используем для создания соединения с сервером БД. Мы включаем в текст программы определения классов QSqlQueryModel и QTableView.

Нашей первой задачей, как всегда, является создание объекта класса QApplication. Затем мы создаем соединение с сервером баз данных. Объект QSqlDatabase, инкапсулирующий соединение с сервером, создается с помощью статического метода QSqlDatabase::addDatabase() Этот метод существует в нескольких вариантах, каждому из которых при вызове можно передать несколько аргументов. Мы передаем методу addDatabase() один аргумент (значения остальных заданы по умолчанию), представляющий собой имя драйвера СУБД. Поскольку я воспользовался PostgreSQL, в программе указывается драйвер QPSQL. Если вы захотите использовать MySQL, вам понадобится драйвер QMYSQL. По умолчанию драйверы скомпилированы в виде отдельных модулей, но если вы хотите, то можете встроить код драйвера в свое приложение. Для этого вам понадобятся исходные тексты Qt. Исходные тексты всех драйверов вы найдете в поддиректориях директории QTDIR/src/sql/drivers/.

Теперь, когда у нас есть объект db класса QSqlDatabase, реализующий соединение с сервером БД, мы должны настроить это соединение. С помощью соответствующих методов объекта db мы указываем имя узла, имя базы данных, имя пользователя и его пароль (если вы создавали базу данных так, как описано выше, имя пользователя и пароль для доступа к ней совпадают с именем пользователя и паролем вашей учетной записи Linux). Настроенное соединение открывается с помощью метода open(). Этот метод возвращает значение типа bool, которое указывает, удалось ли установить соединение с сервером БД. Если open() возвращает false, наша программа выводит жалобное сообщение, затем более подробное сообщение об ошибке (с помощью метода db.lastError().text()) и завершает работу.

После того как соединение с БД успешно установлено мы можем приступить к созданию модели (объект model класса QSqlQueryModel). Главным методом объекта QSqlQueryModel является метод setQuery(), который позволяет указать текст SQL-запроса с БД. В нашем примере запрос выбирает все данные из таблицы music. Глядя внимательно на этот код, вы можете спросить, а откуда объект model узнает, из какой базы данных, иначе говоря, из какого объекта QSqlDatabase, он должен получить данные. Ответ на этот вопрос прост. При вызове статического метода addDatabase(), которым был создан объект db, мы могли бы указать уникальное имя соединения. Поскольку мы этого не сделали, созданное нами соединение с БД стало соединением по умолчанию (это логично, ведь наша программа использует только одну базу данных). Получить объект, реализующий соединение по умолчанию, можно с помощью статического метода QSqlDatabase::database(), вызванного без параметров. Именно так метод setQuery()объекта model определяет нужное соединение с БД. Если бы мы работали с несколькими соединениями, мы могли бы воспользоваться перегруженным вариантом метода setQuery(), которому помимо текста запроса передается ссылка на объект QSqlDatabase.

Метод setHeaderData(), который мы далее вызываем, позволяет назначить произвольные заголовки столбцам таблицы. Это один из немногих косметических методов с помощью которых модель QSqlQueryModel может внести свою лепту в обработку данных (еще один метод removeColumns() позволяет сделать невидимыми отдельные столбцы таблицы). Мы используем метод setHeaderData() для присвоения русских названий столбцам таблицы.

Теперь мы переходим к созданию вида (объект view). Метод setModel() связывает вид с моделью, а метод setWindowTitle() устанавливает заголовок окна. Нам остается вызывать метод show(), чтобы сделать вид действительно видимым и запустить цикл обработки сообщений приложения Qt.

Процедура сборки приложения выглядит несколько сложнее, чем в стандартном случае. После того как мы скомандовали qmake project, нам необходимо доработать содержимое созданного файла .pro. Как уже отмечалось, различные компоненты Qt расположены в разных файлах библиотек, а заголовочные файлы в разных директориях. К сожалению, сама утилита qmake не может определить, какие модули Qt использует наша программа, так что нам придется указать их явным образом. Впрочем, сделать это несложно. Добавим в файл .pro строку


    
QT += sql

Таким образом, мы указываем, что нашей программе потребуется модуль QtSql. В результате в создаваемый make-файл будут включены директивы, подключающие к проекту необходимые библиотеки и указывающие расположение заголовочных файлов, необходимых для работы с базами данных. Мы могли бы подключить компоненты модуля SQL и вручную. Для этого в файл .pro надо было бы добавить строку

LIBS += -lQtSql

которая бы трансформировалась в make-файле в директиву компоновщика на связывание проекта с разделяемой библиотекой libQtSql.so. Заголовочные файлы модуля QtSql расположены по умолчанию в директории /usr/include/QtSql/. Если бы мы делали все вручную, нам бы следовало либо добавить эту директорию в переменную INCLUDEPATH файла .pro, либо заменить в исходных текстах программы директивы типа

#include <QSqlDatabase>

на

#include <QtSql/QSqlDatabase>

Как видите, добавление модуля sql в переменную QT избавило нас от многих хлопот. Теперь наша программа готова к сборке. Командуем:

qmake
make

В результате получаем простенькую программу просмотра содержимого таблицы БД (рис. 1).

Программа просмотра БД

Рисунок 1. Программа для просмотра музыкального каталога

Система Qt 4 подготовила для нас много нового. В следующей статье мы продолжим знакомство с системой Interview Framework и созданными на ее основе компонентами, а также рассмотрим новые вспомогательные средства разработчика Qt.

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


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

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