Глава 6. Управление компоновкой
Каждому размещаемому в форме виджету необходимо задать соответствующие размер и позицию. Qt содержит несколько классов, обеспечивающих компоновку виджетов на форме: QHBoxLayout, QVBoxLayout, QGridLayout и QStackLayout. Эти классы настолько удобно и просто применять, что почти каждый Qt—разработчик их использует либо непосредственно в исходном коде программы, либо через Qt Designer.
Другая причина применения классов Qt по компоновке виджетов — гарантия автоматической адаптации формы к различным шрифтам, языкам и платформам. Если пользователь изменяет настройки шрифта системы, формы приложения немедленно на это отреагируют, изменяя при необходимости свои размеры. И если вы переводите интерфейс пользователя приложения на другие языки, классы компоновки будут учитывать содержание переведенных виджетов, чтобы избежать усечения текста.
К другим классам, управляющим компоновкой, относятся QSplitter, QScrollArea, QMainWindow и QWorkspace. Общая черта этих классов — обеспечение гибкой компоновки виджетов, которой может управлять пользователь. Например, QSplitter обеспечивает наличие разделительной линии, которую пользователь может передвигать для изменения размеров виджетов, a QWorkspace обеспечивает поддержку MDI (multiple document interface — многодокументный интерфейс), позволяющего в главном окне приложения показывать сразу несколько документов. Поскольку эти классы часто используются как альтернатива основным классам компоновки, они также рассматриваются в данной главе.
Компоновка виджетов на форме
Существует три основных способа управления компоновкой дочерних виджетов формы: абсолютное позиционирование, ручная компоновка и применение менеджеров компоновки. Мы рассмотрим по очереди каждый из этих методов, используя в качестве нашего примера диалоговое окно Find File (найти файл), показанное на рис. 6.1.
Рис. 6.1. Окно диалога Find File.
Абсолютное позиционирование является самым негибким способом компоновки виджетов. Он предусматривает жесткое кодирование в программе размеров и позиций дочерних виджетов формы и фиксированный размер самой формы. Ниже показано, какой вид принимает конструктор FindFileDialog при применении абсолютного позиционирования:
01 FindFileDialog::FindFileDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 namedLabel->setGeometry(9, 9, 50, 25);
05 namedLineEdit->setGeometry(65, 9, 200, 25);
06 lookInLabel->setGeometry(9, 40, 50, 25);
07 lookInLineEdit->setGeometry(65, 40, 200, 25);
08 subfoldersCheckBox->setGeometry(9, 71, 256, 23);
09 tableWidget->setGeometry(9, 100, 256, 100);
10 messageLabel->setGeometry(9, 206, 256, 25);
11 findButton->setGeometry(271, 9, 85, 32);
12 stopButton->setGeometry(271, 47, 85, 32);
13 closeButton->setGeometry(271, 84, 85, 32);
14 helpButton->setGeometry(271, 199, 85, 32);
15 setWindowTitle(tr("Find Files or Folders"));
16 setFixedSize(365, 240);
17 }
Абсолютное позиционирование имеет много недостатков:
• пользователь не может изменить размер окна;
• некоторый текст может оказаться отсеченным, если пользователь выбирает необычно большой шрифт или если приложение переводится на другой язык;
• виджеты могут иметь неправильные размеры для некоторых стилей;
• расчет позиций и размеров должен производиться вручную. Этот процесс утомителен и приводит к ошибкам; кроме того, это сильно затрудняет сопровождение.
В качестве альтернативы абсолютному позиционированию используется ручная компоновка. При ручной компоновке виджетам все же придаются абсолютные позиции, но размеры виджетов становятся пропорциональными размеру окна, а не жестко кодируются в программе. Это может достигаться путем переопределения функции формы resizeEvent() для установки геометрических размеров своих дочерних виджетов:
01 FindFileDialog::FindFileDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 SetMinimumSize(265, 190);
05 resize(365, 240);
06 }
07 void FindFileDialog::resizeEvent(QResizeEvent * /* event */)
08 {
09 int extraWidth = width() - minimumWidth();
10 int extraHeight = height() - minimumHeight();
11 namedLabel->setGeometry(9, 9, 50, 25);
12 namedLineEdit->setGeometry(65, 9, 100 + extraWidth, 25);
13 lookInLabel->setGeometry(9, 40, 50, 25);
14 lookInLineEdit->setGeometry(65, 40, 100 + extraWidth, 25);
15 subfoldersCheckBox->setGeometry(9, 71, 156 + extraWidth, 23);
16 tableWidget->setGeometry(9, 100, 156 + extraWidth, 50 + extraHeight);
17 messageLabel->setGeometry(9, 156 + extraHeight, 156 + extraWidth, 25);
18 findButton->setGeometry(171 + extraWidth, 9, 85, 32);
19 stopButton->setGeometry(171 + extraWidth, 47, 85, 32);
20 closeButton->setGeometry(171 + extraWidth, 84, 85, 32);
21 helpButton->setGeometry(171 + extraWidth, 149 + extraHeight, 85, 32);
22 }
Мы устанавливаем в конструкторе FindFileDialog минимальный размер формы на значение 265 × 190 и ее начальный размер на значение 365 × 240. В обработчике событий resizeEvent() мы отдаем все дополнительное пространство виджетам, размеры которых мы хотим увеличить. Это обеспечивает плавное изменение вида формы при изменении пользователем ее размеров.
Рис. 6.2. Изменение размеров диалогового окна, допускающего изменение своих размеров.
Точно так же, как при абсолютном позиционировании, при ручной компоновке в программе приходится жестко задавать много констант, рассчитываемых программистом. Написание подобной программы представляет собой нудное занятие, особенно если проект изменяется. И все-таки существует риск отсечения текста. Этого риска можно избежать, принимая во внимание идеальные размеры дочерних виджетов, но это еще больше усложняет программу.
Самый удобный метод компоновки виджетов на форме — использование менеджеров компоновки Qt. Менеджеры компоновки обеспечивают осмысленные, принимаемые по умолчанию значения параметров для каждого типа виджета и учитывают идеальный размер каждого виджета, который, в свою очередь, обычно зависит от шрифта виджета, его стиля и содержимого. Менеджеры компоновки также учитывают максимальные и минимальные размеры и автоматически подстраивают компоновку в ответ на изменения шрифта, изменения содержимого и изменения размеров окна.
Существует три наиболее важных менеджера компоновки: QHBoxLayout, QVBoxLayout и QGridLayout. Эти классы наследуют QLayout, который обеспечивает основной каркас для менеджеров компоновки. Все эти три класса полностью поддерживаются Qt Designer и могут также использоваться непосредственно в программе.
Ниже приводится программный код FindFileDialog, в котором используются менеджеры компоновки:
01 FindFileDialog::FindFileDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 QGridLayout *leftLayout = new QGridLayout;
05 leftLayout->addWidget(namedLabel, 0, 0);
06 leftLayout->addWidget(namedLineEdit, 0, 1);
07 leftLayout->addWidget(lookInLabel, 1, 0);
08 leftLayout->addWidget(lookInLineEdit, 1, 1);
09 leftLayout->addWidget(subfoldersCheckBox, 2, 0, 1, 2);
10 leftLayout->addWidget(tableWidget, 3, 0, 1, 2);
11 leftLayout->addWidget(messageLabel, 4, 0, 1, 2);
12 QVBoxLayout *rightLayout = new QVBoxLayout;
13 rightLayout->addWidget(findButton);
14 rightLayout->addWidget(stopButtpn);
15 rightLayout->addWidget(closeButton);
16 rightLayout->addStretch();
17 rightLayout->addWidget(helpButton) ;
18 QHBoxLayout *mainLayout = new QHBoxLayout;
19 mainLayout->addLayout(leftLayout);
20 mainLayout->addLayout(rightLayout);
21 setLayout(mainLayout);
22 setWindowTitle(tr("Find Files or Folders"));
23 }
Компоновка обеспечивается одним менеджером компоновки по горизонтали QHBoxLayout, одним менеджером компоновки в ячейках сетки QGridLayout и одним менеджером компоновки по вертикали QVBoxLayout. Менеджер QGridLayout слева и менеджер QVBoxLayout справа размещаются рядом внутри внешнего менеджера QHBoxLayout. Кромка по периметру диалогового окна и промежуток между дочерними виджетами устанавливаются в значения по умолчанию, которые зависят от текущего стиля виджета; они могут быть изменены, если использовать функции QLayout::setMargin() и QLayout::setSpacing().
Такое же диалоговое окно можно было бы создать с помощью визуальных средства разработки Qt Designer, задавая приблизительное положение дочерним виджетам, выделяя те, которые необходимо расположить рядом, и выбирая пункты меню Form | Lay Out Horizontally, Form | Lay Out Vertically или Form | Lay Out in a Grid. Мы использовали данный подход в для создания диалоговых окон Go-to-Cell и Sort приложения Электронная таблица.
Рис. 6.3. Компоновка диалогового окна Find File.
Применение QHBoxLayout и QVBoxLayout достаточно очевидное, однако с QGridLayout дело обстоит несколько сложнее. Менеджер QGridLayout работает с двухмерной сеткой ячеек. Текстовая метка QLabel, расположенная в верхнем левом углу этого менеджера компоновки, имеет координаты (0, 0), a соответствующая строка редактирования QLineEdit имеет координаты (0, 1). Флажок QCheckBox размещается в двух столбцах; он занимает ячейки с координатами (2, 0) и (2, 1). Расположенные под ним объекты QTreeWidget и QLabel также занимают два столбца. Вызовы функции addWidget() имеют следующий формат:
layout->addWidget(виджeт, cтpoкa, cтoлбeц, колСтрок, колСтолбцов);
Здесь виджет является дочерним виджетом, который вставляется в менеджер компоновки, (строка, столбец) — коррдинаты верхней левой ячейки, занимаемой виджетом, колСтрок — количество строк, занимаемое виджетом, и колСтолбцов — количество столбцов, занимаемое виджетом. Если параметры колСтрок и колСтолбцов не заданы, они принимают значение по умолчанию, равное 1.
Вызов addStretch() говорит менеджеру компоновки о необходимости выделения свободного пространства в данной точке. Добавив элемент распорки, мы заставляем менеджер компоновки выделить дополнительное пространство между кнопкой Close и кнопкой Help. B Qt Designer мы можем добиться того же самого эффекта, вставляя распорку. Распорки в Qt Designer отображаются в виде синих «пружинок».
Помимо рассмотренных нами до сих пор случаев использование менеджеров компоновки дает дополнительные выгоды. Если мы добавляем виджет к менеджеру или убираем виджет из него, менеджер компоновки автоматически адаптируется к новой ситуации. То же самое происходит, если мы вызываем hide() или show() для дочернего виджета. Если идеальный размер дочернего виджета изменяется, компоновка автоматически перестраивается, учитывая новый идеальный размер. Кроме того, менеджеры компоновки автоматически устанавливают минимальный размер всей формы на основе минимальных размеров и идеальных размеров дочерних виджетов формы.
В представленных до сих пор примерах мы просто помещали виджеты в менеджеры и использовали распорки для выделения дополнительного пространства. Иногда этого недостаточно для того, чтобы компоновка приняла нужный нам вид. В таких ситуациях мы можем настроить компоновку, изменяя политику размеров и идеальные размеры размещаемых виджетов.
Политика размера виджета говорит системе компоновки, как его следует растягивать или сжимать. Qt обеспечивает разумные, принимаемые по умолчанию значения политик размеров для всех своих встроенных виджетов, но поскольку ни одно принимаемое по умолчанию значение не может учесть всевозможные варианты компоновки, все-таки обычной практикой для разработчиков является изменение политики размеров одного или двух виджетов формы. QSizePolicy имеет как горизонтальный, так и вертикальный компоненты. Ниже приводятся наиболее полезные значения:
• Fixed (фиксированное) означает, что виджет не может увеличиваться или сжиматься. Размер виджета всегда сохраняет значение его идеального размера;
• Minimum означает, что идеальный размер виджета является его минимальным размером. Размер виджета не может стать меньше идеального размера, но он может при необходимости вырасти для заполнения доступного пространства;
• Maximum означает, что идеальный размер виджета является его максимальным размером. Размер виджета может уменьшаться до его минимального идеального размера;
• Preferred (предпочитаемое) означает, что идеальный размервиджета является его предпочитаемым размером, но виджет может при необходимости сжиматься или растягиваться;
• Expanding (расширяемый) означает, что виджет может сжиматься или растягиваться, но впервую очередь он стремится увеличить свои размеры.
На рис. 6.4 приводится иллюстрация смысла различных политик размеров, причем в качестве примера здесь используется текстовая метка QLabel с текстом «Какой-то текст».
На рисунке политики Preferred и Expanding представлены одинаково. Так в чем же их отличие? При изменении размеров формы, содержащей одновременно виджеты с политикой размера Preferred и Expanding, дополнительное пространство отдается виджетам Expanding, а виджеты Preferred по-прежнему будут иметь свой идеальный размер.
Рис. 6.4. Смысл различных политик размеров.
Существует еще две политики размеров: MinimumExpanding и Ignored. Первая была необходима в некоторых редких случаях для старых версий Qt, но теперь она не применяется; предпочтительнее использовать политику Expanding и соответствующим образом переопределить функцию minimumSizeHint(). Последняя напоминает Expanding, но при этом игнорируется идеальный размер виджета и минимальный идеальный его размер.
Кроме горизонтального и вертикального компонентов политики размеров класс QSizePolicy хранит коэффициенты растяжения по горизонтали и вертикали. Эти коэффициенты растяжения могут использоваться для указания того, что различные дочерние виджеты могут растягиваться по-разному при расширении формы. Например, если QTreeWidget располагается над QTextEdit и мы хотим, чтобы QTextEdit был в два раза больше по высоте, чем QTreeWidget, мы можем установить коэффициент растяжения по вертикали для QTextEdit на значение 2, а тот же коэффициент для QTreeWidget — на значение 1.
Другой способ воздействия на компоновку заключается в установке минимального размера, максимального размера или фиксированного размера дочерних виджетов. Менеджер компоновки будет учитывать эти ограничения при компоновке виджетов. Но если этого недостаточно, мы можем всегда создать подкласс дочернего виджета и переопределить функцию sizeHint() для получения необходимого нам идеального размера.
Стековая компоновка
Класс QStackedLayout (менеджер стековой компоновки) управляет компоновкой набора дочерних виджетов или «страниц», показывая в каждый конкретный момент только одну из них и скрывая от пользователя остальные. Сам менеджер QStackedLayout невидим и не содержит внутри себя средства для пользователя по изменению страницы. Показанные на рис. 6.5 небольшие стрелки и темно—серая рамка обеспечиваются Qt Designer, чтобы упростить применение этого менеджера компоновки при проектировании формы. Для удобства в Qt предусмотрен класс QStackedWidget, представляющий собой QWidget со встроенным QStackedLayout.
Рис. 6.5. QStackedLayout.
Страницы нумеруются с 0. Если мы хотим сделать какой-нибудь конкретный виджет видимым, мы можем вызвать функцию setCurrentIndex(), задавая номер страницы. Номер страницы дочернего виджета можно получить с помощью функции indexOf().
Рис. 6.6. Две страницы диалогового окна Preferences.
Показанное на рис. 6.6 диалоговое окно Preferences (настройка предпочтений) представляет собой пример использования QStackedLayout. Окно диалога состоит из виджета QListWidget слева и менеджера стековой компоновки QStackedLayout справа. Каждый элемент в списке QListWidget соответствует одной странице QStackedLayout. Ниже приводится соответствующий программный код конструктора этого диалогового окна:
01 PreferenceDialog::PreferenceDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 listWidget = new QListWidget;
05 listWidget->addItem(tr("Web Browser"));
06 listWidget->addItem(tr("Mail & News"));
07 listWidget->addItem(tr("Advanced"));
08 listWidget->addItem(tr("Appearance"));
09 stackedLayout = new QStackedLayout;
10 stacked Layout->addWidget(appearancePage);
11 stackedLayout->addWidget(webBrowserPage);
12 stackedLayout->addWidget(mailAndNewsPage);
13 stackedLayout->addWidget(advancedPage);
14 connect(listWidget, SIGNAL(currentRowChanged(int)).
15 stackedLayout, SLOT(setCurrentIndex(int)));
16 listWidget->setCurrentRow(0);
17 }
Мы создаем QListWidget и заполняем его названиями страниц. Затем мы создаем QStackedLayout и вызываем для каждой страницы функцию addWidget(). Мы связываем сигнал спискового виджета currentRowChanged(int) с setCurrentIndex(int) менеджера стековой компоновки для переключения страниц и вызываем функцию спискового виджета setCurrentRow() в конце конструктора, чтобы начать со страницы 0.
Подобные формы также очень легко создавать при помощи Qt Designer.
1. Создайте новую форму на основе шаблона «Dialog» или «Widget».
2. Добавьте в форму виджеты QListWidget и QStackedWidget.
3. Заполните каждую страницу дочерними виджетами и менеджерами компоновки. (Для создания новой страницы нажмите на правую кнопку мышки и выберите пункт меню Insert Page (вставить страницу); для перехода с одной страницы на другую щелкните по маленькой левой или правой стрелке, расположенной в верхнем правом углу виджета QStackedWidget.)
4. Расположите виджеты рядом, используя менеджер горизонтальной компоновки.
5. Подсоедините сигнал виджета списка элементов currentRowChanged(int) к слоту стекового виджета setCurrentIndex(int).
6. Установите значение свойства виджета списка элементов currentRow на 0.
Поскольку мы реализовали переключение страниц с помощью предварительно определенных сигналов и слотов, диалоговое окно будет правильно работать при предварительном просмотре в Qt Designer.
Разделители
Разделитель QSplitter представляет собой виджет, который содержит другие виджеты. Виджеты в разделителе отделены друг от друга разделительными линиями. Пользователи могут изменять размеры дочерних виджетов разделителя посредством перемещения разделительных линий. Разделители могут часто использоваться в качестве альтернативы менеджерам компоновки, предоставляя пользователю больше возможностей по управлению компоновкой.
Рис. 6.7. Приложение Splitter.
Дочерние виджеты QSplitter автоматически располагаются рядом (или один под другим) в порядке их создания, причем между соседними виджетами размещаются разделительные линии. Ниже приводится программный код для создания представленного на рис. 6.7 окна:
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 QTextEdit *editor1 = new QTextEdit;
05 QTextEdit *editor2 = new QTextEdit;
06 QTextEdit *editor3 = new QTextEdit;
07 QSplitter splitter(Qt::Horizontal);
08 splitter.addWidget(editor1);
09 splitter.addWidget(editor2);
10 splitter.addWidget(editor3);
11 splitter.show();
12 return app.exec();
13 }
Этот пример состоит из трех полей редактирования QTextEdit, расположенных горизонтально в виджете QSplitter. В отличие от менеджеров компоновки, которые просто размещают в форме дочерние виджеты, а сами не имеют визуального представления, QSplitter наследует QWidget и может использоваться как любой другой виджет.
Рис. 6.8. Виджеты приложения Splitter.
Можно обеспечить сложную компоновку путем применения вложенных горизонтальных и вертикальных разделителей QSplitter. Например, показанное на рис. 6.9 приложение Mail Client (почтовый клиент) состоит из горизонтального QSplitter, который содержит справа от себя вертикальный QSplitter.
Рис. 6.9. Приложение Mail Client в системе Mac OS X.
Ниже приводится программный код конструктора подкласса QMainWindow приложения Mail Client:
01 MailClient::MailClient()
02 {
03 …
04 rightSplitter = new QSplitter(Qt::Vertical);
05 rightSplitter->addWidget(messagesTreeWidget);
06 rightSplitter->addWidget(textEdit);
07 rightSplitter->setStretchFactor(1, 1);
08 mainSplitter = new QSplitter(Qt::Horizontal);
09 mainSplitter->addWidget(foldersTreeWidget);
10 mainSplitter->addWidget(rigntSplitter);
11 mainSplitter->setStretchFactor(1, 1);
12 setCentralWidget(mainSplitter);
13 setWindowTitle(tr("Mail Client"));
14 readSettings();
15 }
После создания трех виджетов, которые мы собираемся выводить на экран, мы создаем вертикальный разделитель rightSplitter и добавляем два виджета, которые мы собираемся отображать справа. Затем мы создаем горизонтальный разделитель mainSplitter и добавляем виджет, который мы хотим отображать слева, и rightSplitter, виджеты которого мы хотим показывать справа. Мы делаем mainSplitter центральным виджетом QMainWindow.
Когда пользователь изменяет размер окна, QSplitter обычно распределяет пространство таким образом, что относительные размеры дочерних виджетов остаются прежними. В примере приложения Mail Client нам не нужен такой режим работы; вместо этого мы хотим, чтобы QTreeWidget и QTableWidget сохраняли свои размеры, и мы хотим отдавать любое дополнительное пространство полю редактирования QTextEdit. Это достигается с помощью двух вызовов функции setStretchFactor(). В первом аргументе задается индекс дочернего виджета разделителя (индексация начинается с нуля), а во втором аргументе — коэффициент растяжения; по умолчанию используется 0.
Рис.6.10. Индексация разделителя в приложении Mail Client.
Первый вызов setStretchFactor() делаем для rightSplitter, устанавливая виджет в позицию 1 (textEdit) и коэффициент растяжения на 1. Второй вызов setStretcnFactor() делаем для mainSplitter, устанавливая виджет в позицию 1 (rightSplitter) и коэффициент растяжения на 1. Это обеспечивает получение всего дополнительного пространства полем редактирования textEdit.
При запуске приложения разделитель QSplitter задает дочерним виджетам соответствующие размеры на основе их первоначального размера (или на основе их идеального размера, если начальный размер не указан). Мы можем передвигать разделительные линии программно, вызывaя фyнкцию QSplitter::setSizes(). Класс QSplitter предоставляет также средство сохранения своего состояния и его восстановления при следующем запуске приложения. Ниже приводится функция writeSettings(), которая сохраняет настройки Mail Client:
01 void MailClient::writeSettings()
02 {
03 QSettings settings("Software Inc.", "Mail Client");
04 settings.beginGroup("mainWindow");
05 settings.setValue("size", size());
06 settings.setValue("mainSplitter", mainSplitter->saveState());
07 settings.setValue("rightSplitter", rightSplitter->saveState());
08 settings.endGroup();
09 }
Ниже приводится соответствующая функция по чтению настроек readSettings():
01 void MailClient::readSettings()
02 {
03 QSettings settings("Software Inc.", "Mail Client");
04 settings.beginGroup("mainWindow");
05 resize(settings.value("size", QSize(480, 360)).toSize());
06 mainSplitter->restoreState(
07 settings.value("mainSplitter").toByteArray());
08 rightSplitter->restoreState(
09 settings.value("rightSplitter").toByteArray());
10 settings.endGroup();
11 }
Разделитель QSplitter полностью поддерживается Qt Designer. Для размещения виджетов в разделителе поместите дочерние виджеты приблизительно в то место, где они должны находиться, выделите их и выберите пункт меню Form | Lay Out Horizontally in Splitter или Form | Lay Out Vertically in Splitter (Форма | Компоновка no roризонтали в разделитель или Форма | Компоновка по вертикали в разделитель).
Области с прокруткой
Класс QScrollArea содержит область отображения, которую можно прокручивать, и две полосы прокрутки. Если мы хотим добавить в виджет полосы прокрутки, значительно проще использовать класс QScrollArea, чем создавать свои собственные экземпляры QScrollBar и самим реализовывать функциональность скроллинга.
Рис. 6.11. Виджеты, составляющие область с прокруткой QScrollArea.
Способ применения QScrollArea состоит в следующем: вызывается функция setWidget() с виджетом, к которому мы хотим добавить полосы прокрутки. QScrollArea автоматически делает этот виджет дочерним (если он еще не является таковым) по отношению к области отображения (он доступен при помощи функции QScrollArea::viewport()). Например, если мы хотим иметь полосы прокрутки вокруг виджета IconEditor, который мы разработали в , мы можем написать такую программу:
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 IconEditor *iconEditor = new IconEditor;
05 iconEditor->setIconImage(QImage(":/images/mouse.png"));
06 QScrollArea scrollArea;
07 scrollArea.setWidget(iconEditor);
08 scrollArea.viewport()->setBackgroundRole(QPalette::Dark);
09 scrollArea.viewport()->setAutoFillBackground(true);
10 scrollArea.setWindowTitle(QObject::tr("Icon Editor"));
11 scrollArea.show();
12 return app.exec();
13 }
Рис. 6.12. Изменение размеров области с прокруткой QScrollArea.
QScrollArea при отображении виджета использует его текущий или идеальный размер, если размеры виджета еще ни разу не изменялись. Делая вызов setWidgetResizable(true), мы указываем QScrollArea на необходимость автоматического изменения размеров виджета, чтобы можно было воспользоваться любым дополнительным пространством за пределами его идеальных размеров.
По умолчанию полосы прокрутки видны на экране только в том случае, когда область отображения меньше дочернего виджета. Мы можем сделать полосы прокрутки постоянно видимыми при помощи установки следующих политик полос прокрутки:
scrollArea.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
scrollArea.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
QScrollArea большую часть своей функциональности наследует от QAbstractScrollArea. Такие классы, как QTextEdit и QAbstractItemView (базовый класс для классов отображения элементов в Qt), являются производными от QAbstractScrollArea, поэтому нам не надо для них формировать оболочку из QScrollArea для получения полос прокрутки.
Прикрепляемые виджеты и панели инструментов
Прикрепляемыми являются виджеты, которые могут крепиться к определенным областям главного окна приложения QMainWindow или быть независимыми «плавающими» окнами. QMainWindow имеет четыре области крепления таких виджетов: одна сверху, одна снизу, одна слева и одна справа от центрального виджета. В таких приложениях, как Microsoft Visual Studio и Qt Linguist, широко используются прикрепляемые окна для обеспечения очень гибкого интерфейса пользователя. В Qt прикрепляемые виджеты представляют собой экземпляры класса QDockWidget.
Каждый прикрепляемый виджет имеет свой собственный заголовок, даже когда он прикреплен. Пользователи могут перемещать прикрепляемые окна с одного места крепления на другое, передвигая полосу заголовка. Они могут также отсоединять прикрепляемое окно от области крепления и сделать его независимым плавающим окном, располагая прикрепляемое окно вне областей крепления. Свободные плавающие прикрепляемые окна всегда находятся «поверх» их главного окна. Пользователи могут закрыть QDockWidget, щелкая по кнопке закрытия, расположенной в заголовке виджета. Любые комбинации этих возможностей можно отключать с помощью вызова QDockWidget::setFeatures().
Рис. 6.13. QMainWindow с прикрепленным виджетом.
В ранних версиях Qt панели инструментов рассматривались как прикрепляемые виджеты, использующие те же самые области крепления. Начиная с Qt 4 панели инструментов размещаются в собственных областях, расположенных по периметру центрального виджета (как показано на рис. 6.14), и они не могут открепляться. Если требуется иметь плавающую панель инструментов, можно просто поместить ее внутрь QDockWindow.
Рис. 6.14. Области крепления виджетов и области панелей инструментов QMainWindow.
Углы, обозначенные пунктирными линиями, могут принадлежать обеим соседним областям крепления. Например, мы могли бы верхний левый угол назначить левой области крепления с помощью вызова QMainWindow::setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea).
Следующий фрагмент программного кода показывает, как для существующего виджета (в данном случае для QTreeWidget) можно оформить оболочку в виде QDockWidget и вставить ее в правую область крепления:
QDockWidget *shapesDockWidget = new QDockWidget(tr("Shapes"));
shapesDockWidget->setWidget(treeWidget);
shapesDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea
| Qt::RightDockWidgetArea);
addDockWidget(Qt::RightDockWidgetArea, shapesDockWidget);
В вызове setAllowedAreas() задаются допустимые области крепления прикрепляемого окна. В нашем случае мы позволяем пользователю перетаскивать прикрепляемое окно только в левую или правую область крепления, где имеется достаточно пространства по вертикали для его нормального отображения. Если допустимые области не задаются явно, пользователь может перетаскивать прикрепляемое окно в любую из четырех областей.
Ниже приводится фрагмент из конструктора подкласса QMainWindow, который показывает, как можно создавать панель инструментов, содержащую QComboBox, QSpinBox и несколько кнопок QToolButton:
QToolBar *fontToolBar = new QToolBar(tr("Font"));
fontToolBar->addWidget(familyComboBox);
fontToolBar->addWidget(sizeSpinBox);
fontToolBar->addAction(boldAction);
fontToolBar->addAction(italicAction);
fontToolBar->addAction(underlineAction);
fontToolBar->setAllowedAreas(Qt::TopToolBarArea
| Qt::BottomToolBarArea);
addToolBar(fontToolBar);
Если мы хотим сохранять позиции всех прикрепляемых виджетов и панелей инструментов, чтобы иметь возможность их восстановления при следующем запуске приложения, мы можем написать почти такой же программный код, как для сохранения состояния разделителя QSplitter, используя функции класса QMainWindow saveState() и restoreState():
01 void MainWindow::writeSettings()
02 {
03 QSettings settings("Software Inc.", "Icon Editor");
04 settings.beginGroup("mainWindow");
05 settings.setValue("size", size());
06 settings.setValue("state", saveState());
07 settings.endGroup();
08 }
09 void MainWindow::readSettings()
10 {
11 QSettings settings("Software Inc.", "Icon Editor");
12 settings.beginGroup("mainWindow");
13 resize(settings.value("size").toSize());
14 restoreState(settings.value("state").toByteArray());
15 settings.endGroup();
16 }
Наконец, QMainWindow обеспечивает контекстное меню, в котором представлены все прикрепляемые окна и панели инструментов. Используя это меню, пользователь может закрывать и восстанавливать прикрепляемые окна и панели инструментов.
Рис. 6.15. Контекстное меню QMainWindow.
Многодокументный интерфейс
Приложения, которые обеспечивают работу со многими документами в центральной области главного окна, называются приложениями с многодокументным интерфейсом или MDI—приложениями. В Qt MDI—приложения создаются с использованием в качестве центрального виджета класса QWorkspace и путем представления каждого документа в виде дочернего окна QWorkspace.
Обычно MDI—приложения содержат пункт главного меню Windows (окна) с командами по управлению окнами и их списком. Активное окно отмечается галочкой. Пользователь может сделать любое окно активным, щелкая по его названию в меню Windows.
В данном разделе для демонстрации способов создания приложения с интерфейсом MDI и способов реализации его меню Windows мы разработаем MDI—приложение Editor (редактор), показанное на рис. 6.16.
Рис. 6.16. MDI—приложение Editor.
Это приложение состоит из двух классов: MainWindow и Editor. Его программный код находится на компакт-диске, и поскольку большая часть его либо совпадает, либо очень похожа на программный код приложения Электронная таблица из , здесь мы представим только новый программный код.
Рис. 6.17. Меню MDI—приложения Editor.
Давайте начнем с класса MainWindow.
01 MainWindow::MainWindow()
02 {
03 workspace = new QWorkspace;
04 setCentralWidget(workspace);
05 connect(workspace, SIGNAL(windowActivated(QWidget *)),
06 this, SLOT(updateMenus()));
07 createActions();
08 createMenus();
09 createToolBars();
10 createStatusBar();
11 setWindowTitle(tr("MDI Editor"));
12 setWindowIcon(QPixmap(":/images/icon.png"));
13 }
В конструкторе MainWindow мы создаем виджет QWorkspace и делаем его центральным виджетом. Мы связываем сигнал windowActivated() класса QWorkspace со слотом, который мы будем использовать для обеспечения актуального состояния меню Window.
01 void MainWindow::newFile()
02 {
03 Editor *editor = createEditor();
04 editor->newFile();
05 editor->show();
06 }
Слот newFile() соответствует пункту меню File | New. Он зависит от закрытой функции createEditor(), создающей дочерний виджет Editor.
01 Editor *MainWindow::createEditor()
02 {
03 Editor *editor = new Editor;
04 connect(editor, SIGNAL(copyAvailable(bool)),
05 cutAction, SLOT(setEnabled(bool)));
06 connect(editor, SIGNAL(copyAvailable(bool)),
07 copyAction, SLOT(setEnabled(bool)));
08 workspace->addWindow(editor);
09 windowMenu->addAction(editor->windowMenuAction());
10 windowActionGroup->addAction(editor->windowMenuAction());
11 return editor;
12 }
Функция createEditor() создает виджет Editor и устанавливает два соединения «сигнал—слот». Эти соединения обеспечивают включение или выключение пунктов меню Edit | Cut и Edit | Copy в зависимости от наличия выделенной области текста.
Поскольку мы используем интерфейс MDI, может оказаться, что работа будет вестись одновременно с несколькими виджетами Editor. На это надо обратить внимание, поскольку мы заинтересованы в ответе на сигнал copyAvailable(bool), поступающий только от активного окна редактора Editor, но не от других окон. Но эти сигналы могут порождаться только активным окном, поэтому это практически не составляет проблему.
После настройки Editor мы добавляем QAction для представления окна в меню Window. Это действие обеспечивается классом Editor, который мы скоро рассмотрим. Мы также добавляем это действие в объект QActionGroup. QActionGroup гарантирует, что в любой момент времени оказывается отмеченной только одна строка меню Window.
01 void MainWindow::open()
02 {
03 Editor *editor = createEditor();
04 if (editor->open()) {
05 editor->show();
06 } else {
07 editor->close();
08 }
09 }
Функция open() соответствует пункту меню File | Open. Этот пункт меню создает Editor для нового документа и вызывает функцию open() для Editor. Имеет смысл выполнять файловые операции в классе Editor, а не в классе MainWindow, поскольку каждый Editor требует поддержки своего собственного состояния.
Если функция open() завершится неудачей, мы просто закроем редактор, поскольку пользователь уже будет уведомлен об ошибке. Мы не обязаны сами явно удалять объект Editor; это происходит автоматически при условии установки атрибута виджета Qt::WA_DeleteOnClose, что и делается в конструкторе Editor.
01 void MainWindow::save()
02 {
03 if (activeEditor()) {
04 activeEditor()->save();
05 }
06 }
Слот save() вызывает функцию Editor::save() для активного редактора, если таковой имеется. И снова программный код по выполнению реальной работы находится в классе Editor.
01 Editor *MainWindow::activeEditor()
02 {
03 return qobject_cast
04 }
Закрытая функция activeEditor() возвращает активное дочернее окно в виде указателя типа Editor или нулевой указатель при отсутствии такого окна.
01 void MainWindow::cut()
02 {
03 if (activeEditor())
04 activeEditor()->cut();
05 }
Слот cut() вызывает функцию Editor::cut() для активного редактора. Мы не приводим слоты copy(), paste() и del(), потому что они имеют такой же вид.
01 void MainWindow::updateMenus()
02 {
03 bool hasEditor = (activeEditor() != 0);
04 bool hasSelection = activeEditor()
05 && activeEditor()->textCursor().hasSelection();
06 saveAction->setEnabled(hasEditor);
07 saveAsAction->setEnabled(hasEditor);
08 pasteAction->setEnabled(hasEditor);
09 cutAction->setEnabled(hasSelection);
10 copyAction->setEnabled(hasSelection);
11 closeAction->setEnabled(hasEditor);
12 closeAllAction->setEnabled(hasEditor);
13 tileAction->setEnabled(hasEditor);
14 cascadeAction->setEnabled(hasEditor);
15 nextAction->setEnabled(hasEditor);
16 previousAction->setEnabled(hasEditor);
17 separatorAction->setVisible (hasEditor);
18 if (activeEditor())
19 activeEditor()->windowMenuAction()->setChecked(true);
20 }
Слот updateMenus() вызывается всякий раз, когда окно становится активным (и когда закрывается последнее окно) для обновления системы меню благодаря помещенному нами в конструктор MainWindow соединению «сигнал—слот».
Большинство пунктов меню имеет смысл при существовании активного окна, поэтому мы их отключаем при отсутствии активного окна. В конце мы вызываем setChecked() для QAction, представляющего активное окно. Благодаря использованию QActionGroup нам не требуется явно сбрасывать флажок предьщущего активного окна.
01 void MainWindow::createMenus()
02 {
03 windowMenu = menuBar()->addMenu(tr("&Window"));
04 windowMenu->addAction(closeAction);
05 windowMenu->addAction(closeAllAction);
06 windowMenu->addSeparator();
07 windowMenu->addAction(tileAction);
08 windowMenu->addAction(cascadeAction);
09 windowMenu->addSeparator();
10 windowMenu->addAction(nextAction);
11 windowMenu->addAction(previousAction);
12 windowMenu->addAction(separatorAction);
13 }
Закрытая функция createMenus() заполняет меню Window командами. Здесь используются типичные для такого рода меню команды, и они легко реализуются с применением слотов closeActiveWindow(), closeAllWindows(), tile() и cascade() класса QWorkspace. Всякий раз, когда пользователь открывает новое окно, в меню Window добавляется список действий. (Это делается в функции createEditor(), которую мы видели.) При закрытии пользователем окна редактора соответствующий ему пункт в меню Window удаляется (поскольку его владельцем является это окно редактора), т.е. пункт меню удаляется из меню Window автоматически.
01 void MainWindow::closeEvent(QCloseEvent *event)
02 {
03 workspace->closeAllWindows();
04 if (activeEditor()) {
05 event->ignore();
06 } else {
07 event->accept();
08 }
09 }
Функция closeEvent() переопределяется для закрытия всех дочерних окон, обеспечивая получение всеми дочерними виджетами сигнала о возникновении события закрытия. Если один из дочерних виджетов «игнорирует» свое событие закрытия (прежде всего из-за того, что пользователь нажал кнопку отмены при выдаче соответствующего сообщения о «несохраненных изменениях»), мы игнорируем событие закрытия для MainWindow; в противном случае мы принимаем его, и в результате Qt закрывает окно. Если бы мы не переопределили функцию closeEvent() в MainWindow, у пользователя не было бы никакой возможности сохранения ни одного из несохраненных изменений.
Теперь мы закончили наш обзор MainWindow, и поэтому мы можем перейти к реализации класса Editor. Класс Editor представляет одно дочернее окно. Он наследует QTextEdit, который обеспечивает функциональность текстового редактора. Точно так же, как любой виджет, который может использоваться в качестве автономного окна, он может использоваться и в качестве дочернего окна в рабочем пространстве интерфейса MDI.
Ниже приводится определение класса:
01 class Editor : public QTextEdit
02 {
03 Q_OBJECT
04 public:
05 Editor(QWidget *parent = 0);
06 bool openFile(const QString &fileName);
07 bool save();
08 bool saveAs();
09 void newFile();
10 bool open();
11 protected:
12 QSize sizeHint() const;
13 QAction *windowMenuAction() const { return action; }
14 void closeEvent(QCloseEvent *event);
15 private slots:
16 void documentWasModified();
17 private:
18 bool okToContinue();
19 bool saveFile(const QString &fileName);
20 void setCurrentFile(const QString &fileName);
21 bool readFile(const QString &fileName);
22 bool writeFile(const QString &fileName);
23 QString strippedName(const QString &fullFileName);
24 QString curFile;
25 bool isUntitled;
26 QString fileFilters;
27 QAction *action;
28 }
Присутствующие в классе MainWindow приложения Электронная таблица четыре закрытые функции имеются также в классе Editor: okToContinue(), saveFile(), setCurrentFile() и strippedName().
01 Editor::Editor(QWidget *parent)
02 : QTextEdit(parent)
03 {
04 action = new QAction(this);
05 action->setCheckable(true);
06 connect(action, SIGNAL(triggered()), this, SLOT(show()));
07 connect(action, SIGNAL(triggered()), this, SLOT(setFocus()));
08 isUntitled = true;
09 fileFilters = tr("Text files (*.txt)\nAll files (*)");
10 connect(document(), SIGNAL(contentsChanged()),
11 this, SLOT(documentWasModified()));
12 setWindowIcon(QPixmap(":/images/document.png"));
13 setAttribute(Qt::WA_DeleteOnClose);
14 }
Сначала мы создаем действие QAction, представляющее редактор в меню приложения Window, и связываем его со слотами show() и setFocus().
Поскольку мы разрешаем пользователям создавать любое количество окон редактора, мы должны предусмотреть соответствующую систему их наименования, чтобы они отличались до первого их сохранения. Один из распространенных методов решения этой проблемы заключается в назначении имен с числами (например, document1.txt). Мы используем переменную isUntitled, чтобы отличить предоставляемые пользователем имена документов и сгенерированные программно.
Мы связываем сигнал текстового документа contentsChanged() c закрытым слотом documentWasModified(). Этот слот просто вызывает setWindowModified(true).
Наконец, мы устанавливаем атрибут Qt::WA_DeleteOnClose для предотвращения утечек памяти при закрытии пользователем окна Editor.
После выпрлнения конструктора мы ожидаем вызова либо функции newFile(), либо функции open().
01 void Editor::newFile()
02 {
03 static int documentNumber = 1;
04 curFile = tr("document%1.txt").arg(documentNumber);
05 setWindowTitle(curFile + "[*]");
06 action->setText(curFile);
07 isUntitled = true;
08 ++documentNumber;
09 }
Функция newFile() генерирует для нового документа имя типа document1.txt. Этот программный код помещен в функцию newFile(), a не в конструктор, поскольку мы не хотим использовать числа при вызове функции open() для открытия существующего документа во вновь созданном редакторе Editor. Поскольку переменная documentNumber объявлена как статическая, она совместно используется всеми экземплярами Editor.
Маркер «[*]» в заголовке окна указывает место, где мы хотим выдавать звездочку при несохраненных изменениях файла для платформ, отличных от Mac OS X. Мы рассматривали этот маркер в .
01 bool Editor::open()
02 {
03 QString fileName = QFileDialog::getOpenFileName(
04 this, tr("Open"), fileFilters);
05 if(fileName.isEmpty())
06 return false;
07 return openFile(fileName);
08 }
Функция open() пытается открыть сущеcтвующий файл при помощи функции openFile().
01 bool Editor::save()
02 {
03 if (isUntitled) {
04 return saveAs();
05 } else {
06 return saveFile(curFile);
07 }
Функция save() используёт переменную isUntitled для определения вида вызываемой функции saveFile() или saveAs().
01 void Editor::closeEvent(QCloseEvent *event)
02 {
03 if (okToContinue()) {
04 event->accept();
05 } else {
06 event->ignore();
07 }
08 }
Функция closeEvent() переопределяется, чтобы разрешить пользователю сохранить несохраненные изменения. Вся логика содержится в функции okToContinue(), которая выводит сообщение «Do you want to save your changes?» (Сохранить изменения?). Если функция okToContinue() возвращает true, мы обрабатывам событие закрытия; в противном случае мы «игнорируем» его и окно оставляем прежним.
01 void Editor::setCurrentFile(const QString &fileName)
02 {
03 curFile = fileName;
04 isUntitled = false;
05 action->setText(strippedName(curFile));
06 document()->setModified(false);
07 setWindowTitle(strippedName(curFile) + "[*]");
08 setWindowModified(false);
09 }
Функция setCurrentFile() вызывается из openFile() и saveFile() для обновления переменных curFile и isUntitled, установки текста заголовка окна и пункта меню, а также для установки значения флажка модификации документа на false. Всякий раз, когда пользователь изменяет текст в редакторе, объект базового класса QTextDocument генерирует сигнал contentsChanged() и устанавливает свой внутренний флажок модификации на значение true.
01 QSize Editor::sizeHint() const
02 {
03 return QSize(72 * fontMetrics().width('x'),
04 25 * fontMetrics().lineSpacing());
05 }
Функция sizeHint() возвращает размер, рассчитанный на основе ширины буквы «x» и высоты строки текста. QWorkspace использует идеальный размер в качестве начального размера окна.
Ниже приводится файл main.cpp MDI—приложения Editor:
01 #include
02 #include "mainwindow.h"
03 int main(int argc, char *argv[])
04 {
05 QApplication app(argc, argv);
06 QStringList args = app.arguments();
07 MainWindow mainWin;
08 if (args.count() > 1) {
09 for (int i = 1; i < args.count(); ++i)
10 mainWin.openFile(args[i]);
11 } else {
12 mainWin.newFile();
13 }
14 mainWin.show();
15 return app.exec();
16 }
Еслй пользователь задает в командной строке какие-нибудь файлы, мы пытаемся их загрузить, в противном случае мы начинаем работу с пустым документом. Такие характерные для Qt опции командной строки, как —style и —font (стиль и шрифт), автоматически убираются из списка аргументов конструктором QApplication. Поэтому, если мы напишем в командной строке
mdieditor -style motif readme.txt
QApplication::arguments() возвратит QStringList с двумя элементами («mdieditor» и «readme.txt»), а МDI—приложение Editor запустится с документом readme.txt.
Интерфейс MDI представляет собой один из способов работы одновременно со многими документами. В системе MacOS Х более предпочтителен подход, связанный с применением нескольких окон верхнего уровня. Этот подход рассматривается в разделе «Работа со многими документами» .
Глава 7. Обработка событий
События генерируются оконной системой или Qt в ответ на различные действия. Когда пользователь нажимает или отпускает клавишу или кнопку мышки, генерируется событие клавиши клавиатуры или кнопки мышки; когда окно впервые выводится на экран, генерируется событие рисования, указывая появившемуся окну на необходимость его прорисовки. Большинство событий генерируются в ответ на действия пользователя, но некоторые события, например, события таймера, генерируются самой системой и не зависят от действий пользователя.
При программировании в Qt нам редко приходится думать о событиях, поскольку виджеты Qt сами генерируют сигналы в ответ на любое существенное событие. События становятся полезными при создании нами своих сооственных виджетов, или когда мы хотим модифицировать поведение существующих виджетов Qt.
События не следует путать с сигналами. Как правило, сигналы полезны при uспользовании виджета, в то время как события полезны при реализации виджета. Например, при применении кнопки QPushButton мы больше заинтересованы в ее сигнале clicked(), чем в обработке низкоуровневых событий мышки или клавиатуры, сгенерировавших этот сигнал. Но если мы реализуем такой класс, как QPushButton, нам необходимо написать программный код для обработки событий мышки и клавиатуры и при необходимости сгенерировать сигнал clicked().
Переопределение обработчиков событий
В Qt событие (event) — это объект, производный от QEvent. Qt обрабатывает более сотни типов событий, каждое из которых идентифицируется определенным значением перечисления. Например, QEvent::type() возвращает QEvent::MouseButtonPress для событий нажатия кнопки мышки.
Для событий многих типов недостаточно тех данных, которые могут храниться в простом объекте QEvent: например, для событий нажатия кнопки мышки необходимо иметь информацию о том, какая кнопка мышки привела к возникновению данного события, а также о том, где находился курсор мышкй в момент возникновения события. Эта дополнительная информация хранится в определенных подклассах QEvent, например, в QMouseEvent.
События уведомляют объекты о себе при помощи своих функций event(), унаследованных от класса QObject. Реализация event() в QWidget передает большинство обычных событий конкретным обработчикам событий, например mousePressEvent(), keyPressEvent() и paintEvent().
Мы уже ознакомились в предыдущих главах со многими обработчиками событий при реализации MainWindow, IconEditor и Plotter. Существует много других типов событий, приводимых в справочной документации по QEvent, и можно также самому создавать и генерировать события. В данной главе мы рассмотрим два распространенных типа событий, заслуживающих более детального обсуждения, а именно события клавиатуры и события таймера.
События клавиатуры обрабатываются путем переопределения функций keyPressEvent() и keyReleaseEvent(). Виджет Plotter переопределяет keyPressEvent(). Обычно нам требуется переопределить только keyPressEvent(), поскольку отпускание клавиш важно только для клавиш—модификаторов, то есть для клавиш Ctrl, Shift и Alt, а их можно проконтролировать в keyPressEvent() при помощи функции QKeyEvent::modifiers(). Например, если бы нам пришлось реализовывать виджет CodeEditor (редактор программного кода), общий вид его функции keyPressEvent() с различной обработкой клавиш Home и Ctrl+Home был бы следующим:
01 void CodeEditor::keyPressEvent(QKeyEvent *event)
02 {
03 switch (event->key()) {
04 case Qt::Key_Home:
05 if (event->modifiers() & Qt::ControlModifier) {
06 goToBeginningOfDocument();
07 } else {
08 goToBeginningOfLine();
09 }
10 break;
11 case Qt::Key_End:
12 …
13 default:
14 QWidget::keyPressEvent(event);
15 }
16 }
Клавиши Tab и Backtab (Shift+Tab) представляют собой особый случай. Они обрабатываются функцией QWidget::event() до вызова keyPressEvent() c установкой фокуса на следующий или предыдущий виджет в фокусной цепочке. Обычно нам нужен именно такой режим работы, но в виджете CodeEditor мы, возможно, предпочтем использовать клавишу табуляции Tab для обеспечения отступа в начале строки. Переопределение функции event() выглядело бы следующим образом:
01 bool CodeEditor::event(QEvent *event)
02 {
03 if (event->type() == QEvent::KeyFress) {
04 QKeyEvent *keyEvent = static_cast
05 if (keyEvent->key() == Qt::Key_Tab) {
06 insertAtCurrentPosition('\t');
07 return true;
08 }
09 }
10 return QWidget::event(event);
11 }
Если событие сгенерировано нажатием клавиши клавиатуры, мы преобразуем объект типа QEvent в QKeyEvent и проверяем, какая клавиша была нажата. Если это клавиша Tab, мы выполняем некоторую обработку и возвращаем true, чтобы уведомить Qt об обработке нами события. Если бы мы вернули false, Qt передала бы cобытие родительскому виджету.
Высокоуровневый метод обработки клавиш клавиатуры заключается в применении класса QAction. Например, если goToBeginningOfLine() и goToBeginningOfDocument() являются открытыми слотами виджета CodeEditor и CodeEditor применяется в качестве центрального виджета класса MainWindow, мы могли бы обеспечить обработку клавиш при помощи следующего программного кода:
01 MainWindow::MainWindow()
02 {
03 editor = new CodeEditor;
04 setCentralWidget(editor);
05 goToBeginningOfLineAction =
06 new QAction(tr("Go to Beginning of Line"), this);
07 goToBeginningOfLineAction->setShortcut(tr("Home"));
08 connect(goToBeginningOfLineAction, SIGNAL(activated()),
09 editor, SLOT(goToBeginningOfLine()));
10 goToBeginningOfDocumentAction =
11 new QAction(tr("Go to Beginning of Document"), this);
12 goToBeginningOfDocumentAction->setShortcut(tr("Ctrl+Home"));
13 connect(goToBeginningOfDocumentAction, SlGNAL(activated()),
14 editor, SLOT(goToBeginningOfDocument());
15 …
16 }
Это позволяет легко добавлять команды в меню или в панель инструментов, что мы видели в . Если команды не отображаются в интерфейсе пользователя, объект QAction можно заменить объектом QShortcut; этот класс используется в QAction для связывания клавиши клавиатуры со своим обработчиком.
По умолчанию связывание клавиши в виджете, выполненное с использованием QAction или QShortcut, будет постоянно действовать, пока активно окно, содержащее этот виджет. Это можно изменить с помощью вызова QAction::setShortcutContext() или QShortcut::setContext().
Другим распространенным типом события является событие таймера. Если большинство других событий возникают в результате действий пользователя, то события таймера позволяют приложениям выполнять какую-то обработку через определенные интервалы времени. События таймера могут использоваться для реализации мигающих курсоров и другой анимации или просто для обновления экрана.
Для демонстрации событий таймера мы реализуем виджет Ticker. Этот виджет отображает текстовый баннер, который сдвигается на один пиксель влево через каждые 30 миллисекунд. Если виджет шире текста, последний повторяется необходимое число раз и заполняет виджет по всей его ширине.
Рис. 7.1. Виджет Ticker.
Ниже приводится заголовочный файл:
01 #ifndef TICKER_H
02 #define TICKER_H
03 #include
04 class Ticker : public QWidget
05 {
06 Q_OBJECT
07 Q_PROPERTY(QString text READ text WRITE setText)
08 public:
09 Ticker(QWidget *parent = 0);
10 void setText(const QString &newText);
11 QString text() const { return myText; }
12 QSize sizeHint() const;
13 protected:
14 void paintEvent(QPaintEvent *event);
15 void timerEvent(QTimerEvent *event);
16 void showEvent(QShowEvent *event);
17 void hideEvent(QHideEvent *event);
18 private:
19 QString myText;
20 int offset;
21 int myTimerId;
22 };
23 #endif
Мы переопределяем в Ticker четыре обработчика событий, с тремя из которых мы до сих пор не встречались: timerEvent(), showEvent() и hideEvent().
Теперь давайте рассмотрим реализацию:
01 #include
02 #include "ticker.h"
03 Ticker::Ticker(QWidget *parent)
04 : QWidget(parent)
05 {
06 offset = 0;
07 myTimerId = 0;
08 }
Конструктор инициализирует смещение offset значением 0. Координата x начала вывода текста рассчитывается на основе значения offset. Таймер всегда имеет ненулевой идентификатор, поэтому мы используем 0, показывая, что таймер еще не запущен.
09 void Ticker::setText(const QString &newText)
10 {
11 myText = newText;
12 update();
13 updateGeometry();
14 }
Функция setText() ycтaнaвливaeт oтoбpaжaeмый тeкcт. Oнa вызывaeт update() для выдачи запроса на перерисовку и updateGeometry() для уведомления всех менеджеров компоновки, содержащих виджет Ticker, об изменении идеального размера.
15 QSizeTicker::sizeHint() const
16 {
17 return fontMetrics().size(0, text());
18 }
Функция sizeHint() возвращает в качестве идеального размера виджета размеры области, занимаемой текстом. Функция QWidget::fontMetrics() возвращает объект QFontMetrics, который можно использовать для получения информации относительно шрифта виджета. В данном случае мы определяем размер заданного текста. (В первом аргументе функции QFontMetrics::size() задается флажок, который не нужен для простых строк, поэтому мы просто передаем 0.)
19 void Ticker::paintEvent(QPaintEvent * /* event */)
20 {
21 QPainter painter(this);
22 int textWidth = fontMetrics().width(text());
23 if (textWidth < 1)
24 return;
25 int х= -offset;
26 while (x < width()) {
27 painter.drawText(x, 0, textWidth, height(),
28 Qt::AlignLeft | Qt::AlignVCenter, text());
29 x += textWidth;
30 }
31 }
Функция paintEvent() отображает текст при помощи функции QPainter::drawText(). Она использует функцию fontMetrics() для определения размера области, занимаемой текстом по горизонтали, и затем выводит текст столько раз, сколько необходимо для заполнения виджета по всей его ширине, учитывая значение смещения offset.
32 void Ticker::showEvent(QShowEvent * /* event */)
33 {
34 myTimerId = startTimer(30);
35 }
функция showEvent() запускает таймер. Вызов QObject::startTimer() возвращает число—идентификатор, которое мы можем использовать позже для идентификации таймера. QObject поддерживает несколько независимых таймеров, каждый из которых использует свой временной интервал. После вызова функции startTimer() Qt генерирует событие таймера приблизительно через каждые 30 миллисекунд, причем точность зависит от базовой операционной системы.
Мы могли бы функцию startTimer() вызвать в конструкторе Ticker, но мы экономим некоторые ресурсы за счет генерации Qt событий таймера только в тех случаях, когда виджет действительно видим.
36 void Ticker::timerEvent(QTimerEvent *event)
37 {
38 if (event->timerId() == myTimerId) {
39 ++offset;
40 if (offset >= fontMetrics().width(text()))
41 offset= 0;
42 scroll(-1, 0);
43 } else {
44 QWidget::timerEvent(event);
45 }
46 }
Функция timerEvent() вызывается системой в соответствующие моменты времени. Она увеличивает смещение offset на 1 для имитации движения по всей области вывода текста. Затем она перемещает содержимое виджета на один пиксель влево при помощи фyнкции QWidget::scroll(). Вполне достаточно было бы вызывать функцию update() вместо scroll(), но вызов функции scroll() более эффективен, потому что она просто перемещает существующие на экране пиксели и генерирует событие рисования для открывшейся области виджета (которая в данном случае представляет собой полосу шириной в один пиксель).
Если событие таймера не относится к нашему таймеру, мы передаем его дальше в наш базовый класс.
47 void Ticker::hideEvent(QHideEvent * /* event */)
48 {
49 killTimer(myTimerId);
50 }
Функция hideEvent() вызывает QObject::killTimer() для остановки таймера.
События таймера являются низкоуровневыми событиями, и если нам необходимо иметь несколько таймеров, это может усложнить отслеживание всех идентификаторов таймеров. В таких ситуациях обычно легче создавать для каждого таймера объект QTimer. QTimer генерирует через заданный временной интервал сигнал timeout(). QTimer также обеспечивает удобный интерфейс для однократных таймеров (то есть таймеров, которые срабатывают только один раз).
Установка фильтров событий
Одним из действительно эффективных средств в модели событий Qt является возможность с помощью некоторого экземпляра объекта QObject контролировать события другого экземпляра объекта QObject еще до того, как они дойдут до последнего.
Предположим, что наш виджет CustomerInfoDialog состоит из нескольких редакторов строк QLineEdit и мы хотим использовать клавишу Space (пробел) для передачи фокуса следующему QLineEdit. Такой необычный режим работы может оказаться полезным для разработки, предназначенной для собственных нужд, и когда пользователи имеют навык работы в таком режиме. Простое решение заключается в создании подкласса QLineEdit и переопределении фyнкции keyPressEvent() для вызова focusNextChild(), и оно выглядит следующим образом:
01 void MyLineEdit::keyPressEvent(QKeyEvent *event)
02 {
03 if (event->key()== Qt::Key_Space) {
04 focusNextChild();
05 } else {
06 QLineEdit::keyPressEvent(event);
07 }
08 }
Этот подход имеет один основной недостаток: если мы используем в форме несколько различных видов виджетов (например, QComboBox и QSpinBox), мы должны также создать их подклассы для обеспечения единообразного поведения. Лучшее решение заключается в перехвате виджетом CustomerInfoDialog событий нажатия клавиш клавиатуры своих дочерних виджетов и в обеспечении необходимого поведения в его программном коде. Это можно сделать при помощи фильтров событий. Настройка фильтров событий сострит из двух этапов:
1. Зарегистрируйте объект—перехватчик с целевым объектом посредством вызова функции installEventFilter() для целевого объекта.
2. Выполните обработку событий целевого объекта в функции eventFilter() перехватчика.
Регистрацию объекта контроля удобно выполнять в конструкторе CustomerInfoDialog:
01 CustomerInfoDialog::CustomerInfoDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 firstNameEdit->installEventFilter(this);
05 lastNameEdit->installEventFilter(this);
06 cityEdit->installEventFilter(this);
07 phoneNumberEdit->installEvehtFilter(this);
08 }
После регистрации фильтра события те из них, которые посылаются виджетам firstNameEdit, lastNameEdit, cityEdit и phoneNumberEdit, сначала будут переданы функции eventFilter() виджета CustomerInfoDialog и лишь затем дойдут по своему прямому назначению. (Если для одного объекта установлено несколько фильтров событий, они вызываются по очереди, начиная с установленного последним и последовательно возвращаясь к первому.)
Ниже приводится функция eventFilter(), которая перехватывает события:
01 bool CustomerInfoDialog::eventFilter(QObject *target, QEvent *event)
02 {
03 if (target == firstNameEdit || target == lastNameEdit
04 || target == cityEdit || target == phoneNumberEdit) {
05 if (event->type() == QEvent::KeyPress) {
06 QKeyEvent *keyEvent = static_cast
07 if (keyEvent->key() == Qt::Key_Space) {
08 focusNextChild();
09 return true;
10 }
11 }
12 }
13 return QDialog::eventFilter(target, event);
14 }
Во-первых, мы проверяем, является ли целевой виджет строкой редактирования QLineEdit. Если событие вызвано нажатием клавиши клавиатуры, мы преобразуем его тип в QKeyEvent и проверяем, какая клавиша нажата. Если нажата клавиша пробела Space, мы вызываем функрию focusNextChild() для перехода фокуса на следующий виджет в фокусной цепочке и возвращаем true для уведомления Qt о завершении нами обработки события. Если бы мы вернули false, Qt отослала бы событие по его прямому назначению,что привело бы к вставке лишнего пробела в строку редактирования QLineEdit.
Если целевым виджетом не является QLineEdit или если событие не вызвано нажатием клавиши Space, мы передаем управление функции базового класса eventFilter(). Целевым виджетом мог бы быть также некоторый виджет, базовый класс которого QDialog осуществляет контроль. (В Qt 4.1 этого не происходит с QDialog. Однако другие классы виджетов в Qt, например QScrollArea, контролируют по различным причинам некоторые свои дочерние виджеты.)
Qt предусматривает пять уровней обработки и фильтрации событий:
1. Мы можем переопределять конкретный обработчик событий.
Переопределение таких обработчиков событий, как mousePressEvent(), keyPressEvent() и paintEvent(), представляет собой очень распространенный способ обработки событий. Мы уже видели много примеров такой обработки.
2. Мы можем переопределять функцию QObject::event().
Путем переопределения функции event() мы можем обрабатывать события до того, как они дойдут до обработчиков соответствующих событий. Этот подход очень хорош для изменения принятого по умолчанию поведения клавиши табуляции Tab, что было показано ранее. Он также используется для обработки редких событий, для которых не предусмотрены отдельные обработчики событий (например, QEvent::HoverEnter). При переопределении функции event() нам необходимо вызывать функцию базового класса event() для обработки тех событий, которые мы сами не обрабатываем.
3. Мы можем устанавливать фильтр событий для отдельного объекта QObject.
После регистрации объекта с помощью функции installEventFilter() все события целевого объекта сначала передаются функции контролирующего объекта eventFilter(). Если для одного объекта установлено несколько фильтров, они действуют поочередно, начиная с того, который установлен последним, и кончая тем, который установлен первым.
4. Мы можем устанавливать фильтр событий для объекта QApplication.
После регистрации фильтра для qApp (уникальный объект типа QApplication) каждое событие каждого объекта приложения передается функции eventFilter() до его передачи любым другим фильтрам событий. Этот подход очень удобен для отладки. Он может также использоваться для обработки событий мышки, посылаемых для отключенных виджетов, которые обычно отклоняются QApplication.
5. Мы можем создать подкласс QApplication и переопределить функцию notify().
Qt вызывает QApplication::notify() для генерации события. Переопределение этой функции представляет собой единственный способ получения доступа ко всем событиям до того, как ими займутся фильтры событий. Пользоваться фильтрами событий, как правило, удобнее, поскольку параллельно может существовать любое количество фильтров событий и только одна функция notify().
События многих типов, в том числе события мышки и клавиатуры, могут передаваться дальше по системе объектов приложения. Если событие не было обработано ни на пути к целевому объекту, ни самим целевым объектом, процесс обработки события повторяется, но теперь в качестве нового целевого объекта используется родительский объект. Этот процесс продолжается, управление передается от одного родительского объекта к другому до тех пор, пока либо событие не будет обработано, либо не будет достигнут объект самого верхнего уровня.
Рис. 7.2. Передача события в диалоговом окне.
На рис. 7.2 показано, как событие нажатия клавиши пересылается в диалоговом окне от дочернего объекта к родительскому. Когда пользователь нажимает клавишу на клавиатуре, сначала событие передается виджету, на котором установлен фокус — в данном случае это расположенный в нижнем правом углу флажок QCheckBox. Если QCheckBox не обрабатывает это событие, Qt передает его объекту QGroupBox и в конце концов объекту QDialog.
Обработка событий во время продолжительных процессов
Когда мы вызываем QApplication::exec(), тем самым начинаем цикл обработки событий Qt. При запуске пpилoжeния Qt генерирует несколько событий для отображения на экране виджетов. После этого начинает выполняться цикл обработки событий: постоянно проверяется их возникновение, и эти события отправляются к объектам QObject данного приложения.
Во время обработки события могут генерироваться другие события, которые ставятся в конец очереди событий Qt. Если слишком много времени уходит на обработку одного события, интерфейс пользователя становится невосприимчивым к действиям пользователя. Например, любые сгенерированные оконной системой события во время сохранения файла на диск не будут обрабатываться до тех пор, пока весь файл не будет записан. В ходе записи файла приложение не будет отвечать на запросы оконной системы на перерисовку приложения.
Одно из решений заключается в применении многопоточной обработки: один процесс для работы с интерфейсом пользователя приложения и другой процесс для выполнения операции сохранения файла (или любой другой длительной операции). В этом случае интерфейс пользователя приложения сможет реагировать на события в процессе выполнения операции сохранения файла. Мы рассмотрим способы обеспечения такого режима работы в .
Более простое решение заключается в выполнении частых вызовов функции QApplication::processEvents() в программном коде сохранения файла. Данная функция говорит Qt о необходимости обработки ожидающих в очереди событий и затем возвращает управление вызвавшей ее функции. Фактически функция QApplication::exec() представляет собой не более чем вызов функции processEvents() в цикле while.
Ниже приводится пример того, как мы можем сохранить работоспособность интерфейса пользователя при помощи функции processEvents(), причем за основу взят программный код сохранения файла в приложении Spreadsheet:
01 bool Spreadsheet::writeFile(const QString &fileName)
02 {
03 QFile file(fileName);
04 …
05 for (int row = 0; row < RowCount; ++row) {
06 for (int column = 0; column < ColumnCount; ++column) {
07 QString str = formula(row, column);
08 if (!str.isEmpty())
09 out << quint16(row) << quint16(column) << str;
10 }
11 qApp->processEvents();
12 }
13 return true;
14 }
При использовании этого метода существует опасность того, что пользователь может закрыть главное окно во время выполнения операции сохранения файла или даже выбрать повторно File | Save, что приведет к непредсказуемому результату. Наиболее простое решение заключается в замене вызова
qApp->processEvents();
на вызов
qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
который указывает Qt на необходимость игнорирования событий мышки и клавиатуры.
Часто нам хочется показывать индикатор состояния процесса QProgressDialog в ходе выполнения продолжительной операции. QProgressDialog имеет полоску индикатора, информирующую пользователя о ходе выполнения операции приложением. QProgressDialog также содержит кнопку Cancel, которая позволяет пользователю прекратить выполнение операции. Ниже приводится программный код, применяющий данный подход при сохранении файла приложения Электронная таблица:
01 bool Spreadsheet::writeFile(const QString &fileName)
02 {
03 QFile file(fileName);
04 …
05 QProgressDialog progress(this);
07 progress.setLabelText(tr("Saving %1").arg(fileName));
08 progress.setRange(0, RowCount);
09 progress.setModal(true);
10 for (int row = 0; row < RowCount; ++row) {
11 progress.setValue(row);
12 qApp->processEvents();
13 if (progress.wasCanceled()) {
14 file.remove();
15 return false;
16 }
17 for (int column = 0; column < ColumnCount; ++column) {
18 QString str = formula(row, column);
19 if (!str.isEmpty())
20 out << quint16(row) << quint16(column) << str;
21 }
22 }
23 return true;
24 }
Мы создаем QProgressDialog, в котором RowCount является общим количеством шагов. Затем при обработке каждой строки мы вызываем функцию setValue() для обновления состояния индикатора. QProgressDialog автоматически вычисляет процент завершения операции путем деления текущего значения индикатора на общее количество шагов. Мы вызываем функцию QApplication::processEvents() для обработки любых событий перерисовки либо нажатия пользователем кнопки мышки или клавиши клавиатуры (например, чтобы разрешить пользователю нажимать кнопку Cancel). Если пользователь нажимает кнопку Cancel, мы прекращаем операцию сохранения файла и удаляем файл.
Мы не вызываем для QProgressDialog функцию show(), так как индикатор состояния сам делает это. Если оказывается так, что операция выполняется быстро, прежде всего из-за малого размера файла или высокого быстродействия компьютера, QProgressDialog обнаружит это и вообще не станет выводить себя на экран.
Кроме многопоточности и применения QProgressDialog существует совершенно другой способ работы с продолжительными операциями. Вместо выполнения заданной обработки сразу по поступлении запроса пользователя мы можем отложить эту обработку до момента перехода приложения в состояние ожидания. Этим способом можно пользоваться в тех случаях, когда обработку можно легко прерывать и затем возобновлять, поскольку мы не можем предсказать, как долго приложение будет в состоянии ожидания.
В Qt этот подход можно реализовать путем применения 0—миллисекундного таймера. Таймеры этого типа paботают при отсутствии ожидающих событий. Ниже приводится пример реализации функции timerEvent(), которая демонстрирует обработку в состоянии ожидании:
01 void Spreadsheet::timerEvent(QTimerEvent *event)
02 {
03 if(event->timerId() == myTimerId) {
04 while (step < MaxStep &&
05 !qApp->hasPendingEvents()) {
06 performStep(step);
07 ++step;
08 }
09 } else {
10 QTableWidget::timerEvent(event);
11 }
12 }
Если фyнкция hasPendingEvents() возвращает true, мы останавливаем процесс и передаем управление обратно Qt. Этот процесс будет возобновлен после обработки Qt всех своих ожидающих событий.
Глава 8. Графика 2D и 3D
Основу используемых в Qt средств графики 2D составляет класс QPainter (рисовальщик Qt). Этот класс может использоваться для рисования геометрически фигур (точек, линий, прямоугольников, эллипсов, дуг, сегментов и секторов окружности, многоугольников и кривых Безье), а также пиксельных карт, изображений и текста. Кроме того, QPainter поддерживает такие продвинутые функции, как сглаживание линий (antialiasing) при начертании фигур и букв в тексте, альфа—смешение (alpha blending), плавный переход цветов (gradient filling) и цепочки графических элементов (vector paths). QPainter также поддерживает преобразование координат, что делает графику 2D независимой от разрешающей способности.
QPainter может использоваться для вычерчивания на таких «устройствах рисования», как QWidget, QPixmap или QImage. QPainter удобно применять, когда мы программируем пользовательские виджеты или классы пользовательских графических элементов с особым внешним видом и режимом работы. Класс QPainter можно также использовать совместно с QPrinter для вывода графики на печатающее устройство и для генерации файлов PDF. Это значит, что во многих случаях мы можем использовать тот же самый программный код при отображении данных на экран и при получении напечатанных отчетов.
В качестве альтернативы классам QPainter можно использовать OpenGL. OpenGL является стандартной библиотекой графических средств 2D и 3D. Модуль QtOpenGL позволяет очень легко интегрировать OpenGL в приложения Qt.
Рисование при помощи QPainter
Чтобы начать рисовать на устройстве рисования (обычно это виджет), мы просто создаем объект QPainter и передаем ему указатель на устройство. Например:
void MyWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
…
}
Мы можем рисовать различные фигуры, используя функции QPainter вида draw…(). На рис 8.1 приведены наиболее важные из них.
Рис. 8.1. Часто используемые функции draw…() рисовальщика QPainter.
Параметры настройки QPainter влияют на режим рисования. Некоторые из них устанавливаются на параметры настройки устройства, а другие инициализируются значениями по умолчанию. Тремя основными параметрами настройки рисовальщика являются перо, кисть и шрифт:
• Перо используется для отображения прямых линий и контуров фигур. Оно имеет цвет, толщину, стиль линии, стиль окончания линии и стиль соединения линий.
• Кисть представляет собой шаблон, который используется для заполнения геометрических фигур. Он обычно имеет цвет и стиль, но может также представлять собой текстуру (пиксельную карту, повторяющуюся бесконечно) или цветовой градиент.
• Шрифт используется для отображения текста. Шрифт имеет много атрибутов, в том числе название и размер.
Эти настройки можно в любое время модифицировать при помощи функций setPen(), setBrush() и setFont(), вызываемых для объектов QPen, QBrush или QFont.
Рис. 8.2. Стили окончания линий и стили соединения линий.
Рис. 8.3. Стили пера.
Рис. 8.4. Определенные в Qt стили кисти.
Рис. 8.5. Примеры геометрических фигур.
Давайте рассмотрим несколько примеров. Ниже приводится программный код для вычерчивания эллипса, показанного на рис. 8.5 (а):
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(QPen(Qt::black, 12, Qt::DashDotLine, Qt::RoundCap));
painter.setBrush(QBrush(Qt::green, Qt::SolidPattern));
painter.drawEllipse(80, 80, 400, 240);
Вызов setRenderHint() включает режим сглаживания линий, указывая QPainter на необходимость использования по краям цветов различной интенсивности, чтобы уменьшить визуальное искажение, которое обычно заметно, когда края фигуры представляются пикселями. В результате края воспринимаются более ровными на тех платформах и устройствах, которые поддерживают эту функцию.
Ниже приводится программный код для вычерчивания сектора эллипса, показанного на рис. 8.5 (б):
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(QPen(Qt::black, 15, Qt::SolidLine, Qt::RoundCap, Qt::MiterJoin));
painter.setBrush(QBrush(Qt::blue, Qt::DiagCrossPattern));
painter.drawPie(80, 80, 400, 240, 60 * 16, 270 * 16);
Два последних аргумента функции drawPie() задаются в шестнадцатых долях градуса.
Ниже приводится программный код для вычерчивания кривой Безье третьего порядка, показанной на рис. 8.5 (в):
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
QPainterPath path;
path.moveTo(80, 320);
path.cubicTo(200, 80, 320, 80, 480, 320);
painter.setPen(QPen(Qt::black, 8));
painter.drawPath(path);
Класс QPainterPath может определять произвольные фигуры векторной графики, соединяя друг с другом основные графические элементы: прямые линии, эллипсы, многоугольники, дуги, кривые Безье второго и третьего порядка и другие цепочки графических элементов рисовальщика (painter paths). Такие цепочки являются законченными элементарными рисунками в том смысле, что любая фигура или любая комбинация фигур может быть представлена в виде некоторой цепочки графических элементов.
Цепочка графических элементов определяет контур, а область внутри контура можно заполнить какой-нибудь кистью. В примере, представленном на рис. 8.5 (в), мы не задавали кисть, поэтому нарисован только контур.
В трех представленных выше примерах используются встроенные шаблоны кисти (Qt::SolidPattern, Qt::DiagCrossPattern и Qt::NoBrush). В современных приложениях градиентные заполнители являются популярной альтернативой однородным заполнителям. Цветовые градиенты основаны на интерполяции цветов, обеспечивающей сглаженные переходы между двумя или более цветами. Они часто применяются для получения эффекта трехмерности изображения, например стиль Plastique использует цветовые градиенты при воспроизведении кнопок QPushButton.
Qt поддерживает три типа цветовых градиентов: линейный, конический и радиальный. В примере таймера духовки, который приводится в следующем разделе, в одном виджете используется комбинация всех трех типов градиентов для того, чтобы изображение выглядело реалистически.
Рис. 8.6. Кисти QPainter с цветовыми градиентами.
• Линейные градиенты определяются двумя контрольными точками и рядом «цветовых отметок» на линии, соединяющей эти точки. Например, линейный градиент на рис. 8.6 создан при помощи следующего программного кода:
QLinearGradient gradient(50, 100, 300, 350);
gradient.setColorAt(0.0, Qt::white);
gradient.setColorAt(0.2, Qt::green);
gradient.setColorAt(1.0, Qt::black);
Мы задали три цвета в трех разных позициях между двумя контрольными точками. Позиции представляются в виде чисел с плавающей точкой в диапазоне между 0 и 1, где 0 соответствует первой контрольной точке, а 1 — последней контрольной точке. Цвет между этими позициями интерполируется.
• Радиальные градиенты определяются центральной точкой (хс, ус), радиусом r и точкой фокуса (xf, yf), которая дополняет цветовые метки. Центральная точка и радиус определяют окружность. Изменение цвета распространяется во все стороны из точки фокуса, которая может совпадать с центральной точкой или может быть любой другой точкой внутри окружности.
• Конические градиенты определяются центральной точкой (хс, ус) и углом α. Изменение цвета распространяется вокруг центральной точки подобно перемещению секундной стрелки часов.
До сих пор мы говорили о настройках пера, кисти и шрифта рисовальщика. QPainter имеет другие параметры настройки, влияющие на способ рисования фигур и текста:
• Кисть фона (background brush) используется для заполнения фона геометрических фигур (то есть под шаблоном кисти), текста или пиксельной карты, когда в качестве режима отображения фона задан Qt::OpaqueMode (непрозрачный режим) (по умолчанию используется режим Qt::TransparentMode — прозрачный).
• Исходная точка кисти (brush origin) задает точку начала отображения шаблона кисти, в качестве которой обычно используется точка верхнего левого угла виджета.
• Границы области рисования (clip region) определяют область рисования устройства. Операции рисования, которые выходят за пределы этой области, игнорируются.
• Область отображения, окно и универсальная матрица преобразования (viewport, window и world matfix) определяют способ перевода логических координат QPainter в физические координаты устройства рисования. По умолчанию системы логических и физических координат совпадают. Системы координат рассматриваются в следующем разделе.
• Режим композиции (composition mode) определяет способ взаимодействия новых выводимых пикселей с пикселями, уже присутствующими на устройстве рисования. По умолчанию используется режим «source over», при котором новые пиксели рисуются поверх существующих. Этот режим поддерживается только определенными устройствами, и он рассматривается позже в данной главе.
В любой момент времени мы можем сохранить в стеке текущее состояние рисовальщика, вызывая функцию save(), и восстановить его позже, вызывая функцию restore(). Это может быть полезно, если требуется временно изменить некоторые параметры настройки рисовальщика и затем их восстановить в прежние значения, как мы это увидим в следующем разделе.
Преобразования рисовальщика
В используемой по умолчанию координатной системе рисовальщика QPainter точка (0, 0) находится в левом верхнем углу устройства рисования; значение координат x увеличивается при перемещении вправо, а значение координат у увеличивается при перемещении вниз. Каждый пиксель занимает область 1 × 1 в координатной системе, применяемой по умолчанию.
Необходимо помнить об одной важной особенности: центр пикселя имеет «полупиксельные» координаты. Например, пиксель в верхнем левом углу занимает область между точками (0, 0) и (1, 1), а его центр находится в точке (0.5, 0.5). Если мы просим QPainter нарисовать пиксель, например, в точке (100, 100), его координаты будут смещены на величину +0.5 по обоим направлениям, и в результате нарисованный пиксель будет иметь центр в точке (100.5, 100.5).
На первый взгляд эта особенность представляет лишь теоретический интерес, однако она имеет важные практические последствия. Во-первых, смещение +0.5 действует только при отключении сглаживания линий (режим по умолчанию); если режим сглаживания линий включен и мы пытаемся нарисовать пиксель черного цвета в точке (100, 100), QPainter фактически выведет на экран четыре светло-серых пикселя в точках (99.5, 99.5), (99.5, 100.5), (100.5, 99.5) и (100.5, 100.5), чтобы создалось впечатление расположения пикселя точно в точке соприкосновения всех этих четырех пикселей. Если этот эффект нежелателен, его можно избежать, указывая полупиксельные координаты, например (100.5, 100.5).
При начертании таких фигур, как линии, прямоугольники и эллипсы, действуют аналогичные правила. На рис 8.7 показано, как изменяется результат вызова drawRect(2, 2, 6, 5) в зависимости от ширины пера, когда сглаживание линий отключено. В частности, важно отметить, что прямоугольник 6 × 5, вычерчиваемый пером с шириной 1, фактически занимает область размером 7 × 6. Это не делалось прежними инструментальными средствами, в том числе в ранних версиях Qt, но такой подход существенен для получения действительно масштабируемой, независимой от разрешающей способности векторной графики.
Рис. 8.7. Вычерчивание прямоугольника 6 × 5 при отсутствии сглаживания линий.
Теперь, когда мы ознакомились с используемой по умолчанию координатной системой, мы можем внимательно рассмотреть возможные ее изменения при использовании рисовальщиком QPainter области отображения, окна и универсальной матрицы преобразования. (В данном контексте термин «окно» не является обозначением окна виджета верхнего уровня, а термин «область отображения» никак нe связан с областью отображения QScrollArea.)
Термины «область отображения» и «окно» сильно связаны друг с другом. Область отображения является произвольным прямоугольником, заданным в физических координатах. Окно определяет такой же прямоугольник, но в логических координатах. При рисовании мы задаем координаты точек в логической системе координат, и эти координаты с помощью линейного алгебраического преобразования переводятся в физическую систему координат на основе текущих настроек связи «окно—область отображения».
По умолчанию область отображения и окно устанавливаются на прямоугольную область устройства рисования. Например, если этим устройством является виджет размером 320 × 200, область отображения и окно представляют собой одинаковый прямоугольник 320 × 200, верхний левый угол которого располагается в позиции (0, 0). В данном случае системы логических и физических координат совпадают.
Механизм «окно—область отображения» удобно применять для создания программного кода, который не будет зависеть от размера или разрешающей способности устройства рисования. Например, если мы хотим обеспечить логические координаты в диапазоне от (—50, —50) до (+50, +50) с (0, 0) в середине, мы можем задать окно следующим образом:
painter.setWindow(-50, -50, 100, 100):
Пара аргументов (—50, —50) задает начальную точку, а пара аргументов (100, 100) задает ширину и высоту. Это означает, что точка с логическими координатами (—50, —50) теперь соответствует точке с физическими координатами (0, 0), а точка с логическими координатами (+50, +50) соответствует точке с физическими координатами (320, 200). В этом примере мы не изменяли область отображения.
Рис. 8.8. Преобразование логических координат в физические координаты.
Теперь очередь дошла до универсальной матрицы преобразования. Эта матрица используется как дополнение к преобразованию «окно—область отображения». Она позволяет нам перемещать начало координат, изменять масштаб, поворачивать и обрезать графические элементы. Например, если бы нам понадобилось отобразить текст под углом 45°, мы бы использовали такой программный код:
QMatrix matrix;
matrix.rotate(45.0);
painter.setMatrix(matrix);
painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));
Логические координаты, которые мы передаем функции drawText(), преобразуются при помощи универсальной матрицы и затем переводятся в физические координаты, используя связь «окно—область отображения».
Если мы задаем несколько преобразований, они осуществляются в порядке поступления. Например, если мы хотим использовать точку (10, 20) в качестве точки поворота, мы можем перенести начало координат окна, выполнить поворот и затем сделать обратный перенос начала координат окна, устанавливая его в прежнее положение.
QMatrix matrix;
matrix.translate(-10.0, -20.0);
matrix.rotate(45.0);
matrix.translate(+10.0, +20.0);
painter.setMatrix(matrix);
painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));
Более удобно осуществлять преобразования путем применения соответствующих функций класса QPainter — translate(), scale(), rotate() и shear():
painter.translate(-10.0, -20.0);
painter.rotate(45.0);
painter.translate(+10.0, +20.0);
painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));
Но если мы хотим регулярно делать одни и те же преобразования, то они будут выполняться более эффективно при их хранении в объекте QMatrix и затем применении универсальной матрицы преобразования для рисовальщика всякий раз, когда требуется выполнить преобразование.
Рис. 8.9. Виджет OvenTimer.
Для иллюстрации преобразования рисовальщика мы рассмотрим программный код виджета OvenTimer (таймер духовки), показанного на рис. 8.9. Виджет OvenTimer имитирует кухонные таймеры, которые широко использовались до того, как в духовках стали применяться встроенные часы. Пользователь может повернуть ручку и установить требуемую длительность. Диск переключателя автоматически поворачивается против часовой стрелки, пока не достигнет отметки 0, в результате чего OvenTimer генерирует сигнал timeout().
01 class OvenTimer : public QWidget
02 {
03 Q_OBJECT
04 public:
05 OvenTimer(QWidget *parent = 0);
06 void setDuration(int secs);
07 int duration() const;
08 void draw(QPainter *painter);
09 signals:
10 void timeout();
11 protected:
12 void paintEvent(QPaintEvent *event);
13 void mousePressEvent(QMouseEvent *event);
14 private:
15 QDateTime finishTime;
16 QTimer *updateTimer;
17 QTimer *finishTimer;
18 }
Класс OvenTimer наследует QWidget и переопределяет две виртуальные функции: paintEvent() и mousePressEvent().
const double DegreesPerMinute = 7.0;
const double DegreesPerSecond = DegreesPerMinute / 60;
const int MaxMinutes = 45;
const int MaxSeconds = MaxMinutes * 60;
const int UpdateInterval = 1;
Мы начнем с нескольких констант, управляющих внешним видом и режимом работы таймера духовки.
01 OvenTimer::OvenTimer(QWidget *parent)
02 : QWidget(parent)
03 {
04 finishTime = QDateTime::currentDateTime();
05 updateTimer = new QTimer(this);
06 connect(updateTimer, SIGNAL(timeouf()),
07 this, SLOT(update()));
08 finishTimer = new QTimer(this);
09 finishTimer->setSingleShot(true);
10 connect(finishTimer, SIGNAL(timeout()),
11 this, SIGNAL(timeout()));
12 connect(finishTimer, SIGNAL(timeout()),
13 updateTimer, SLOT(stop()));
14 }
В конструкторе мы создаем два объекта QTimer: updateTimer используется для обновления внешнего вида виджета через каждую секунду, a finishTimer генерирует сигнал виджета timeout() при достижении отметки 0. Объект finishTimer должен генерировать только один сигнал тайм-аута, поэтому мы вызываем setSingleShot(true); по умолчанию таймеры запускаются повторно, пока они не будут остановлены или не будут уничтожены. Последний вызов connect() является оптимизационным и обеспечивает прекращение обновления виджета каждую секунду, когда таймер неактивен.
01 void OvenTimer::setDuration(int secs)
02 {
03 if (secs > MaxSeconds) {
04 secs = MaxSeconds;
05 } else if (secs <= 0) {
06 secs = 0;
07 }
08 finishTime = QDateTime::currentDateTime().addSecs(secs);
09 if (secs > 0) {
10 updateTimer->start(UpdateInterval * 1000);
11 finishTimer->start(secs * 1000);
12 } else {
13 updateTimer->stop();
14 finishTimer->stop();
15 }
16 update();
17 }
Функция setDuration() выставляет таймер духовки, задавая требуемое количество секунд. Время окончания мы рассчитываем путем добавления продолжительности его работы к текущему времени, полученному функцией QDateTime::currentDateTime(), и сохраняем его в закрытой переменной finishTime. B конце мы вызываем update() для перерисовки виджета с новой продолжительностью работы.
Переменная finishTime имеет тип QDateTime. Поскольку она содержит дату и время, мы избегаем ошибки из-за смены суток, когда текущее время оказывается до полуночи, а время окончания — после полуночи.
01 int OvenTimer::duration() const
02 {
03 int secs = QDateTime::currentDateTime().
04 secsTo(finishTime);
05 if (secs < 0)
06 secs = 0;
07 return secs;
08 }
Функция duration() возвращает количество секунд, оставшееся до завершения работы таймера. Если таймер неактивен, мы возвращаем 0.
01 void OvenTimer::mousePressEvent(QMouseEvent *event)
02 {
03 QPointF point = event->pos() - rect().center();
04 double theta = atan2(-point.x(), -point.y()) * 180 / 3.14159265359;
05 setDuration(duration() + int(theta / DegreesPerSecond));
06 update();
07 }
Если пользователь щелкает по этому виджету, мы находим ближайшую метку, используя тонкую, но эффективную математическую формулу, а результат идет на установку новой продолжительности таймера. Затем мы генерируем событие перерисовки. Метка, по которой щелкнул пользователь, теперь будет располагаться сверху поворотного диска и будет поворачиваться против часовой стрелки до тех пор, пока не будет достигнуто значение 0.
01 void OvenTimer::paintEvent(QPaintEvent * /* event */)
02 {
03 QPainter painter(this);
04 painter.setRenderHint(QPainter::Antialiasing, true);
05 int side = qMin(width(), height());
06 painter.setViewport((width() - side) / 2, (height() - side) / 2,
07 side, side);
08 painter.setWindow(-50, -50, 100, 100);
09 draw(&painter);
10 }
B paintEvent() мы устанавливаем область отображения на максимальный квадрат, который можно разместить внутри виджета, и мы устанавливаем окно на прямоугольник (—50, —50, 100, 100), то есть на прямоугольник с размерами 100 × 100, который покрывает пространство от точки (—50, —50) до точки (+50, +50). Шаблонная функция qMin возвращает наименьшее из двух значений аргументов. Затем мы вызываем функцию draw() для фактического вывода рисунка на экран.
Рис. 8.10. Вид виджета OvenTimer при трех различных размерах.
Если область отображения не была бы квадратом, таймер духовки принял бы форму эллипса, когда форма виджета перестанет быть квадратной после изменения его размеров. Чтобы избежать такой деформации, мы должны устанавливать область отображения и окно на прямоугольник с одинаковым соотношением сторон.
Теперь давайте рассмотрим программный код рисования:
01 void OvenTimer::draw(QPainter *painter)
02 {
03 static const int triangle[3][2] = {
04 { -2, -49 }, { +2, -49 }, { 0, -47 }
05 };
10 QPen thickPen(palette().foreground(), 1.5);
11 QPen thinPen(palette().foreground(), 0.5);
12 QColor niceBlue(150, 150, 200);
13 painter->setPen(thinPen);
14 painter->setBrush(palette().foreground());
15 painter->drawPolygon(QPolygon(3, &triangle[0][0]));
Мы начинаем с отображения маленького треугольника в позиции 0 в верхней части виджета. Этот треугольник задается в программе тремя фиксированными координатами, и мы используем функцию drawPolygon() для его воспроизведения.
Одно из удобств применения механизма «окно—область отображения» заключается в том, что мы можем при программировании в командах рисования жестко задавать координаты точек и тем не менее добиваться необходимого изменения размеров.
16 QConicalGradient coneGradient(0, 0, -90.0);
17 coneGradient.setColorAt(0.0, Qt::darkGray);
18 coneGradient.setColorAt(0.2, niceBlue);
19 coneGradient.setColorAt(0.5, Qt::white);
20 coneGradient.setColorAt(1.0, Qt::darkGray);
21 painter->setBrush(coneGradient);
22 painter->drawEllipse(-46, -46, 92, 92);
Мы рисуем внешнюю окружность и заполняем ее, используя конический градиент. Центр градиента находится в точке (0, 0), а его угол равен —90°.
23 QRadialGradient haloGradient(0, 0, 20, 0, 0);
24 haloGradient.setColorAt(0.0, Qt::lightGray);
25 haloGradient.setColorAt(0.8, Qt::darkGray);
26 haloGradient.setColorAt(0.9, Qt::white);
27 haloGradient.setColorAt(1.0, Qt::black);
28 painter->setPen(Qt::NoPen);
29 painter->setBrush(haloGradient);
30 painter->drawEllipse(-20, -20, 40, 40);
Мы заполняем внутреннюю окружность, используя радиальный градиент. Центр и фокус градиента располагаются в точке (0, 0). Радиус градиента равен 20.
31 QLinearGradient knobGradient(-7, -25, 7, -25);
32 knobGradient.setColorAt(0.0, Qt::black);
33 knobGradient.setColorAt(0.2, niceBlue);
34 knobGradient.setColorAt(0.3, Qt::lightGray);
35 knobGradient.setColorAt(0.8, Qt::white);
36 knobGradient.setColorAt(1.0, Qt::black);
37 painter->rotate(duration() * DegreesPerSecond);
38 painter->setBrush(knobGradient);
39 painter->setPen(thinPen);
40 painter->drawRoundRect(-7, -25, 14, 50, 150, 50);
41 for (int i = 0; i <= MaxMinutes; ++i) {
42 if (i % 5 == 0) {
43 painter->setPen(thickPen);
44 painter->drawLine(0, -41, 0, -44);
45 painter->drawText(-15, -41, 30, 25,
46 Qt::AlignHCenter | Qt::AlignTop,
47 QString::number(i));
48 } else {
49 painter->setPen(thinPen);
50 painter->drawLine(0, -42, 0, -44);
51 }
52 painter->rotate(-DegreesPerMinute);
53 }
54 }
Мы вызываем функцию rotate() для поворота системы координат рисовальщика. В старой системе координат нулевая отметка находилась сверху; теперь нулевая отметка перемещается для установки соответствующего времени, которое остается до срабатывания таймера. После каждого поворота мы снова рисуем ручку таймера, поскольку его ориентация зависит от угла поворота.
В цикле for мы рисуем минутные отметки по внешней окружности и отображаем количество минут через каждые 5 минутных меток. Текст размещается в невидимом прямоугольнике под минутной отметкой. В конце каждой итерации цикла мы поворачиваем рисовальщик по часовой стрелке на 7°, что соответствует одной минуте. При рисовании минутной отметки следующий раз она будет отображаться в другом месте окружности, хотя мы передаем одни и те же координаты функциям drawLine() и drawText().
В этом программном коде в цикле for имеется незаметная погрешность, которая быстро стала бы очевидной, если бы мы выполнили больше итераций. При каждом вызове rotate() мы фактически умножаем текущую универсальную матрицу преобразования на матрицу поворота, получая новую универсальную матрицу преобразования. Ошибка округления чисел с плавающей точкой еще больше увеличивает неточность универсальной матрицы преобразования. Ниже показан один из возможных способов решения этой проблемы путем перезаписи программного кода с использованием save() и restore() для сохранения и восстановления первоначальной матрицы преобразования на каждом шаге итерации:
41 for (int i = 0; i <= MaxMinutes; ++i) {
42 painter->save();
43 painter->rotate(-i * DegreesPerMinute);
44 if (i % 5 == 0) {
45 painter->setPen(thickPen);
46 painter->drawLine(0, -41, 0, -44);
47 painter->drawText(-15, -41, 30, 25,
48 Qt::AlignHCenter | Qt::AlignTop,
49 QString::number(i));
50 } else {
51 painter->setPen(thinPen);
52 painter->drawLine(0, -42, 0, -44);
53 }
54 painter->restore();
55 }
При другом способе реализации таймера духовки нам нужно было бы самим рассчитывать координаты (x, y), используя функции sin() и cos() для определения их позиции на окружности. Но тогда нам все же пришлось бы выполнять перенос и поворот системы координат для отображения текста под некоторым углом.
Высококачественное воспроизведение изображения при помощи QImage
При рисовании мы можем столкнуться с необходимостью принятия компромиссных решений относительно скорости и точности. Например, в системах X11 и Mac OS X рисование по виджету QWidget или по пиксельной карте QPixmap основано на применении родного для платформы графического процессора (paint engine). В системе X11 это обеспечивает минимальную связь с Х—сервером; посылаются только команды рисования, а не данные реального изображения. Основным недостатком этого подхода является то, что возможности Qt ограничиваются родными для данной платформы средствами поддержки:
• в системе Х11 такие возможности, как сглаживание линий и поддержка дробных координат, доступны только в том случае, если Х—сервер использует расширение X Render;
• в системе Mac OS X родной графический процессор, обеспечивающий сглаживание линий, использует алгоритмы рисования многоугольников, которые отличаются от алгоритмов в X11 и Windows, что приводит к получению немного других результатов.
Когда точность важнее эффективности, мы можем рисовать по QImage и копировать результат на экран. В этом случае Qt всегда использует собственный внутренний графический процессор и результат на всех платформах получается идентичным. Единственное ограничение заключается в том, что QImage, по которому мы рисуем, должен создаваться с аргументом QImage::Format_RGB32 или QImage::Format_ARGB32_Premultiplied.
Второй формат почти идентичен обычному формату ARGB32 (0xaarrggbb); отличие в том, что красный, зеленый и синий компоненты «предварительно умножаются» на альфа—компонент. Это значит, что значения RGB, которые обычно находятся в диапазоне от 0x00 до 0xFF, теперь принимают значения от 0x00 до значения альфа-компонента. Например, синий цвет с прозрачностью 50% представляется значением 0x7F0000FF в формате ARGB32, но он имеет значение 0x7F00007F в формате ARGB32 с предварительным умножением компонент, и, аналогично, темно-зеленый цвет с прозрачностью 75% имеет значение 0x3F008000 в формате ARGB32 и значение 0x3F002000 в фopмaтe ARGB32 с предварительным умножением компонент.
Предположим, что мы хотим использовать сглаживание линий при рисовании виджета и нам нужно получить хорошие результаты даже в системах X11, которые не используют расширение X Render. Обработчик событий paintEvent(), предполагающий применение X Render для сглаживания линий, мог бы выглядеть следующим образом:
01 void MyWidget::paintEvent(QPaintEvent *event)
02 {
03 QPainter painter(this);
04 painter.setRenderHint(QPainter::Antialiasing, true);
05 draw(&painter);
06 }
Ниже показано, как можно переписать виджетную функцию paintEvent() для применения независимого от платформы графического процессора Qt:
01 void MyWidget::paintEvent(QPaintEvent *event)
02 {
03 QImage image(size(), QImage::Format_ARGB32_Premultiplied);
04 QPainter imagePainter(&image);
05 imagePainter.initFrom(this);
06 imagePainter.setRenderHint(QPainter::Antialiasing, true);
07 imagePainter.eraseRect(rect());
08 draw(&imagePainter);
09 imagePainter.end();
10 QPainter widgetPainter(this);
11 widgetPainter.drawImage(0,0, image);
12 }
Мы создаем объект QImage с тем же размером, который имеет виджет, в формате ARGB32 с умножением компонент, и объект QPainter для рисования по изображению. Вызов initFrom() инициализирует в рисовальщике перо, фон и шрифт значениями, используемыми виджетом. Мы рисуем, используя QPainter как обычно, а в конце еще раз используем объект QPainter для копирования изображения на виджет.
Этот подход дает одинаково высококачественный результат на всех платформах, за исключением воспроизведения шрифта, что зависит от установленных в системе шрифтов.
Особенно эффективным средством графического процессора Qt является его поддержка режимов композиции. Эти режимы определяют способ слияния исходного и нового пикселя при рисовании. Это относится ко всем операциям рисования, в том числе относящимся к перу, кисти, градиенту и изображению.
Режимом композиции по умолчанию является QImage::CompositionMode_SourceOver, означающий, что исходный пиксель (тот, который рисуется в данный момент) налагается поверх существующего на изображении пикселя, причем альфа—компонент исходного пикселя определяет степень его прозрачности. На рис. 8.11 показан результат рисования полупрозрачной бабочки поверх тестового шаблона при использовании разных режимов.
Рис. 8.11. Режимы композиции QPainter.
Режимы композиции устанавливаются функцией QPainter::setCompositionMode(). Например, ниже показано, как можно создать объект QImage, объединяющий пиксели бабочки и тестового шаблона с помощью операции XOR:
QImage resultImage = checkerPatternImage;
QPainter painter(&resultImage);
painter.setCompositionMode(QPainter::CompositionMode_Xor);
painter.drawImage(0, 0, butterflyImage);
Следует иметь в виду, что операция QImage::CompositionMode_Xor применяется к альфа—компоненту. Это означает, что если мы применим операцию XOR при наложении белого цвета (0xFFFFFFFF) на белый цвет, мы получим прозрачный цвет (0х00000000), а не черный цвет(0хFF000000).
Вывод на печатающее устройство
Вывод на печатающее устройство в Qt подобен рисованию по QWidget, QPixmap или QImage. Порядок действий при этом будет следующим:
1. Создайте в качестве устройства рисования объект QPrinter.
2. Выведите на экран диалоговое окно печати QPrintDialog, позволяя пользователю выбрать печатающее устройство и установить некоторые параметры печати.
3. Создайте объект QPainter для работы с QPrinter.
4. Нарисуйте страницу, используя QPainter.
5. Вызовите функцию QPrinter::newPage() для перехода на следующую страницу.
6. Повторяйте пункты 4 и 5 до тех пор, пока не будут распечатаны все страницы.
В операционных системах Windows и Mac OS X QPrinter использует системные драйверы принтеров. В системе Unix он формирует файл PostScript и передает его lp или lpr (или другой программе, установленной функцией QPrinter::setPrintProgram()). QPrinter может также использоваться для генерации файлов PDF, если вызвать setOutputFormat(QPrinter::PdfFormat).
Давайте начнем с рассмотрения какого-нибудь простого примера по распечатке одной страницы. Первый пример распечатывает объект QImage:
01 void PrintWindow::printImage(const Qlmage &image)
02 {
03 QPrintDialog printDialog(&printer, this);
04 if (printDialog.exec()) {
05 QPainter painter(&printer);
06 QRect rect = painter.viewport();
07 QSize size = image.size();
08 size.scale(rect.size(), Qt::KeepAspectRatio);
09 painter.setViewport(rect.x(). rect.y(), size.width(), size.height());
10 painter.setWindow (image.rect());
11 painter.drawImage(0, 0, image);
12 }
13 }
Рис. 8.12. Вывод на печатающее устройство объекта QImage.
Мы предполагаем, что класс PrintWindow имеет переменную—член printer типа QPrinter. Мы могли бы просто поместить QPrinter в стек в функции printImage(), но тогда не сохранялись бы настройки пользователя при переходе от одной печати к другой.
Мы создаем объект QPrintDialog и вызываем функцию exec() для вывода на экран диалогового окна печати. Оно возвращает true, если пользователь нажал кнопку OK; в противном случае оно возвращает false. После вызова функции exec() объект QPrinter готов для использования. (Можно также печатать, не используя QPrintDialog, а напрямую вызывая функции—члены класса QPrinter для подготовки печати.)
Затем мы создаем QPainter для рисования на QPrinter. Мы устанавливаем окно на прямоугольник изображения и область отображения на прямоугольник с тем же соотношением сторон, и мы рисуем изображение в позиции (0, 0).
По умолчанию окно QPrinter инициализируется таким образом, что разрешающая способность принтера будет аналогична разрешающей способности экрана (обычно она составляет примерно от 72 до 100 точек на дюйм), позволяя легко использовать для печати программный код по рисованию виджета. Здесь это не имеет значения, поскольку мы сами задали параметры нашего окна.
Вывод на печатающее устройство элементов, занимающих не более одной страницы, выполняется достаточно просто, но во многих приложениях приходится печатать несколько страниц. В таких случаях мы должны сначала нарисовать одну страницу и затем вызвать функцию newPage() для перехода на следующую страницу. Здесь возникает проблема определения того количества информации, которое будет печататься на одной странице. Существует два подхода при обработке многостраничных документов в Qt:
• Мы можем преобразовать наши данные в формат HTML и затем воспроизвести их с применением класса QTextDocument, процессора форматированного текста Qt.
• Мы можем выполнить рисование и разбивку на страницы вручную.
Мы рассмотрим по очереди оба подхода. В качестве примера мы распечатаем цветочный справочник: список названий цветов с текстовым описанием. Каждый элемент этого справочника представляется строкой формата «название: описание», например:
Miltonopsis santanae: Самый опасный вид орхидеи.
Поскольку данные каждого цветка представлены одной строкой, мы можем представить цветочный справочник при помощи одного объекта QStringList. Ниже приводится функция печати цветочного справочника, использующая процессор форматированного текста Qt:
01 void PrintWindow::printFlowerGuide(const QStringList &entries)
02 {
03 QString html;
04 foreach(QString entry, entries) {
05 QStringList fields = entry.split(": ");
06 QString title = Qt::escape(fields[0]);
07 QString body = Qt::escape(fields[1]);
08 html = "
"
10 "" + title + " \n |
" + body"
11 + "\n |
\n";
12 }
13 printHtml(html);
14 }
На первом этапе QStringList преобразуется в формат HTML. Каждый цветок представляется таблицей HTML с двумя ячейками. Мы используем функцию Qt::escape() для замены специальных символов «&», «<», «>» на соответствующие элементы формата HTML(«&», «<», «>»). Затем мы вызываем функцию printHtml() для печати текста.
01 void PrintWindow::printHtml(const QString &html)
02 {
03 QPrintDialog printDialog(&printer, this);
04 if (printDialog.exec()) {
05 QPainter painter(&printer);
06 QTextDocument textDocument;
07 textDocument.setHtml(html);
08 textDocument.print(&printer);
09 }
10 }
Функция printHtml() выводит диалоговое окно QPrintDialog и выполняет печать документа HTML. Она может без изменений повторно использоваться в любом приложении Qt для распечатки страниц произвольного текста в формате HTML.
Рис. 8.13. Вывод на печать цветочного справочника с применением QTextDocument.
Преобразование документа в формат HTML и использование QTextDocument для его распечатки являются самым удобным способом печати отчетов и других сложных документов. В тех случаях, когда требуется обеспечить больший контроль, мы можем вручную выполнить компоновку страниц и их рисование. Давайте теперь посмотрим, как можно напечатать цветочный справочник при помощи класса QPainter. Ниже приводится новая версия функции printFlowerGuide():
01 void PrintWindow::printFlowerGuide(const QStringList &entries)
02 {
03 QPrintDialog printDialog(&printer, this);
04 if (printDialog.exec()) {
05 QPainter painter(&printer);
06 QList
07 paginate(&painter, &pages, entries);
08 printPages(&painter, pages);
09 }
10 }
После настройки принтера и построения объекта рисовальщика мы вызываем вспомогательную функцию paginate() для определения содержимого каждой страницы. В результате получается вектор списков QStringList, причем каждый список QStringList содержит элементы одной страницы. Результат мы передаем функции printPages().
Например, предположим, что цветочный справочник содержит всего 6 элементов, которые мы обозначим буквами А, Б, В, Г, Д и E. Теперь предположим, что имеется достаточно места для элементов А и Б на первой странице, В, Г и Д на второй странице и Е на третьей странице. Тогда список pages содержал бы список [А, Б] в элементе с индексом 0, список [В, Г, Д] в элементе с индексом 1 и список [E] в элементе с индексом 2.
01 void PrintWindow::paginate(QPainter *painter, QList
02 const QStringList &entries)
03 {
04 QStringList currentPage;
05 int pageHeight = painter->window().height() - 2 * LargeGap;
06 int у = 0;
07 foreach (QString entry, entries) {
08 int height = entryHeight(painter, entry);
09 if (у + height > pageHeight && !currentPage.empty()) {
10 pages->append(currentPage);
11 currentPage.clear();
12 y = 0;
13 }
14 currentPage.append(entry);
15 у += height + MediumGap;
16 }
17 if (!currentPage.empty())
18 pages->append(currentPage);
19 }
Функция paginate() распределяет элементы справочника цветов по страницам. Ее работа основана на применении функции entryHeight(), рассчитывающей высоту каждого элемента. Она также учитывает наличие сверху и снизу страницы полей с размером LargeGap.
Мы выполняем цикл по элементам и добавляем их в конец текущей страницы до тех пор, пока не окажется, что элемент не вмещается на страницу; затем мы добавляем текущую страницу в конец списка pages и начинаем формировать новую страницу.
01 int PrintWindow::entryHeight(QPainter *painter, const QString &entry)
02 {
03 int textWidth = painter->window().width() - 2 * SmallGap;
04 QString title = fields[0];
05 QString body = fields[1];
06 QStringList fields = entry.split(": ");
07 int maxHeight = painter->window().height();
08 painter->setFont(titleFont);
09 QRect titleRect = painter->boundingRect(0, 0, textWidth, maxHeight,
10 Qt::TextWordWrap, title);
11 painter->setFont(bodyFont);
12 QRect bodyRect = painter->boundingRect(0, 0, textWidth, maxHeight,
13 Qt::TextWordWrap, body);
14 return titleRect.height() + bodyRect.height() + 4 * SmallGap;
15 }
Функция entryHeight() использует QPainter::boundingRect() для вычисления размера области, занимаемой одним элементом по вертикали. На рис. 8.14 показана компоновка элементов одного цветка на странице и проиллюстрирован смысл констант SmallGap и MediumGap.
Рис. 8.14. Компоновка элементов справочника цветов на странице.
01 void PrintWindow::printPages(QPainter *painter,
02 const QList
03 {
04 int firstPage = printer.fromPage() - 1;
05 if (firstPage >= pages.size())
06 return;
07 if (firstPage == -1)
08 firstPage = 0;
09 int lastPage = printer.toPage() - 1;
10 if (lastPage == -1 || lastPage >= pages.size())
11 lastPage = pages.size() - 1;
12 int numPages = lastPage - firstPage + 1;
13 for (int i = 0; i < printer.numCopies(); ++i) {
14 for (int j = 0; j < numPages; ++j) {
15 if (i != 0 || j != 0)
16 printer.newPage();
17 int index;
18 if (printer.pageOrder() == QPrinter::FirstPageFirst) {
19 index = firstPage + j;
20 } else {
21 index = lastPage - j;
22 }
23 printPage(painter, pages[index], index + 1);
24 }
25 }
26 }
Функция printPages() предназначена для печати каждой страницы функцией printPage() с обеспечением правильного числа и правильной последовательности вызовов последней. Применяя QPrintDialog, пользователь может запросить распечатку нескольких копий, указать диапазон страниц или запросить распечатку страниц в обратной последовательности. Мы сами должны включать или отключать эти опции, используя функцию QPrintDialog::setEnabledOptions().
Мы начинаем с определения диапазона печати. Функции QPrinter fromPage() и toPage() возвращают заданные пользователем номера страниц или 0, если диапазон не указан. Мы вычитаем 1, потому что наш список страниц pages нумеруется с нуля, и устанавливаем переменные firstPage и lastPage (первая и последняя страницы) на охват всех страниц, если диапазон не задан пользователем.
Затем мы печатаем каждую страницу. Внешний цикл for определяется количеством копий, запрошенных пользователем. Большинство драйверов принтеров поддерживают печать нескольких копий, поэтому для них функция QPrinter::numCopies() всегда возвращает 1. Если драйвер принтера не может печатать несколько копий, numCopies() возвращает количество копий, запрошенное пользователем, и за печать этого количества копий отвечает приложение. (В примере с QImage, приведенном ранее в данном разделе, мы для простоты проигнорировали numCopies().)
Рис. 8.15 аналогичен 8.13.
Внутренний цикл for выполняется по всем страницам. Если страница не первая, мы вызываем newPage(), чтобы сбросить на печатающее устройство старую страницу и начать рисование новой страницы. Мы вызываем printPage() для распечатки каждой страницы.
01 void PrintWindow::printPage(QPainter *painter,
02 const QStringList &entries, int pageNumber)
03 {
04 painter->save();
05 painter->translate(0, LargeGap);
06 foreach (QString entry, entries) {
07 QStringList fields = entry.split(": ");
08 QString title = fields[0];
09 QString body = fields[1];
10 printBox(painter, title, titleFont, Qt::lightGray);
11 printBox(painter, body, bodyFont, Qt::white);
12 painter->translate(0, MediumGap);
13 }
14 painter->restore();
15 painter->setFont(footerFont);
16 painter->drawText(painter->window(),
17 Qt::AlignHCenter | Qt::AlignBottom,
18 QString::number(pageNumber));
19 }
Функция printPage() обрабатывает в цикле все элементы справочника цветов и печатает их при помощи двух вызовов функции printBox(): один для заголовка (название цветка) и другой для «тела» (описание цветка). Она также отображает номер страницы внизу по центру страницы.
01 void PrintWindow::printBox(QPainter *painter, const QString &str,
02 const QFont &font, const QBrush &brush)
03 {
04 painter->setFont(font);
05 int boxWidth = painter->window().width();
06 int textWidth = boxWidth - 2 * SmallGap;
07 int maxHeight = painter->window().height();
08 QRect textRect = painter->boundingRect(SmallGap, SmallGap,
09 textWidth, maxHeight, Qt::TextWordWrap, str);
10 int boxHeight = textRect.height() + 2 * SmallGap;
11 painter->setPen(QPen(Qt::black, 2, Qt::SolidLine));
12 painter->setBrush(brush);
13 painter->drawRect(0, 0, boxWidth, boxHeight);
14 painter->drawText(textRect, Qt::TextWordWrap, str);
15 painter->translate(0, boxHeight);
16 }
Рис. 8.16. Компоновка страницы справочника по цветам.
Функция printBox() вычерчивает контур блока, затем отображает текст внутри него.
Графические средства OpenGL
OpenGL является стандартным программным интерфейсом, предназначенным для воспроизведения графики 2D и 3D. Приложения Qt могут отображать графику 3D, используя модуль QtOpenGL, который рассчитан на применение системной библиотеки OpenGL. При изложении данного раздела предполагается, что вы знакомы с OpenGL. Если вы не знакомы с OpenGL, хорошо начинать его изучение с посещения сайта .
Рис. 8.17. Приложение Тетраэдр.
Вывод графики при помощи OpenGL в приложении Qt выполняется достаточно просто: мы должны создать подкласс QGLWidget, переопределить несколько виртуальных функций и собрать приложение вместе с библиотеками QtOpenGL и OpenGL. Из-за того, что QGLWidget наследует QWidget, большая часть наших знаний остается применимой и здесь. Основное отличие заключается в том, что вместо QPainter для выполнения графических операций мы используем стандартные функции библиотеки OpenGL.
Для демонстрации этого подхода мы рассмотрим программный код приложения Тетраэдр, показанного на рис. 8.17. Это приложение отображает в пространстве тетраэдр или четырехгранник, грани которого имеют различные цвета. Пользователь может поворачивать тетраэдр, нажимая кнопку мышки и перемещая ее. Пользователь может задавать цвет поверхности грани путем двойного щелчка с последующим выбором цвета в диалоговом окне QColorDialog, которое выдается на экран.
01 class Tetrahedron : public QGLWidget
02 {
03 Q_OBJECT
04 public:
05 Tetrahedron(QWidget *parent = 0);
06 protected:
07 void initializeGL();
08 void resizeGL(int width, int height);
09 void paintGL();
10 void mousePressEvent(QMouseEvent *event);
11 void mouseMoveEvent(QMouseEvent *event);
12 void mouseDoubleClickEvent(QMouseEvent *event);
13 private:
14 void draw();
15 int faceAtPosition(const QPoint &pos);
16 GLfloat rotationX;
17 GLfloat rotationY;
18 GLfloat rotationZ;
19 QColor faceColors[4];
20 QPoint lastPos;
21 }
Класс Tetrahedron наследует QGLWidget. Функции класса QGLWidget initializeGL(), resizeGL() и paintGL() переопределяются. Обработчики событий мышки класса QWidget переопределяются обычным образом.
01 Tetrahedron::Tetrahedron(QWidget *parent)
02 : QGLWidget(parent)
03 {
04 setFormat(QGLFormat(QGL::DoubleBuffer | QGL::DepthBuffer)
05 rotationX = -21.0;
06 rotationY = -57.0;
07 rotationZ = 0.0;
08 faceColors[0] = Qt::red;
09 faceColors[1] = Qt::green;
10 faceColors[2] = Qt::blue;
11 faceColors[3] = Qt::yellow;
12 }
В конструкторе мы вызываем функцию QGLWidget::setFormat() для установки контекста экрана OpenGL и инициализируем закрытые переменные этого класса.
01 void Tetrahedron::initializeGL()
02 {
03 qglClearColor(Qt::black);
04 glShadeModel(GL_FLAT);
05 glEnable(GL_DEPTH_TEST);
06 glEnable(GL_CULL_FACE);
07 }
Функция initializeGL() вызывается только один раз перед вызовом функции paintGL(). Именно в этом месте мы можем задавать контекст воспроизведения OpenGL, определять списки отображаемых элементов и выполнять остальную инициализацию.
Весь программный код является стандартным кодом OpenGL, за исключением вызовов функции qglClearColor() класса QGLWidget. Если бы мы захотели строго придерживаться стандартных возможностей OpenGL, мы вместо этого вызывали бы функцию glClearColor() при использовании режима RGBA и glClearIndex() при использовании режима индексированных цветов.
01 void Tetrahedron::resizeGL(int width, int height)
02 {
03 glViewport(0, 0, width, height);
04 glMatrixMode(GL_PROJECTION);
05 glLoadIdentity();
06 GLfloat x = GLfloat(width) / height;
07 glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0);
08 glMatrixMode(GL_MODELVIEW);
09 }
Функция resizeGL() вызывается один раз перед первым вызовом функции paintGL(), но после вызова функции initializeGL(). Oнa также всегда вызывается при изменении размера виджета. Именно в этом месте мы можем задавать область отображения OpenGL, ее проекцию и делать любые другие настройки, зависящие от размера виджета.
01 void Tetrahedron::paintGL()
02 {
03 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
04 draw();
05 }
Функция paintGL() вызывается всякий раз, когда необходимо перерисовать виджет. Это напоминает функцию QWidget::paintEvent(), но вместо функций класса QPainter здесь мы используем функции библиотеки OpenGL. Реальное рисование выполняется закрытой функцией draw().
01 void Tetrahedron::draw()
02 {
04 static const GLfloat P1[3]= { 0.0, -1.0, +2.0 };
05 static const GLfloat P2[3] = { +1.73205081, -1.0, -1.0 };
06 static const GLfloat P3[3] = { -1.73205081, -1.0, -1.0 };
07 static const GLfloat P4[3] = { 0.0, +2.0, 0.0 };
08 static const GLfloat * const coords[4][3] = {
09 { P1, P2, РЗ }, { P1, РЗ, P4 }, { P1, P4, P2 }, { P2, P4, РЗ }
10 };
11 glMatrixMode(GL_MODELVIEW);
12 glLoadIdentity();
13 glTranslatef(0.0, 0.0, -10.0);
14 glRotatef(rotationX, 1.0, 0.0, 0.0);
15 glRotatef(rotationY, 0.0, 1.0, 0.0);
16 glRotatef(rotationZ, 0.0, 0.0, 1.0);
17 for (int i = 0; i < 4; ++i) {
18 glLoadName(i);
19 glBegin(GL_TRIANGLES);
20 qglColor(faceColors[i]);
21 for (int j = 0; j < 3; ++j) {
22 glVertex3f(coords[i][j][0],
23 coords[i][j][1], coords[i][j][2]);
24 }
25 glEnd();
26 }
27 }
В функции draw() мы рисуем тетраэдр, учитывая повороты по осям x, у и z, а также цвета в массиве faceColors. Везде вызываются стандартные функции библиотеки OpenGL, за исключением вызова qglColor(). Вместо этого мы могли бы использовать одну из функций OpenGL — glColor3d() или glIndex() — в зависимости от используемого режима.
01 void Tetrahedron::mousePressEvent(QMouseEvent *event)
02 {
03 lastPos = event->pos();
04 }
05 void Tetrahedron::mouseMoveEvent(QMouseEvent *event)
06 {
07 GLfloat dx = GLfloat(event->x() - lastPos.x()) / width();
08 GLfloat dy = GLfloat(event->y() - lastPos.y()) / height();
09 if (event->buttons() & Qt::LeftButton) {
10 rotationX += 180 * dy;
11 rotationY += 180 * dx;
12 updateGL();
13 } else if (event->buttons() & Qt::RightButton) {
14 rotationX += 180 * dy;
15 rotationZ += 180 * dx;
16 updateGL();
17 }
18 lastPos = event->pos();
19 }
Функции класса QWidget mousePressEvent() и mouseMoveEvent() переопределяются, чтобы разрешить пользователю поворачивать изображение щелчком мышки и ее перемещением. Левая кнопка мышки позволяет пользователю поворачивать вокруг осей x и у, а правая кнопка мышки — вокруг осей x и z.
После модификации переменных rotationX и rotationY или rotationZ мы вызываем функцию updateGL() для перерисовки сцены.
01 void Tetrahedron::mouseDoubleClickEvent(QMouseEvent *event)
02 {
03 int face = faceAtPosition(event->pos());
04 if (face != -1) {
05 QColor color = QColorDialog::getColor(faceColors[face], this);
06 if (color.isValid()) {
07 faceColors[face] = color;
08 updateGL();
09 }
10 }
11 }
Функция mouseDoubleClickEvent() класса QWidget переопределяется, чтобы разрешить пользователю устанавливать цвет грани тетраэдра с помощью двойного щелчка. Мы вызываем закрытую функцию faceAtPosition() для определения той грани, на которой находится курсор (если он вообще находится на какой-нибудь грани). При двойном щелчке по грани тетраэдра мы вызываем функцию QColorDialog::getColor() для получения нового цвета для этой грани. Затем мы обновляем массив цветов faceColors новым цветом, и мы вызываем функцию updateGL() для перерисовки экрана.
01 int Tetrahedron::faceAtPosition(const QPoint &pos)
02 {
03 const int MaxSize = 512;
04 GLuint buffer[MaxSize];
05 GLint viewport[4];
06 glGetIntegerv(GL_VIEWPORT, viewport);
07 glSelectBuffer(MaxSize, buffer);
08 glRenderMode(GL_SELECT);
09 glInitNames();
10 glPushName(0);
11 glMatrixMode(GL_PROJECTION);
12 glPushMatrix();
13 glLoadIdentity();
14 gluPickMatrix(GLdouble(pos.x()),
15 GLdouble(viewport[3] - pos.y()),
16 5.0, 5.0, viewport);
17 GLfloat x = GLfloat(width()) / height();
18 glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0);
19 draw();
20 glMatrixMode(GL_PROJECTION);
21 glPopMatrix();
22 if (!glRenderMode(GL_RENDER))
23 return -1;
24 return buffer[3];
25 }
Функция faceAtPosition() возвращает номер грани для заданной точки виджета или —1, если данная точка не попадает на грань. Программный код этой функции, выполненной с помощью средств OpenGL, немного сложен. Фактически мы переводим работу в режим GL_SELECT, чтобы воспользоваться возможностями OpenGL по идентификации элементов изображения, и затем получаем номер грани (ее «имя») из записи нажатия OpenGL.
Ниже приводится файл main.cpp:
01 #include
02 #include
03 #include "tetrahedron.h"
04 using namespace std;
05 int main(int argc, char *argv[])
06 {
07 QApplication app(argc, argv);
08 if (!QGLFormat::hasOpenGL()) {
09 cerr << "This system has no OpenGL support" << endl;
10 return 1;
11 }
12 Tetrahedron tetrahedron;
13 tetrahedron.setWindowTitle(QObject::tr("Tetrahedron"));
14 tetrahedron.resize(300, 300);
15 tetrahedron.show();
16 return app.exec();
17 }
Если система пользователя не поддерживает OpenGL, мы выдаем на консоль сообщение об ошибке и сразу же возвращаем управление.
Для сборки приложения совместно с модулем QtOpenGL и системной библиотекой OpenGL файл .pro должен содержать следующий элемент:
QT += opengl
Этим заканчивается разработка приложения Тетраэдр. Более подробную информацию о модуле QtOpenGL вы найдете в справочной документации по классам QGLWidget, QGLFormat, QGLContext, QGLColormap и QGLPixelBuffer.
Глава 9. Технология «drag-and-drop»
Технология «drag-and-drop» («перетащить и отпустить») является современным и интуитивным способом передачи информации внутри одного приложения или между разными приложениями. Она часто является дополнением к операциям с буфером обмена по перемещению и копированию данных.
В данной главе мы увидим, как можно добавить в приложение Qt возможность поддержки технологии «drag-and-drop» и как обрабатывать пользовательские форматы. Затем мы используем программный код этой технологии для реализации операций с буфером обмена. Такое повторное использование данного программного кода возможно благодаря тому, что оба механизма основаны на применении одного класса QMimeData — базового класса, обеспечивающего представление данных в нескольких форматах.
Обеспечение поддержки технологии «drag-and-drop»
Технология «drag-and-drop» состоит из двух действий: перетаскивание «захваченных» объектов и их «освобождение». Виджеты в Qt могут использоваться в качестве переносимых объектов, в качестве места отпускания этих объектов или в обоих качествах.
В нашем первом примере мы показываем, как приложение Qt принимает объект, перенесенный из другого приложения. Приложение Qt представляет собой главное окно, использующее текстовый редактор QTextEdit в качестве центрального виджета. Когда пользователь переносит текстовый файл с рабочего стола компьютера или из проводника файловой системы и оставляет его в окне этого приложения, оно загружает файл в QTextEdit.
Ниже приводится пример определения класса MainWindow:
01 class MainWindow : public QMainWindow
02 {
03 Q_OBJECT
04 public:
05 MainWindow();
06 protected:
07 void dragEnterEvent(QDragEnterEvent *event);
08 void dropEvent(QDropEvent *event);
09 private:
10 bool readFile(const QString &fileName);
11 QTextEdit *textEdit;
12 }
Класс MainWindow переопределяет функции dragEnterEvent() и dropEvent() класса QWidget. Поскольку целью примера является демонстрация механизма «drag-and-drop», большая часть функциональности класса главного окна здесь не рассматривается.
01 MainWindow::MainWindow()
02 {
03 textEdit = new QTextEdit;
04 setCentralWidget(textEdit);
05 textEdit->setAcceptDrops(false);
06 setAcceptDrops(true);
07 setWindowTitle(tr("Text Editor"));
08 }
В конструкторе мы создаем QTextEdit и назначаем его в качестве центрального виджета. По умолчанию QTextEdit принимает переносимые текстовые объекты из других приложений, и если пользователь отпускает на этом виджете файл, имя этого файла будет вставлено в текст. Поскольку события отпускания объектов передаются от дочерних виджетов к родительским, отключая возможность отпускать переносимый объектв области отображения QTextEdit и включая ее для главного окна, мы получаем события отпускания объектов в MainWindow для всего главного окна.
01 voidMainWindow::dragEnterEvent(QDragEnterEvent *event)
02 {
03 if (event->mimeData()->hasFormat("text/uri-list"))
04 event->acceptProposedAction();
05 }
Функция dragEnterEvent() вызывается всякий раз, когда пользователь переносит объект на какой-нибудь виджет. Если мы вызываем функцию acceptProposedAction() при обработке этого события, мы указываем, что пользователь может отпустить переносимый объект в данном виджете. По умолчанию виджет не смог бы принять переносимый объект. Qt автоматически изменяет форму курсора для уведомления пользователя о возможности или невозможности приема объекта виджетом.
Здесь мы хотим позволить пользователю переносить файлы, но не более того. Для этого мы проверяем MIME—тип переносимого объекта. MIME—тип text/uri-list используется для хранения списка универсальных идентификаторов ресурсов (URI — universal resource identifier), в качестве которых могут выступать имена файлов, адреса URL (например, адресные пути HTTP и FTP) или идентификаторы других глобальных ресурсов. Стандартные типы MIME определяются Агентством по выделению имен и уникальных параметров протоколов сети Интернет (Internet Assigned Numbers Authority — IANA). Они состоят из типа и подтипа, разделенных слешем. Типы MIME используются буфером обмена и механизмом «drag-and-drop» для идентификации различных типов данных. Официальный список MIME—типов доступен по адресу .
01 void MainWindow::dropEvent(QDropEvent *event)
02 {
03 QList
04 if (urls.isEmpty())
05 return;
06 QString fileName = urls.first().toLocalFile();
07 if (fileName.isEmpty())
08 return;
09 if (readFile(fileName))
10 setWindowTitle(tr("%1 -%2").arg(fileName)
11 .arg(tr("Drag File")));
12 }
Функция dropEvent() вызывается, когда пользователь отпускает объект на виджете. Мы вызываем функцию QMimeData::urls() для получения списка адресов QUrl. Обычно пользователи переносят одновременно только один файл, но возможен также перенос сразу нескольких выделенных файлов. Если имеется несколько URL или полученный URL оказывается нелокальным, мы немедленно возвращаем управление.
QWidget содержит также функции dragMoveEvent() и dragLeaveEvent(), но для большинства приложений не потребуется их переопределять.
Второй пример показывает, как следует инициировать перетаскивание объекта и принимать его после отпускания. Мы создадим подкласс QListWidget, который будет поддерживать механизм «drag-and-drop» и входить в приложение Project Chooser (составитель проектов), показанное на рис. 9.1.
Рис. 9.1. Приложение Project Chooser.
Приложение Project Chooser предоставляет пользователю два виджета со списками имен людей. Каждый список представляет проект. Пользователь может с помощью механизма «drag-and-drop» перевести человека из одного проекта в другой.
Программный код по обеспечению механизма «drag-and-drop» находится в подклассе QListWidget. Ниже приводится определение класса:
01 class ProjectListWidget : public QListWidget
02 {
03 Q_OBJECT
04 public:
05 ProjectListWidget(QWidget *parent= 0);
06 protected:
07 void mousePressEvent(QMouseEvent *event);
08 void mouseMoveEvent(QMouseEvent *event);
09 void dragEnterEvent(QDragEnterEvent *event);
10 void dragMoveEvent(QDragMoveEvent *event);
11 void dropEvent(QDropEvent *event);
12 private:
13 void startDrag();
14 QPoint startPos;
15 };
ProjectListWidget переопределяет пять обработчиков событий, которые объявлены в QWidget.
01 ProjectListWidget::ProjectListWidget(QWidget *parent)
02 : QListWidget(parent)
03 {
04 setAcceptDrops(true);
05 }
В конструкторе мы обеспечиваем возможность приема переносимого объекта в виджете со списком.
01 void ProjectListWidget::mousePressEvent(QMouseEvent *event)
02 {
03 if (event->button() == Qt::LeftButton)
04 startPos = event->pos();
05 QListWidget::mousePressEvent(event);
06 }
Когда пользователь нажимает левую кнопку мышки, мы сохраняем позицию мышки в закрытой переменной startPos. Мы вызываем определенную в классе QListWidget функцию mousePressEvent() для обеспечения обработки в QListWidget обычным образом события нажатия кнопкй мышки.
01 void ProjectListWidget::mouseMoveEvent(QMouseEvent *event)
02 {
03 if (event->buttons() & Qt::LeftButton) {
04 int distance = (event->pos() - startPos).manhattanLength();
05 if (distance >= QApplication::startDragDistance())
06 startDrag();
07 }
08 QListWidget::mouseMoveEvent(event);
09 }
Действие, при котором пользователь перемещает курсор мышки и одновременно держит нажатой левую кнопку, мы рассматриваем как начало перетаскивания объекта. Мы вычисляем расстояние между текущей позицией мышки и позицией нажатия левой кнопки мышки. Если это расстояние превышает рекомендованное в QApplication расстояние для регистрации начала перетаскивания (обычно 4 пикселя), мы вызываем закрытую функцию startDrag() для запуска процесса перетаскивания объекта. Это предотвращает инициирование процесса перетаскивания из-за дрожания руки пользователя.
01 void ProjectListWidget::startDrag()
02 {
03 QListWidgetItem *item = currentItem();
04 if (item) {
05 QMimeData *mimeData = new QMimeData;
06 mimeData->setText(item->text());
07 QDrag *drag = new QDrag(this);
08 drag->setMimeData(mimeData);
09 drag->setPixmap(QPixmap(":/images/реrson.png"));
10 if (drag->start(Qt::MoveAction) == Qt::MoveAction)
11 delete item;
12 }
13 }
В функции startDrag() мы создаем объект типа QDrag с указанием this в качестве родительского элемента. Объект QDrag хранит данные в объекте QMimeData. В нашем примере мы обеспечиваем данные типа text/plain, используя функцию QMimeData::setText(). Класс QMimeData содержит несколько функций, предназначенных для обработки наиболее распространенных типрв объектов переноса (изображений, адресов URL, цветов и т.д.); он может обрабатывать произвольные типы MIME, представленные массивами QByteArray. Вызов QDrag::setPixmap() задает пиктограмму, которая следует за курсором в процессе перетаскивания объекта.
Вызов функции QDrag::start() запускает операцию перетаскивания объекта и ждет, пока пользователь не отпустит перетаскиваемый объект или не отменит перетаскивание. В аргументе этой функции задается перечень поддерживаемых «операций перетаскивания» (Qt::CopyAction, Qt::MoveAction и Qt::LinkAction); она возвращает ту операцию перетаскивания, которая была выполнена (или Qt::IgnoreAction, если не было выполнено никакой операции). Тип выполняемой операции зависит от того, какие операции допускаются исходным виджетом, какие операции поддерживает целевой виджет и какие клавиши—модификаторы нажаты в момент отпуска переносимого объекта. После вызова этой функции Qt становится владельцем переносимого объекта и удалит его, когда он станет ненужным.
01 void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event)
02 {
03 ProjectListWidget *source =
04 qobject_cast
05 if (source && source != this) {
06 event->setDropAction(Qt::MoveAction);
07 event->accept();
08 }
09 }
Виджет ProjectListWidget не только инициирует перенос объектов, он также является местом приема таких объектов, если они приходят от другого виджета ProjectListWidget того же самого приложения. QDragEnterEvent::source() возвращает указатель на виджет, который инициирует перенос, если этот виджет принадлежит тому же самому приложению; в противном случае он возвращает нулевой указатель. Мы используем qobject_cast
01 void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
02 {
03 ProjectListWidget *source =
04 qobject_cast
05 if (source && source != this) {
07 event->setDropAction(Qt::MoveAction);
08 event->accept();
09 }
10 }
Программный код функции dragMoveEvent() идентичен тому, что мы делали в функции dragEnterEvent(). Он необходим, потому что нам приходится переопределять реализацию этой функции в классе QListWidget (в действительности в классе QAbstractItemView).
01 void ProjectListWidget::dropEvent(QDropEvent *event)
02 {
03 ProjectListWidget *source =
04 qobject_cast
05 if (source && source != this) {
06 addItem(event->mimeData()->text());
07 event->setDropAction(Qt::MoveAction);
08 event->accept();
09 }
10 }
В DropEvent() мы используем функцию QMimeData::text() для получения перенесенного текста и создаем элемент с этим текстом. Нам также необходимо воспринять данное событие как «операцию перетаскивания», чтобы указать исходному виджету на то, что он может теперь удалить первоначальную версию перенесенного элемента.
«Drag-and-drop» — мощный механизм передачи данных между приложениями. Однако в некоторых случаях его можно реализовать, не используя предусмотренные в Qt средства механизма «drag-and-drop». Если нам требуется переносить данные внутри одного виджета некоторого приложения, во многих случаях мы можем просто переопределить функции mousePressEvent() и mouseReleaseEvent().
Поддержка пользовательских типов переносимых объектов
До сих пор в представленных примерах мы полагались на поддержку QMimeData распространенных типов MIME. Так, мы вызывали QMimeData::setText() для создания объекта переноса текста и использовали QMimeData:urls() для получения содержимого объекта переноса типа text/uri-list. Если мы хотим перетаскивать обычный текст, текст в формате HTML, изображения, адреса URL или цвета, мы можем спокойно использовать класс QMimeData. Но если мы хотим перетаскивать пользовательские данные, необходимо сделать выбор между следующими альтернативами:
1. Мы можем обеспечить произвольные данные в виде массива QByteArray, используя функцию QMimeData::setData(), и извлекать их позже, используя функцию QMimeData::data().
2. Мы можем создать подкласс QMimeData и переопределить функции formats() и retrieveData() для обработки наших пользовательских типов данных.
3. Для выполнения операций механизма «drag-and-drop» в рамках одного приложения мы можем создать подкласс QMimeData и хранить данные в любых структурах данных.
Первый подход не требует никаких подклассов, но имеет некоторые недостатки: нам необходимо преобразбвать наши структуры данных в тип QByteArray, даже если переносимый объект не принимается, а если требуется обеспечить несколько MIME—типов, чтобы можно было хорошо взаимодействовать с самыми разными приложениями, нам придется сохранять несколько копий данных (по одной на каждый тип MIME). Если данные имеют большой размер, это может излишне замедлять работу приложения. При использовании второго и третьего подходов можно избежать или свести к минимуму эти проблемы. В этом случае мы получаем полное управление и можем использовать эти два подхода совместно.
Для демонстрации этих подходов мы покажем, как можно добавить возможности технологии «drag-and-drop» в виджет QTableWidget. Будет поддерживаться перенос следующих типов MIME: text/plain, text/html и text/csv. При применении первого подхода инициирование переноса выглядит следующим образом:
01 void MyTableWidget::mouseMoveEvent(QMouseEvent *event)
02 {
03 if (event->buttons() & Qt::LeftButton) {
04 int distance = (event->pos() - startPos).manhattanLength();
05 if(distance >= QApplication::startDragDistance())
06 startDrag();
07 }
08 QTableWidget::mouseMoveEvent(event);
09 }
10 void MyTableWidget::startDrag()
11 {
12 QString plainText= selectionAsPlainText();
13 if (plainText.isEmpty())
14 return;
15 QMimeData *mimeData = new QMimeData;
16 mimeData->setText(plainText);
17 mimeData->setHtml(toHtml(plainText));
18 mimeData->setData("text/csv", toCsv(plainText).toUtf8());
19 QDrag *drag = new QDrag(this);
20 drag->setMimeData(mimeData);
21 if (drag->start(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction)
22 deleteSelection();
23 }
Закрытая функция startDrag() вызывается из mouseMoveEvent() для инициирования переноса выделенной прямоугольной области. Мы устанавливаем типы MIME text/plain и text/html, используя функции setText() и setHtml(), а тип text/csv мы устанавливаем функцией setData(), которая принимает произвольный тип MIME и массив QByteArray. Программный код для функции selectionAsString() более или менее совпадает с кодом функции Spreadsheet::copy(), рассмотренной в
01 QString MyTableWidget::toCsv(const QString &plainText)
02 {
03 QString result = plainText;
04 result.replace("\\", "\\\\");
05 result.replace("\"", "\\\"");
06 result.replace("\t", "\", \"")
07 result.replace("\n", "\"\n\"");
08 result.prepend("\"");
09 result.append("\"");
10 return result;
11 }
12 QString MyTableWidget::toHtml(const QString &plainText)
13 {
14 QString result = Qt::escape(plainText);
15 result.replace("\t", "
16 result.replace("\n", "\n
17 result.prepend("
");
18 result.append("\n |
19 return result;
20 }
Функции toCsv() и toHtml() преобразуют строку со знаками табуляции и конца строки в формат CSV (comma—separated values — значения, разделенные запятыми) и HTML соответственно. Например, данные
Red Green Blue
Cyan Yellow Magenta
преобразуются в
"Red", "Green", "Blue"
"Cyan", "Yellow", "Magenta"
или в
Red | Green | Blue
|
Cyan | Yellow | Magenta
|
Преобразование выполняется самым простым из возможных способов с применением функции QString::replace(). Для удаления специальных символов формата HTML мы используем функцию Qt::escape().
01 void MyTableWidget::dropEvent(QDropEvent *event)
02 {
03 if (event->mimeData()->hasFormat("text/csv")) {
04 QByteArray csvData = event->mimeData()->data("text/csv");
05 QString csvText = QString::fromUtf8(csvData);
06 …
07 event->acceptProposedAction();
08 } else if (event->mimeData()->hasFormat("text/plain")) {
09 QString plainText = event->mimeData()->text();
10 …
11 event->acceptProposedAction();
12 }
13 }
Хотя мы предоставляем данные в трех разных форматах, мы принимаем в dropEvent() только два из них. Если подьзователь переносит ячейки из таблицы QTableWidget в редактор HTML, нам нужно, чтобы ячейки были преобразованы в таблицу HTML. Но если пользователь переносит произвольный текст HTML в таблицу QTableWidget, мы не станем его принимать.
Для того чтобы этот пример заработал, нам потребуется также вызвать setAcceptDrops(true) и setSelectionMode(ContiguousSelection) в конструкторе MyTableWidget.
Теперь мы переделаем этот пример, но на этот раз мы создадим подкласс QMimeData, чтобы отложить или избежать (потенциально затратных) преобразований между элементами QTableWidgetltem и массивом QByteArray. Ниже приводится определение нашего подкласса:
01 class TableMimeData : public QMimeData
02 {
03 Q_OBJECT
04 public:
05 TableMimeData(const QTableWidget *tableWidget,
06 const QTableWidgetSelectionRange &range);
07 const QTableWidget *tableWidget() const
08 { return myTableWidget; }
09 QTableWidgetSelectionRange range() const { return myRange; }
10 QStringList formats() const;
11 protected:
12 QVariant retrieveData(const QString &format,
13 QVariant::Type preferredType) const;
14 private:
15 static QString toHtml(const QString &plainText);
16 static QString toCsv(const QString &plainText);
17 QString text(int row, int column) const;
18 QString rangeAsPlainText() const;
19 const QTableWidget *myTableWidget;
20 QTableWidgetSelectionRange myRange;
21 QStringList myFormats;
22 };
Вместо реальных данных мы храним объект QTableWidgetSelectionRange, который определяет область переносимых ячеек и сохраняет указатель на QTableWidget. Функции formats() и retrieveData() класса QMimeData переопределяются.
01 TableMimeData::TableMimeData(const QTableWidget *tableWidget,
02 const QTableWidgetSelectionRange &range)
03 {
04 myTableWidget = tableWidget;
05 myRange = range;
06 myFormats << "text/csv" << "text/html" << "text/plain";
07 }
В конструкторе мы инициализируем закрытые переменные.
01 QStringList TableMimeData::formats() const
02 {
03 return myFormats;
04 }
Функция formats() возвращает список MIME—типов, находящихся в объекте MIME—данных. Последовательность форматов обычно несущественна, однако на практике желательно первыми указывать «лучшие» форматы. Приложения, поддерживающие несколько форматов, иногда будут использовать первый подходящий.
01 QVariant TableMimeData::retrieveData(const QString &format,
02 QVariant::Type preferredType) const
03 {
04 if (format == "text/plain") {
05 return rangeAsPlainText();
06 } else if (format =="text/csv") {
07 return toCsv(rangeAsPlainText()); }
08 else if (format == "text/html") {
09 return toHtml(rangeAsPlainText());
10 } else {
11 return QMimeData::retrieveData(format, preferredType);
12 }
13 }
Функция retrieveData() возвращает данные для заданного MIME—типа в виде объекта QVariant. Параметр format обычно содержит одну из строк, возвращенных функцией formats(), однако нам не следует на это рассчитывать, поскольку не все приложения проверяют MIME—тип на соответствие форматам функции formats(). Предусмотренные в классе QMimeData функции получения данных text(), html(), urls(), imageData(), colorData() и data() реализуются с помощью функции retrieveData().
Параметр preferredType определяет тип, который следует поместить в объект QVariant. Здесь мы его игнорируем и рассчитываем на то, что QMimeData преобразует при необходимости возвращенное значение в требуемый тип.
01 void MyTableWidget::dropEvent(QDropEvent *event)
02 {
03 const TableMimeData *tableData =
04 qobject_cast
05 if (tableData) {
06 const QTableWidget *otherTable = tableData->tableWidget();
07 QTableWidgetSelectionRange otherRange = tableData->range();
08 …
09 event->acceptProposedAction();
10 } else if (event->mimeData()->hasFormat("text/csv")) {
11 QByteArray csvData = event->mimeData()->data("text/csv");
12 QString csvText = QString::fromUtf8(csVData);
13 …
14 event->acceptProposedAction();
15 } else if (event->mimeData()->hasFormat("text/plain")) {
16 QString plainText = event->mimeData()->text();
17 …
18 event->acceptProposedAction();
19 }
20 QTableWidget::mouseMoveEvent(event);
21 }
Функция dropEvent() аналогична функции с тем же названием, которую мы рассматривали ранее в данном разделе, но на этот раз мы ее оптимизируем, делая вначале проверку возможности приведения типа QMimeData в тип TableMimeData. Если qobject_cast
В этом примере мы кодировали CSV—текст, используя кодировку UTF-8. Если бы мы хотели быть уверенными в применении правильной кодировки, мы могли бы использовать параметр charset в MIME—типе text/plain для явного задания типа кодировки. Ниже приводится несколько примеров:
text/plain; charset=US-ASCII
text/plain; charset=ISO-8859-1
text/plain; charset=Shift_JIS
text/plain; charset=UTF-8
Работа с буфером обмена
Большинство приложений тем или иным образом используют встроенные в Qt средства работы с буфером обмена. Например, класс QTextEdit обеспечивает поддержку слотов cut(), copy() и paste(), а также клавиш быстрого вызова команд, и поэтому дополнительное программирование почти (или совсем) не требуется.
При создании нами собственных классов мы можем осуществлять доступ к буферу обмена с помощью функции QApplication::clipboard(), которая возвращает указатель на объект приложения QClipboard. Обработка системного буфера обмена выполняется просто: вызывайте функции setText(), setImage() или setPixmap() для помещения данных в буфер обмена, и функции text(), image() или pixmap() для считывания данных из буфера обмена. Мы уже приводили примеры работы с буфером обмена в приложении Электронная таблица из .
Для некоторых приложений может оказаться недостаточно встроенных функциональных возможностей. Например, нам могут потребоваться данные, которые не являются просто текстом или изображением, или мы захотим обеспечить работу с многими различными форматами данных с целью достижения максимальной совместимости с другими приложениями. Эта проблема очень напоминает ту, с которой мы столкнулись при обеспечении механизма «drag-and-drop», и решение также будет аналогичным: мы можем создать подкласс QMimeData и переопределить несколько виртуальных функций.
Если наше приложение поддерживает механизм «drag-and-drop» через пользовательский подкласс QMimeData, мы можем просто повторно использовать пользовательский подкласс QMimeData и помещать его в буфер обмена, используя функцию setMimeData(). Для получения данных мы можем вызвать функцию mimeData() для буфера обмена.
В системе X11, как правило, можно вставлять выделенные объекты нажатием средней кнопки мышки, которая имеет три кнопки. Это делается путем применения отдельной «выделенной области» буфера обмена. Если вам нужно,чтобы ваш виджет поддерживал такую операцию буфера обмена вместе со стандартными операциями, вы должны передавать QClipboard::Selection в качестве дополнительного аргумента в различных вызовах операций буфера обмена. Например, ниже приводится возможная реализация функции mouseReleaseEvent() текстового редактора, поддерживающего вставку по нажатии средней кнопки мышки.
01 void MyTextEditor::mouseReleaseEvent(QMouseEvent *event)
02 {
03 QClipboard *clipboard = QApplication::clipboard();
04 if (event->button() == Qt::MidButton
05 && clipboard->supportsSelection()) {
06 QString text = clipboard->text(QClipboard::Selection);
07 pasteText(text);
08 }
09 }
В системе X11 функция supportsSelection() возвращает true. На других платформах она возврашает false.
Если мы хотим получать уведомления о каждом изменении содержимого буфера обмена, мы можем соединить сигнал QClipboard::dataChanged() с пользовательским слотом.
Глава 10. Классы отображения элементов
Многие приложения позволяют пользователям выполнять поиск, просмотр и редактирование отдельных элементов, принадлежащих набору данных. Эти данные могут храниться в файлах, в базе данных или на сетевом сервере. Обычно работа с подобными наборами данных осуществляется в Qt с использованием классов отображения элементов.
В ранних версиях Qt виджеты отображения элементов заполнялись содержимым всего набора данных; пользователи обычно выполняли необходимые операции по поиску и редактированию данных, находящихся в виджете, в какой-то момент сделанные изменения записывались обратно в источник данных. Хотя этот метод вполне понятен и прост в применении, он не совсем подходит для очень больших наборов данных и для ситуаций, когда требуется отображать одни и те же данные в двух или более разных виджетах.
В языке Smalltalk получил популярность гибкий подход к визуальному отображению больших наборов данных: модель—представление—контроллер (model—view—controller — MVC). В подходе MVC модель представляет набор данных и отвечает за обеспечение отображаемых данных и за запись всех изменений в источник данных. Каждый тип набора данных имеет свою собственную модель, однако предоставляемый моделью программный интерфейс отображения элементов одинаков для наборов данных любого типа. Представление отвечает за то, как данные отображаются для пользователя. При использовании любого большого набора данных только ограниченная область данных будет видима в любой момент времени, поэтому только эти данные будут запрашиваться представлением. Контроллер — это посредник между пользователем и представлением; он преобразует действия пользователя в запросы по просмотру или редактированию данных, которые представление по мере необходимости передает в модель.
Рис. 10.1. Архитектура модель/представление в Qt.
В Qt используется вдохновленная подходом MVC архитектура модель/представление. Здесь модель имеет такие же функции, как и в классическом методе MVC. Но вместо контроллера в Qt используется немного другое понятие: делегат (delegate). Делегат обеспечивает более тонкое управление воспроизведением и редактированием элементов. Для каждого типа представления в Qt предусмотрен делегат по умолчанию. Для большинства приложений вполне достаточно пользоваться таким делегатом, поэтому обычно нам не приходится заботиться о нем.
Применяя архитектуру Qt модель/представление, мы можем использовать модели, которые представляют только те данные, которые действительно необходимы для отображения в представлении. Это значительно повышает скорость обработки очень больших наборов данных и уменьшает потребности в памяти по сравнению с подходом, требующим считывания всех данных. Связывая одну модель с двумя или более представлениями, мы можем предоставить пользователю возможность за счет незначительных дополнительных издержек просматривать данные и взаимодействовать с ними различными способами. Qt автоматически синхронизирует множественные представления данных — изменения в одном из представлений отражаются во всех других. Дополнительное преимущество архитектуры модель/представление проявляется в том, что если мы решаем изменить способ хранения исходных данных, нам просто потребуется изменить модель; представления по-прежнему будут работать правильно.
Во многих случаях пользователю необходимо работать только с относительно небольшим количеством элементов. В такой ситуации, как правило, мы можем использовать удобные классы Qt по отображению элементов (QListWidget, QTableWidget и QTreeWidget), непосредственно заполняя все элементы значениями. Эти классы работают подобно классам отображения элементов в предыдущих версиях Qt. Они хранят свои данные в «элементах» (например, QTableWidget содержит элементы QTableWidgetltem). При реализации этих удобных классов используются пользовательские модели, обеспечивающие появление требуемых элементов в представлениях.
Рис. 10.2. Одна модель может обслуживать несколько представлений.
При использовании больших наборов данных часто оказывается недопустимым дублирование данных. В этих случаях мы можем применять классы Qt по отображению элементов (QListView, QTableView и QTreeView) в сочетании с моделью данных, которой может быть как пользовательская модель, так и одна из заранее определенных в Qt моделей. Например, если набор данных хранится в базе данных, мы можем использовать QTableView в сочетании с QSqlTableModel.
Применение удобных классов отображения элементов
Удобные Qt—подклассы отображения элементов обычно использовать проще, чем определять пользовательскую модель, и они особенно удобны, когда разделение модели и представления нам не дает преимущества. Мы использовали этот подход в , когда создавали подклассы QTableWidget и QTableWidgetItem для реализации функциональности электронной таблицы.
В данном разделе мы покажем, как можно применять удобные классы отображения элементов для вывода на экран элементов. В первом примере приводится используемый только для чтения виджет QListWidget, во втором примере — редактируемый QTableWidget и в третьем примере — используемый только для чтения QTreeWidget.
Мы начинаем с простого диалогового окна, которое позволяет пользователю выбрать из списка символ, используемый в блок-схемах программ. Каждый элемент состоит из пиктограммы, текста и уникального идентификатора.
Сначала покажем фрагмент заголовочного файла диалогового окна:
01 class FlowChartSymbolPicker : public QDialog {
02 Q_OBJECT
03 public:
04 FlowChartSymbolPicker(const QMap
05 QWidget *parent = 0);
06 int selectedId() const { return id; }
07 void done(int result);
08 …
09 }
Рис. 10.3. Приложение Выбор символа блок—схемы (Flowchart Symbol Picker).
При создании диалогового окна мы должны передать его конструктору ассоциативный массив QMap
01 FlowChartSymbolPicker::FlowChartSymbolPicker(
02 const QMap
03 : QDialog(parent)
04 {
05 id = -1;
06 listWidget = new QListWidget;
07 listWidget->setIconSize(QSize(60, 60));
08 QMapIterator
09 while (i.hasNext()) {
10 i.next();
11 QListWidgetItem *item = new QListWidgetItem(i.value(),
12 listWidget);
13 item->setIcon(iconForSymbol(i.value()));
14 item->setData(Qt::UserRole, i.key());
15 …
16 }
17 }
Мы инициализируем id (идентификатор последнего выбранного элемента) значением —1. Затем мы конструируем QListWidget — удобный виджет отображения элементов. Мы проходим в цикле по всем элементам ассоциативного массива символов блок—схемы symbolMap и для каждого создаем объект QListWidgetItem. Конструктор QListWidgetItem принимает выводимую на экран строку QString и родительский виджет QListWidget.
Затем задаем пиктограмму элемента и вызываем setData() для сохранения в QListWidgetItem идентификатора элемента. Закрытая функция iconForSymbol() возвращает QIcon для заданного имени символа.
QListWidgetItem может выступать в разных ролях, каждой из которых соответствует определенный объект QVariant. Самыми распространенными ролями являются Qt::DisplayRole, Qt::EditRole и Qt::IconRole, и для них предусмотрены удобные функции по установке и получению их значений (setText(), setIcon()), но имеется также несколько других ролей. Кроме того, мы можем определить пользовательские роли, задавая числовое значение, равное или большее, чем Qt::UserRole. В нашем примере мы используем Qt::UserRole при сохранении идентификатора каждого элемента.
В непоказанной части конструктора создаются кнопки, выполняется компоновка виджетов и задается заголовок окна.
01 void FlowChartSymbolPicker::done(int result)
02 {
03 id = -1;
04 if (result == QDialog::Accepted) {
05 QListWidgetItem *item = listWidget->currentItem();
06 if (item)
07 id = item->data(Qt::UserRole).toInt();
08 }
09 QDialog::done(result);
10 }
Функция done() класса QDialog переопределяется. Она вызывается при нажатии пользователем кнопок ОК или Cancel. Если пользователь нажимает кнопку OK, мы получаем доступ к соответствующему элементу и извлекаем идентификатор, используя функцию data(). Если бы нас интересовал текст элемента, мы могли бы его получить с помощью вызова item->data(Qt::DisplayRole).toString() или более простого вызова item->text().
По умолчанию QListWidget используется только для чтения. Если бы мы хотели разрешить пользователю редактировать элементы, мы могли бы соответствующим образом установить переключатели редактирования представления, используя QAbstractItemView::setEditTriggers(), например QAbstractItemView::AnyKeyPressed означает, что пользователь может инициировать редактирование элемента, просто начав вводить символы с клавиатуры. Можно было бы поступить по-другому и предусмотреть кнопку редактирования Edit (и, возможно, кнопки добавления и удаления Add и Delete) и связать их со слотами, чтобы можно было программно управлять операциями редактирования.
Теперь, когда мы увидели, как можно использовать удобный класс отображения элементов для просмотра и выбора данных, мы рассмотрим пример, в котором можно редактировать данные. Мы снова используем диалоговое окно, представляющее на этот раз набор точек с координатами (x, у), которые может редактировать пользователь.
Рис. 10.4. Приложение Редактор координат.
Как и в предыдущем примере, мы основное внимание уделим программному коду, относящемуся к представлению элементов, и начнем с конструктора.
01 CoordinateSetter::CoordinateSetter(QList
02 QWidget *parent)
03 : QDialog(parent)
04 {
05 coordinates = coords;
06 tableWidget = new QTableWidget(0, 2);
07 tableWidget->setHorizontalHeaderLabels(
08 QStringList() << tr("X") << tr("Y"));
09 for (int row = 0; row < coordinates->count(); ++row) {
10 QPointF point = coordinates->at(row);
11 addRow();
12 tableWidget->item(row, 0)->setText(
13 QString::number(point.x()));
14 tableWidget->item(row, 1)->setText(
15 QString::number(point.y()));
16 }
17 …
18 }
Конструктор QTableWidget принимает начальное число строк и столбцов таблицы, выводимой на экран. Каждый элемент в QTableWidget представлен объектом QTableWidgetltem, включая элементы заголовков строк и столбцов. Функция setHorizontalHeaderLabels() задает заголовки всем столбцам, используя соответствующий текст из переданного списка строк. По умолчанию QTableWidget обеспечивает заголовки строк числовыми метками, начиная с 1; именно это нам и нужно, поэтому нам не приходится задавать вручную заголовки строк.
После создания и центровки заголовков столбцов мы в цикле просматриваем все переданные нам данные с координатами. Для каждой пары (x, у) мы создаем два элемента QTableWidgetItem, соответствующие координатам x и у. Эти элементы добавляются в таблицу, используя функцию QTableWidget::setItem(), в аргументах которой кроме самого элемента задаются его строка и столбец.
По умолчанию виджет QTableWidget разрешает редактирование. Пользователь может редактировать любую ячейку таблицы, установив на нее курсор и нажав F2 или просто вводя текст с клавиатуры. Все сделанные пользователем изменения автоматически сохранятся в элементах QTableWidgetltem. Запретить редактирование мы можем с помощью вызова setEditTriggers(QAbstractItemView::NoEditTriggers).
01 void CoordinateSetter::addRow()
02 {
03 int row = tableWidget->rowCount();
04 tableWidget->insertRow(row);
05 QTableWidgetltem *item0 = new QTableWidgetltem;
06 item0->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
07 tableWidget->setItem(row, 0, item0);
08 QTableWidgetltem *item1 = new QTableWidgetltem;
09 item1->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
10 tableWidget->setItem(row, 1, item1);
11 tableWidget->setCurrentItem(item0);
12 }
Слот addRow() вызывается, когда пользователь щелкает по кнопке Add Row (добавить строку). Мы добавляем в конец таблицы новую строку, используя insertRow(). Если пользователь попытается отредактировать какую-нибудь ячейку новой строки, QTableWidget автоматически создаст новый объект QTableWidgetItem.
01 void CoordinateSetter::done(int result)
02 {
03 if (result == QDialog::Accepted) {
04 coordinates->clear();
05 for (int row = 0; row < tableWidget->rowCount(); ++row) {
06 double x = tableWidget->item(row, 0)->text().toDouble();
07 double у = tableWidget->item(row, 1)->text().toDouble();
08 coordinates->append(QPointF(x, y));
09 }
10 }
11 QDialog::done(result);
12 }
Наконец, когда пользователь нажимает кнопку OK, мы очищаем координаты, переданные ранее в диалоговое окно, и создаем новый набор на основе координат в элементах виджета QTableWidget.
В качестве нашего третьего и последнего примера применения в Qt удобных виджетов отображения элементов мы рассмотрим некоторые фрагменты приложения, которое показывает параметры настройки Qt—приложения, используя QTreeWidget. Данный виджет по умолчанию используется только для чтения.
Рис. 10.5. Приложение Просмотр параметров настройки (Settings Viewer).
Ниже приводится фрагмент конструктора:
01 SettingsViewer::SettingsViewer(QWidget *parent)
02 : QDialog(parent)
03 {
04 organization = "Trolltech";
05 application = "Designer";
06 treeWidget = new QTreeWidget;
08 treeWidget->setColumnCount(2);
09 treeWidget->setHeaderLabels(
10 QStringList() << tr("Key") << tr("Value"));
11 treeWidget->header()->setResizeMode(0, QHeaderView::Stretch);
12 treeWidget->header()->setResizeMode(1, QHeaderView::Stretch);
13 …
14 setWindowTitle(tr("Settings Viewer"));
15 readSettings();
16 }
Для получения доступа к параметрам настройки приложения необходимо создать объект QSettings с указанием в параметрах названия организации и имени приложения. Мы устанавливаем значения по умолчанию (приложение «Designer» компании «Trolltech») и затем создаем новый объект QTreeWidget. В конце мы вызываем фyнкцию readSettings().
01 void SettingsViewer::readSettings()
02 {
03 QSettings settings(organization, application);
04 treeWidget->clear();
05 addChildSettings(settings, 0, "");
06 treeWidget->sortByColurnn(0);
07 treeWidget->setFocus();
08 setWindowTitle(tr("Settings Viewer - %1 by %2")
09 .arg(application).arg(organization));
10 }
Параметры настройки приложения хранятся в виде набора ключей и значений, имеющих иерархическую структуру. Закрытая функция addChildSettings() принимает объект параметров настройки, родительский элемент QTreeWidgetItem и текущую «группу». Группа в QSettings аналогична каталогу файловой системы. Функция addChildSettings() может вызывать себя рекурсивно для прохода по произвольной структуре типа дерева. При первом ее вызове из функции readSettings() передается 0, задавая корень в качестве родительского объекта.
01 void SettingsViewer::addChildSettings(QSettings &settings,
02 QTreeWidgetItem *parent, const QString &group)
03 {
04 QTreeWidgetItem *item;
05 settings.beginGroup(group);
06 foreach (QString key, settings.childKeys()) {
07 if (parent) {
08 item = new QTreeWidgetItem(parent);
09 } else {
10 item = new QTreeWidgetItem(treeWidget);
11 }
12 item->setText(0, key);
13 item->setText(1, settings.value(key).toString());
14 }
15 foreach (QString group, settings.childGroups()) {
16 if (parent) {
17 item = new QTreeWidgetItem(parent);
18 } else {
19 item = new QTreeWidgetItem(treeWidget);
20 }
21 item->setText(0, group);
21 addChildSettings(settings, item, group);
22 }
23 settings.endGroup();
24 }
Функция addChildSettings() используется для создания всех элементов QTreeWidgetItem. Она проходит по всем ключам текущего уровня в иерархии параметров настройки и для каждого ключа создает один объект QTableWidgetItem. Если в качестве родительского элемента задан 0, мы создаем дочерний элемент собственно виджета QTreeWidget (т.е. создается элемент верхнего уровня); в противном случае мы создаем дочерний элемент для объекта parent. В первый столбец записывается имя ключа, а во второй столбец — соответствующее ему значение.
Затем эта функция выполняется для каждой группы текущего уровня. Для каждой группы создается новый объект QTreeWidgetItem, причем в первый столбец записывается имя группы. Затем эта функция рекурсивно вызывает саму себя с указанием группового элемента в качестве родительского для заполнения виджета QTreeWidget дочерними элементами группы.
Показанные в данном разделе виджеты отображения элементов позволяют нам использовать стиль программирования, который очень похож на тот, который применялся в ранних версиях Qt: чтение всего набора данных в виджет отображения элементов с использованием объектов, представляющих отдельные элементы данных, и (если элементы допускают редактирование) их запись обратно в источник данных. В последующих разделах мы выйдем за рамки этого простого подхода и воспользуемся всеми преимуществами, которые дает архитектура Qt модель/представление.
Применение заранее определенных моделей
В Qt заранее определено несколько моделей, предназначенных для использования с классами представлений:
• QStringListModel — хранит список строк;
• QStandardltemModel — хранит данные произвольной иерархической структуры;
• QDirModel — формирует структуру локальной файловой системы;
• QSqlQueryModel — формирует набор результата SQL—запроса;
• QSqlTableModel — формирует SQL—таблицу;
• QSqlRelationalTableModel — формирует SQL—таблицу с внешними ключами (foreign keys);
• QSortFilterProxyModel — сортирует и/или пропускает через фильтр другую модель.
В данном разделе мы рассмотрим способы применения моделей QStringListModel, QDirModel и QSortFilterProxyModel. SQL—модели рассматриваются в .
Давайте начнем с простого диалогового окна, которое может применяться для добавления, удаления и редактирования списка строк QStringList, где каждая строка представляет лидера команды.
Ниже приводится соответствующий фрагмент конструктора:
01 TeamLeadersDialog::TeamLeadersDialog(const QStringList &leaders,
02 QWidget *parent)
03 : QDialog(parent)
04 {
05 model = new QStringListModel(this);
06 model->setStringList(leaders);
07 listView = new QListView;
08 listView->setModel(model);
09 listView->setEditTriggers(QAbstractItemView::AnyKeyPressed
10 | QAbstractItemView::DoubleClicked);
11 …
12 }
Рис. 10.6. Приложение Лидеры команд (Team Leaders).
Мы начнем с создания и заполнения модели QStringListModel. Затем создадим представление QListView и свяжем его с только что созданной моделью. Установим также некоторые переключатели редактирования, чтобы позволить пользователю редактировать строку, просто вводя символ или делая двойной щелчок. По умолчанию все переключатели редактирования сброшены для QListView, фактически делая это представление пригодным только для чтения.
01 void TeamLeadersDialog::insert()
02 {
03 int row = listView->currentIndex().row();
04 model->insertRows(row, 1);
05 QModelIndex index = model->index(row);
07 listView->setCurrentIndex(index);
08 listView->edit(index);
09 }
Когда пользователь нажимает на кнопку Insert (вставка), вызывается слот insert(). Этот слот начинает с получения номера строки текущего элемента в списке. Каждый элемент данных модели имеет соответствующий «индекс модели», представленный объектом QModelIndex. Мы подробно рассмотрим индексы модели в следующем разделе, а в данный момент нам достаточно знать, что индекс имеет три основных компонента: строку, столбец и указатель на модель, к которой он принадлежит. В модели одномерного списка столбец всегда равен 0.
Имея номер строки, мы вставляем одну новую строку в данную позицию. Вставка выполняется в модели, и модель автоматически обновляет списковое представление. Затем мы устанавливаем текущий индекс спискового представления на пустую строку, которую мы только что вставили. Наконец, мы устанавливаем в списковом представлении режим редактирования для новой строки, как будто пользователь нажал какую-нибудь клавишу клавиатуры или дважды щелкнул, чтобы начать редактирование.
01 void TeamLeadersDialog::del()
02 {
03 model->removeRows(listView->currentIndex().row(), 1);
04 }
В конструкторе сигнал clicked() кнопки Delete (удалить) связывается со слотом del(). Поскольку мы только что удалили текущую строку, мы можем вызвать removeRows() для текущей позиции индекса и для значения 1 счетчика строк. Как и при выполнении вставки, мы полагаемся на то, что модель должным образом обновит представление.
01 QStringList TeamLeadersDialog::leaders() const
02 {
03 return model->stringList();
04 }
Наконец, функция leaders() позволяет считывать отредактированные строки, когда диалоговое окно закрыто.
Создать TeamLeadersDialog можно было бы на основе универсального диалогового окна редактирования списка строк, просто параметризируя заголовок этого окна. Другое часто используемое пользователями универсальное диалоговое окно отображает список файлов или каталогов. В следующем примере применяется класс QDirModel, который моделирует файловую систему компьютера и позволяет показывать (или скрывать) различные атрибуты файлов. Эта модель может применять фильтр для ограничения типов элементов файловой системы при их выводе на экран и упорядочивать элементы различными способами.
Рис. 10.7. Приложение Просмотр каталога.
Мы начнем с создания и настройки модели и представления в конструкторе диалогового окна Просмотр каталога (Directory Viewer).
01 DirectoryViewer::DirectoryViewer(QWidget *parent)
02 : QDialog(parent)
03 {
04 model = new QDirModel;
05 model->setReadOnly(false);
06 model->setSorting(QDir::DirsFirst | QDir::IgnoreCase | QDir::Name);
07 treeView = new QTreeView;
08 treeView->setModel(model);
09 treeView->header()->setStretchLastSection(true);
10 treeView->header()->setSortIndicator(0, Qt::AscendingOrder);
11 treeView->header()->setSortIndicatorShown(true);
12 treeView->header()->setClickable(true);
13 QModelIndex index = model->index(QDir::currentPath());
14 treeView->expand(index);
15 treeView->scrollTo(index);
16 treeView->resizeColumnToContents(0);
17 …
18 }
После создания модели мы обеспечиваем возможность ее редактирования и устанавливаем различные начальные атрибуты упорядочивания. Затем мы создаем объект QTreeView для отображения на экране данных модели. Заголовок QTreeView может использоваться пользователем для управления сортировкой. Делая заголовок восприимчивым к щелчкам мышки, пользователь может сортирбвать данные по выбранному им в заголовке столбцу, причем повторные щелчки переключают направление сортировки, т.е сортировку по возрастанию на сортировку по убыванию и наоборот. После настройки заголовков представления данных в виде дерева мы получаем индекс модели текущего каталога и обеспечиваем просмотр содержимого этого каталога, раскрывая при необходимости его подкаталоги, используя expand(), и устанавливая изображение на его начало, используя scrollTo(). Затем мы обеспечиваем ширину первого столбца, достаточную для размещения всех элементов без вывода многоточия (…).
Во фрагменте конструктора, который здесь не показан, мы связываем кнопки Create Directory (создать каталог) и Remove (удалить) со слотами, выполняющими соответствующие действия. Нам не нужно иметь кнопку Rename (переименовать), поскольку пользователи могут переименовывать элементы каталога по месту, нажимая клавишу F2 и осуществляя ввод символов с клавиатуры.
01 voidDirectoryViewer::createDirectory()
02 {
03 QModelIndex index;
04 if (!index.isValid())
05 return;
06 QString dirName = QInputDialog::getText(this,
07 tr("Create Directory"), tr("Directory name"));
08 if (!dirName.isEmpty()) {
09 if (!model->mkdir(index,dirName).isValid())
10 QMessageBox::information(this,
11 tr("Create Directory"),
12 tr("Failed to create the directory"));
13 }
14 }
Если пользователь вводит имя каталога в диалоговом окне ввода, мы пытаемся создать в текущем каталоге подкаталог с этим именем. Функция QDirModel::mkdir() принимает индекс родительского каталога и имя нового каталога; она возвращает индекс модели созданного каталога. Если операция завершается неудачей, возвращается недействительный индекс модели.
Последний пример в этом разделе показывает, как следует применять модель QSortFilterProxyModel. В отличие от других заранее определенных моделей, эта модель использует какую-нибудь существующую модель и управляет данными, которые проходят между базовой моделью и представлением. В нашем примере базовой является модель QStringListModel, которая проинициализирована списком названий цветов, распознаваемых Qt (полученных функцией QColor::colorNames()). Пользователь может ввести строку фильтра в строке редактирования QLineEdit и указать ее тип (регулярное выражение, шаблон или фиксированная строка), используя поле с выпадающим списком.
Рис. 10.8. Приложение Названия цветов (ColorNames).
Ниже приводится фрагмент конструктора ColorNamesDialog:
01 ColorNamesDialog::ColorNamesDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 sourceModel = new QStringListModel(this);
05 sourceModel->setStringList(QColor::colorNames());
06 proxyModel = new QSortFilterProxyModel(this);
07 proxyModel->setSourceModel(sourceModel);
08 proxyModel->setFilterKeyColumn(0);
09 listView = new QListView;
10 listView->setModel(proxyModel);
11 syntaxComboBox = new QComboBox;
12 syntaxComboBox->addItem(tr("Regular expression"), QRegExp::RegExp);
13 syntaxComboBox->addItem(tr("Wildcard"), QRegExp::Wildcard);
14 syntaxComboBox->addItem(tr("Fixed string"), QRegExp::FixedString);
15 …
16 }
Модель QStringListModel создается и пополняется обычным образом. После этого создается модель QSortFilterProxyModel. Мы передаем базовую модель, используя функцию setSourceModel(), и указываем прокси на необходимость фильтрации по столбцу 0 базовой модеди. Функция QComboBox::addItem() принимает необязательный аргумент дополнительных данных типа QVariant; мы используем его для хранения значения QRegExp::PatternSyntax c текстом, определяющим тип фильтра данного элемента.
01 void ColorNamesDialog::reapplyFilter()
02 {
03 QRegExp::PatternSyntax syntax =
04 QRegExp::PatternSyntax(syntaxComboBox->itemData(
05 syntaxComboBox->currentIndex()).toInt());
06 QRegExp regExp(filterLineEdit->text(), Qt::CaseInsensitive, syntax);
07 proxyModel->setFilterRegExp(regExp);
08 }
Слот reapplyFilter() вызывается при всяком изменении пользователем строки фильтра или типа шаблона фильтрации в поле с выпадающим списком. Мы создаем объект QRegExp, используя текст в строке редактирования. Затем устанавливаем тип шаблона фильтрации на тот, который имеется в данных текущего элемента и отображается в соответствующем поле с выпадающим списком. Когда мы вызываем setFilterRegExp(), новый фильтр становится активным и автоматически обновляется представление данных.
Реализация пользовательских моделей
Заранее определенные в Qt модели предлагают удобные средства обработки и просмотра данных. Однако некоторые источники данных не могут эффективно использоваться для этих моделей, и в этих случаях необходимо создавать пользовательские модели, оптимизированные на применение таких источников данных.
Прежде чем перейти к созданию пользовательских моделей, давайте рассмотрим ключевые концепции архитектуры Qt модель/представление. В модели каждый элемент имеет индекс модели и набор атрибутов, называемых ролями, которые могут принимать произвольные значения. Ранее в данной главе мы видели, что наиболее распространенными ролями являются Qt::DisplayRole и Qt::EditRole. Другие роли используются для вспомогательных данных (например, Qt::ToolTipRole, Qt::StatusTipRole и Qt::WhatsThisRole) или для управления основными атрибутами отображения (например, Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole и Qt::BackgroundColorRole).
Рис. 10.9. Схематическое представление моделей Qt.
В модели списка можно пользоваться только одним индексным компонентом — номером строки, получить доступ к которому можно с помощью функции QModelIndex::row(). В модели таблицы используется два индексных компонента — номер строки и номер столбца, получить доступ к которым можно с помощью функции QModelIndex::row() и QModelIndex::column(). В моделях списка и таблицы родительский элемент всех остальных элементов является корневым элементом, который представдяется недействительным индексом модели QModelIndex. Представленные в данном разделе первые два примера показывают, как можно реализовать пользовательские модели таблиц.
Модель дерева подобна модели таблицы при следующих отличиях. Как и в модели таблицы, родительский элемент элементов верхнего уровня является корневым (имеет недействительный QModelIndex), однако родительский элемент любого другого элемента занимает другое место в иерархии элементов. Доступ к родительским элементам можно получить при помощи функции QModelIndex::parent(). Каждый элемент имеет свои ролевые данные и может иметь или не иметь дочерние элементы, каждый из которых является таким же элементом. Поскольку любой элемент может иметь дочерние элементы, такую структуру данных можно определить рекурсивно (в виде дерева), что будет продемонстрировано в последнем примере данного раздела.
В первом примере этого раздела представлена модель таблицы, используемой только для чтения; она показывает курсы различных валют относительно друг друга.
Рис. 10.10. Приложение Курсы валют (Currencies).
Это приложение можно было бы реализовать при помощи простой таблицы, но мы хотим использовать пользовательскую модель, чтобы можно было воспользоваться определенными свойствами данных для обеспечения минимального расхода памяти. Если бы мы хранили в таблице 162 валюты, действующие в настоящее время, нам бы потребовалось хранить 162 × 162 = 26 244 значения; в представленной ниже пользовательской модели необходимо хранить только 162 значения (значение каждой валюты относительно доллара США).
Класс CurrencyModel будет использоваться совместно со стандартным табличным представлением QTableView. Модель CurrencyModel пополняется элементами QMap
QMap
currencyMap.insert("AUD", 1.3259);
currencyMap.insert("CHF", 1.2970);
…
currencyMap.insert("SGD", 1.6901);
currencyMap.insert("USD", 1.0000);
CurrencyModel currencyModel;
currencyModel.setCurrencyMap(currencyMap);
QTableView tableView;
tableView.setModel(¤cyModel);
tableView.setAlternatingRowColors(true);
Теперь мы можем перейти к реализации модели, начиная с ее заголовка:
01 class CurrencyModel : public QAbstractTableModel
02 {
03 public:
04 CurrencyModel(QObject *parent = 0);
05 void setCurrencyMap(const QMap
06 int rowCount(const QModelIndex &parent) const;
07 int columnCount(const QModelIndex &parent) const;
08 QVariant data(const QModelIndex &index, int role) const;
09 QVariant headerData(int section, Qt::Orientation orientation,
10 int role) const;
11 private:
12 QString currencyAt(int offset) const;
13 QMap
14 };
Для нашей модели мы использовали подкласс QAbstractTableModel, поскольку он лучше всего подходит к нашему источнику данных. Qt содержит несколько базовых классов моделей, включая QAbstractListModel, QAbstractTableModel и QAbstractItemModel. Класс QAbstractItemModel используется для поддержки разнообразных моделей, в том числе тех, которые построены на рекурсивных структурах данных, а классы QAbstractListModel и QAbstractTableModel удобно применять для одномерных и двумерных наборов данных.
Рис. 10.11. Дерево наследования для абстрактных классов моделей.
Для модели таблицы, используемой только для чтения, мы должны переопределить три функции: rowCount(), columnCount() и data(). В данном случае мы также переопределили функцию headerData() и обеспечили функцию инициализации данных (setCurrencyMap()).
01 CurrencyModel::CurrencyModel(QObject*parent)
02 : QAbstractTableModel(parent)
03 {
04 }
В конструкторе нам ничего не надо делать, кроме передачи базовому классу parent в качестве параметра.
01 int CurrencyModel::rowCount(const QModelIndex &
02 /* родительский элемент */) const
03 {
04 return currencyMap.count();
05 }
06 int CurrencyModel::columnCount(const QModelIndex &
07 /* родительский элемент */) const
08 {
09 return currencyMap.count();
10 }
В этой табличной модели счетчики строк и столбцов представляют собой номера валют в ассоциативном массиве валют. Параметр parent не имеет смысла в модели таблицы; он здесь указан, потому что rowCount() и columnCount() наследуются от более обобщенного базового класса QAbstractItemModel, поддерживающего иерархические структуры.
01 QVariant CurrencyModel::data(const QModelIndex &index, int role) const
02 {
03 if (!index.IsValid())
04 return QVariant();
05 if (role == Qt::TextAlignmentRole) {
06 return int(Qt::AlignRight | Qt::AlignVCenter);
07 } else if (role == Qt::DisplayRole) {
08 QString rowCurrency = currencyAt(index.row());
09 QString columnCurrency = currencyAt(index.column());
10 if (currencyMap.value(rowCurrency) == 0.0)
11 return "####";
12 double amount = currencyMap.value(columnCurrency)
13 / currencyMap.value(rowCurrency);
14 return QString("%1").arg(amount, 0, 'f', 4);
15 }
16 return QVariant();
17 }
Функция data() возвращает значение любой одной роли элемента. Элемент определяется индексом QModelIndex. В модели таблицы представляют интерес такие компоненты QModelIndex, как номер строки и номер столбца, получить доступ к которым можно с помощью функций row() и column().
Если используется роль Qt::TextAlignmentRole, мы возвращаем значение, подходящее для выравнивания чисел. Если используется роль Qt::DisplayRole, мы находим значение каждой валюты и вычисляем курс обмена.
Мы могли бы возвращать рассчитанное значение типа double, но тогда нам пришлось бы контролировать количество позиций после десятичной точки при отображении числа (если мы не используем пользовательский делегат). Вместо этого мы возвращаем значение в виде строки, отформатированной нужным нам образом.
01 QVariant CurrencyModel::headerData(int section,
02 Qt::Orientation /* ориентация */, int role) const
03 {
04 if (role != Qt::DisplayRole)
05 return QVariant();
06 return currencyAt(section);
07 }
Функция headerData() вызывается представлением для пополнения своих горизонтальных и вертикальных заголовков. Параметр section содержит номер строки или столбца (в зависимости от ориентации). Поскольку строки и столбцы содержат одинаковые коды валют, нам не надо заботиться об ориентации, а просто вернуть код валюты для заданного значения section.
01 void CurrencyModel::setCurrencyMap(const QMap
02 {
03 currencyMap = map;
04 reset();
05 }
Вызывающая программа может изменить набор валют, используя функцию setCurrencyMap(). Вызов QAbstractItemModel::reset() указывает любому представлению, что все данные в используемой модели недействительны; это вынуждает их запросить свежие данные для тех элементов, которые видны на экране.
01 QString CurrencyModel::currencyAt(int offset) const
02 {
03 return (currencyMap.begin() + offset).key();
04 }
Функция currencyAt() возвращает ключ (код валюты), который находится по указанному смещению в ассоциативном массиве валют. Мы используем итератор в стиле STL для поиска элемента и вызываем для него функцию key().
Как мы только что могли убедиться, нетрудно создавать модели, используемые только для чтения, и при определенном характере исходных данных в хорошо спроектированной модели в принципе можно сэкономить память и увеличить быстродействие. В следующем примере приложения Города (Cities) также используется табличная модель, но на этот раз все данные вводятся пользователем.
Это приложение используется для хранения расстояний между любыми двумя городами. Как и в предыдущем примере, мы могли бы просто использовать табличный виджет QTableWidget и хранить один элемент для каждой пары городов. Однако пользовательская модель могла бы быть более эффективной, потому что расстояние от любого города А до любого другого города В не зависит от того, будем ли мы путешествовать от А до В или от В до А, поэтому элементы с одной стороны от главной диагонали получаются путем зеркального отражения другой.
Для сравнения пользовательской модели с простой таблицей предположим, что у нас имеется три города: А, В и С. Для обеспечения всех сочетаний нам пришлось бы хранить девять значений. В аккуратно спроектированной модели потребовалось бы только три элемента: (А, В), (A, С) и (В, С).
Рис. 10.12. Приложение Города.
Ниже показано, как мы настраиваем и используем модель:
QStringList cities;
cities << "Arvika" << "Boden" << "Eskilstuna" << "Falun"
<< "Filipstad" << "Halmstad" << "Helsingborg" << "Karlstad"
<< "Kiruna" << "Kramfors" << "Motala" << "Sandviken"
<< "Skara" << "Stockholm" << "Sundsvall" << "Trelleborg";
CityModel CityModel;
cityModel.setCities(cities);
QTableView tableView;
tableView.setModel(&cityModel);
tableView.setAlternatingRowColors(true);
Мы должны переопределить те же самые функции, которые мы переопределяли в предыдущем примере. Кроме того, для обеспечения возможности редактирования модели мы должны переопределить setData() и flags(). Ниже приводится определение класса:
01 class CityModel : public QAbstractTableModel
02 {
03 Q_OBJECT
04 public:
05 CityModel(QObject *parent = 0);
06 void setCities(const QStringList &cityNames);
07 int rowCount(const QModelIndex &parent) const;
08 int columnCount(const QModelIndex &parent) const;
09 QVariant data(const QModelIndex &index, int role) const;
10 bool setData(const QModelIndex &index, const QVariant &value,
11 int role);
12 QVariant headerData(int section, Qt::Orientation orientation,
13 int role) const;
14 Qt::ItemFlags flags(const QModelIndex &index) const;
15 private:
16 int offsetOf(int row, int column) const;
17 QStringList cities;
18 QVector
19 };
В этой модели мы используем две структуры данных: cities типа QStringList для хранения названий городов, и distances типа QVector
01 CityModel::CityModel(QObject *parent)
02 : QAbstractTableModel(parent)
03 {
04 }
Конструктор передает параметр parent базовому классу и больше ничего не делает.
01 int CityModel::rowCount(const QModelIndex &
02 /* родительский элемент */) const
03 {
04 return cities.count();
05 }
06 int CityModel::columnCount(const QModelIndex &
07 /* родительский элемент */) const
08 {
09 return cities.count();
10 }
Поскольку мы имеем квадратную матрицу городов, количество строк и столбцов равно количеству городов в нашем списке.
01 QVariant CityModel::data(const QModelIndex &index, int role) const
02 {
03 if (!index.isValid())
04 return QVariant();
05 if (role == Qt::TextAlignmentRole) {
06 return int(Qt::AlignRight | Qt::AlignVCenter);
07 } else if (role == Qt::DisplayRole) {
08 if (index.row() == index.column())
09 return 0;
10 int offset = offsetOf(index.row(), index.column());
11 return distances[offset];
12 }
13 return QVariant();
14 }
Функция data() аналогична той же функции в нашей модели CurrencyModel. Она возвращает 0, если строка и столбец имеют одинаковый номер, потому что в этом случае два города одинаковы; в противном случае она находит в векторе distances элемент для заданной строки и заданного столбца, возвращая расстояние для этой конкретной пары городов.
01 QVariant CityModel::headerData(int section,
02 Qt::Orientation /* ориентация */,
03 int role) const
04 {
05 if (role == Qt::DisplayRole)
06 return cities[section];
07 return QVariant();
08 }
Функция headerData() имеет простой вид, потому что наша таблица является квадратной матрицей, в которой строки и столбцы имеют идентичные заголовки. Мы просто возвращаем название города, расположенное с заданным смещением в списке строк cities.
01 bool CityModel::setData(const QModelIndex &index,
02 const QVariant &value, int role)
03 {
04 if (index.isValid() && index.row() != index.column()
05 && role == Qt::EditRole) {
06 int offset = offsetOf(index.row(), index.column());
07 distances[offset] = value.toInt();
08 QModelIndex transposedIndex = createIndex(
09 index.column(), index.row());
10 emit dataChanged(index, index);
11 emit dataChanged(transposedIndex, transposedIndex);
12 return true;
13 }
14 return false;
15 }
Функция setData() вызывается при редактировании элемента пользователем. Если индекс модели действителен, два города различны и модифицируемый элемент данных имеет ролевой атрибут Qt::EditRole, эта функция сохраняет введенное пользователем значение в векторе distances.
Функция createIndex() используется для формирования индекса модели. Она нужна для получения индекса модели элемента, который расположен по другую сторону от главной диагонали и который соответствует элементу с установленным значением, поскольку оба элемента должны показывать одинаковые данные. Функция createIndex() принимает сначала строку и затем столбец; здесь мы передаем параметры в обратном порядке, чтобы получить индекс модели элемента, расположенного по другую строну диагонали напротив элемента, определенного индексом index.
Мы генерируем сигнал dataChanged() с указанием индекса модели элемента, который изменился. Эта функция принимает два индекса модели, поскольку возможна ситуация, когда изменения относятся к некоторой прямоугольной области, охватывающей несколько строк и столбцов, поэтому передаются индекс верхнего левого угла и индекс нижнего правого угла этой области. Генерируем также сигнал dataChanged() для индекса противоположного элемента, чтобы представление обновило его отображение на экране. Наконец, мы возвращаем true или false, указывая на успешность или неуспешность редактирования.
01 Qt::ItemFiags CityModel::flags(const QModelIndex &index) const
02 {
03 Qt::ItemFlags flags = QAbstractItemModel::flags(index);
04 if (index.row() != index.column())
05 flags |= Qt::ItemIsEditable;
06 return flags;
07 }
Функция flags() используется моделью для того, чтобы можно было сообщить о допустимых действиях с элементом (например, допускает ли он редактирование). По умолчанию эта функция для модели QAbstractTableModel возвращает Qt::ItemIsSelectable | Qt::ItemIsEnabled. Мы добавляем флажок Qt::ItemIsEditable для всех элементов, кроме расположенных по диагонали (которые всегда равны 0).
01 void CityModel::setCities(const QStringList &cityNames)
02 {
03 cities = cityNames;
04 distances.resize(cities.count() * (cities.count() - 1) / 2);
05 distances.fill(0);
06 reset();
07 }
Если задан новый список городов, мы устанавливаем закрытую переменную типа QStringList на новый список, изменяем размеры и очищаем вектор расстояний, а затем вызываем функцию QAbstractItemModel::reset(), чтобы уведомить все представления о необходимости обновления всех видимых элементов.
01 int CityModel::offsetOf(int row, int column) const
02 {
03 if (row < column)
04 qSwap(row, column);
05 return (row * (row - 1) / 2) + column;
06 }
Закрытая функция offsetOf() вычисляет индекс заданной пары городов для вектора расстояний distances. Например, предположим, что мы имеем города А, В, С и D, и пользователь обновляет элемент со строкой 3 и столбцом 1, т. е. (B, D). Тогда индекс вектора расстояний будет равен 3 × (3 — 1) / 2 + 1 = 4. Если бы пользователь вместо этого изменил элемент со строкой 1 и столбцом 3, т.е. (D, В), благодаря применению функции qSwap(), выполнялись бы точно такие же вычисления и возвращалось бы то же самое значение.
Рис. 10.13. Структуры данных cities и distances и табличная модель.
Последний пример в данном разделе представляет собой модель, которая показывает дерево грамматического разбора заданного регулярного выражения. Регулярное выражение состоит из одного или нескольких термов, разделяемых символами '|'. Так, регулярное выражение «alpha|bravo|charlie» содержит три терма. Каждый терм представляет собой последовательность из одного или нескольких факторов: например, терм «bravo» состоит из пяти факторов (каждая буква является фактором). Факторы могут состоять из атома и необязательного квантификатора (quantifier), например '*', '+' и '?'. Поскольку регулярные выражения могут иметь подвыражения, заключенные в скобки, они могут быть представлены рекурсивными деревьями грамматического разбора.
Регулярное выражение, показанное на рис. 10.14, «ab|(cd)?e» означает, что за 'a' следует 'b' или допускается два варианта: за 'c' идет 'd' и затем 'e' или просто имеется 'e'. Поэтому подойдут строки «ab» и «cde», но не подойдут строки «bc» или «cd».
Рис. 10.14. Приложение Парсер регулярных выражений.
Приложение Парсер регулярных выражений (Regexp Parser) состоит из четырех классов:
• RegExpWindow — окно, которое позволяет пользователю вводить регулярное выражение и показывает соответствующее дерево грамматического разбора;
• RegExpParser формирует дерево грамматического разбора для заданного регулярного выражения;
• RegExpModel — модель дерева, используемая деревом грамматического разбора;
• Node (вершина) представляет один элемент в дереве грамматического разбора.
Давайте начнем с класса Node:
01 class Node {
02 public:
03 enum Type { RegExp, Expression, Term, Factor, Atom, Terminal };
04 Node(Type type, const QString &str = "");
05 ~Node();
06 Type type;
07 QString str;
08 Node *parent;
09 QList
10 };
Каждая вершина имеет тип, строку (которая может быть пустой), ссылку на родительский элемент (которая может быть нулевой) и список дочерних вершин (который может быть пустым).
01 Node::Node(Type type, const QString &str)
02 {
03 this->type = type;
04 this->str = str;
05 parent = 0;
06 }
Конструктор просто инициализирует тип и строку вершины. Поскольку все данные открыты, в программном коде, использующим Node, можно непосредственно манипулировать типом, строкой, родительским элементом и дочерними элементами.
01 Node::~Node()
02 {
03 qDeleteAll(children);
04 }
Функция qDeleteAll() проходит no всем указателям контейнера и вызывает оператор delete для каждого из них. Она не устанавливает указатели в 0, поэтому, если она используется вне деструктора, обычно за ней следует вызов функции clear() для контейнера, содержащего указатели.
Теперь, когда мы определили элементы наших данных (представленные вершиной Node), мы готовы создать модель:
01 class RegExpModel : public QAbstractItemModel
02 {
03 public:
04 RegExpModel(QObject *parent = 0);
05 ~RegExpModel();
06 void setRootNode(Node *node);
07 QModelIndex index(int row, int column,
08 const QModelIndex &parent) const;
09 QModelIndex parent(const QModelIndex &child) const;
10 int rowCount(const QModelIndex &parent) const;
11 int columnCount(const QModelIndex &parent) const;
12 QVariant data(const QModelIndex &index, int role) const;
13 QVariant headerData(int section,
14 Qt::Orientation Orientation, int role) const;
15 private:
16 Node *nodeFromIndex(const QModelIndex &index) const;
17 Node *rootNode;
18 };
На этот раз мы построили подкласс на основе класса QAbstractItemModel, а не на основе его удобного подкласса QAbstractTableModel, потому что мы хотим создать иерархическую модель. Нам необходимо переопределить те же самые функции и, кроме того, требуется реализовать функции index() и parent(). Для установки данных модели предусмотрена функция setRootNode(), при вызове которой должна задаваться корневая вершина дерева грамматического разбора.
01 RegExpModel::RegExpModel(QObject *parent)
02 : QAbstractItemModel(parent)
03 {
04 rootNode = 0;
05 }
В конструкторе модели нам надо просто задать корневой вершине безопасное нулевое значение и передать указатель parent базовому классу.
01 RegExpModel::~RegExpModel()
02 {
03 delete rootNode;
04 }
В деструкторе мы удаляем корневую вершину. Если корневая вершина имеет дочерние вершины, то каждая из них удаляется и эта процедура повторяется рекурсивно деструктором Node.
01 void RegExpModel::setRootNode(Node *node)
02 {
03 delete rootNode;
04 rootNode = node;
05 reset();
06 }
При установке новой корневой вершины мы начинаем с удаления предыдущей корневой вершины (и всех ее дочерних вершин). Затем мы устанавливаем новое значение для корневой вершины и вызываем функцию reset() для уведомления всех представлений о необходимости обновления отображаемых данных всеми видимыми элементами.
01 QModelIndex RegExpModel::index(int row, int column,
02 const QModelIndex &parent) const
03 {
04 if (!rootNode)
05 return QModelIndex();
06 Node *parentNode = nodeFromIndex(parent);
07 return createIndex(row, column, parentNode->children[row]);
08 }
Функция index() класса QAbstractItemModel переопределяется. Она всегда вызывается, когда в модели или в представлении требуется создать индекс QModelIndex для конкретного дочернего элемента (или для элемента самого верхнего уровня, если parent имеет недействительное значение QModelIndex). В табличных и списковых моделях нам не требуется переопределять эту функцию, потому что обычно оказываются достаточным реализации по умолчанию моделей QAbstractListModel и QАЬstractTableModel.
В нашей реализации index(), если не задано дерево грамматического разбора, мы возвращаем недействительный индекс QModelIndex. В противном случае мы создаем QModelIndex с заданными строкой, столбцом и Node * для запрошенного дочернего элемента. В иерархических моделях знание строки и столбца элемента относительно своего родителя оказывается недостаточным для уникальной идентификации элемента; мы должны также знать, кто является его родителем. Для этого можно хранить в QModelIndex указатель на внутреннюю вершину. В объекте QModelIndex кроме номеров строк и столбцов допускается хранение указателя void * или значения типа int.
Указатель Node * на дочерний элемент можно получить из списка дочерних элементов children родительской вершины. Указатель на родительскую вершину извлекается из индекса модели parent, используя закрытую функцию nodeFromIndex():
01 Node *RegExpModel::nodeFromIndex(
02 const QModelIndex &index) const
03 {
04 if (index.isValid()) {
05 return static_cast
06 } else {
07 return rootNode;
07 }
Функция nodeFromIndex() приводит тип void * заданного индекса в тип Node * или возвращает указатель на корневую вершину, если индекс недостоверен, поскольку недостоверный индекс модели используется для представления корня модели.
01 int RegExpModel::rowCount(const QModelIndex
02 &parent) const
03 {
04 Node *parentNode = nodeFromlndex(parent);
05 if (!parentNode)
06 return 0;
07 return parentNode->children.count();
08 }
Число строк для заданного элемента определяется просто количеством дочерних элементов.
01 int RegExpModel::columnCount(const QModelIndex &
02 /* родительский элемент */) const
03 {
04 return 2;
05 }
Число столбцов фиксировано и равно 2. Первый столбец содержит типы вершин; второй столбец содержит значения вершин.
01 QModelIndex RegExpModel::parent(const QModelIndex
02 &child) const
03 {
04 Node*node = nodeFromIndex(child);
05 if (!node)
06 return QModelIndex();
07 Node *parentNode = node->parent;
08 if (!parentNode)
09 return QModelIndex();
10 Node *grandparentNode = parentNode->parent;
11 if (!grandparentNode)
12 return QModelIndex();
13 int row = grandparentNode->children.indexOf(parentNode);
14 return createIndex(row, child.column(), parentNode);
15 }
Получить QModelIndex родительского элемента из дочернего немного сложнее, чем найти дочерний элемент родителя. Можно легко получить родительскую вершину, применяя сначала функцию nodeFromIndex() и поднимаясь затем вверх с помощью указателя на родительский элемент, но для получения номера строки (позиции родительской верщины в соответствующем списке дочерних вершин) мы должны перейти к родителю родительского элемента и найти в его списке дочерних элементов значение индекса первого родителя (родителя исходной дочерней вершины).
01 QVariant RegExpModel::data(const QModelIndex
02 &index, int role) const
03 {
04 if (role != Qt::DisplayRole)
05 return QVariant();
06 Node *node = nodeFromIndex(index);
07 if (!node)
08 return QVariant();
09 if (index.column() == 0) {
10 switch (node->type) {
11 case Node::RegExp:
12 return tr("RegExp");
13 case Node::Expression:
14 return tr("Expression");
15 case Node::Term:
16 return tr("Term");
17 case Node::Factor:
18 return tr("Factor");
19 case Node::Atom:
20 return tr("Atom");
21 case Node::Terminal:
22 return tr("Terminal");
23 default:
24 return tr("Unknown");
25 }
26 } else if (index.column() == 1) {
27 return node->str;
28 }
29 return QVariant();
30 }
В функции data() получаем для запрошенного элемента указатель Node * и используем его для получения доступа к данным соответствующей вершины. Если вызывающая программа запрашивает какую-нибудь роль, отличную от Qt::DisplayRole, или если не удается получить вершину Node для заданного индекса модели, мы возвращаем недействительное значение типа QVariant. Если столбец равен 0, возвращаем название типа вершины; если столбец равен 1, вбзвращаем значение вершины (ее строку).
01 QVariant RegExpModel::headerData(int section,
02 Qt::Orientation orientation, int role) const
03 {
04 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
05 if (section == 0) {
06 return tr("Node");
07 else if (section == 1) {
08 return tr("Value");
09 }
10 }
11 return QVariant();
12 }
При переопределении функции headerData() мы возвращаем соответствующие метки горизонтального заголовка. Класс QTreeView, который используется для визуального представления иерархических моделей, не имеет заголовков строк, поэтому мы их игнорируем.
Теперь, когда рассмотрены классы Node и RegExpModel, давайте посмотрим, как создается корневая вершина, когда пользователь изменяет текст в строке редактирования.
01 void RegExpWindow::regExpChanged(const QString®Exp)
02 {
03 RegExpParser parser;
04 Node *rootNode = parser.parse(regExp);
05 regExpModel->setRootNode(rootNode);
06 }
При изменении пользователем текста в строке редактирования вызывается слот главного окна regExpChanged(). В этом слоте выполняется синтаксический анализ введенного пользователем текста, и парсер возвращает указатель на корневую вершину дерева грамматического разбора.
Мы не показываем класс RegExpParser, потому что он не имеет отношения к графическому интерфейсу или программированию модели/представления. Полный исходный код для этого примера находится на компакт-диске.
В данном разделе мы увидели, как можно создавать три различные пользовательские модели. Многие модели значительно проще приведенных выше и обеспечивают соответствие один к одному между элементами и индексами модели. В самой системе Qt находятся дополнительные примеры применения архитектуры модель/представление вместе с подробной документацией.
Реализация пользовательских делегатов
Воспроизведение и редактирование в представлениях отдельных элементов выполняются с помощью делегатов. В большинстве случаев возможности делегата, предоставляемого представлением по умолчанию, оказываются достаточными. Если нам требуется более тонкое управление воспроизведением элементов, мы сможем этого добиться, просто используя пользовательскую модель: при переопределении функции data() можем предусмотреть обработку ролей Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole и Qt::BackgroundColorRole, а также тех, которые используются делегатом по умолчанию. Например, в приведенных выше приложениях Города и Курсы валют мы применяли Qt::TextAlignmentRole для выравнивания чисел вправо.
Если нам требуется еще больший контроль, можем создать наш собственный класс делегата и связать его с нужными нам представлениями. В показанном ниже диалоговом окне Редактор фонограмм (Track Editor) используется пользовательский делегат. В этом окне отображаются названия музыкальных фонограмм и их длительность. Данные в модели будут представлены просто строками QString (названия) и значениями типа int (секунды), однако длительность будет разбита на минуты и секунды, а ее редактирование будет выполняться, используя QTimeEdit.
Рис. 10.15. Приложение Редактор фонограмм.
Диалоговое окно Редактор фонограмм использует QTableWidget — удобный подкласс отображения элементов, который работает с объектами QTableWidgetltem. Данные представлены в виде списка фонограмм Track:
01 class Track
02 {
03 public:
04 Track(const QString &title = "", int duration = 0);
05 QString title;
06 int duration;
07 };
Ниже приводится фрагмент конструктора, показывающий, как создается и пополняется табличный виджет:
01 TrackEditor::TrackEditor(QList
02 : QDialog(parent)
03 {
04 this->tracks = tracks;
05 tableWidget = new QTableWidget(tracks->count(), 2);
06 tableWidget->setItemDelegate(new TrackDelegate(1));
07 tableWidget->setHorizontalHeaderLabels(
08 QStringList() << tr("Track") << tr("Duration"));
09 for (int row = 0; row < tracks->count(); ++row) {
10 Track track = tracks->at(row);
11 QTableWidgetltem *item0 = new QTableWidgetItem(track.titie);
12 tableWidget->setItem(row, 0, item0);
13 QTableWidgetltem *item1 = new QTableWidgetItem(
14 QString::number(track.duration));
15 item1->setTextAlignment(Qt::AlignRight);
16 tableWidget->setItem(row, 1, item1);
17 }
18 …
19 }
Конструктор создает табличный виджет и, вместо того чтобы просто использовать делегата по умолчанию, связывает виджет с нашим пользовательским делегатом TrackDelegate, передавая ему номер столбца, содержащего временные данные. Мы начинаем с установки заголовков столбцов и затем проходим в цикле по всем данным, устанавливая для каждой строки название фонограммы и ее длительность.
В остальной части конструктора и диалогового окна TrackEditor нет ничего необычного, поэтому теперь рассмотрим класс trackDelegate, который обеспечивает воспроизведение и редактирование данных фонограммы.
01 class TrackDelegate : public QItemDelegate
02 {
03 Q_OBJECT
04 public:
05 TrackDelegate(int durationColumn, QObject *parent = 0);
06 void paint(QPainter *painter, const
07 QStyleOptionViewItem &option,
08 const QModelIndex &index) const;
09 QWidget *createEditor(QWidget *parent,
10 const QStyleOptionViewItem &option,
11 const QModelIndex &index) const;
12 void setEditorData(QWidget *editor,
13 const QModelIndex &index) const;
14 void setModelData(QWidget *editor,
15 QAbstractItemModel *model,
16 const QModelIndex &index) const;
17 private slots:
18 void commitAndCloseEditor();
19 private:
20 int durationColumn;
21 };
Мы используем QItemDelegate в качестве нашего базового класса, чтобы можно было воспользоваться возможностями делегата по умолчанию. Так же мы могли бы использовать QAbstractItemDelegate, если бы хотели начать с чистого листа. Для обеспечения в делегате возможности редактирования данных мы должны реализовать функции createEditor(), setEditorData() и setModelData(). Кроме того, реализуем функцию paint() для изменения отображения столбца длительностей.
01 TrackDelegate::TrackDelegate(int durationColumn, QObject *parent)
02 : QItemDelegate(parent)
03 {
04 this->durationColumn = durationColumn;
05 }
Параметр конструктора durationColumn указывает делегату, какой номер столбца содержит длительность фонограммы.
01 void TrackDelegate::paint(QPainter *painter,
02 const QStyleOptionViewItem &option,
03 const QModelIndex &index) const
04 {
05 if (index.column() == durationColumn) {
06 int secs = index.model()->data(index, Qt::DisplayRole).toInt();
07 QString text= QString("%1:%2")
08 .arg(secs/60, 2, 10, QChar('0'))
09 .arg(secs % 60, 2, 10, QChar('0'));
10 QStyleOptionViewItem myOption = option;
11 myOption.displayAlignment = Qt::AlignRight | Qt::AlignVCenter;
12 drawDisplay(painter, myOption, myOption.rect, text);
13 drawFocus(painter, myOption, myOption.rect);
14 } else {
15 QItemDelegate::paint(painter, option, index);
16 }
17 }
Поскольку мы собираемся отображать длительность в виде «минуты : секунды», мы переопределили функцию paint(). Вызов arg() принимает целое число, выводимое в виде строки, допустимое количество символов в строке, основание целого числа (10 для десятичного числа) и символ—заполнитель.
Для выравнивания текста вправо копируем текущие опции стиля и заменяем установленное по умолчанию выравнивание. После этого вызываем QItemDelegate::drawDisplay() для вывода текста, затем вызываем QItemDelegate::drawFocus() для прорисовки фокусного прямоугольника в том случае, если данный элемент получил фокус, и ничего не делая в противном случае. Функцией drawDisplay() очень удобно пользоваться, особенно совместно с нашими собственными опциями стиля. Мы могли бы также рисовать, используя рисовальщик непосредственно.
01 QWidget *TrackDelegate::createEditor(QWidget *parent,
02 const QStyleOptionViewItem &option,
03 const QModelIndex &index) const
04 {
05 if (index.column() == durationColumn) {
06 QTimeEdit *timeEdit = new QTimeEdit(parent);
07 timeEdit->setDisplayFormat("mm:ss");
08 connect(timeEdit, SIGNAL(editingFinished()),
09 this, SLOT(commitAndCloseEditor()));
10 return timeEdit;
11 } else {
12 return QItemDelegate::createEditor(parent, option, index);
13 }
14 }
Мы собираемся управлять редактированием только длительностей фонограмм, предоставляя делегату по умолчанию управление редактированием названий фонограмм. Это обеспечивается проверкой столбца, для которого запрашивается редактирование. Если это столбец длительности, создаем объект QTimeEdit, устанавливаем соответствующий формат отображения и соединяем его сигнал editingFinished() с нашим слотом commitAndCloseEditor(). Для других столбцов передаем управление редактированием делегату по умолчанию.
01 void TrackDelegate::commitAndCloseEditor()
02 {
03 QTimeEdit *editor = qobject_cast
04 emit commitData(editor);
05 emit closeEditor(editor);
06 }
Если пользователь нажимает клавишу Enter или убирает фокус из QTimeEdit (но не путем нажатия клавиши Esc), генерируется сигнал editingFinished() и вызывается слот commitAndCloseEditor(). Этот слот генерирует сигнал commitData() для уведомления представления о том, что имеются новые данные для замены существующих. Он также генерирует сигнал closeEditor() для уведомления представления о том, что редактор больше не нужен, и модель его удалит. Получить доступ к редактоpy можно с помощью функции QObject::sender(), которая возвращает объект, выдавший сигнал, запустивший данный слот. Если пользователь отказывается от работы с редактором (нажимая клавишу Esc), представление просто удалит этот редактор.
01 void TrackDeIegate::setEditorData(QWidget *editor,
02 const QModelindex &index) const
03 {
04 if (index.column() == durationColumn) {
05 int secs = index.model()->data(index, Qt::DisplayRole).toInt();
06 QTimeEdit *timeEdit = qobject_cast
07 timeEdit->setTime(QTime(0, secs / 60, secs % 60));
08 } else {
09 QItemDelegate::setEditorData(editor, index);
10 }
11 }
Когда пользователь инициирует редактирование, представление вызывает createEditor() для создания редактора и затем setEditorData() для инициализации редактора текущими данными элемента. Если редактор вызывается для столбца длительности, получаем из данных элемента длительность фонограммы в секундах и устанавливаем значение QTimeEdit на соответствующее количество минут и секунд; в противном случае мы позволяем делегату по умолчанию выполнить инициализацию.
01 void TrackDelegate::setModelData(QWidget *editor,
02 QAbstractItemModel *model, const QModelIndex &index) const
03 {
04 if (index.column() == durationColumn) {
05 QTimeEdit *timeEdit = qobject_cast
06 QTime time = timeEdit->time();
07 int secs = (time.minute() * 60) + time.second();
08 model->setData(index, secs);
09 } else {
10 QItemDelegate::setModelData(editor, model, index);
11 }
12 }
Если пользователь прекращает редактирование (например, щелкнув левой кнопкой мышки за пределами виджета редактора или нажав клавишу Enter или Tab), а не отменяет его, модель должна быть обновлена данными редактора. Если редактировалась длительность, извлекаем минуты и секунды из QTimeEdit и устанавливаем поле данных на соответствующее значение секунд.
Мы вполне можем (хотя в данном случае это делать необязательно) создать пользовательский делегат, который обеспечит более тонкое управление редактированием и воспроизведением любого элемента модели. В нашем случае пользовательский делегат управляет только конкретным столбцом, но поскольку QModelIndex передается всем функциям класса QItemDelegate, которые нами переопределяются, мы можем контролировать любой столбец, строку, прямоугольную область, родительский элемент или любое их сочетание вплоть до управления при необходимости на уровне отдельных элементов.
В данной главе мы представили достаточно подробный обзор архитектуры Qt модель/представление. Мы показали, как можно использовать удобные подклассы отображения элементов, как применять заранее определенные в Qt модели и как создавать пользовательские модели и пользовательские делегаты. Однако архитектура модель/представление настолько богата, что мы не смогли раскрыть все ее возможности из-за ограниченности объема книги. Например, мы могли бы создать пользовательское представление, которое отображает свои элементы не в виде списка, таблицы или дерева. Это делается в примере Диаграмма (Chart), который находится в каталоге Qt examples/itemviews/chart; этот пример содержит пользовательское представление, которое воспроизводит модель данных в виде круговой диаграммы.
Кррме того, для одной модели можно использовать несколько представлений, и это не потребует особых усилий. Любое редактирование одного представления автоматически и немедленно отразится на других представлениях. Такие возможности особенно полезны при просмотре больших наборов данных, когда пользователь может захотеть увидеть блоки данных, расположенные далеко друг от друга. Эта архитектура поддерживает также выделения областей: когда два или более представления используются одной моделью, каждому представлению может быть предоставлена возможность иметь свою собственную независимую выделенную область или такие области могут совместно использоваться разными представлениями.
В онлайновой документации Qt всесторонне рассматриваются вопросы программирования классов по отображению элементов. См. , где приводится список всех таких классов, и , где даются дополнительная информация и ссылки на соответствующие примеры, включенные в Qt.
Глава 11. Классы—контейнеры
Классы—контейнеры являются обычными шаблонными классами (template classes), которые предназначены для хранения в памяти элементов заданного типа. С++ уже предлагает много контейнеров в составе стандартной библиотеки шаблонов (STL — Standard Template Library), которая входит в стандартную библиотеку С++.
Qt обеспечивает свои собственные классы—контейнеры, поэтому в Qt—программах мы можем использовать как контейнеры Qt, так и контейнеры STL. Главное преимущество Qt—контейнеров — одинаковое поведение на всех платформах и неявное совместное использование данных. Неявное совместное использование или «копирование при записи» — это оптимизация, позволяющая передавать контейнеры целиком без существенного ухудшения производительности. Qt—контейнеры также снабжены простыми в применении классами итераторов в стиле Java; используя QDataStream, они могут быть оформлены в виде потоков данных и обычно приводят к меньшему объему программного кода в исполняемых модулях, чем при применении соответствующих STL—контейнеров. Наконец, для некоторого оборудования, на котором может работать Qtopia Core (версия Qt для мобильных устройств), единственно доступными являются Qt—контейнеры.
Qt предлагает как последовательные контейнеры, например QVector
Qt также содержит обобщенные алгоритмы, которые могут выполняться над произвольными контейнерами. Например, алгоритм qSort() сортирует последовательный контейнер, a qBinaryFind() выполняет двоичный поиск в упорядоченном последовательном контейнере. Эти алгоритмы аналогичны тем, которые предлагаются STL.
Если вы знакомы с контейнерами STL и библиотека STL уже установлена на платформах, на которых вы работаете, можете их использовать вместо контейнеров Qt или как дополнение к ним. Для получения более подробной информации относительно функций и классов STL достаточно неплохо начать с веб-сайта STL компании «SGI»: .
В данной главе мы также рассмотрим классы QString, QByteArray и QVariant, поскольку они имеют много общего с контейнерами. QString представляет собой 16-битовую строку символов в коде Unicode, которая широко используется в программном интерфейсе Qt. QByteArray является массивом 8-битовых символов типа char, которым удобно пользоваться для хранения произвольных двоичных данных. QVariant может хранить значения большинства типов С++ и Qt.
Последовательные контейнеры
Вектор QVector
Рис. 11.1. Вектор чисел двойной точности.
Если нам заранее известно необходимое количество его элементов, мы можем задать начальный размер при его определении и использовать оператор [ ] для заполнения его элементами; в противном случае мы должны либо затем изменить его размер, либо добавлять элементы в конец вектора. В приведенном ниже примере мы указываем начальный размер вектора:
QVector
vect[0] = 1.0;
vect[1] = 0.540302;
vect[2] = -0.416147;
Ниже та же самая задача решается путем объявления пустого вектора и применения функции append(), которая добавляет элементы в конец вектора:
QVector
vect.append(1.0);
vect.append(0.540302);
vect.append(-0.416147);
Вместо append() можно использовать оператор <<:
vect << 1.0 << 0.540302 << -0.416147;
Организовать цикл просмотра элементов вектора можно при помощи оператора [ ] и функции count():
double sum = 0.0;
for (int i = 0; i < vect.count(); ++i)
sum += vect[i];
Элементы вектора, которым не было присвоено какое-нибудь значение явным образом, инициализируются при помощи стандартного конструктора класса элемента. Основные типы и указатели инициализируются нулевым значением.
Вставка элементов в начало или в середину вектора QVector
Рис. 11.2. Связанный список значений типа double.
Связанные списки не обеспечивают оператор [ ], поэтому необходимо использовать итераторы для прохода по всем элементам. Итераторы также используются для указания позиции элементов. Например, в следующем фрагменте программного кода выполняется вставка строки «Tote Hosen» между «Clash» и «Ramones»:
QLinkedList
list.append("Clash");
list.append("Ramones");
QLinkedList
list.insert(i, "Tote Hosen");
Более подробно итераторы будут рассмотрены позже в данном разделе.
Последовательный контейнер QList
Класс QStringList является подклассом QList
QStack
Во всех до сих пор рассмотренных контейнерах тип элемента T может являться базовым типом (например, int или double), указателем или классом, который имеет стандартный конструктор (т.е. конструктор без аргументов), конструктор копирования и оператор присваивания. К таким классам относятся QByteArray, QDateTime, QRegExp, QString и QVariant. Этим свойством не обладают классы Qt, которые наследуют QObject, поскольку последний не имеет конструктора копирования и оператора присваивания. На практике это не составляет проблему, потому что мы можем просто хранить в контейнере указатели на такие типы данных, а не сами объекты QObject.
Тип T также может быть контейнером; в этом случае следует иметь в виду, что необходимо разделять рядом стоящие угловые скобки пробелами, в противном случае компилятор будет сбит с толку, воспринимая >> как оператор. Например:
QList
Кроме только что упомянутых типов в качестве типа элементов контейнера может задаваться любой пользовательский класс, отвечающий описанным ранее критериям. Ниже дается пример такого класса:
01 class Movie
02 {
03 public:
04 Movie(const QString &title = "", int duration = 0);
05 void setTitle(const QString &title) { myTitle = title; }
06 QString title() const { return myTitle; }
07 void setDuration(int duration) { myDuration = duration; }
08 QString duration() const { return myDuration; }
09 private:
10 QString myTitle;
11 int myDuration;
12 };
Этот класс имеет конструктор, для которого необязательно указывать аргументы (хотя он может иметь до двух аргументов). Он также имеет конструктор копирования и оператор присваивания, которые обеспечиваются С++ по умолчанию. В этом классе достаточно обеспечить копирование между его членами, поэтому нам нет необходимости реализовывать свои собственные конструктор копирования и оператор присваивания.
Qt имеет две категории итераторов, используемых для прохода по элементам контейнера: итераторы в стиле Java и итераторы в стиле STL. Итераторами в стиле Java легче пользоваться, в то время как итераторы в стиле STL более мощные и могут использоваться совместно с алгоритмами Qt и STL.
С каждым классом—контейнером могут использоваться два типа итераторов в стиле Java: итератор, используемый только для чтения, и итератор, используемый как для чтения, так и для записи. Классами итераторов первого типа являются QVectorIterator
Рис. 11.3. Допустимые позиции итераторов в стиле Java.
Прежде всего следует иметь в виду, что итераторы в стиле Java не ссылаются непосредственно на элементы. Вместо этого они могут указывать на позицию перед первым элементом, после последнего элемента или между двумя элементами. Обычно организованный с их помощью цикл выглядит следующим образом:
QList
…
QListIterator
while (i.hasNext()) {
do_something(i.next());
}
Итератор инициализируется контейнером, для прохода по которому он будет использован. В этот момент итератор располагается непосредственно перед первым элементом. Вызов функции hasNext() возвращает true, если имеется элемент справа от итератора. Функция next() возвращает элемент, расположенный справа от итератора, и перемещает итератор в следующую допустимую позицию.
Проход в обратном направлении выполняется аналогично, с тем отличием, что сначала вызывается функция toBack() для размещения итератора после последнего элемента.
QListIterator
i.toBack();
while (i.hasPrevious()) {
do_something(i.previous());
}
Функция hasPrevious() возвращает true, если имеется элемент слева от итератора; функция previous() возвращает элемент, расположенный слева от итератора, и перемещает итератор назад на одну позицию. Возможен другой взгляд на функции next() и previous(): они возвращают тот элемент, через который только что прошел итератор.
Рис. 11.4. Влияние функций previous() и next() на итераторы в стиле Java.
Допускающие запись итераторы (mutable iterators) имеют функции для вставки, модификации и удаления элементов в ходе просмотра контейнеров. В показанном ниже цикле из списка удаляются отрицательные числа:
QMutableListIterator
while (i.hasNext()) {
if (i.next() < 0.0)
i.remove();
}
Функция remove() всегда работает с последним пройденным элементом. Она так же ведет себя при проходе элементов в обратном направлении:
QMutableListIterator
i.toBack();
while (i.hasPrevious()) {
if (i.previous() < 0.0)
i.remove();
}
Аналогично допускающие запись итераторы в стиле Java имеют функцию setValue(), которая модифицирует последний пройденный элемент. Ниже показано, как можно заменить отрицательные числа их абсолютным значением:
QMutableListIterator
while (i.hasNext()) {
int val = i.next();
if (val < 0.0)
i.setValue(-val);
}
Кроме того, можно вставлять элемент в текущую позицию итератора с помощью функции insert(). После этого итератор перемещается в позицию между новым элементом и следующим за ним.
Кроме итераторов в стиле Java каждый класс последовательных контейнеров C
Функция контейнера begin() возвращает итератор в стиле STL, ссылающийся на первый элемент контейнера (например, list[0]), в то время как функция контейнера end() возвращает итератор, ссылающийся на элемент «после последнего элемента» (например, list[5] для списка размером 5). Если контейнер пустой, функции begin() и end() возвращают одинаковое значение. Это может использоваться для проверки наличия хотя бы одного элемента в контейнере, хотя для этой цели более удобно пользоваться функцией isEmpty().
Рис. 11.5. Допустимые позиции итераторов в стиле STL.
Синтаксис применения итераторов в стиле STL моделирует синтаксис применения указателей С++. Мы можем использовать операторы ++ и —— для перехода на следующий или предыдущий элемент, а также унарный оператор * для извлечения значения элемента из позиции текущего итератора. Для вектора vector
В показанном ниже примере каждое значение в списке QList
QList
while (i ! = list.end()) {
*i = qAbs(*i);
++i;
}
Несколько функций Qt возвращают контейнер. Если мы хотим в цикле обработать такое возвращенное значение функции, используя итератор в стиле STL, мы должны сделать копию контейнера и в цикле обрабатывать эту копию. Например, приводимый ниже программный код показывает, как правильно следует обрабатывать в цикле список типа QList
QList
QList
while (i != list.end()) {
do_something(*i);
++i;
}
Ниже дается пример неправильного программного кода:
// Неправильный программный код
QList
while (i != splitter->sizes().end()) {
do_something(*i);
++i;
}
Это происходит из-за того, что функция QSplitter::sizes() возвращает новый список QList
При использовании итераторов в стиле Java, предназначенных только для чтения, нам не надо создавать копию. Итератор обеспечит копию незаметно для нас, гарантируя всегда просмотр в цикле данных, только что возвращенных функцией. Например:
QListIterator
while (i.hasNext()) {
do_something(i.next());
}
Подобное копирование контейнера может показаться неэффективным, но это не так из-за оптимизации посредством так называемого неявного совместного использования даннъис (implicit sharing). Это означает, что операция копирования Qt—контейнера выполняется почти так же быстро, как копирование одного указателя. Только если скопированная строка изменяется, тогда данные действительно копируются — и все это делается автоматически и незаметно для пользователя. Поэтому неявное совместное использование иногда называют «копированием при записи» (copy on write).
Привлекательность неявного совместного использования данных заключается в том, что эта оптимизация выполняется так, что мы можем не думать о ней; она просто работает сама по себе и не требует от нас какого-то дополнительного программного кода. В то же время неявное совместное использование данных способствует тому, что программист следует четкому стилю, возвращая все объекты по значению. Рассмотрим следующую функцию:
01 QVector
02 {
03 QVector
04 for (int i = 0; i <360; ++i)
05 vect[i] = sin(i / (2 * M_PI));
06 return vect;
07 }
Вызов этой функции выглядит следующим образом:
QVector
В отличие от этого подхода, STL склоняет нас к передаче вектора в виде неконстантной ссылки, чтобы избежать копирования, происходящего из-за возвращения функцией значения, хранимого в переменной:
01 using namespace std;
02 void sineTable(vector
03 {
04 vect.resize(360);
05 for (int i = 0; i < 360; ++i)
06 vect[i] = sin(i / (2 * M_PI));
07 }
В результате вызов будет не столь простым и менее понятным:
vector
sineTable(table);
В Qt применяется неявное совместное использование данных во всех ее контейнерах и во многих других классах, включая QByteArray, QBrush, QFont, QImage, QPixmap и QString. Это делает применение этих классов очень эффективным при передаче по значению, как аргументов функции, так и возвращаемых функциями значений.
Неявное совместное использование данных в Qt гарантирует, что данные не будут копироваться, если мы их не модифицируем. Чтобы получить максимальные выгоды от применения этой технологии, необходимо выработать в себе две новые привычки при программировании. Одна связана с использованием функции at() вместо оператора [ ] при доступе только для чтения к (неконстантному) вектору или списку. Поскольку при применении Qt—контейнеров нельзя сказать, оказывается ли [ ] с левой стороны оператора присваивания или нет, предполагается самое худшее и принудительно выполняется действительное копирование (deep сору), в то время как at() не допускается в левой части оператора присваивания.
Подобная проблема возникает при прохождении контейнера с помощью итераторов в стиле STL. Когда вызываются функции begin() или end() для неконстантного контейнера, Qt всегда принудительно выполняет действительное копирование при совместном использовании данных. Решение, позволяющее избавиться от этой неэффективности, состоит в применении по мере возможности const_iterator, constBegin() и constEnd().
В Qt предусмотрен еще один, последний метод прохода по элементам последовательного контейнера — оператор цикла foreach. Он выглядит следующим образом:
QLinkedList
…
foreach (Movie movie, list) {
if (movie.title() == "Citizen Kane") {
cout << "Found Citizen Kane" << endl;
break;
}
}
Псевдоключевое слово foreach реализуется с помощью стандартного цикла for. На каждом шаге цикла переменная цикла (movie) устанавливается на новый элемент, начиная с первого элемента контейнера и затем двигаясь вперед. Цикл foreach автоматически использует копию контейнера при входе в цикл, и по этой причине модификации контейнера в ходе цикла не влияют на сам цикл.
Поддерживаются операторы цикла break и continue. Если тело цикла состоит из одного оператора, необязательно указывать скобки. Как и для оператора for, переменная цикла может определяться вне цикла, например:
QLinkedList
Movie movie;
…
foreach (movie, list) {
if (movie.title() == "Citizen Kane") {
cout << "Found Citizen Kane" << endl;
break;
}
}
Определение переменной цикла вне цикла — единственная возможность для контейнеров, содержащих типы данных с запятой (например, QPair
Как работает неявное совместное использование данных
Неявное совместное использование данных работает автоматически и незаметно для пользователя, поэтому нам не надо в программном коде предусматривать специальные операторы для обеспечения этой оптимизации. Но поскольку хочется знать, как это работает, мы рассмотрим пример и увидим, что скрывается от нашего внимания. В этом примере используются строки типа QString — одного из многих неявно совместно используемых Qt—классов:
QString str1 = "Humpty";
QString str2 = str1;
Мы присваиваем переменной str1 значение «Humpty» (Humpty-Dumpty — Шалтай—Болтай) и переменную str2 приравниваем к переменной str1. К этому моменту оба объекта QString ссылаются на одну и ту же внутреннюю структуру данных в памяти. Кроме символьных данных эта структура данных имеет счетчик ссылок, показывающий, сколько строк QString ссылается на одну структуру данных. Поскольку обе переменные ссылаются на одни данные, счетчик ссылок будет иметь значение 2.
str2[0] = 'D';
Когда мы модифицируем переменную str2, выполняется действительное копирование данных, чтобы переменные str1 и str2 ссылались на разные структуры данных и их изменение приводило к изменению их собственных копий данных. Счетчик ссылок данных переменной str1 («Humpty») принимает значение 1, и счетчик ссылок данных переменной str2 («Dumpty») тоже принимает значение 1. Значение 1 счетчика ссылок означает, что данные не используются совместно.
str2.truncate(4);
Если мы снова модифицируем переменную str2, никакого копирования не будет происходить, поскольку счетчик ссылок данных переменной str2 имеет значение 1. Функция truncate() непосредственно обрабатывает значение переменной str2, возвращая в результате строку «Dump». Счетчик ссылок по-прежнему имеет значение 1.
str1 = str2;
Когда мы присваиваем строку str2 строке str1, счетчик ссылок для данных str1 снижается до 0 и приводит к тому, что теперь никакая строка типа QString не содержит значения «Humpty». Память освобождается. Обе строки QStrings теперь ссылаются на значение «Dump», счетчик ссылок которого теперь имеет значение 2.
Часто не пользуются возможностью совместного использования данных в многопоточных программах из-за условий гонок при доступе к счетчикам ссылок. В Qt этой проблемы не возникает. Классы—контейнеры используют инструкции ассемблера при реализации атомарных операций со счетчиками. Эта технология доступна пользователям Qt через применение классов QSharedData и QSharedDataPointer.
Ассоциативные контейнеры
Ассоциативный контейнер содержит произвольное количество элементов одинакового типа, индексируемых некоторым ключом. Qt содержит два основных класса ассоциативных контейнеров: QМар<К, T> и QHash
QMap
Рис. 11.6. Ассоциативный массив, связывающий QString с int.
Простой способ вставки элементов в ассоциативный массив состоит в использовании функции insert():
QMap
map.insert("eins", 1);
map.insert("sieben", 7);
map.insert("dreiundzwanzig", 23);
Можно поступить по-другому — просто присвоить значение заданному ключу:
map["eins"] = 1;
map["sieben"] = 7;
map["dreiundzwanzig"] = 23;
Оператор [ ] может использоваться как для вставки, так и для поиска. Но если этот оператор используется для поиска значения, для которого не существует ключа, будет создан новый элемент с данным ключом и пустым значением. Чтобы не создавать случайно пустые элементы, вместо оператора [ ] можно использовать функцию value():
int val = map.value("dreiundzwanzig");
Если ключ отсутствует в ассоциативном массиве, возвращается значение по умолчанию, создаваемое стандартным конструктором данного типа значений. Для базовых типов и указателей возвращается нуль. Мы можем определить другое значение, используемое по умолчанию, с помощью второго аргумента функции value(), например:
int seconds = map.value("delay", 30);
Это эквивалентно следующим операторам:
int seconds = 30;
if (map.contains("delay"))
seconds = map.value("delay");
Типы данных К и T в ассоциативном массиве QMap
Класс QMap
Обычно ассоциативные массивы имеют одно значение для каждого ключа: если новое значение присваивается существующему ключу, старое значение заменяется новым, чтобы не было элементов с одинаковыми ключами. Можно иметь несколько пар ключ—значение с одинаковым ключом, если использовать функцию insertMulti() или удобный подкласс QMultiMap
QMultiMap
multiMap.insert(1, "one"); multiMap.insert(1, "eins");
multiMap.insert(1, "uno");
QList
QHash
Кроме стандартных требований, которым должен удовлетворять любой тип значений, хранимых в контейнере, для типа К в QHash
QHash
Хэш-таблицы обычно имеют одно значение на каждый ключ, однако одному ключу можно присвоить несколько значений, используя функцию insertMulti() или удобный подкласс QMultiHash
Кроме QHash
Для прохода по всем парам ключ—значение, находящимся в ассоциативном контейнере, проще всего использовать итератор в стиле Java. Поскольку итераторы должны обеспечивать доступ и к ключу, и к значению, итераторы в стиле Java работают с ассоциативными контейнерами немного иначе, чем с последовательными контейнерами. Основное отличие проявляется в том, что функции next() и previous() возвращают пару ключ—значение, а не просто одно значение. Компоненты ключа и значения можно извлечь из объекта пары с помощью функций key() и value(). Например:
QMap
…
int sum = 0;
QMapIterator
while (i.hasNext())
sum += i.next().value();
Если требуется получить доступ как к ключу, так и к значению, мы можем просто игнорировать значение, возвращаемое функциями next() и previous(), и использовать функции итератора key() и value(), которые работают с последним пройденным элементом.
QMapIterator
while (i.hasNext()) {
i.next();
if (i.value() > largestValue) {
largestKey = i.key();
largestValue = i.value();
}
}
Допускающие запись итераторы имеют функцию setValue(), которая модифицирует значение, содержащееся в текущем элементе:
QMutableMapIterator
while (i.hasNext()) {
i.next();
if (i.value()< 0.0)
i.setValue(-i.value());
}
Итераторы в стиле STL также имеют функции key() и value(). Для неконстантных типов итераторов value() возвращает неконстантную ссылку, позволяя нам изменять значение в ходе просмотра контейнера. Следует отметить, что хотя эти итераторы называются итераторами «в стиле STL», они существенно отличаются от итераторов STL контейнера map
Оператор цикла foreach также работает с ассоциативными контейнерами, но только с компонентом значение пар ключ—значение. Если нужны как ключи, так и значение, мы можем вызвать функции keys() и values(const К &) во внутреннем цикле foreach:
QMultiMap
…
foreach (QString key, map.keys()) {
foreach (int value, map.values(key)) {
do_something(key, value);
}
}
Обобщенные алгоритмы
В заголовочном файле
Заголовочный файл STL
Алгоритм qFind() выполняет поиск конкретного значения в контейнере. Он принимает «начальный» и «конечный» итераторы и возвращает итератор, ссылающийся на первый подходящий элемент, или «конечный» итератор, если нет подходящих элементов. В представленном ниже примере i устанавливается на list.begin() + 1, a j устанавливается на list.end().
QStringList list;
list << "Emma" << "Karl" << "James" << "Mariette";
QStringList::iterator i = qFind(list.begin(), list.end(), "Karl");
QStringList::iterator j = qFind(list.begin(), list.end(), "Petra");
Алгоритм qBinaryFind() выполняет поиск подобно алгоритму qFind(), за исключением того, что он предполагает упорядоченность элементов в возрастающем порядке и использует двоичный поиск в отличие от линейного поиска в qFind().
Алгоритм qFill() заполняет контейнер конкретным значением:
QLinkedList
qFill(list.begin(), list.end(), 1009);
Как и другие алгоритмы, основанные на применении итераторов, qFill() может выполняться для части контейнера, если соответствующим образом установить аргументы. В следующем фрагменте программного кода первые пять элементов вектора инициализируются значением 1009, а последние пять элементов — значением 2013:
QVector
qFill(vect.begin(), vect.begin() + 5, 1009);
qFill(vect.end() - 5, vect.end(), 2013);
Алгоритм qCopy() копирует значения одного контейнера в другой.
QVector
qCopy(list.begin(), list.end(), vect.begin());
Алгоритм qCopy() может также использоваться для копирования элементов в рамках одного контейнера, если исходный диапазон и целевой диапазон не перекрываются. В следующем фрагменте программного кода мы заменяем последние два элемента списка первыми двумя элементами:
qCopy(list.begin(), list.begin() + 2, list.end() - 2);
Алгоритм qSort() сортирует элементы контейнера в порядке их возрастания.
qSort(list.begin(), list.end());
По умолчанию qSort() использует оператор < для сравнения элементов. Для сортировки элементов по убыванию передайте qGreater
qSort(list.begin(), list.end(), qGreater
Мы можем использовать третий параметр для определения пользовательского критерия сортировки. Например, ниже приводится функция сравнения «меньше, чем», которая выполняет сравнение строк QString без учета регистра:
bool insensitiveLessThan(const QString &str1, const QString &str2)
{
return str1.toLower() < str2.toLower();
}
Тогда вызов qSort() будет таким:
QStringList list;
qSort(list.begin(), list.end(), insensitiveLessThan);
Алгоритм qStableSort() аналогичен qSort(), за исключением того, что он гарантирует сохранение порядка следования одинаковых элементов. Этот алгоритм стоит применять в тех случаях, когда критерий сортировки учитывает только часть значения элемента и пользователь видит результат сортировки. Мы использовали qStableSort() в для реализации сортировки в приложении Электронная таблица.
Алгоритм qDeleteAll() вызывает оператор delete для каждого указателя, хранимого в контейнере. Он имеет смысл только для контейнеров, в качестве элементов которых используются указатели. После вызова этого алгоритма элементы по-прежнему присутствуют в контейнере, и для их удаления используется функция clear(). Например:
qDeleteAll(list);
list.clear();
Алгоритм qSwap() выполняет обмен значений двух переменных. Например:
int x1 = line.x1();
int x2 = line.x2();
if (x1 > x2)
qSwap(x1, x2);
Наконец, заголовочный файл
Строки, массивы байтов и объекты произвольного типа
QString, QByteArray и QVariant — три класса, которые имеют много общего с контейнерами и могут использоваться в некоторых контекстах как альтернатива контейнерам. Кроме того, как и контейнеры, эти классы используют неявное совмещение данных для уменьшения расхода памяти и повышения быстродействия.
Мы начнем с рассмотрения типа QString. Строковые данные применяются в любой программе с графическим пользовательским интерфейсом и не только непосредственно для пользовательского интерфейса, но часто и в качестве структур данных. В стандартном составе С++ содержится два типа строк: традиционные символьные массивы языка С с завершающим символом «\0» и класс std::string. Класс QString содержит 16-битовые значения в коде Unicode. Unicode содержит в качестве подмножеств коды ASCII и Latin-1 с их обычным числовым представлением. Но поскольку QString имеет 16-битовые значения, он может представлять тысячи других символов, используемых для записи букв большинства мировых языков. Дополнительную информацию по кодировке Unicode вы найдете в .
При использовании QString не стоит беспокоиться о таких не очень понятных вещах, как выделение достаточного объема памяти или гарантирование завершения данных символом '\0'. Концептуально строки QString можно рассматривать как вектор символов QChar. Внутри QString могут быть символы '\0'. Функция length() возвращает размер строки, включая символы '\0'.
Класс QString содержит бинарный оператор +, обеспечивающий конкатенацию двух строк, и оператор += для добавления одной строки в конец другой. Поскольку QString заранее автоматически добавляет память в конец данных строки, построение строки путем повторения операций добавления символов в конец строки выполняется очень быстро. Ниже приводится пример обоих операторов:
QString str = "User: ";
str += userName + "\n";
Существует также функция QString::append(), которая делает то же самое, что и оператор +=:
str = "User: ";
str.append(userName);
str.append("\n");
Совершенно другой способ объединения строк заключается в использовании функции sprintf() класса QString:
str.sprintf("%s %.1f%%", "perfect competition", 100.0);
Данная функция поддерживает спецификаторы формата, используемые функцией библиотеки С++ sprintf(). В приведенном выше примере переменной str присваивается значение «perfect competition 100.0%» (абсолютно безупречное соревнование).
Имеется еще один способ составления строк из других строк или чисел, и он заключается в использовании функции arg():
str = QString("%1 %2 (%3s-%4s)")
.arg("реrmissive").arg("society").arg(1950).arg(1970);
В этом примере «%1» заменяется словом «permissive» (либеральное), «%2» заменяется словом «society» (общество), «%3» заменяется на «1950» и «%4» заменяется на «1970». В результате получаем «permissive society (1950s — 1970s)» (либеральное общество в 1950—70 годах). Функция arg() перегружается для обработки различных типов данных. В некоторых случаях используются дополнительные параметры для управления шириной поля, базой числа или точностью числа с плавающей точкой. В целом гораздо лучше использовать arg(), а не sprintf(), поскольку эта функция сохраняет тип, полностью поддерживает Unicode и позволяет трансляторам изменять порядок параметров «%1».
QString может преобразовывать числа в строки, используя статическую функцию QString::number():
str = QString::number(59.6);
Или это можно сделать при помощи функции setNum():
str.setNum(59.6);
Обратное преобразование строки в число осуществляется при помощи функций toInt(), toLongLong(), toDouble() и так далее. Например:
bool ok;
double d = str.toDouble(&ok);
Этим функциям передается необязательный параметр—ссылка на переменную типа bool, которая устанавливается на значение true или false в зависимости от успешности преобразования. Если преобразование завершается неудачей, эти функции возвращают 0.
Имея некоторую строку, нам часто приходится выделять какую-то ее часть. Функция mid() возвращает подстроку заданной длины (второй аргумент), начиная с указанной позиции (первый аргумент). Например, следующий программный код выводит на консоль слово «pays»:
QString str = "polluter pays principle";
qDebug() << str.mid(9, 4);
Существуют также функции left() и right(), которые выполняют аналогичную работу. Обеим функциям передается количество символов n, и они возвращают первые и последние n символов строки. Например, следующий программный код выдает на консоль слова «polluter principle»:
QString str = "polluter pays principle";
qDebug() << str.left(8) << " " << str.right(9);
Если требуется определить, содержится ли в строке конкретный символ, подстрока или соответствует ли строка регулярному выражению, мы можем использовать один из вариантов функции indexOf() класса QString:
QString str = "the middle bit";
int i = str.indexOf("middle");
В результате i становится равным 4. Функция indexOf() возвращает -1 при неудачном поиске и принимает в качестве необязательных аргументов начальную позицию и флажок учета регистра.
Если мы просто хотим проверить начальные или конечные символы строки, мы можем использовать функции startsWith () и endsWith():
if (url.startsWith("http:") && url.endsWith(".png"))
Это проще и быстрее, чем:
if (url.left(5) == "http:" && url.right(4) == ".png")
Оператор сравнения строк == зависит от регистра. Если сравниваются строки, которые пользователь видит на экране, обычно правильным решением будет использование функции localeAwareCompare(), а если необходимо сделать сравнение не зависимым от регистра, мы можем использовать функции toUpper() или toLower(). Например:
if (fileName.toLower() == "readme.txt")
Если мы хотим заменить определенную часть строки другой подстрокой, мы можем использовать функцию replace():
QString str= "a cloudy day";
str.replace(2, 6, "sunny");
Результатом является «sunny day» (солнечный день). Этот программный код может быть переписан с применением функций remove() и insert():
str.remove(2, 6);
str.insert(2, "sunny");
Во-первых, мы удаляем шесть символов, начиная с позиции 2, и в результате получаем строку «а_ _day» (с двумя пробелами), затем мы вставляем слово «sunny» в позицию 2.
Существуют перегруженные версии функции replace(), которые заменяют все подстроки, совпадающие со значением первого аргумента, вторым аргументом. Например, ниже показано, как можно заменить все символы «&» в строке на «&»:
str.replace("&", "&");
Часто требуется удалять из строки пробельные символы (пробелы, символы табуляции и перехода на новую строку). QString имеет функцию, которая удаляет эти символы с обоих концов строки:
QString str = " ВОВ \t THE \nDOG \n";
qDebug() << str.trimmed();
Строку str можно представить в виде
_ _ _ВОВ_\t_THE_ _\nDOG_\n
Строка, возвращаемая функцией trimmed(), имеет вид
ВОВ_\t_THE_ _\nDOG
При обработке введенных пользователем данных нам часто необходимо, кроме удаления пробельных символов с обоих концов строки, заменить каждую последовательность таких символов одним пробелом. Именно это выполняет функция simplified():
QString str = " ВОВ \t THE \nDOG \n";
qDebug() << str.simplified();
Строка, возвращаемая функцией simplified(), имеет вид
ВОВ_THE_DOG
Строку можно разбить на подстроки типа QStringList при помощи функции QList::split():
QString str = "polluter pays principle";
QStringList words = str.split(" ");
В приведенном выше примере мы разбиваем строку «polluter pays principle» на три подстроки: «polluter», «pays» и «principle». Функция split() имеет необязательный третий аргумент, показывающий, надо ли оставлять пустые подстроки (режим по умолчанию) или нет.
Элементы списка QStringList могут объединяться в одну строку при помощи функции join(). Передаваемый функции join() аргумент вставляется между каждой парой объединяемых строк. Например, ниже показано, как создавать одну строку из всех строк списка QStringList, расположенных в алфавитном порядке и разделенных символом перехода на новую строку:
words.sort();
str = words.join("\n");
При обработке строк нам часто приходится определять, пустая строка или нет. Это делается при помощи вызова функции isEmpty() или проверкой равенства нулю возвращаемого функцией length() значения.
Преобразование строк const char * в QString в большинстве случаев выполняется автоматически, например:
str += " (1870)";
Здесь мы добавляем строку const char * в конец строки QString без выполнения явного преобразования. Для явного преобразования const char * в QString выполните приведение типа в QString или вызовите функцию fromAscii() или fromLatin1(). (Работа с литеральными строками в других кодировках рассматривается в .)
Для преобразования QString в const char * используйте функцию toAscii() или toLatin1(). Эти функции возвращают QByteArray, который может быть преобразован в const char *, используя QByteArray::data() или QByteArray::constData(). Например:
printf("User: %s\n", str.toAscii().data());
Для удобства в Qt предусмотрен макрос qPrintable(), который эквивалентен последовательности функций toAscii().constData():
printf("User: %s\n", qPrintable(str));
Когда мы вызываем функции data() или constData() для объектов типа QByteArray, владельцем возвращаемой строки будет этот объект. Это означает, что нам не надо беспокоиться о возможных утечках памяти — Qt вернет нам память. С другой стороны, мы должны проявлять осторожность и не использовать указатель слишком долго. Если объект QByteArray не хранится в переменной, он будет автоматически удален в конце выполнения оператора.
Программный интерфейс класса QByteArray очень похож на программный интерфейс класса QString. Такие функции, как left(), right(), mid(), toLower(), toUpper(), trimmed() и simplified(), существуют в QByteArray и имеют такую же семантику, как и соответствующие функции в QString. QByteArray полезно использовать для хранения неформатированных двоичных данных и строк с 8-битовой кодировкой текста. В целом мы рекомендуем использовать QString для хранения текста, а не QByteArray, потому что QString поддерживает кодировку Unicode.
Для удобства QByteArray всегда автоматически обеспечивает наличие символа '\0' после последнего байта, облегчая передачу объекта QByteArray функции, принимающей const char *. QByteArray также может содержать внутри себя символы '\0', что позволяет использовать этот тип для хранения произвольных двоичных данных.
В некоторых ситуациях требуется в одной переменной хранить данные различных типов. Один из таких методов заключается в представлении этих данных в виде QByteArray или QString. Например, в виде строки можно хранить как текстовое значение, так и числовое значение. Эти подходы обеспечивают максимальную гибкость, но лишают некоторых преимуществ С++, в частности связанных с безопасностью типов и высокой эффективностью. Qt обеспечивает значительно более удобный способ для хранения данных различного типа: QVariant.
Класс QVariant может содержать значения многих типов Qt, включая QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize и QString, а также такие основные числовые типы С++, как double и int. Класс QVariant может, кроме того, содержать контейнеры QMap
Широкое распространение получило применение этого типа в классах отображения элементов, в модуле баз данных и в классе QSettings, позволяя считывать и записывать данные элементов, данные базы данных и пользовательские настройки в виде любого значения, допускаемого типом QVariant. Пример этого мы уже видели в , когда объекты QRect, QStringList и пара булевых значений передавались функции QSettings::setValue() и затем считывались как объекты QVariant.
Можно создавать произвольно сложные структуры данных, используя тип QVariant для обеспечения вложенных структур контейнеров:
QMap
pearMap["Standard"] = 1.95;
pearMap["Organic"] = 2.25;
QMap
fruitMap["Orange"] = 2.10;
fruitMap["Pineapple"] = 3.85;
fruitMap["Pear"] = pearMap;
Здесь мы создали отображение со строковыми ключами (названия продукции) и значениями, которыми могут быть либо числа с плавающей точкой (цены), либо отображения. Отображение верхнего уровня содержит три ключа: «Orange», «Pear» и «Pineapple» (апельсин, груша и ананас). Значение, связанное с ключом «Реаг», является отображением, содержащим два ключа «Standard» и «Organic» (стандартный и экологически чистый). При проходе по ассоциативному массиву, содержащему объекты QVariant, нам необходимо использовать функцию type() для проверки находящегося в QVariant типа, чтобы можно было его правильно обработать.
Способ создания подобным образом структур данных может быть очень привлекательным, поскольку мы можем создавать любые структуры данных. Но удобство применения типа QVariant достигается за счет снижения эффективности и читаемости программы. Для хранения наших данных, как правило, предпочтительнее использовать соответствующий класс языка С++ там, где это возможно.
QVariant используется мета-объектной системой Qt и поэтому является частью модуля QtCore. Тем не менее, когда мы собираем приложение с модулем QtGui, QVariant может хранить такие типы, связанные с графическим пользовательским интерфейсом, как QColor, QFont, QIcon, QImage или QPixmap:
QIcon icon("open.png");
QVariant variant = icon;
Для извлечения значений этих типов из QVariant мы можем следующим образом использовать шаблонную функцию—член QVariant::value
QIcon icon = variant.value
Функция value
QVariant может также использоваться для хранения пользовательских типов данных при условии обеспечения ими стандартного конструктора и конструктора копирования. Чтобы это заработало, прежде всего необходимо зарегистрировать тип, используя макрос Q_DECLARE_METATYPE() обычно в заголовочном файле после определения класса:
Q_DECLARE_METATYPE(BusinessCard)
Это позволяет нам написать следующий программный код:
BusinessCard businessCard;
QVariant variant = QVariant::fromValue(businessCard);
if (variant.canConvert
BusinessCard card = variant.value
}
Эти шаблонные функции—члены не будут работать с компилятором MSVC 6 из-за ограничений последнего. Если вы не можете отказаться от этого компилятора, вместо указанных функций используйте глобальные функции qVariantFromValue(), qVariantValue
Если в пользовательском типе данных предусмотрены операторы << и >> для записи и чтения из потока данных QDataStream, их можно зарегистрировать, используя функцию qRegisterMetaTypeStreamOperators
qRegisterMetaTypeStreamOperators
В данной главе основное внимание было уделено контейнерам Qt, а также классам QString, QByteArray и QVariant. Кроме этих классов Qt имеет несколько других контейнеров. Один из них — QPair
Алгоритмы Qt, включая несколько не рассмотренных здесь, например qCopyBackward() и qEqual(), описаны в документации Qt, которую можно найти по адресу . Более подробное описание контейнеров Qt, в том числе информацию об их временных и объемных характеристиках, можно найти на странице .
Глава 12. Ввод—вывод
Почти в каждом приложении приходится читать или записывать файлы или выполнять другие операции ввода—вывода. Qt обеспечивает великолепную поддержку ввода—вывода при помощи QIODevice — мощной абстракции «устройств», способных читать и записывать блоки байтов. Qt содержит следующие подклассы QIODevice:
• QFile — получает доступ к файлам, находящимся в локальной файловой системе или внедренным в исполняемый модуль,
• QTemporaryFile — создает временные файлы в локальной файловой системе и получает доступ к ним,
• QBuffer — считывает или записывает данные в QByteArray,
• QProcess — запускает внешние программы и обеспечивает связь между процессами,
• QTcpSocket — передает поток данных по сети, используя протокол TCP,
• QUdpSocket — передает и принимает из сети дейтаграммы UDP.
QProcess, QTcpSocket и QUdpSocket являются последовательными устройствами, т.е. они позволяют получить доступ к данным только один раз, начиная с первого байта и последовательно продвигаясь к последнему байту. QFile, QTemporaryFile и QBuffer являются устройствами произвольного доступа и позволяют считывать байты многократно из любой позиции; они используют функцию QIODevice::seek() для изменения положения указателя файла.
Кроме этих устройств Qt предоставляет два класса высокоуровневых потоков данных, которые можно использовать для чтения и записи на любое устройство ввода—вывода: QDataStream для двоичных данных и QTextStream для текста. Эти классы учитывают такие аспекты, как порядок байтов и кодировка текста, позволяя работающим на разных платформах и в разных странах приложениям Qt считывать и записывать файлы друг друга. Это делает классы Qt по вводу—выводу более удобными, чем соответствующие классы стандартного С++, при использовании которых решать подобные проблемы приходится прикладному программисту.
QFile позволяет легко получать доступ к отдельным файлам, независимо от того, располагаются они в файловой системе или оказываются внедренными в исполняемый модуль приложения как ресурсы. Для приложений, которым приходится работать с целыми наборами файлов, в Qt предусмотрены классы QDir и QFileInfo, которые позволяют работать с каталогами и получать сведения о файлах, расположенных внутри каталогов.
Класс QProcess позволяет нам запускать внешние программы и устанавливать связь с ними через стандартные каналы ввода, вывода и ошибок (cin, cout и cerr). Мы можем устанавливать переменные среды и рабочий каталог, которые будут использоваться внешним приложением. По умолчанию связь с процессом осуществляется в асинхронном режиме (без блокировок), но все же остается возможной блокировка определенных операций.
Работа с сетью, а также чтение и запись документов XML настолько важные темы, что будут рассмотрены отдельно в и , специально им посвященным.
Чтение и запись двоичных данных
Самый простой способ загрузки и сохранения двоичных данных в Qt — получить экземпляр класса QFile, открыть файл и получить к нему доступ через объект QDataStream. QDataStream обеспечивает независимый от платформы формат памяти, который поддерживает такие базовые типы С++, как int и double, и многие типы данных Qt, включая QByteArray, QFont, QImage, QPixmap, QString и QVariant, а также классы—контейнеры Qt, например QList
Ниже показано, как можно сохранить целый тип QImage и QMap
QImage image("philip.png");
QMap
map.insert("red", Qt::red);
map.insert("green", Qt::green);
map.insert("blue", Qt::blue);
QFile file("facts.dat");
if (!file.open(QIODevice::WriteOnly)) {
cerr << "Cannot open file for writing: "
<< qPrintable(file.errorString()) << endl;
return;
}
QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_1);
out << quint32(0x12345678) << image << map;
Если не удается открыть файл, мы информируем об этом пользователя и возвращаем управление. Макрос qPrintable() возвращает const char *, принимая QString. (Можно было бы поступить по-другому и использовать функцию QString::toStdString(), возвращающую тип std::string, для которого в
При успешном открытии файла мы создаем QDataStream и определяем его номер версии. Номер версии — это целое число, влияющее на представление в Qt типов данных (базовые типы данных С++ всегда представляются одинаково). В Qt 4.1 большинство сложных форматов имеют версию 7. Мы можем либо жестко закодировать в программе константу 7, либо использовать символическое имя QDataStream::Qt_4_1.
Чтобы обеспечить представление значения 0x12345678 в виде 32-битового целого числа без знака на всех платформах, мы приводим его тип к quint32 — типу данных, размер которого всегда равен точно 32 битам. Для обеспечения функциональной совместимости QDataStream по умолчанию использует прямой порядок байтов (big-endian); это можно изменить, вызывая функцию setByteOrder().
Нам не надо явно закрывать файл, поскольку это делается автоматически, когда переменная типа QFile выходит из области видимости. Если необходимо убедиться в том, что данные действительно записаны, мы можем вызвать функцию flush() и проверить возвращаемое значение (true при успешном завершении).
Программный код для чтения данных является зеркальным отражением кода, используемого нами для записи данных:
quint32 n;
QImage image;
QMap
QFile file("facts.dat");
if (!file.open(QIODevice::ReadOnly)) {
cerr << "Cannot open file for reading: "
<< qPrintable(file.errorString()) << endl;
return;
}
QDataStream in(&file);
in.setVersion(QDataStream::Qt_4_1);
in >> n >> image >> map;
При чтении используется та же самая версия QDataStream, которую мы использовали при записи. Это условие должно выполняться всегда. Жестко кодируя номер версии, мы гарантируем успешное чтение и запись данных приложением (при условии компиляции приложения с версией Qt 4.1 или более поздней версией Qt).
QDataStream так хранит данные, что мы сможем их считать обратно без особых усилий. Например, QByteArray представляется в виде структуры с 32-битовым счетчиком байтов, за которым идут сами байты. Используя функции readRawBytes() и writeRawBytes(), QDataStream может также применяться для чтения и записи неформатированных байтов, не имеющих заголовка в виде счетчика байтов.
Обрабатывать ошибки при чтении данных из потока QDataStream достаточно просто. Этот поток данных имеет функцию status(), возвращающую значения QDataStream::Ok, QDataStream::ReadPastEnd или QDataStream::ReadCorruptData. При возникновении ошибки оператор >> всегда считывает нулевые или пустые значения. Это означает, что во многих случаях можно просто считывать файл целиком, не беспокоясь о возможных ошибках, и в конце удостовериться в успешном выполнении чтения, проверив получаемое функцией status() значение.
QDataStream работает с разнообразными типами данных С++ и Qt; полный их список доступен в сети Интернет по адресу . Кроме того, можно добавить поддержку своих собственных пользовательских типов, перегружая операторы << и >>. Ниже приводится определение пользовательского типа данных, которое может быть использовано совместно с QDataStream:
01 class Painting
02 {
03 public:
04 Painting() { myYear = 0; }
05 Painting(const QString &title, const QString &artist, int year) {
06 myTitle = title;
07 myArtist = artist;
08 myYear = year;
09 }
10 void setTitle(const QString &title) { myTitle = title; }
11 QString title() const { return myTitle; }
12 …
13 private:
14 QString myTitle;
15 QString myArtist;
16 int myYear;
17 };
18 QDataStream &operator << (QDataStream &out, const Painting &painting);
19 QDataStream &operator >> (QDataStream &in, Painting &painting);
Ниже показана возможная реализация оператора <<:
01 QDataStream &operator << (QDataStream &out, const Painting &painting)
02 {
03 out << painting.title() << painting.artist()
04 << quint32(painting.year());
05 return out;
06 }
Для вывода Painting мы просто выводим две строки типа QString и значение типа quint32. В конце функции мы возвращаем поток. Этот обычный в С++ прием позволяет использовать последовательность операторов << для вывода данных в поток. Например:
out << painting1 << painting2 << painting3;
Реализация оператора >> аналогична реализации оператора <<.
01 QDataStream &operator >> (QDataStream &in, Painting &painting)
02 {
03 QString title;
04 QString artist;
05 quint32 year;
06 in >> title >> artist >> year;
07 painting = Painting(title, artist, year);
08 return in;
09 }
Обеспечение в пользовательских типах данных операторов ввода—вывода в поток дает несколько преимуществ. Одно из них заключается в том, что это позволяет нам выводить в поток контейнеры с пользовательскими типами. Например:
QList
out << paintings;
Мы можем так же просто считывать контейнеры:
QList
in >> paintings;
Это привело бы к ошибке компиляции, если бы тип Painting не поддерживал операции << или >>. Еще одно преимущество обеспечения потоковых операторов в пользовательских типах заключается в возможности хранения этих типов в виде объектов QVariant, что расширяет возможности их применения, например, в объектах QSettings. Это будет работать при условии предварительной регистрации типа с помощью функции qRegisterMetaTypeStreamOperators
При использовании QDataStream Qt обеспечивает чтение и запись каждого типа, включая контейнеры с произвольным числом элементов. Это освобождает нас от структурирования того, что мы записываем, и от выполнения какого бы то ни было синтаксического анализа того, что мы считываем. Необходимо лишь гарантировать чтение всех типов в той же последовательности, в какой они были записаны, предоставляя Qt обработку всех деталей.
QDataStream имеет смысл использовать как для своих собственных пользовательских форматов файлов, так и для стандартных двоичных форматов. Мы можем считывать и записывать стандартные форматы двоичных данных, используя потоковые операторы для базовых типов (например, quint16 или float) или при помощи функций readRawBytes() и writeRawBytes(). Если QDataStream используется только для чтения и записи «чистых» типов данных С++, нет необходимости вызывать функцию setVersion().
До сих пор мы загружали и сохраняли данные, жестко задавая в программе версию потока QDataStream::Qt_4_1. Этот подход прост, и он надежно работает, но он имеет один небольшой недостаток: мы не сможем воспользоваться новыми форматами и обновленными версиями форматов. Например, если в более поздней версии Qt добавится новый атрибут к QFont (кроме размера точки, наименования шрифта и так далее) и мы жестко закодируем номер версии Qt_4_1, этот атрибут не будет сохраняться и загружаться. Существует два решения. Первое решение заключается во включении номера версии QDataStream в файл:
QDataStream out(&file);
out << quint32(MagicNumber) << quint16(out.version());
(MagicNumber — это константа, которая уникально идентифицирует тип файла.) В этом случае мы всегда будем записывать данные с применением последней версии QDataStream (каким бы результат ни был). При считывании файла мы считываем номер версии потока:
01 quint32 magic;
02 quint16 streamVersion;
03 QDataStream in(&file);
04 in >> magic >> streamVersion;
05 if (magic != MagicNumber) {
06 cerr << "File is not recognized by this application" << endl;
07 return false;
08 } else if (streamVersion > in.version()) {
09 cerr << "File is from a more recent version of the application"
10 << endl;
11 return false;
12 }
13 in.setVersion(streamVersion);
Мы можем считывать данные, если версия потока меньше или совпадает с версией, используемой в приложении; в противном случае мы выдаем сообщение об ошибке.
Если файл использует формат с собственным номером версии, мы можем его использовать для определения номера версии потока, а не хранить этот номер в явном виде. Предположим, что файл сформирован в формате версии 1.3 нашего приложения. Тогда мы могли бы записать данные следующим образом:
QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_1);
out << quint32(MagicNumber) << quint16(0x0103);
При считывании данных мы определяем версию QDataStream на основе номера версии приложения:
01 QDataStream in(&file);
02 in >> magic >> appVersion;
03 if (magic != MagicNumber) {
04 cerr << "File is not recognized by this application" << endl;
05 return false;
06 } else if (appVersion > 0x0103) {
07 cerr << "File is from a more recent version of the application"
08 << endl;
09 return false;
10 }
11 if (appVersion < 0x0103) {
12 in.setVersion(QDataStream::Qt_3_0);
13 } else {
14 in.setVersion(QDataStream::Qt_4_1);
15 }
В этом примере мы говорим, что для любого файла, сохраненного в приложении с версией меньшей, чем 1.3, используется версия 4 потока данных (Qt_3_0), а для файлов, сохраненных в приложении с версией 1.3, используется версия 7 потока данных (Qt_4_1).
Итак, существует три политики работы с версиями потоков данных QDataStream: жесткое кодирование номера версии, запись и чтение номера версии в явном виде и использование различных жестко закодированных номеров версий в зависимости от версии приложения. Можно применять любую из этих политик для гарантирования чтения данных новой версией приложения, записанных в старой версии, даже если сборка новой версии приложения выполняется с более свежей версией Qt. После выбора политики обработки версий QDataStream чтение и запись двоичных данных в Qt становятся простыми и надежными.
Если мы хотим выполнить чтение или запись за один шаг, мы не должны использовать QDataStream, а вместо этого мы должны вызывать функции write() и readAll() класса QIODevice. Например:
01 bool copyFile(const QString &source, const QString &dest)
02 {
03 QFile sourceFile(source);
04 if (!sourceFile.open(QIODevice::ReadOnly))
05 return false;
06 QFile destFile(dest);
07 if (!destFile.open(QIODevice::WriteOnly))
08 return false;
09 destFile.write(sourceFile.readAll());
10 return sourceFile.error() == QFile::NoError
11 && destFile.error() == QFile::NoError;
12 }
В строке, где вызывается readAll(), все содержимое входного файла считывается в QByteArray, который затем передается функции write() для записи в выходной файл. Хранение всех данных в QByteArray ведет к большему расходу памяти, чем при последовательном чтении элементов, однако это дает некоторые преимущества. Например, мы можем затем использовать функции qCompress() и qUncompress() для упаковки и распаковки данных.
Существуют другие сценарии, когда прямой доступ к QIODevice оказывается более подходящим, чем использование QDataStream. Класс QIODevice имеет функцию peek(), которая возвращает следующие байты данных, перемещая позицию устройства, а также функцию ungetChar(), которая возвращает считанный байт в поток. Эти функции работают как на устройствах произвольного доступа (таких, как файлы), так и на последовательных устройствах (таких, как сетевые сокеты). Имеется также функция seek(), которая используется для установки позиции устройств, поддерживающих произвольный доступ.
Двоичные форматы файлов являются наиболее универсальным и компактным средством хранения данных, a QDataStream позволяет легко получить доступ к двоичным данным. Кроме примеров в данном разделе мы уже видели в , как QDataStream применяется для чтения и записи файлов в приложении Электронная таблица, и мы снова встретим этот класс в , где он будет использоваться для чтения и записи файлов курсоров в системе Windows.
Чтение и запись текста
Хотя двоичные форматы файлов обычно более компактные, чем текстовые форматы, они плохо воспринимаются человеком и не могут им редактироваться. Там, где последнее играет важную роль, можно использовать текстовые форматы. Qt предоставляет класс QTextStream для чтения и записи простых текстовых файлов или файлов других текстовых форматов, например HTML, XML, и файлы исходных текстов программ. Работа с XML—файлами рассматривается отдельно в .
QTextStream обеспечивает преобразование между Unicode и локальной кодировкой системы или любой другой кодировкой и незаметно для пользователя справляется с различными соглашениями относительно окончаний строк, принятыми в разных операционных системах («\r\n» в Windows, «\n» в Unix и Mac OS X). QTextStream использует 16-битовый тип QChar в качестве основного элемента данных. Кроме символов и строк QTextStream поддерживает основные числовые типы С++, преобразуя их в строку и обратно. Например, в следующем фрагменте программного кода выполняется запись строки «Thomas M. Disch: 334\n» в файл sf-book.txt:
QFile file("sf-book.txt");
if (!file.open(QIODevice::WriteOnly)) {
cerr << "Cannot open file for writing: "
<< qPrintable(file.errorString()) << endl;
return;
}
QTextStream out(&file);
out << "Thomas M. Disch: " << 334 << endl;
Записать текст очень просто, однако его чтение может оказаться трудной задачей, поскольку текстовый формат данных (в отличие от двоичного формата данных, записанных с помощью QDataStream) в принципе двусмысленный. Давайте рассмотрим следующий пример:
out << "Norway" << "Sweden";
Если out является объектом типа QTextStream, то данные в действительности записываются в виде строки «NorwaySweden». Мы не можем рассчитывать на то, что приведенная ниже строка правильно считает данные:
in >> str1 >> str2;
Фактически произойдет то, что строка str1 получит все слово «NorwaySweden», а строка str2 ничего не получит. При использовании класса QDataStream не возникнет таких трудностей, поскольку он сохраняет длину каждой строки в начале символьных данных.
Для сложных форматов файлов может потребоваться полнофункциональный парсер. Такой парсер мог бы считывать символ за символом при помощи оператора >> для типа QChar или строку за строкой при помощи функции QTextStream::readLine(). В конце этого раздела мы представим два небольших примера, в одном из которых входной файл считывается построчно, а в другом он считывается посимвольно. Для того чтобы использовать парсеры, работающие с целым текстом, мы могли бы считать весь файл за один шаг, используя функцию QTextStream::readAll(), если бы нас не волновал расход памяти или если бы мы знали, что файл будет небольшим.
По умолчанию QTextStream использует локальную кодировку системы (например, ISO 8859-1 или ISO 8859-15 в Америке и в большей части Европы) при чтении и записи. Это можно изменить, используя функцию setCodec():
stream.setCodec("UTF-8");
В этом примере используется кодировка UTF-8, совместимая с популярной кодировкой ASCII и позволяющая представить весь набор символов Unicode. Дополнительная информация о кодировке Unicode и о поддержке кодировок классом QTextStream приводится в («Интернационализация»).
QTextStream имеет различные опции, аналогичные опциям
out << showbase << uppercasedigits << hex << 12345678;
Ниже перечислены функции, устанавливающие опции для QTextStream (рис. 12.1):
• setIntegerBase(int):
0 — основание обнаруживается автоматически по префиксу (при чтении),
2 — двоичное представление,
8 — восьмеричное представление,
10 — десятичное представление,
16 — шестнадцатеричное представление.
• setNumberFlags(NumberFlags):
ShowBase — показывать префикс для оснований 2 («0b»), 8 («0») или 16 («0x»),
ForceSign — всегда показывать знак перед числами,
ForcePoint — всегда показывать десятичную точку,
UppercaseBase — префиксы оснований выдавать на верхнем регистре,
UppercaseDigits — буквы шестнадцатеричных чисел выдавать на верхнем регистре.
• setRealNumberNotation(RealNumberNotation):
FixedNotation — формат с фиксированной точкой (например, 0.000123),
ScientificNotation — научный формат (например, 0.12345678e-04),
SmartNotation — формат с фиксированной точкой или научный формат в зависимости от того, какой из них компактнее.
• setRealNumberPrecision(int) — устанавливает максимальное количество генерируемых цифр (по умолчанию 6).
• setFieldWidth(int) — устанавливает минимальный размер поля.
• setFieldAlignment(FieldAlignment):
AlignLeft — выравнивание влево, заполнитель занимает правую часть поля,
AlignRight — выравнивание вправо, заполнитель занимает левую часть поля,
AlignCenter — выравнивание по центру, заполнитель занимает оба края поля,
AlignAccountingStyle — заполнитель занимает область между знаком и числом.
• setPadChar(QChar) — устанавливает символ, используемый в качестве заполнителя (пробел по умолчанию).
Опции можно также устанавливать с помощью функций—членов:
out.setNumberFlags(QTextStream::ShowBase
| QTextStream::UppercaseDigits);
out.setIntegerBase(16);
out << 12345678;
Класс QTextStream, как и QDataStream, работает с каким-нибудь подклассом QIODevice: QFile, QTemporaryFile, QBuffer, QProcess, QTcpSocket или QUdpSocket. Кроме того, его можно использовать непосредственно со строкой типа QString. Например:
QString str;
QTextStream(&str) << oct << 31 << " " << dec << 25 << endl;
В результате переменная str будет иметь значение «37 25\n», поскольку десятичное число 31 представляется восьмеричным числом 37. В данном случае не требуется устанавливать кодировку, поскольку QString всегда использует Unicode.
Теперь рассмотрим простой пример текстового формата файлов. В приложении Электронная таблица, описанном в , мы использовали двоичный формат для хранения данных этого приложения. Данные представляют собой последовательность троек (строка, столбец, формула) — по одной на каждую непустую ячейку. Запись данных в виде текста выполняется просто; ниже показан фрагмент пересмотренной версии функции Spreadsheet::writeFile():
QTextStream out(&file);
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
QString str = formula(row, column);
if (!str.isEmpty())
out << row << " " << column << " " << str << endl;
}
}
Мы использовали простой формат, когда одна строка соответствует одной ячейке, причем пробелы разделяют номер строки и номер столбца, а также номер столбца и формулу. Формула может содержать пробелы, но мы предполагаем, что она не может содержать ни одного символа '\n' (который используется для завершения строки). Теперь давайте рассмотрим соответствующий программный код, предназначенный для чтения файла:
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
QStringList fields = line.split(' ');
if (fields.size() >= 3) {
int row = fields.takeFirst().toInt();
int column = fields.takeFirst().toInt();
setFormula(row, column, fields.join(' '));
}
}
Мы считываем одним оператором одну строку данных приложения Электронная таблица. Функция readLine() удаляет завершающий символ '\n'. Функция QString::split() возвращает список строк, разбивая строку на части согласно обнаруженным символам—разделителям. Например, при обработке строки «5 19 Total value» будет получен список из четырех элементов [«5», «19», «Total», «value»].
Данные могут быть извлечены, если имеется по крайней мере три поля. Функция QStringList::takeFirst() удаляет первый элемент списка и возвращает удаленный элемент. Мы используем ее для извлечения номеров строк и столбцов. Мы не делаем никакой проверки ошибок; если считываемый номер строки или номер столбца оказывается не числом, функция QString::toInt() возвратит 0. Вызывая функцию setFormula(), мы помещаем оставшиеся поля в одну строку.
В нашем втором примере с QTextStream мы будем посимвольно считывать текстовый файл и затем выводить этот же текст, удаляя из строки завершающие пробелы и заменяя символы табуляции пробелами. Всю эту работу делает функция tidyFile():
01 void tidyFile(QIODevice *inDevice, QIODevice *outDevice)
02 {
03 QTextStream in(inDevice);
04 QTextStream out(outDevice);
05 const int TabSize = 8;
06 int endlCount = 0;
07 int spaceCount = 0;
08 int column = 0;
09 QChar ch;
10 while (!in.atEnd()) {
11 in >> ch;
12 if (ch == '\n') {
13 ++endlCount;
14 spaceCount = 0;
15 column = 0;
16 } else if (ch == '\t') {
17 int size = TabSize - (column % TabSize);
18 spaceCount += size;
19 column += size;
20 } else if (ch == ' ') {
21 ++spaceCount;
22 ++column;
23 } else {
24 while (endlCount > 0) {
25 out << endl;
26 --endlCount;
27 column = 0;
28 }
29 while (spaceCount > 0) {
30 out << ' ';
31 --spaceCount;
32 ++column;
33 }
34 out << ch;
35 ++column;
36 }
37 }
38 out << endl;
39 }
Мы создаем для ввода и вывода данных объекты QTextStream, полученные на базе устройств QIODevice, переданных конструктору. Мы поддерживаем три переменные для контроля состояния: счетчик новых строк, счетчик пробелов и текущую позицию столбца в текущей строке (для преобразования символов табуляции в правильное количество пробелов).
Синтаксический анализ выполняется в цикле while, на каждом шаге которого считывается из входного файла один символ. В этой функции в некоторых местах делаются тонкие вещи. Например, хотя TabSize устанавливается на значение 8, мы заменяем символы табуляции достаточно точным числом пробелов, чтобы достигнуть следующей метки табуляции, а не грубо заменять каждый символ табуляции восемью пробелами. При встрече символа новой строки, символа табуляции и пробелов мы просто обновляем состояние данных. Только при получении символа нового вида мы выполняем вывод данных, а перед записью символа записываем ожидающие вывода символы новой строки и пробелы (чтобы учесть пробельные строки и сохранить отступы) и обновляем состояние.
01 int main()
02 {
03 QFile inFile;
04 QFile outFile;
05 inFile.open(stdin, QFile::ReadOnly);
06 outFile.open(stdout, QFile::WriteOnly);
07 tidyFile(&inFile, &outFile);
08 return 0;
09 }
В этом примере не нужен объект QApplication, потому что мы используем только инструментальные классы Qt. Список всех инструментальных классов приводится на веб-странице . Мы предполагаем, что эта программа используется как фильтр, например:
tidy < cool.cpp > cooler.cpp
Эту программу можно легко расширить, позволяя ей работать с именами файлов, указанными в командной строке, если они заданы, а в противном случае использовать ее для фильтрации потока ввода cin в поток вывода cout.
Поскольку это приложение консольное, его файл .pro немного отличается от используемого нами в приложениях с графическим интерфейсом:
TEMPLATE = app
QT = core
CONFIG += console
CONFIG -= app_bundle
SOURCES = tidy.cpp
Мы собираем приложение только с QtCore, поскольку здесь не используется функциональность графического пользовательского интерфейса. Затем мы указываем, что необходимо включить консольный вывод в Windows и не нужно размещать приложение в каталоге (bundle) приложений системы Mac OS X.
При чтении и записи простых ASCII—файлов и файлов с кодировкой ISO 8859-1 (Latin-1) можно непосредственно использовать программный интерфейс QIODevice вместо класса QTextStream. Поступать так имеет смысл только в редких случаях, поскольку в большинстве приложений требуется в некоторых случаях поддержка других кодировок и только QTextStream обеспечивает такую поддержку безболезненно. Если вы все-таки хотите писать текст непосредственно на устройство QIODevice, необходимо явно указать флажок QIODevice::Text в функции open(), например:
file.open(QIODevice::WriteOnly | QIODevice::Text);
Этот флажок говорит устройству QIODevice о том, что при записи в системе Windows необходимо преобразовывать символы '\n' в последовательность «\r\n». При чтении он говорит устройству, что необходимо игнорировать символы '\r' при работе на любой платформе. Теперь можно рассчитывать на то, что конец каждой строки обозначается символом новой строки '\n' вне зависимости от принятых на этот счет соглашений в операционной системе.
Работа с каталогами
Класс QDir обеспечивает независимые от платформы средства работы с каталогами и получение информации о файлах. Для демонстрации способов применения класса QDir мы напишем небольшое консольное приложение, которое подсчитывает размер дискового пространства, занимаемого всеми изображениями в указанном каталоге во всех его подкаталогах, вне зависимости от глубины их расположения.
Основу приложения составляет функция imageSpace(), которая рекурсивно подсчитывает общий размер изображений в заданном каталоге:
01 qlonglong imageSpace(const QString &path)
02 {
03 qlonglong size = 0;
04 QDir dir(path);
05 QStringList filters;
06 foreach (QByteArray format, QImageReader::supportedImageFormats())
07 filters += "*." + format;
08 foreach (QString file, dir.entryList(filters, QDir::Files))
09 size += QFileInfo(dir, file).size();
10 foreach (QString subDir, dir.entryList(QDir::Dirs
11 | QDir::NoDotAndDotDot))
12 size += imageSpace(path + QDir::separator() + subDir);
13 return size;
14 }
Мы начнем с создания объекта QDir для заданного пути, который может задаваться относительно текущего каталога или в виде полного пути. Мы передаем функции entryList() два аргумента. Первый аргумент содержит список фильтров имен файлов, разделенных пробелами. Шаблоны этих фильтров могут содержать символы «*» и «?». В этом примере мы применяем фильтры для включения только тех файлов, которые может считывать QImage. Второй аргумент задает тип нужных нам элементов (обычные файлы, каталоги, дисководы и так далее).
Мы выполняем цикл по списку файлов, подсчитывая их совокупный размер. Класс QFileInfo позволяет нам осуществлять доступ к таким атрибутам файлов, как их размер, права доступа, владелец и времена создания, изменения и последнего доступа.
Второй вызов функции entryList() получает все подкаталоги данного каталога. Мы выполняем цикл по ним (исключая . и ..) и рекурсивно вызываем функцию imageSpace() для получения совокупного размера изображений.
Для образования пути к каждому подкаталогу мы к текущему каталогу подсоединяем имя подкаталога, разделяя их слешем. Класс QDir использует символ «/» в качестве разделителя каталогов на всех платформах и распознает символ «\» в системе Windows. Представляя пути пользователю, мы можем вызвать статическую функцию QDir::convertSeparators() для преобразования слешей в соответствующий разделитель конкретной платформы.
Давайте добавим функцию main() в нашу небольшую программу:
01 int main(int argc, char *argv[])
02 {
03 QCoreApplication app(argc, argv);
04 QStringList args = app.arguments();
05 QString path = QDir::currentPath();
06 if (args.count() > 1)
07 path = args[1];
08 cout << "Space used by images in " << qPrintable(path)
09 << " and its subdirectories is "
10 << (imageSpace(path) / 1024) << " KB" << endl;
11 return 0;
12 }
Мы используем функцию QDir::currentPath() для получения пути текущего каталога. Мы могли бы поступить по-другому и использовать функцию QDir::homePath() для получения домашнего каталога пользователя. Если пользователь указал путь в командной строке, мы используем именно его. Наконец, мы вызываем нашу функцию imageSpace() для расчета размера пространства, занимаемого изображениями.
Класс QDir содержит и другие функции для работы с файлами и каталогами, включая entryInfoList() (которая возвращает список объектов QFileInfo), rename(), exists(), mkdir() и rmdir(). Класс QFile содержит несколько удобных статических функций, в том числе remove() и exists().
Ресурсы, внедренные в исполняемый модуль
До сих пор в этой главе мы говорили о доступе к данным, которые находятся на внешних устройствах, но в Qt можно также внедрять двоичные данные или текст в исполняемый модуль приложения. Это обеспечивается ресурсной системой Qt. В других главах мы использовали файлы ресурсов для внедрения файлов изображений в исполняемый модуль, однако внедрять можно любой файл. Читать внедренные файлы можно с использованием QFile, как будто это обычные файлы, расположенные в файловой системе.
Ресурсы преобразуются в программный код С++ ресурсным компилятором Qt (rcc). Мы можем указать qmake, что необходимо включить специальные правила для выполнения rcc, добавляя следующую строку в файл .pro:
RESOURCES = myresourcefile.qrc
Файл myresourcefile.qrc — это XML—файл, который содержит список файлов, внедренных в исполняемый модуль.
Допустим, создается приложение, которое сохраняет подробную контактную информацию. Ради удобства пользователей мы хотим внедрить международные телефонные коды в исполняемый модуль. Если файл находится в подкаталоге datafiles каталога сборки приложения, файл ресурсов может выглядеть следующим образом:
В приложении ресурсы опознаются по префиксу пути :/. В этом примере файл телефонных кодов имеет путь :/datafiles/phone-codes.dat и может быть считан как любой другой файл, используя QFile.
Преимуществом внедрения данных в исполняемый модуль является невозможность их потери и возможность создания действительно автономных исполняемых модулей (если использовалась статическая компоновка). Двумя недостатками являются необходимость замены всего исполняемого модуля при изменении внедренных данных и увеличение размера исполняемого модуля из-за дополнительного расхода памяти под внедренные данные.
Ресурсная система Qt обладает дополнительными возможностями, которые не представлены в этом примере, включая поддержку псевдонимов файлов и локализацию. Информацию по этим возможностям можно найти на веб-странице
Связь между процессами
Класс QProcess позволяет выполнять внешние программы и взаимодействовать с ними. Этот класс работает асинхронно и в фоновом режиме, из-за чего интерфейс пользователя по-прежнему будет реагировать на действия пользователя. QProcess посылает сигналы, уведомляющие нас о получении данных или о завершении работы.
Мы кратко рассмотрим программный код небольшого приложения, обеспечивающего интерфейс пользователя для внешней программы преобразования изображений. В нашем случае мы используем программу convert из пакета программ ImageMagick, который свободно распространяется на всех основных платформах.
Рис. 12.2. Приложение Image Converter.
Интерфейс пользователя приложения Image Converter (конвертор изображений) был создан при помощи Qt Designer. Файл .ui находится на компакт-диске, который входит в состав данной книги. Здесь мы основное внимание уделим подклассу, который является наследником сгенерированного компилятором uic класса Ui::ConvertDialog, и начнем с заголовочного файла:
01 #ifndef CONVERTDIALOG_H
02 #define CONVERTDIALOG_H
03 #include
04 #include
05 #include "ui_convertdialog.h"
06 class ConvertDialog : public QDialog,
07 public Ui::ConvertDialog
08 {
09 Q_OBJECT
10 public:
11 ConvertDialog(QWidget *parent = 0);
12 private slots:
13 void on_browseButton_clicked();
14 void on_convertButton_clicked();
15 void updateOutputTextEdit();
16 void processFinished(int exitCode, QProcess::ExitStatus exitStatus);
17 void processError(QProcess::ProcessError error);
18 private:
19 QProcess process;
20 QString targetFile;
21 };
22 #endif
Этот заголовочный файл создается по тому знакомому образцу, который используется в подклассах форм Qt Designer. Благодаря механизму автоматического связывания QtDesigner слоты on_browseButton_clicked() и on_convertButton_clicked() автоматически связываются с сигналом clicked() кнопок Browse (просмотреть) и Convert (преобразовать).
01 ConvertDialog::ConvertDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 setupUi(this);
05 connect(&process, SIGNAL(readyReadStandardError()),
06 this, SLOT(updateOutputTextEdit()));
07 connect(&process, SIGNAL(finished(int, QProcess::ExitStatus)),
08 this, SLOT(processFinished(int, QProcess::ExitStatus)));
09 connect(&process, SIGNAL(error(QProcess::ProcessError)),
10 this, SLOT(processError(QProcess::ProcessError)));
11 }
Вызов setupUi() создает и компонует все виджеты форм, устанавливает соединения сигнал—слот для слотов on_objectName_signalName() и связывает кнопку Quit (выйти) с функцией QDialog::accept(). После этого мы вручную связываем три сигнала объекта QProcess с тремя закрытыми слотами. Любые сообщения внешнего процесса для потока cerr мы будем обрабатывать в функции updateOutputTextEdit().
01 void ConvertDialog::on_browseButton_clicked()
02 {
03 QString initialName = sourceFileEdit->text();
04 if (initialName.isEmpty())
05 initialName = QDir::homePath();
06 QString fileName = QFileDialog::getOpenFileName(this,
07 tr("Choose File"), initialName);
08 fileName = QDir::convertSeparators(fileName);
09 if (!fileName.isEmpty()) {
10 sourceFileEdit->setText(fileName);
11 convertButton->setEnabled(true);
12 }
13 }
Сигнал clicked() кнопки Browse (просмотреть) автоматически связывается в функции setupUi() со слотом on_browseButton_clicked(). Если пользователь ранее выбирал какой-нибудь файл, мы инициализируем диалоговое окно выбора файла именем этого файла; в противном случае мы используем домашний каталог пользователя.
01 void ConvertDialog::on_convertButton_clicked()
02 {
03 QString sourceFile = sourceFileEdit->text();
04 targetFile = QFileInfo(sourceFile).path()
05 + QDir::separator() + QFileInfo(sourceFile).baseName()
06 + "." + targetFormatComboBox->currentText().toLower();
07 convertButton->setEnabled(false);
08 outputTextEdit->clear();
09 QStringList args;
10 if (enhanceCheckBox->isChecked())
11 args << "-enhance";
12 if (monochromeCheckBox->isChecked())
13 args << "-monochrome";
14 args << sourceFile << targetFile;
15 process.start("convert", args);
16 }
Когда пользователь нажимает кнопку Convert (преобразовать), мы копируем имя исходного файла и изменяем его расширение в соответствии с новым форматом файла. Мы используем зависимый от платформы разделитель каталогов ('/' или '\' возвращается функцией QDir::separator()) вместо жесткого кодирования этих символов, поскольку пользователь будет видеть имя файла.
Затем отключаем кнопку Convert, чтобы пользователь не мог случайно запустить одновременно несколько процессов преобразования, и очищаем поле текстового редактора, используемое нами для отображения информации о состоянии.
Для инициирования внешнего процесса мы вызываем функцию QProcess::start() с именем программы, которая должна выполняться (convert), и всеми ее аргументами. В данном случае мы передаем флажки -enhance и -monochrome, если пользователь выбрал соответствующие опции, и затем имена исходного и целевого файлов. Тип выполняемого преобразования программа convert определяет по расширениям файлов.
01 void ConvertDialog::updateOutputTextEdit()
02 {
03 QByteArray newData = process.readAllStandardError();
04 QString text = outputTextEdit->toPlainText()
05 + QString::fromLocal8Bit(newData);
06 outputTextEdit->setPlainText(text);
07 }
При всякой записи внешним процессом в поток cerr вызывается слот updateOutputTextEdit(). Мы считываем текст сообщения об ошибке и добавляем его в существующий текст QTextEdit.
01 void ConvertDialog::processFinished(int exitCode,
02 QProcess::ExitStatus exitStatus)
03 {
04 if (exitStatus == QProcess::CrashExit) {
05 outputTextEdit->append(tr("Conversion program crashed"));
06 } else if (exitCode != 0) {
07 outputTextEdit->append(tr("Conversion failed"));
08 } else {
09 outputTextEdit->append(tr("File %1 created").arg(targetFile));
10 }
11 convertButton->setEnabled(true);
12 }
По окончании процесса мы уведомляем пользователя о результате и включаем кнопку Convert.
01 void ConvertDialog::processError(QProcess::ProcessError error)
02 {
03 if (error == QProcess::FailedToStart) {
04 outputTextEdit->append(tr("Conversion program not found"));
05 convertButton->setEnabled(true);
06 }
07 }
Если процесс не удается запустить, QProcess генерирует сигнал error() вместо finished(). Мы выдаем сообщение об ошибке и включаем кнопку Convert.
В этом примере преобразования файлов выполнялись асинхронно, т.е. QProcess запускал программу convert и сразу же возвращал управление приложению. Это сохраняет работоспособность пользовательского интерфейса во время выполнения преобразований в фоновом режиме. Но в некоторых ситуациях необходимо, чтобы внешний процесс завершился, и только после этого мы сможем идти дальше в нашем приложении; в таких случаях требуется синхронная работа QProcess.
Одним из распространенных примеров, где желателен синхронный режим работы, является приложение, обеспечивающее редактирование простых текстов с применением текстового редактора, предпочитаемого пользователем. Такое приложение реализуется достаточно просто с помощью QProcess. Например, пусть в QTextEdit содержится простой текст и имеется кнопка Edit, при нажатии на которую выполняется слот edit().
01 void ExternalEditor::edit()
02 {
03 QTemporaryFile outFile;
04 if (!outFile.open())
05 return;
06 QString fileName = outFile.fileName();
07 QTextStream out(&outFile);
08 out << textEdit->toPlainText();
09 outFile.close();
10 QProcess::execute(editor, QStringList() << options << fileName);
11 QFile inFile(fileName);
12 if (!inFile.open(QIODevice::ReadOnly))
13 return;
14 QTextStream in(&inFile);
15 textEdit->setPlainText(in.readAll());
16 }
Мы используем QTemporaryFile для создания пустого файла с уникальным именем. Мы не задаем аргументы функции QTemporaryFile::open(), поскольку для нас подходит ее режим по умолчанию, по которому файл открывается для чтения и записи. Мы записываем содержимое поля редактирования во временный файл и затем закрываем файл, потому что некоторые текстовые редакторы не могут работать с уже открытыми файлами.
Статическая функция QProcess::execute() запускает внешний процесс и блокирует работу приложения до завершения процесса. Аргумент editor в строке типа QString содержит имя исполняемого модуля редактора (например, «gvim»). Аргумент options является списком QStringList (который содержит один элемент, «—f», если мы используем gvim).
После закрытия пользователем текстового редактора процесс завершает свою работу и функция execute() возвращает управление. Затем мы открываем временный файл и считываем его содержимое в QTextEdit. QTemporaryFile автоматически удаляет временный файл, когда объект выходит из области видимости.
При синхронной работе QProcess нет необходимости устанавливать соединения сигнал—слот. Если требуется более тонкое управление, чем то, которое обеспечивает статическая функция execute(), мы можем использовать альтернативный подход. Это означает создание объекта QProcess и вызов для него функции start() с последующей установкой блокировки путем вызова функции QProcess::waitForStarted(), после успешного завершения которой вызывается функция QProcess::waitForFinished(). Пример применения этого подхода можно найти в справочной документации по классу QProcess.
В данном разделе мы использовали QProcess, чтобы получить доступ к уже существующей функциональности. Применение уже имеющегося приложения может сократить время разработки и избавить нас от лишних деталей, которые играют второстепенную роль при достижении главной цели нашего приложения. Другой способ получения доступа к уже существующей функциональности заключается в компоновке приложения с соответствующей библиотекой. Но если нет подходящей библиотеки, хорошим решением может быть запуск консольного приложения с помощью QProcess.
QProcess может также применяться для запуска других приложений с графическим пользовательским интерфейсом, например веб—браузера или почтового клиента. Однако если нашей целью является связь между приложениями, а не просто запуск одного из другого, то лучше установить прямую связь между приложениями, используя Qt—классы, предназначенные для работы с сетью, или расширение ActiveQt для Windows.
Глава 13. Базы данных
Модуль QtSql средств разработки Qt обеспечивает независимый от платформы и типа базы данных интерфейс для доступа с помощью языка SQL к базам данных. Этот интерфейс поддерживается набором классов, использующих архитектуру Qt модель/представление для интеграции средств доступа к базам данных с интерфейсом пользователя. Эта глава предполагает знакомство с Qt—классами архитектуры модель/представление, рассмотренными в .
Связь с базой данных обеспечивается объектом QSqlDatabase. Qt использует драйверы для связи с программным интерфейсом различных баз данных. Версия Qt для настольных компьютеров (Qt Desktop Edition) включает в себя следующие драйверы:
QDB2 — IBM DB2 версии 7.1 и выше,
QIBASE — InterBase компании Borland,
QMYSQL — MySQL,
QOCI — Oracle (Oracle Call Interface, интерфейс вызовов Oracle),
QODBC — ODBC (включает Microsoft SQL Server),
QPSQL — PostgreSQL версий 6.x и 7.x,
QSQLITE — SQLite версии 3 и выше,
QSQLITE2 — SQLite версии 2,
QTDS — Sybase Adaptive Server.
Из-за лицензионных ограничений не все драйверы входят в состав издания Qt с открытым исходным кодом (Qt Open Source Edition). При настройке конфигурации Qt драйверы SQL можно либо непосредственно включить в состав Qt, либо использовать как подключаемые модули (plugins). Qt поставляется вместе с SQLite — общедоступной, не нуждающейся в сервере базой данных.
Для пользователей, хорошо знакомых с синтаксисом SQL, класс QSqlQuery предоставляет средства, позволяющие непосредственно выполнять произвольные команды SQL и обрабатывать их результаты. Для пользователей, предпочитающих иметь дело с высокоуровневым интерфейсом базы данных, который не требует знания синтаксиса SQL, классы QSqlTableModel и QSqlRelationalTableModel являются подходящими абстракциями. Эти классы представляют таблицы SQL в том же виде, как и классы других моделей Qt (рассмотренных в ). Они могут использоваться самостоятельно для кодирования в программе просмотра и редактирования данных или могут подключаться к представлениям, с помощью которых конечные пользователи будут сами просматривать и редактировать данные.
Qt также позволяет легко программировать такие распространенные идиомы баз данных, как отображение зависимых представлений для записей, связанных отношением «главная—подчиненные» (master—detail), и возможность многократной детализации выводимых на экран данных (drill-down), что продемонстрируют некоторые примеры этой главы.
Соединение с базой данных и выполнение запросов
Для выполнения запросов SQL мы должны сначала установить соединение с базой данных. Обычно настройка соединений с базой данных выполняется отдельной функцией, которую мы вызываем при запуске приложения. Например:
01 bool createConnection()
02 {
03 QSqlDatabase *db = QSqlDatabase::addDatabase("QOCI8");
04 db->setHostName("mozart.konkordia.edu");
05 db->setDatabaseName("musicdb");
06 db->setUserName("gbatstone");
07 db->setPassword("T17aV44");
08 if (!db->open()) {
09 db->lastError().showMessage();
10 return false;
11 }
12 return true;
13 }
Во-первых, мы вызываем функцию QSqlDatabase::addDatabase() для создания объекта QSqlDatabase. Первый аргумент функции addDatabase() задает драйвер базы данных, который Qt должна использовать для доступа к базе данных. В данном случае мы используем MySQL (??? — в коде QOCI, Oracle Call Interface).
Затем мы устанавливаем имя хоста базы данных, имя базы данных, имя пользователя и пароль, и мы открываем соединение. Если функция open() завершается неудачей, мы выводим сообщение об ошибке, используя QSqlError::showMessage().
Обычно функцию createConnection() вызывают в main():
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 if (!createConnection())
05 return 1;
06 return app.exec();
07 }
После установки соединения мы можем применять QSqlQuery для выполнения любой инструкции SQL, поддерживаемой используемой базой данных. Ниже приводится пример выполнения команды SELECT:
QSqlQuery query;
query.exec("SELECT title, year FROM cd WHERE year >= 1998");
После вызова функции exec() мы можем просмотреть результат запроса:
while (query.next()) {
QString title = query.value(0).toString();
int year = query.value(1).toInt();
cerr << qPrintable(title) << ": " << year << endl;
}
Мы вызываем функцию next() один раз для позиционирования QSqlQuery на первую запись полученного набора. Последующие вызовы next() продвигают указатель записи на одну позицию дальше, пока не будет достигнут конец, когда функция next() возвращает false. Если результирующий набор (result set) пустой (или запрос завершается неудачей), первый вызов функции next() возвратит false.
Функция value() возвращает значение поля, как QVariant. Поля пронумерованы начиная с 0 в порядке их указания в команде SELECT. Класс QVariant может содержать многие типы С++ и Qt, включая int и QString. Другие типы данных, которые могут храниться в базе данных, преобразуются в соответствующие типы С++ и Qt и хранятся в QVariant. Например, VARCHAR представляется в виде QString, a DATETIME — в виде QDateTime.
Класс QSqlQuery содержит некоторые другие функции для просмотра результирующего набора: first(), last(), previous() и seek(). Эти функции удобны, но для некоторых баз данных они могут выполняться медленнее и расходовать памяти больше, чем функция next(). При работе с большими наборами данных мы можем осуществить простую оптимизацию, вызывая функцию QSqlQuery::setForwardOnly(true) перед вызовом exec(), и только затем использовать next() для просмотра результирующего набора.
Ранее мы задавали запрос SQL в аргументе функции QSqlQuery::exec(), но, кроме того, мы можем передавать его непосредственно конструктору, который сразу же выполнит его:
QSqlQuery query("SELECT title, year FROM cd WHERE year >= 1998");
Мы можем проверить наличие ошибки, вызывая функцию isActive() для запроса:
if (!query.isActive())
QMessageBox::warning(this, tr("Database Error"),
query.lastError().text());
Если ошибки нет, запрос становится «активным» и мы можем использовать next() для перемещения по результирующему набору.
Выполнение команды INSERT осуществляется почти так же просто, как и команды SELECT:
QSqlQuery query("INSERT INTO cd (id, artistid, title, year) "
"VALUES (203, 102, 'Living in America', 2002)");
После этого функция numRowsAffected() возвращает количество строк, которые были изменены инструкцией SQL (или —1, если возникла ошибка).
Если нам необходимо вставлять много записей или если мы хотим избежать преобразования значений в строковые данные (и правильного преобразования специальных символов), мы можем использовать функцию prepare() для указания полей в шаблоне запроса и затем присваивания им необходимых нам значений. Qt поддерживает как стиль Oracle, так и стиль ODBC для всех баз данных, применяя, где возможно, «родной» интерфейс базы данных или имитируя его в противном случае. Ниже приводится пример, в котором используется синтаксис Oracle для представления поименованных полей:
QSqlQuery query;
query.prepare("INSERT INTO cd (id, artistid, title, year) "
"VALUES (:id, :artistid, :title, :year)");
query.bindValue(":id", 203);
query.bindValue(":artistid", 102);
query.bindValue(":title", "Living in America");
query.bindValue(":year", 2002);
query.exec();
Ниже приводится тот же пример позиционного представления полей в стиле ODBC:
QSqlQuery query;
query.prepare("INSERT INTO cd (id, artistid, title, year) "
"VALUES (?, ?, ?, ?)");
query.addBindValue(203);
query.addBindValue(102);
query.addBindValue("Living in America");
query.addBindValue(2002);
query.exec();
После вызова функции exec() мы можем вызвать bindValue() или addBindValue() для присваивания новых значений, затем снова вызвать exec() для выполнения запроса уже с новыми значениями.
Такие шаблоны часто используются для задания двоичных строковых данных, содержащих символы не в коде ASCII или Latin-1. Незаметно для пользователя Qt использует Unicode в тех базах данных, которые поддерживают Unicode, а в тех, которые не делают этого, Qt также незаметно для пользователя преобразует строковые данные в соответствующую кодировку.
Qt поддерживает SQL—транзакции в тех базах данных, где они предусмотрены. Для запуска транзакции мы вызываем функцию transaction() для объекта QSqlDatabase, представляющего соединение с базой данных. Для завершения транзакции мы вызываем либо функцию commit(), либо функцию rollback(). Например, ниже показано, как мы можем найти внешний ключ (foreign key) и выполнить команду INSERT внутри транзакции:
QSqlDatabase::database().transaction();
QSqlQuery query;
query.exec("SELECT id FROM artist WHERE name= 'Gluecifer'");
if (query.next()) {
int artistId = query.value(0).tolnt();
query.exec("INSERT INTO cd (id, artistid, title, year) "
"VALUES (201, " + QString::number(artistId)
+ ", 'Riding the Tiger', 1997)");
}
QSqlDatabase::database().commit();
Функция QSqlDatabase::database() возвращает объект QSqlDatabase, представляющий соединение, созданное нами при вызове createConnection(). Если транзакция не может запуститься, функция QSqlDatabase::transaction() возвращает false. Некоторые базы данных не поддерживают транзакции. В этом случае функции transaction(), commit() и rollback() ничего не делают. Мы можем проверить возможность поддержки базой данных транзакций путем вызова функции hasFeature() для объекта QSqlDriver, связанного с базой данных:
QSqlDriver *driver = QSqlDatabase::database().driver();
if (driver->hasFeature(QSqlDriver::Transactions))
…
Можно проверить наличие в базе данных ряда других возможностей, включая поддержку объектов BLOB (Binary Large Objects — большие двоичные объекты), Unicode и подготовленных запросов.
В приводимых до сих пор примерах мы предполагали, что в приложении используется одно соединение с базой данных. Если мы хотим создать несколько соединений, мы можем передавать название соединения в качестве второго аргумента функции addDatabase(). Например:
QSqlDatabase *db = QSqlDatabase::addDatabase("QPSQL", "OTHER");
db. setHostName("saturn.mcmanamy.edu");
db.setDatabaseName("starsdb");
db.setUserName("hilbert");
db.setPassword("ixtapa7");
Мы можем затем получить указатель на объект QSqlDatabase, передавая название соединения функции QSqlDatabase::database():
QSqlDatabase db = QSqlDatabase::database("OTHER");
Для выполнения запросов с другим соединением мы передаем объект QSqlDatabase конструктору QSqlQuery:
QSqlQuery query(db);
query.exec("SELECT id FROM artist WHERE name = 'Mando Diao'");
Несколько соединений полезны, если мы хотим выполнять одновременно несколько транзакций, поскольку каждое соединение может использоваться только для одной активной транзакции. Когда мы используем несколько соединений с базой данных, мы можем все-таки иметь одно непоименованное соединение и QSqlQuery будет использовать это соединение, если не указано поименованное соединение.
Кроме QSqlQuery Qt содержит класс QSqlTableModel — интерфейс высокого уровня, позволяя нам не использовать выражения SQL «в чистом виде» для выполнения наиболее распространенных SQL—команд (SELECT, INSERT, UPDATE и DELETE). Этот класс может использоваться автономно без какого-либо графического пользовательского интерфейса или в качестве источника данных для QListView или QTableView.
Ниже приводится пример использования QSqlTableModel для выполнения команды SELECT:
QSqlTableModel model;
model.setTable("cd");
model.setFilter("year >= 1998");
model.select();
Это эквивалентно запросу
SELECT * FROM cd WHERE year >= 1998
Просмотр результирующего набора выполняется путем получения заданной записи функцией QSqlTableModel::record() и доступа к отдельным полям с помощью функции value():
for (int i = 0; i < model.rowCount(); ++i) {
QSqlRecord record = model.record(i);
QString title = record.value("title").toString();
int year = record.value("year").toInt();
cerr << qPrintable(title) << ": " << year << endl;
}
Функция QSqlRecord::value() принимает либо имя поля, либо индекс поля. При работе с большими наборами данных рекомендуется задавать поля с помощью их индексов. Например:
int titleIndex = model.record().indexOf("title");
int yearIndex = model.record().indexOf("year");
for (int i = 0; i < model.rowCount(); ++i) {
QSqlRecord record = model.record(i);
QString title = record.value(titleIndex).toString();
int year = record.value(yearIndex).toInt();
cerr << qPrintable(title) << ": " << year << endl;
}
Для вставки записи в таблицу базы данных мы действуем так же, как если бы делали вставку в двумерную модель: сначала вызываем функцию insertRow() для создания новой пустой строки (записи) и затем используем setData() для установки значения каждого столбца (поля записи).
QSqlTableModel model;
model.setTable("cd");
int row = 0;
model.insertRows(row, 1);
model.setData(model.index(row, 0), 113);
model.setData(model.index(row, 1), "Shanghai My Heart");
model.setData(model.index(row, 2), 224);
model.setData(model.index(row, 3), 2003);
model.submitAll();
После вызова submitAll() запись может быть перемещена в другую позицию, зависящую от упорядоченности таблицы. Вызов submitAll() возвратит false, если вставка окажется неудачной.
Важным отличием модели SQL от стандартной модели является необходимость вызова в модели SQL функции submitAll() для записи всех изменений в базу данных
Для обновления записи мы должны сначала установить QSqlTableModel на запись, которую мы хотим модифицировать (например, используя функции select()). Затем мы извлекаем запись, обновляем соответствующие поля и записываем наши изменения обратно в базу данных:
QSqlTableModel model;
model.setTable("cd");
model.setFilter("id = 125");
model.select();
if (model.rowCount() == 1) {
QSqlRecord record = model.record(0);
record.setValue("title", "Melody A.M.");
record.setValue("year", record.value("year").toInt() + 1);
model.setRecord(0, record);
model.submitAll();
}
Если имеется запись, удовлетворяющая заданному фильтру, доступ к ней мы получаем при помощи функции QSqlTableModel::record(). Мы осуществляем наши изменения и вновь записываем в базу данных запись с новыми значениями полей.
Кроме того, обновление можно выполнить при помощи функции setData(), как это делается для модели, отличной от SQL—модели. Для получения доступа к полям записи используются индексы модели с указанием номера строки (записи) и столбца (поля):
model.select();
if (model.rowCount() == 1) {
model.setData(model.index(0, 1), "Melody A.M.");
model.setData(model.index(0, 3),
model.data(model.index(0, 3)).toInt() + 1);
model.submitAll();
}
Удаление записи напоминает ее обновление:
model.setTable("cd");
model.setFilter("id = 125");
model.select();
if (model.rowCount() == 1) {
model.removeRows(0, 1);
model.submitAll();
}
В вызове removeRows() указываются номер строки первой удаляемой записи и количество удаляемых записей. В следующем примере удаляются все записи, удовлетворяющие фильтру:
model.setTable("cd");
model.setFilter("year < 1990");
model.select();
if (model.rowCount() > 0) {
model.removeRows(0, model.rowCount());
model.submitAll();
}
Классы QSqlQuery и QSqlTableModel обеспечивают интерфейс между Qt и базой данных SQL. Используя эти классы, можно создавать формы, представляющие данные пользователям и позволяющие им вставлять, обновлять и удалять записи.
Представление данных в табличной форме
Во многих случаях табличное представление является самым простым представлением набора данных для пользователей. В этом и последующих разделах мы рассмотрим простое приложение CD Collection (Коллекция компакт-дисков), в котором модель QSqlTableModel и ее подкласс QSqlRelationalTableModel используются для просмотра и взаимодействия пользователей с данными, хранимыми в базе данных.
Главная форма показывает представление «master—detail» для компакт-дисков и дорожек текущего компакт-диска (рис. 13.1).
Рис. 13.1. Приложение CD Collection.
В приложении используются три таблицы, определенные следующим образом:
CREATE TABLE artist (
id INTEGER PRIMARY KEY,
name VARCHAR(40) NOT NULL,
country VARCHAR(40));
CREATE TABLE cd (
id INTEGER PRIMARY KEY,
title VARCHAR(40) NOT NULL,
artistid INTEGER NOT NULL,
year INTEGER N0T NULL,
FOREIGN KEY (artistid) REFERENCES artist);
CREATE TABLE track (
id INTEGER PRIMARY KEY,
title VARCHAR(40) NOT NULL,
duration INTEGER NOT NULL,
cdid INTEGER NOT NULL,
FOREIGN KEY (cdid) REFERENCES cd);
Некоторые базы данных не поддерживают внешние ключи. В этом случае мы должны убрать фразы FOREIGN KEY. Пример будет все-таки работать, но база данных не будет поддерживать целостность на уровне ссылок.
Рис. 13.2. Таблицы приложения CD Collection.
В этом разделе мы создадим диалоговое окно, позволяющее пользователю редактировать список артистов, используя простую форму с таблицей. Пользователь может вставлять, обновлять или удалять артистов при помощи кнопок формы. Обновления можно делать напрямую, просто редактируя текст ячеек. Изменения вносятся в базу данных при нажатии пользователем кнопки Enter или при переходе на другую запись.
Рис. 13.3. Диалоговое окно ArtistForm.
Ниже приводится определение класса для диалогового окна ArtistForm:
01 class ArtistForm : public QDialog
02 {
03 Q_OBJECT
04 public:
05 ArtistForm(const QString &name, QWidget *parent = 0);
06 private slots:
07 void addArtist();
08 void deleteArtist();
09 void beforeInsertArtist(QSqlRecord &record);
10 private:
11 enum {
12 Artist_Id = 0,
13 Artist_Name = 1,
14 Artist_Country = 2
15 };
16 QSqlTableModel *model;
17 QTableView *tableView;
18 QPushButton *addButton;
19 QPushButton *deleteButton;
20 QPushButton *closeButton;
21 };
Конструктор этого класса очень похож на конструктор, который использовался бы для создания формы, построенной для модели, отличной от SQL—модели:
01 ArtistForm::ArtistForm(const QString &name, QWidget *parent)
02 : QDialog(parent)
03 {
04 model = new QSqlTableModel(this);
05 model->setTable("artist");
06 model->setSort(Artist_Name, Qt::AscendingOrder);
07 model->setHeaderData(Artist_Name, Qt::Horizontal, tr("Name"));
08 model->setHeaderData(Artist_Country, Qt::Horizontal, tr("Country"));
09 model->select();
10 connect(model, SIGNAL(beforeInsert(QSqlRecord &)),
11 this, SLOT(beforeInsertArtist(QSqlRecord &)));
12 tableView = new QTableView;
13 tableView->setModel(model);
14 tableView->setColumnHidden(Artist_Id, true);
15 tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
16 tableView->resizeColumnsToContents();
17 for (int row = 0; row < model->rowCount(); ++row) {
18 QSqlRecord record = model->record(row);
19 if (record.value(Artist_Name).toString() == name) {
20 tableView->selectRow(row);
21 break;
22 }
23 }
24 …
25 }
Конструктор начинается с создания объекта QSqlTableModel. Мы передаем this в качестве родителя, чтобы владельцем модели стала форма. Нами выбрана сортировка по столбцу 1 (задается константой Artist_Name), который соответствует полю имени. Если бы мы не задали заголовки столбцов, то использовались бы имена полей. Мы предпочитаем их указать, чтобы обеспечить правильный регистр и локализацию.
Затем создается QTableView для визуального отображения модели. Мы не показываем поле id и устанавливаем такую ширину столбцов, которая будет достаточна для размещения в них текста без необходимости вывода многоточия.
Конструктор ArtistForm принимает имя артиста, который будет выбран при выводе на экран диалогового окна. Мы проходим по записям таблицы artist и выбираем этого артиста. Остальная часть программного кода конструктора используется для создания кнопок и подключения к ним слотов, а также для компоновки дочерних виджетов в диалоговом окне.
01 void ArtistForm::addArtist()
02 {
03 int row = model->rowCount();
04 model->insertRow(row);
05 QModelIndex index = model->index(row, Artist_Name);
06 tableView->setCurrentIndex(index);
07 tableView->edit(index);
08 }
Для добавления нового артиста мы вставляем одну пустую строку в конец табличного представления QTableView. Теперь пользователь может вводить имя нового артиста и его страну. Если пользователь подтверждает вставку, нажимая кнопку Enter, генерируется сигнал beforeInsert(), и после этого новая запись вставляется в базу данных.
01 void ArtistForm::beforeInsertArtist(QSqlRecord &record)
02 {
03 record.setValue("id", generateId("artist"));
04 }
В конструкторе мы связываем сигнал модели beforeInsert() с этим слотом. Мы передаем неконстантную ссылку на запись непосредственно перед ее вставкой в базу данных. Здесь мы устанавливаем значение поля id.
Поскольку нам потребуется вызывать функцию generateId() несколько раз, мы определяем ее как inline—функцию в заголовочном файле и включаем ее каждый раз по мере необходимости. Ниже дается простой (и неэффективный) способ ее реализации:
01 inline int generateId(const QString &table)
02 {
03 QSqlQuery query;
04 query.exec("SELECT MAX(id) FROM " + table);
05 int id = 0;
06 if (query.next())
07 id = query.value(0).tolnt() + 1;
08 return id;
09 }
Функция generateId() может гарантированно работать правильно, если она выполняется в рамках контекста одной транзакции соответствующей команды INSERT. Некоторые базы данных поддерживают средство автоматической генерации полей, и обычно значительно лучше использовать предусмотренные в базе данных специальные средства поддержки этой операции.
Удаление — это последняя операция, которую позволяет сделать диалоговое окно ArtistForm. Вместо каскадного удаления (вскоре будет рассмотрено) мы разрешаем удалять артистов только в том случае, если в коллекции нет их компакт-дисков.
01 void ArtistForm::deleteArtist()
02 {
03 tableView->setFocus();
04 QModelIndex index = tableView->currentIndex();
05 if (!index.isValid())
06 return;
07 QSqlRecord record = model->record(index.row());
08 QSqlTableModel cdModel;
09 cdModel.setTable("cd");
10 cdModel.setFilter("artistid = " + record.value("id").toString());
11 cdModel.select();
12 if (cdModel.rowCount() == 0) {
13 model->removeRow(tableView->currentIndex().row());
14 } else {
15 QMessageBox::information(this, tr("Delete Artist"),
16 tr("Cannot delete %1 because there are CDs associated "
17 "with this artist in the collection.")
18 .arg(record.value("name").toString()));
19 }
20 }
Если выделена какая-то запись, мы проверяем наличие компакт-дисков у данного артиста, и если они отсутствуют, мы сразу же удаляем эту запись артиста. В противном случае мы выводим на экран окно с сообщением о причине невыполнения удаления. Строго говоря, здесь следовало бы использовать транзакцию, потому что из программного кода видно, что между вызовами функций cdModel.select() и model->removeRow() у артиста может появиться свой компакт-диск. Транзакция будет рассмотрена в следующем разделе.
Создание форм по технологии «master—detail»
Теперь мы рассмотрим главную форму, которая реализует подход «master—detail». Главный вид представляет собой список компакт-дисков. Вид описания деталей представляет собой список дорожек текущего компакт-диска. Это диалоговое окно является главным окном приложения CD Collection (Коллекция компакт-дисков); оно показано на рис. 13.1.
01 class MainForm : public QWidget
02 {
03 Q_OBJECT
04 public:
05 MainForm();
06 private slots:
07 void addCd();
08 void deleteCd();
09 void addTrack();
10 void deleteTrack();
11 void editArtists();
12 void currentCdChanged(const QModelIndex &index);
13 void beforeInsertCd(QSqlRecord &record);
14 void beforeInsertTrack(QSqlRecord &record);
15 void refreshTrackViewHeader();
16 private:
17 enum {
18 Cd_Id = 0,
19 Cd_Title = 1,
20 Cd_ArtistId = 2,
21 Cd_Year = 3
22 };
23 enum {
24 Track_Id = 0,
25 Track_Title = 1,
26 Track_Duration = 2,
27 Track_CdId = 3
28 };
29 QSqlRelationalTableModel *cdModel;
30 QSqlTableModel *trackModel;
31 QTableView *cdTableView;
32 QTableView *trackTableView;
33 QPushButton *addCdButton;
34 QPushButton *deleteCdButton;
35 QPushButton *addTrackButton;
36 QPushButton *deleteTrackButton;
37 QPushButton *editArtistsButton;
38 QPushButton *quitButton;
39 };
Мы используем для таблицы компакт-дисков cd модель QSqlRelationalTableModel, а не простую модель QSqlTableModel, потому что нам придется работать с внешними ключами. Мы рассмотрим по очереди все функции, начиная с конструктора, который мы разобьем на несколько секций из-за его большого размера.
01 MainForm::MainForm()
02 {
03 cdModel = new QSqlRelationalTableModel(this);
04 cdModel->setTable("cd");
05 cdModel->setRelation(Cd_ArtistId,
06 QSqlRelation("artist", "id", "name"));
07 cdModel->setSort(Cd_Title, Qt::AscendingOrder);
08 cdModel->setHeaderData(Cd_Title, Qt::Horizontal, tr("Title"));
09 cdModel->setHeaderData(Cd_ArtistId, Qt::Horizontal, tr("Artist"));
10 cdModel->setHeaderData(Cd_Year, Qt::Horizontal, tr("Year"));
11 cdModel->select();
Конструктор начинается с настройки модели QSqlRelationalTableModel, которая управляет таблицей cd. Вызов setRelation() указывает модели на то, что ее поле artistid (индекс которого находится в переменной Cd_ArtistId) содержит идентификатор id внешнего ключа из таблицы артистов artist и что вместо идентификаторов необходимо выводить на экран содержимое соответствующего поля name. Если пользователь переходит в режим редактирования этого поля (например, нажимая клавишу F2), модель автоматически выведет на экран поле с выпадающим списком имен всех артистов, и если пользователь выбирает другого артиста, таблица cd будет обновлена.
12 cdTableView = new QTableView;
13 cdTableView->setModel(cdModel);
14 cdTableView->setItemDelegate(new QSqlRelationalDelegate(this));
15 cdTableView->setSelectionMode(QAbstractItemView::SingleSelection);
16 cdTableView->setSelectionBehavior(QAbstractItemView::SelectRows);
17 cdTableView->setColumnHidden(Cd_Id, true);
18 cdTableView->resizeColumnsToContents();
Настройка представления таблицы cd выполняется аналогично тому, что мы уже делали. Единственным существенным отличием является применение QSqlRelationalDelegate вместо делегата по умолчанию. Именно этот делегат обеспечивает работу с внешними ключами.
19 trackModel = new QSqlTableModel(this);
20 trackModel->setTable("track");
21 trackModel->setHeaderData(Track_Title, Qt::Horizontal, tr("Title"));
22 trackModel->setHeaderData(Track_Duration, Qt::Horizontal,
23 tr("Duration"));
24 trackTableView = new QTableView;
25 trackTableView->setModel(trackModel);
26 trackTableView->setItemDelegate(
27 new TrackDelegate(Track_Duration, this));
28 trackTableView->setSelectionMode(QAbstractItemView::SingleSelection);
29 trackTableView->setSelectionBehavior(QAbstractItemView::SelectRows);
Для дорожек мы собираемся выводить на экран только названия песен и их длительности, поэтому достаточно использовать модель QSqlTableModel. (Поля id и cdid, используемые в рассмотренном ниже слоте currentCdChanged(), не выводятся на экран.) Единственно, на что следует обратить внимание в этой части программного кода, — это использование разработанного в класса TrackDelegate, показывающего времена дорожек в виде «минуты:секунды» и позволяющего их редактировать с помощью удобного класса QTimeEdit.
Создание представлений и кнопок, их компоновка и соединения сигнал—слот не содержат ничего особенного, поэтому из оставшейся части конструктора мы покажем только несколько не совсем очевидных соединений.
30 …
31 connect(cdTableView->selectionModel(),
32 SIGNAL(currentRowChanged(const QModelIndex &,
33 const QModelIndex &)),
34 this, SLOT(currentCdChanged(const QModelIndex &)));
35 connect(cdModel, SIGNAL(beforeInsert(QSqlRecord &)),
36 this, SLOT(beforeInsertCd(QSqlRecord &)));
37 connect(trackModel, SIGNAL(beforeInsert(QSqlRecord &)),
38 this, SLOT(beforeInsertTrack(QSqlRecord &)));
39 connect(trackModel, SIGNAL(rowsInserted(
40 const QModelIndex &, int, int)),
41 this, SLOT(refreshTrackViewHeader()));
42 …
43 }
Первое соединение необычно, поскольку вместо связывания виджета мы связываем модель выборки. Класс QItemSelectionModel используется для отслеживания выборок в представлениях. Связанный с моделью выборки представления таблицы, наш слот currentCdChanged() будет вызываться при всяком перемещении пользователя от одной записи к другой.
01 void MainForm::currentCdChanged(const QModelIndex &index)
02 {
03 if (index.isValid()) {
04 QSqlRecord record = cdModel->record(index.row());
05 int id = record.value("id").toInt();
06 trackModel->setFilter(QString("cdid = %1").arg(id));
07 } else {
08 trackModel->setFilter("cdid = -1");
09 }
10 trackModel->select();
11 refreshTrackViewHeader();
12 }
Этот слот вызывается при каждой смене текущего компакт-диска. Это происходит при переходе пользователя к другому компакт-диску (щелкая мышкой по соответствующей строке или используя клавиши Up и Down). Если компакт-диск недействителен (например, если вообще нет компакт-дисков или был вставлен новый компакт-диск, или текущий компакт-диск был только что удален), мы устанавливаем идентификатор cdid таблицы дорожек track в значение —1 (недействительный идентификатор, которому не соответствует никакая запись).
Затем, установив фильтр, мы выбираем ему соответствующие записи дорожек. Функция refreshTrackViewHeader() будет рассмотрена вскоре.
01 void MainForm::addCd()
02 {
03 int row = 0;
04 if (cdTableView->currentIndex().isValid())
05 row = cdTableView->currentIndex().row();
06 cdModel->insertRow(row);
07 cdModel->setData(cdModel->index(row, Cd_Year),
08 QDate::currentDate().year());
09 QModelIndex index = cdModel->index(row, Cd_Title);
10 cdTableView->setCurrentIndex(index);
11 cdTableView->edit(index);
12 }
Когда пользователь нажимает клавишу Add CD (добавить компакт-диск), в таблицу cdTableView вставляется новая пустая строка и мы переходим в режим редактирования. Мы также устанавливаем значение по умолчанию для поля year. В этот момент пользователь может редактировать запись, заполняя пустые поля и выбирая артиста из выпадающего списка, который автоматически выдается моделью QSqlRelationalTableModel благодаря вызову setRelation(), a также изменяя год, если не подходит значение по умолчанию. Если пользователь подтверждает вставку нажатием клавиши Enter, запись вставляется. Пользователь может отменить вставку, нажав клавишу Esc.
01 void MainForm::beforeInsertCd(QSqlRecord &record)
02 {
03 record.setValue("id", generateId("cd"));
04 }
Этот слот вызывается, когда cdModel генерирует свой сигнал beforeInsert(). Мы используем его для заполнения поля id, как это делалось при вставке нового артиста, и здесь применимо то же самое предостережение: данная операция должна выполняться в рамках транзакции, а в идеальном случае должно использоваться зависимое от базы данных средство создания идентификаторов (например, автоматическая генерация идентификаторов).
01 void MainForm::deleteCd()
02 {
03 QModelIndex index = cdTableView->currentIndex();
04 if (!index.isValid())
05 return;
06 QSqlDatabase db = QSqlDatabase::database();
07 db.transaction();
08 QSqlRecord record = cdModel->record(index.row());
09 int id = record.value(Cd_Id).toInt();
10 int tracks = 0;
11 QSqlQuery query;
12 query.exec(QString("SELECT COUNT(*) FROM track WHERE cdid = %1")
13 .arg(id));
14 if (query.next())
15 tracks = query.value(0).tolnt();
16 if (tracks > 0) {
17 int r = QMessageBox::question(this, tr("Delete CD"),
18 tr("Delete \"%1\" and all its tracks?")
19 .arg(record.value(Cd_ArtistId).toString()),
20 QMessageBox::Yes | QMessageBox::Default,
21 QMessageBox::No | QMessageBox::Escape);
22 if (r == QMessageBox::No) {
23 db.rollback();
24 return;
25 }
26 query.exec(QString("DELETE FROM track WHERE cdid = %1")
27 .arg(id));
28 }
29 cdModel->removeRow(index.row());
30 cdModel->submitAll();
31 db.commit();
32 currentCdChanged(QModelIndex());
33 }
Когда пользователь нажимает клавишу Delete CD (удалить компакт-диск), вызывается этот слот. Если имеется текущий компакт-диск, мы определяем, сколько у него дорожек. Если нет ни одной дорожки, мы просто удаляем запись компакт-диска. Если имеется по крайней мере одна дорожка, мы просим пользователя подтвердить удаление, и, если он нажимает кнопку Yes, мы удаляем все дорожки и затем запись самого компакт-диска. Все это делается в рамках транзакции, поэтому каскадное удаление либо совсем не будет выполнено, либо выполнится полностью при условии, что ваша база данных поддерживает транзакции.
Обработка данных дорожки очень похожа на обработку данных компакт-диска. Для обновления данных пользователь может просто редактировать ячейки. Что касается длительностей дорожек, то класс TrackDelegate гарантирует удобный формат отображения времен и они легко могут редактироваться с использованием QTimeEdit.
01 void MainForm::addTrack()
02 {
03 if (!cdTableView->currentIndex().isValid())
04 return;
05 int row = 0;
06 if (trackTableView->currentIndex().isValid())
07 row = trackTableView->currentIndex().row();
08 trackModel->insertRow(row);
09 QModelIndex index = trackModel->index(row, Track_Title);
10 trackTableView->setCurrentIndex(index);
11 trackTableView->edit(index);
12 }
Эта функция работает так же, как addCd(), со вставкой в представление новой пустой строки.
01 void MainForm::beforeInsertTrack(QSqlRecord &record)
02 {
03 QSqlRecord cdRecord = cdModel->record(cdTableView->currentIndex().row());
04 record.setValue("id", generateId("track"));
05 record.setValue("cdid", cdRecord.value(Cd_Id).toInt());
06 }
Если пользователь подтверждает вставку, инициированную функцией addTrack(), указанная выше функция вызывается для заполнения полей id и cdid. Упомянутые ранее предостережения применимы, конечно, и в этом случае.
01 void MainForm::deleteTrack()
02 {
03 trackModel->removeRow(trackTableView->currentIndex().row());
04 if (trackModel->rowCount() == 0)
05 trackTableView->horizontalHeader()->setVisible(false);
06 }
Если пользователь нажимает кнопку Delete Track (удалить дорожку), мы сразу же удаляем дорожку. Если предпочтительнее подтверждать удаление, мы могли бы легко выдать окно с сообщением и кнопками Yes и No.
01 void MainForm::refreshTrackViewHeader()
02 {
03 trackTableView->horizontalHeader()->setVisible(
04 trackModel->rowCount() > 0);
05 trackTableView->setColumnHidden(Track_Id, true);
06 trackTableView->setColumnHidden(Track_CdId, true);
07 trackTableView-> resizeColumnsToContents();
08 }
Слот refreshTrackViewHeader() вызывается из различных мест; он гарантирует вывод на экран горизонтального заголовка в представлении дорожек только в случае наличия дорожек. Он не показывает поля идентификаторов id и cdid и изменяет видимые размеры столбцов таблицы в зависимости от текущего содержимого таблицы.
01 void MainForm::editArtists()
02 {
03 QSqlRecord record = cdModel->record(cdTableView->currentIndex().row());
04 ArtistForm artistForm(record.value(Cd_ArtistId).toString(), this);
05 artistForm.exec();
06 cdModel->select();
07 }
Этот слот, вызывается при нажатии пользователем кнопки Edit Artists (правка артистов). Он обеспечивает вывод на экран данных о компакт-дисках текущего артиста, вызывая форму ArtistForm, рассмотренную в предыдущем разделе, и делая выборку по соответствующему артисту. Если нет текущей записи, функция record() возвратит безвредную пустую запись, которая не будет соответствовать (и поэтому не будет выбрана) никакому артисту в форме артистов. В действительности при вызове record.value(Cd_ArtistId), используемого из-за применения модели QSqlRelationalTableModel, которая идентификаторам артистов ставит в соответствие их имена, возвращается имя артиста (а оно будет пустой строкой, если запись пустая). В конце мы снова выбираем данные модели cdModel, что заставляет cdTableView обновить свои видимые ячейки. Это делается для того, чтобы гарантировать правильный вывод на экран имен артистов, поскольку некоторые из них пользователь мог изменить в диалоговом окне ArtistForm.
Для проектов, использующих SQL—классы, необходимо добавить строку
QT += sql
в файлы .pro; это обеспечит сборку приложения с библиотекой QtSql.
Данная глава показывает, что Qt—классы архитектуры модель/представление позволяют достаточно просто просматривать и редактировать данные, размещенные в базах данных SQL. В тех случаях, когда внешние ключи ссылаются на таблицы с большим количеством записей (например, тысячи записей и больше), по-видимому, лучше всего создать свой собственный делегат и использовать его для представления формы со «списком значений» и с возможностями поиска, а не полагаться на выпадающие списки модели QSqlRelationalTableModel. Кроме того, в ситуациях, когда требуется отображать записи в виджете формы, мы должны обеспечить это сами в своем программном коде — использовать QSqlQuery или QSqlTableModel для взаимодействия с базой данных и связать содержимое виджетов пользовательского интерфейса (который мы хотим использовать для представления и редактирования данных) с соответствующей базой данных.
Глава 14. Работа с сетью
Qt обеспечивает классы QFtp и QHttp для работы с протоколами FTP и HTTP. Эти протоколы удобно применять для скачивания файлов из сети и их загрузки на удаленный компьютер, а также в случае применения протокола HTTP для передачи запросов на веб—серверы и получения результатов.
Qt также предоставляет низкоуровневые классы QTCPSocket и QUdpSocket, которые реализуют транспортные протоколы TCP и UDP. TCP — это надежный, ориентированный на соединение протокол, который оперирует потоками данных, циркулирующими между узлами сети, в то время как UDP — ненадежный, не ориентированный на соединения протокол, основанный на передаче дискретных пакетов от одних сетевых узлов к другим. Оба протокола могут использоваться для создания клиентских и серверных сетевых приложений. В серверных приложениях необходимо также использовать класс QTcpServer для обработки входящих ТСР—соединений.
Написание FTP—клиентов
Класс QFtp реализует клиентскую часть протокола FTP в Qt. Он предлагает различные функции для выполнения самых распространенных операций протокола FTP и позволяет выполнять произвольные команды FTP.
Класс QFtp работает асинхронно. Когда мы вызываем такие функции, как get() или put(), управление сразу же возвращается к нам, а пересылка данных осуществляется после передачи управления обратно в цикл обработки событий Qt. Это обеспечивает работоспособность интерфейса пользователя во время выполнения команд FTP.
Мы начнем с примера чтения одного файла с помощью функции get(). В этом примере создается консольное приложение с именем ftpget, которое скачивает удаленный файл, указанный в командной строке. Давайте начнем с функции main():
01 int main(int argc, char *argv[])
02 {
03 QCoreApplication app(argc, argv);
04 QStringList args = app.arguments();
05 if (args.count() != 2) {
06 cerr << "Usage: ftpget url" << endl << "Example:" << endl
07 << " ftpget ftp://ftp.trolltech.com/mirrors" << endl;
08 return 1;
09 }
10 FtpGet getter;
11 if (!getter.getFile(QUrl(args[1])))
12 return 1;
13 QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit()));
14 return app.exec();
15 }
Мы создаем объект класса QCoreApplication, а не его подкласса QApplication, чтобы избежать сборки с библиотекой QtGui. Функция QCoreApplication::arguments() возвращает аргументы командной строки в виде списка QStringList, первым элементом которого является имя вызванной программы, а все специфичные для Qt аргументы, такие как —style, удаляются. Центральными моментами в функции main() являются конструирование объекта FtpGet и вызов функции getFile(). Если этот вызов оказывается успешным, мы позволяем циклу событий выполняться до тех пор, пока файл не будет полностью скачан.
Всю работу делает подкласс FtpGet, который определяется следующим образом:
01 class FtpGet : public QObject
02 {
03 Q_OBJECT
04 public:
05 FtpGet(QObject *parent = 0);
06 bool getFile(const QUrl &url);
07 signals:
08 void done();
09 private slots:
10 void ftpDone(bool error);
11 private:
12 QFtp ftp;
13 QFile file;
14 …
15 };
Класс имеет открытую функцию getFile(), которая считывает файл по указанному адресу URL. Класс QUrl имеет высокоуровневый интерфейс для извлечения различных частей URL, таких как имя файла, путь, протокол и порт.
Класс FtpGet имеет закрытый слот ftpDone(bool), который вызывается после окончания операции пересылки файла, и сигнал done(), который генерируется при завершении скачивания файла. Этот класс имеет также две закрытые переменные. Переменная ftp имеет тип QFtp и инкапсулирует соединение с сервером FTP; переменная file используется для записи скачанного из сети файла на диск.
01 FtpGet::FtpGet(QObject *parent)
02 : QObject(parent)
03 {
04 connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool)));
05 }
В конструкторе мы подсоединяем сигнал QFtp::done(bool) к нашему закрытому слоту ftpDone(bool). QFtp генерирует сигнал done(bool) после завершения обработки всех запросов. Параметр типа bool показывает, возникла ошибка или нет.
01 bool FtpGet::getFile(const QUrl &url)
02 {
03 if (!url.isValid()) {
04 cerr << "Error: Invalid URL" << endl;
05 return false;
06 }
07 if (url.scheme() != "ftp") {
08 cerr << "Error: URL must start with 'ftp:'" << endl;
09 return false;
10 }
11 if (url.path().isEmpty()) {
12 cerr << "Error: URL has no path" << endl;
13 return false;
14 }
15 QString localFileName = QFileInfo(url.path()).fileName();
16 if (localFileName.isEmpty())
17 localFileName = "ftpget.out";
18 file.setFileName(localFileName);
19 if (!file.open(QIODevice::WriteOnly)) {
20 cerr << "Error: Cannot open "
21 << qPrintable(file.fileName()) << " for writing: "
22 << qPrintable(file.errorString()) << endl;
23 return false;
24 }
25 ftp.connectToHost(url.host(), url.port(21));
26 ftp.login();
27 ftp.get(url.path(), &file);
28 ftp.close();
29 return true;
30 }
Функция getFile() начинается с проверки переданного ей URL. Если возникла проблема, функция выводит в поток cerr сообщение об ошибке и возвращает false, указывая на неудачное скачивание файла.
Мы не обязываем пользователя указывать имя локального файла и пытаемся сами создать осмысленное имя на основе URL, а при неудаче используем имя ftpget.out. Если не удается открыть файл, мы печатаем сообщение об ошибке и возвращаем false.
Затем мы выполняем последовательность из четырех команд FTP, используя наш объект QFtp. Вызов url.port(21) возвращает номер порта, указанный в URL, или порт 21, если URL не содержит порта. Поскольку функции login() не передаются ни имя пользователи, ни пароль, делается попытка анонимного входа в систему. Второй аргумент функции get() задает выходное устройство ввода—вывода.
Команды FTP ставятся в очередь и обрабатываются в цикле обработки событий Qt. Завершение всех команд регистрируется сигналом done(bool) объекта QFtp, который мы подсоединили к слоту ftpDone(bool) в конструкторе.
01 void FtpGet::ftpDone(bool error)
02 {
03 if (error) {
04 cerr << "Error: " << qPrintable(ftp.errorString()) << endl;
05 } else {
06 cerr << "File downloaded as " << qPrintable(file.fileName()) << endl;
07 }
08 file.close();
09 emit done();
10 }
После выполнения всех команд FTP мы закрываем файл и генерируем сигнал done(). Может показаться странным, что мы закрываем файл именно здесь, а не после вызова ftp.close() в конце функции getFile(), но следует помнить, что команды FTP выполняются асинхронно и их выполнение вполне может быть еще не закончено после возврата управления функцией getFile(). Только после генерации объектом QFtp сигнала done() мы можем быть уверены, что скачивание файла завершено и теперь можно спокойно закрывать файл.
Класс QFtp поддерживает несколько FTP—команд, включая connectToHost(), login(), close(), list(), cd(), get(), put(), remove(), mkdir(), rmdir() и rename(). Все эти функции отправляют какую-то команду FTP и возвращают число, идентифицирующее эту команду. Можно также управлять режимом передачи (по умолчанию используется пассивная передача) и типом передачи (двоичный по умолчанию).
Произвольные команды FTP можно выполнять при помощи функции rawCommand(). Ниже приводится пример выполнения команды SITE CHMOD:
ftp.rawCommand("SITE CHMOD 755 fortune");
QFtp генерирует сигнал commandStarted(int) в начале выполнения команды и сигнал commandFinished(int, bool) после завершения выполнения команды. Параметр типа int является числом, которое идентифицирует команду. Если мы собираемся отслеживать результаты выполнения отдельных команд, мы можем сохранять эти идентификаторы при постановке команд в очередь. Отслеживание идентификаторов обеспечивает более оперативную обратную связь с пользователем. Например:
01 bool FtpGet::getFile(const QUrl &url)
02 {
03 …
04 connectId = ftp.connectToHost(url.host(), url.port(21));
05 loginId = ftp.login();
06 getId = ftp.get(url.path(), &file);
07 closeId = ftp.close();
08 return true;
09 }
10 void FtpGet::ftpCommandStarted(int id)
11 {
12 if (id == connectId) {
13 сегг << "Connecting..." << endl;
14 } else if (id == loginId) {
15 cerr << "Logging in..." << endl;
16 …
17 }
Другой способ обеспечения обратной связи заключается в подключении к сигналу stateChanged() класса QFtp, который генерируется при всяком изменении состояния соединения (QFtp::Connecting, QFtp::Connected, QFtp::LoggedIn и т.д.).
В большинстве приложений нас интересует только результат исполнения всей последовательности команд, а не каких-то конкретных команд. В таком случае мы можем просто подключить сигнал done(bool), который генерируется всякий раз, когда очередь команд становится пустой.
При возникновении ошибки QFtp автоматически очищает очередь команд. Это означает, что при неудачном подсоединении или входе пользователя в систему оставшиеся в очереди команды никогда не выполнятся. Если мы после возникновения ошибки зададим новые команды с использованием того же объекта QFtp, они будут поставлены в очередь и затем выполнены.
В файл приложения .pro необходимо добавить следующую строку для сборки приложения совместно с библиотекой QtNetwork:
QT += network
Теперь мы рассмотрим более сложный пример. Программа командной строки spider (паук) скачивает все файлы, расположенные в каталоге FTP—сервера, рекурсивно просматривая каждый его подкаталог. Вся логика работы с сетью содержится в классе Spider:
01 class Spider : public QObject
02 {
03 Q_OBJECT
04 public:
05 Spider(QObject *parent = 0);
06 bool getDirectory(const QUrl &url);
07 signals:
08 void done();
09 private slots:
10 void ftpDone(bool error);
11 void ftpListInfo(const QUrlInfo &urlInfo);
12 private:
13 void processNextDirectory();
14 QFtp ftp;
15 QList
16 QString currentDir;
17 QString currentLocalDir;
18 QStringList pendingDirs;
19 };
Начальный каталог определяется как объект типа QUrl и устанавливается при помощи функции getDirectory().
01 Spider::Spider(QObject *parent)
02 : QObject(parent)
03 {
04 connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool)));
05 connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)),
06 this, SLOT(ftpListInfo(const QUrlInfo &)));
07 }
В конструкторе мы устанавливаем два соединения сигнал—слот. Когда мы выдаем запрос на получение списка элементов каталога в getDirectory(), QFtp генерирует сигнал listInfo(const QUrlInfo &) для каждого найденного имени. Этот сигнал подключается к слоту с именем ftpListInfo(), который скачивает файл из сети по указанному адресу URL.
01 bool Spider::getDirectory(const QUrl &url)
02 {
03 if (!url.isValid()) {
04 cerr << "Error: Invalid URL" << endl;
05 return false;
06 }
07 if (url.scheme() != "ftp") {
08 cerr << "Error: URL must start with 'ftp:'" << endl;
09 return false;
10 }
11 ftp.connectToHost(url.host(), url.port(21));
12 ftp.login();
13 QString path = url.path();
14 if (path.isEmpty())
15 path = "/";
16 pendingDirs.append(path);
17 processNextDirectory();
18 return true;
19 }
Выполнение функции getDirectory() начинается с некоторых основных проверок, и если все нормально, делается попытка установить FTP—соединение. Она отслеживает пути, которые необходимо будет обрабатывать, и вызывает функцию processNextDirectory(), чтобы начать скачивание корневого каталога.
01 void Spider::processNextDirectory()
02 {
03 if (!pendingDirs.isEmpty()) {
04 currentDir = pendingDirs.takeFirst();
05 currentLocalDir = "downloads/" + currentDir;
06 QDir(".").mkpath(currentLocalDir);
07 ftp.cd(currentDir);
08 ftp.list();
09 } else {
10 emit done();
11 }
12 }
Функция processNextDirectory() принимает первый удаленный каталог из списка каталогов, ожидающих обработки, pendingDirs, и создает соответствующий каталог в локальной файловой системе. После этого она указывает объекту QFtp на необходимость изменения каталога на принятый ею каталог и затем получения списка его файлов. Для каждого файла, обрабатываемого функцией list(), генерируется сигнал listInfo(), приводящий к вызову слота ftpListInfo().
Когда все каталоги оказываются обработанными, эта функция генерирует сигнал done(), обозначающий завершение скачивания.
01 void Spider::ftpListInfo(const QUrlInfo &urlInfo)
02 {
03 if (urlInfo.isFile()) {
04 if (urlInfo.isReadable()) {
05 QFile *file = new QFile(currentLocalDir + "/"
06 + urlInfo.name());
07 if (!file->open(QIODevice::WriteOnly)) {
08 cerr << "Warning: Cannot open file << qPrintable(
09 QDir::convertSeparators(file->fileName()))
10 << endl;
11 return;
12 }
13 ftp.get(urlInfo.name(), file);
14 openedFiles.append(file);
15 }
16 } else if (urlInfo.isDir() && !urlInfo.isSymLink()) {
17 pendingDirs.append(currentDir + "/" + urlInfo.name());
18 }
19 }
Параметр urlInfo слота ftpListInfo() содержит информацию о файле в сети. Если это обычный файл (не каталог) и его можно считывать, мы вызываем функцию get() для его загрузки. Объект QFile, используемый для загрузки файла, создается с помощью оператора new, и указатель на него хранится в списке openedFiles.
Если содержащиеся в QUrlInfo сведения об удаленном каталоге говорят, что он не является символической связью, этот каталог добавляется к списку pendingDirs. Мы пропускаем символические связи, поскольку они легко могут привести к бесконечной рекурсии.
01 void Spider::ftpDone(bool error)
02 {
03 if (error) {
04 cerr << "Error: " << qPrintable(ftp.errorString()) << endl;
05 } else {
06 cout << "Downloaded " << qPrintable(currentDir) << " to "
07 << qPrintable(QDir::convertSeparators(
08 QDir(currentLocalDir).canonicalPath()));
09 }
10 qDeleteAll(openedFiles);
11 openedFiles.clear();
12 processNextDirectory();
13 }
Слот ftpDone() вызывается после завершения всех команд FTP или при возникновении ошибки. Мы удаляем объекты QFile для предотвращения утечек памяти, а также для закрытия всех файлов. Наконец, мы вызываем функцию processNextDirectory(). Если какие-нибудь каталоги остались, весь процесс повторяется для следующего каталога в списке; в противном случае скачивание файлов прекращается и генерируется сигнал done().
Если ошибок нет, последовательность команд FTP и сигналов будет такой:
connectToHost(host, port)
login()
cd(directory_1)
list()
emit listInfo(file_1_1)
get(file_1_1)
emit listInfo(file_1_2)
get(file_1_2)
…
emit done()
…
cd(directory_N)
list()
emit listInfo(file_N_1)
get(file_N_1)
emit listInfo(file_N_2)
get(file_N_2)
…
emit done()
Если файл фактически оказывается каталогом, он добавляется в список pendingDirs и, когда завершается скачивание последнего файла, полученного текущей командой list(), выдается новая команда cd(), за которой следует новая команда list() для следующего каталога, ожидающего обработки, и весь процесс повторяется для нового каталога. Скачиваются новые файлы, и в список pendingDirs добавляются новые каталоги до тех пор, пока не будут скачаны все файлы из всех каталогов и список pendingDirs в результате не станет пустым.
Если возникнет сетевая ошибка при загрузке пятого файла, скажем, из двадцати файлов в каталоге, остальные файлы не будут скачаны. Если бы мы захотели скачать как можно больше файлов, то один из способов заключается в выполнении по одной операции GET и ожидании сигнала done(bool) перед выполнением новой операции GET. В функции listInfo() мы бы просто добавили имя файла в конец списка QStringList вместо немедленного вызова get(), а в слоте done(bool) мы бы вызывали функцию get() для следующего загружаемого файла из списка QStringList. Последовательность команд выглядела бы так:
connectToHost(host, port)
login()
cd(directory_1)
list()
…
cd(directory_N)
list()
emit listInfo(file_1_1)
emit listInfo(file_1_2)
…
emit listInfo(file_N_1)
emit listInfo(file_N_2)
…
emit done()
get(file_1_1)
emit done()
get(file_1_2)
emit done()
…
get(file_N_1)
emit done()
get(file_N_2)
emit done()
…
Еще одно решение могло бы заключаться в применении одного объекта QFtp для каждого файла. Это позволило бы нам скачивать файлы из сети параллельно, используя отдельные FTP—соединения.
01 int main(int argc, char *argv[])
02 {
03 QCoreApplication app(argc, argv);
04 QStringList args = app.arguments();
05 if (args.count() != 2) {
06 cerr << "Usage: spider url" << endl << "Example:" << endl
07 << " spider ftp://ftp.trolltech.com/freebies/leafnode" << endl;
08 return 1;
09 }
10 Spider spider;
11 if (!spider.getDirectory(QUrl(args[1])))
12 return 1;
13 QObject::connect(&spider, SIGNAL(done()), &app, SLOT(quit()));
14 return app.exec();
15 }
Функция main() завершает программу. Если пользователь не задает адрес URL в командной строке, мы выдаем сообщение об ошибке и завершаем программу.
В обоих примерах применения протокола FTP данные, полученные функцией get(), записывались в объект QFile. Это не обязательно должно быть так. Если бы мы захотели хранить данные в памяти, мы могли бы использовать QBuffer — подкласс QIODevice, являющийся оболочкой массива QByteArray. Например:
QBuffer *buffer= new QBuffer;
buffer->open(QIODevice::WriteOnly);
ftp.get(urlInfo.name(), buffer);
Мы могли бы также не задавать в функции get() аргумент с устройством ввода—вывода или передать нулевой указатель. Класс QFtp тогда генерирует сигнал readyRead() при поступлении каждой новой порции данных и данные могут считываться при помощи функции read() или readAll().
Написание НТТР—клиента
Класс QHttp реализует клиентскую часть протокола HTTP в Qt. Он содержит различные функции для выполнения самых распространенных операций протокола HTTP, включая get() и post(), и обеспечивает средство выполнения произвольных запросов HTTP. Если вы прочитали предыдущий раздел о классе QFtp, вы обнаружите, что существует много общего у классов QFtp и QHttp.
Класс QHttp работает асинхронно. Когда мы вызываем такие функции, как get() или post(), управление сразу же возвращается к нам, а пересылка данных осуществляется после передачи управления обратно в цикл обработки событий Qt. Это обеспечивает работоспособность интерфейса пользователя во время обработки запросов HTTP.
Мы рассмотрим пример консольного приложения с именем httpget, демонстрирующего способ загрузки файла с использованием протокола HTTP. Мы не приводим здесь заголовочный файл, поскольку данный пример очень напоминает пример ftpget, который мы использовали в предыдущем разделе.
01 HttpGet::HttpGet(QObject *parent)
02 : QObject(parent)
03 {
04 …
05 connect(&http, SIGNAL(done(bool)), this, SLOT(httpDone(bool)));
06 }
В конструкторе мы подсоединяем сигнал done(bool) объекта QHttp к закрытому слоту httpDone(bool).
01 bool HttpGet::getFile(const QUrl &url)
02 {
03 if (!url.isValid()) {
04 сегг << "Error: Invalid URL" << endl;
05 return false;
06 }
07 if (url.scheme() != "http") {
08 cerr << "Error: URL must start with 'http:'" << endl;
09 return false;
10 }
11 if (url.path().isEmpty()) {
12 cerr << "Error: URL has no path" << endl;
13 return false;
14 }
15 QString localFileName = QFileInfo(url.path()).fileName();
16 if (localFileName.isEmpty())
17 localFileName = "httpget.out";
18 file.setFileName(localFileName);
19 if (!file.open(QIODevice::WriteOnly)) {
20 cerr << "Error: Cannot open "
21 << qPrintable(file.fileName()) << " for writing: "
22 << qPrintable(file.errorString()) << endl;
23 return false;
24 }
25 http.setHost(url.host(), url.port(80));
26 http.get(url.path(), &file);
27 http.close();
28 return true;
29 }
Функция getFile() проверяет ошибочные ситуации так же, как рассмотренная ранее функция FtpGet::getFile(), и использует тот же подход при задании имени локального файла. При загрузке файлов с веб-сайта не требуется входить в систему, поэтому мы просто указываем хост и порт (используя стандартный для HTTP порт 80, если его нет в URL) и скачиваем данные в файл, заданный вторым аргументом функции QHttp::get().
Запросы HTTP ставятся в очередь и обрабатываются асинхронно в цикле обработки событий Qt. На завершение выполнения запросов указывает сигнал done(bool) объекта QHttp, который мы подсоединили к слоту httpDone(bool) в конструкторе.
01 void HttpGet::httpDone(bool еггог)
02 {
03 if (еггог) {
04 сегг << "Еггог: " << qPrintable(http.errorString()) << endl;
05 } else {
06 сегг << "File downloaded as " << qPrintable(file.fileName()) << endl;
07 }
08 file.close();
09 emit done();
10 }
После выполнения запросов HTTP мы файл закрываем, уведомляя пользователя о возникновении ошибки.
Функция main() очень похожа на такую же функцию в примере ftpget:
01 int main(int argc, char *argv[])
02 {
03 QCoreApplication app(argc, argv);
04 QStringList args = app.arguments();
05 if (args.count() != 2) {
06 cerr << "Usage: httpget url" << endl << "Example:" << endl
07 << " httpget http://doc.trolltech.com/qq/index.html" << endl;
08 return 1;
09 }
10 HttpGet getter;
11 if (!getter.getFile(QUrl(args[1])))
12 return 1;
13 QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit()));
14 return app.exec();
15 }
Класс QHttp содержит много операций, включая setHost(), get(), post() и head(). Если для входа на сайт необходимо выполнить аутентификацию пользователя, setUser() может использоваться для установки имени пользователя и пароля. QHttp может использовать сокет, указанный программистом, а не свой собственный внутренний QTcpSocket. Это делает возможным применение безопасного сокета QtSslSocket (который предоставляется компонентом Qt Solution компании «Trolltech») для работы с HTTP через SSL.
Мы можем применять функцию post() для пересылки пар «имя = значение» в сценарий CGI:
http.setHost("www.example.com");
http.post("/cgi/somescript.py", "x=200&y=320", &file);
Мы можем передавать данные в виде 8-битовой строки либо передавать открытое устройство QIODevice, например QFile. Для обеспечения большего контроля мы можем использовать функцию request(), которая принимает произвольные заголовок и данные HTTP. Например:
QHttpRequestHeader header("POST", "/search.html");
header.setValue("Host", "www.trolltech.com");
header.setContentType("application/x-www-form-urlencoded");
http.setHost(www.trolltech.com);
http.request(header, "qt-interest=on&search=opengl");
QHttp генерирует сигнал requestStarted(int) в начале выполнения команды и сигнал requestFinished(int, bool) после завершения выполнения команды. Параметр типа int является числом, которое идентифицирует запрос. Если мы собираемся отслеживать результаты выполнения отдельных запросов, мы можем сохранять эти идентификаторы при постановке запросов в очередь. Отслеживание идентификаторов обеспечивает более оперативную обратную связь с пользователем.
В большинстве приложений нас интересует результат исполнения всей последовательности команд. Это легко достигается путем подсоединения сигнала done(bool), который генерируется всякий раз, когда очередь запросов становится пустой.
При возникновении ошибки очередь запросов автоматически очищается. Но если мы после возникновения ошибки зададим новые запросы с использованием того же объекта QHttp, они будут поставлены в очередь и затем выполнены в обычном порядке.
Как и QFtp, класс QHttp содержит сигнал readyRead(), а также функции read() и readAll(), которые мы можем использовать вместо указания устройства ввода—вывода.
Написание клиент—серверных приложений на базе TCP
Классы QTcpSocket и QTcpServer могут использоваться для реализации клиентов и серверов TCP. TCP — это транспортный протокол, который составляет основу большинства прикладных протоколов сети Интернет, включая FTP и HTTP, и который может также использоваться для создания пользовательских протоколов.
TCP является потокоориентированным протоколом. Для приложений данные представляются в виде большого потока данных, очень напоминающего большой однородный файл. Протоколы высокого уровня, построенные на основе TCP, являются либо строкоориентированными, либо блокоориентированными:
• строкоориентированные протоколы передают текстовые данные построчно, завершая каждую строку символом перехода на новую строку;
• блокоориентированные протоколы передают данные в виде двоичных блоков. Каждый блок имеет в начале поле, где указан его размер, и затем идут байты данных.
Класс QTcpSocket наследует QIODevice через класс QAbstractSocket, и поэтому чтение с него или запись на него могут производиться с применением средств класса QDataStream или QTextStream. Одно существенное отличие чтения данных из сети по сравнению с чтением обычного файла заключается в том, что мы должны быть уверены в получении достаточного количества данных от партнерского узла (peer) перед использованием оператора >>. В противном случае результат может быть непредсказуемым.
В данном разделе мы рассмотрим программный код клиента и сервера, которые используют пользовательский протокол блочной передачи. Клиент называется Trip Planner (планировщик путешествий) и позволяет пользователям составлять план путешествия на поезде. Сервер называется Trip Server (сервер путешествий) и обеспечивает клиента информацией о путешествии. Мы начнем с написания клиентского приложения Trip Planner.
Приложение Trip Planner содержит поле From (из пункта), поле To (до пункта), поле Date (дата), поле Approximate Time (приблизительное время) и два переключателя, определяющие приблизительное время отправления или прибытия. Когда пользователь нажимает клавишу Search, приложение посылает запрос на сервер, который возвращает список железнодорожных рейсов, которые удовлетворяют критериям пользователя. Этот список отображается в виджете QTableWidget в окне Trip Planner. В нижней части окна расположены текстовая метка QLabel, показывающая состояние последней операции, и индикатор состояния процесса QProgressBar.
Рис. 14.1. ПриложениеТпр Planner.
Пользовательский интерфейс приложения Trip Planner был создан при помощи QtDesigner в файле tripplanner.ui. Ниже мы основное внимание уделим исходному коду подкласса QDialog, который реализует функциональность приложения:
#include "ui_tripplanner.h"
01 class TripPlanner : public QDialog, public Ui::TripPlanner
02 {
03 Q_OBJECT
04 public:
05 TripPlanner(QWidget *parent = 0);
06 private slots:
07 void connectToServer();
08 void sendRequest();
09 void updateTableWidget();
10 void stopSearch();
11 void connectionClosedByServer();
12 void error();
13 private:
14 void closeConnection();
15 QTcpSocket tcpSocket;
16 quint16 nextBlockSize;
17 };
Класс TripPlanner наследует не только QDialog, но и Ui::TripPlanner (который генерируется компилятором uic, используя файл tripplanner.ui). Переменная—член tcpSocket инкапсулирует соединение TCP. Переменная nextBlockSize используется при синтаксическом анализе блоков, поступивших с сервера.
01 TripPlanner::TripPlanner(QWidget *parent)
02 : QDialog(parent)
03 {
04 setupUi(this);
05 QDateTime dateTime = QDateTime::currentDateTime();
06 dateEdit->setDate(dateTime.date());
07 timeEdit->setTime(QTime(dateTime.time().hour(), 0));
08 progressBar->hide();
09 progressBar->setSizePolicy(QSizePolicy::Preferred,
10 QSizePolicy::Ignored);
11 tableWidget->verticalHeader()->hide();
12 tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
13 connect(searchButton, SIGNAL(clicked()),
14 this, SLOT(connectToServer()));
15 connect(stopButton, SIGNAL(clicked()), this, SLOT(stopSearch()));
16 connect(&tcpSocket, SIGNAL(connected()),
17 this, SLOT(sendRequest()));
18 connect(&tcpSocket, SIGNAL(disconnected()),
19 this, SLOT(connectionClosedByServer()));
20 connect(&tcpSocket, SIGNAL(readyRead()),
21 this, SLOT(updateTableWidget()));
22 connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
23 this, SLOT(error()));
24 }
В конструкторе мы инициализируем поля редактирования даты и времени текущей датой и временем. Мы также не показываем индикатор состояния программы, потому что он необходим только при активном соединении. В Qt Designer свойства minimum и maximum индикатора состояния устанавливались в 0. Это определяет поведение QProgressBar как индикатора занятости вместо стандартного индикатора, показывающего процент выполнения работы.
В конструкторе мы также связываем сигналы connected(), disconnected(), readyRead() и error(QAbstractSocket::SocketError) класса QTcpSocket с закрытыми слотами.
01 void TripPlanner::connectToServer()
02 {
03 tcpSocket.connectToHost("tripserver.zugbahn.de", 6178);
04 tableWidget->setRowCount(0);
05 searchButton->setEnabled(false);
06 stopButton->setEnabled(true);
07 statusLabel->setText(tr("Connecting to server..."));
08 progressBar->show();
09 nextBlockSize = 0;
10 }
Слот connectToServer() выполняется, когда пользователь нажимает клавишу Search для запуска процедуры поиска. Мы вызываем функцию connectToHost() объекта типа QTcpSocket для подсоединения к серверу, который, как мы предполагаем, доступен через порт 6178 по вымышленному адресу хоста tripserver.zugbahn.de. (Если вы собираетесь проверить работу этого примера на вашей машине, замените имя хоста на QHostAddress::LocalHost.) Вызов connectToHost() выполняется асинхронно; эта функция всегда немедленно возвращает управление. Соединение обычно устанавливается позже. Объект QTcpSocket генерирует сигнал connected(), если соединение успешно осуществлено и действует, или error(QAbstractSocket::SocketError), если соединение завершилось неудачей.
Затем мы обновляем интерфейс пользователя, в частности делаем видимым индикатор состояния приложения.
Наконец, мы устанавливаем переменную nextBlockSize на 0. Эта переменная содержит длину следующего блока, полученного от сервера. Мы задали значение 0, поскольку еще не знаем размер следующего блока.
01 void TripPlanner::sendRequest()
02 {
03 QByteArray block;
04 QDataStream out(&block, QIODevice::WriteOnly);
05 out.setVersion(QDataStream::Qt_4_1);
06 out << quint16(0) << quint8('S') << fromComboBox->currentText()
07 << toComboBox->currentText() << dateEdit->date()
08 << timeEdit->time();
09 if (departureRadioButton->isChecked()) {
10 out << quint8('D');
11 } else {
12 out << quint8('A');
13 }
14 out.device()->seek(0);
15 out << quint16(block.size() - sizeof(quint16));
16 tcpSocket.write(block);
17 statusLabel->setText(tr("Sending request..."));
18 }
Слот sendRequest() выполняется, когда объект QTcpSocket генерирует сигнал connected(), уведомляя об установке соединения. Задача этого слота — сгенерировать запрос к серверу с передачей всей введенной пользователем информации.
Запрос является двоичным блоком следующего формата:
• quint16 — размер блока в байтах (не учитывая данное поле),
• quint8 — тип запроса (всегда «S»),
• QString — пункт отправления,
• QString — пункт прибытия,
• QDate — дата поездки,
• QTime — примерное время отправления или прибытия,
• quint8 — признак времени отправления («D») или прибытия («А»).
Сначала мы записываем данные в массив типа QByteArray с именем block. Мы не можем писать данные непосредственно в QTcpSocket, поскольку мы не знаем размер блока, который будет отсылаться первым, пока не разместим все данные в блоке.
Сначала мы записываем 0 в поле размера блока и затем размещаем остальные данные. Затем мы делаем вызов seek(0) для устройства ввода—вывода (для установки на начало буфера QBuffer, создаваемого автоматически классом QDataStream), чтобы встать на начало массива байтов и переписать первоначальный 0 фактическим размером блока данных. Эта величина рассчитывается как размер блока за вычетом sizeof(quint16) (то есть 2), чтобы исключить поле с размером блока из общей суммы байтов. После этого мы вызываем функцию write() для объекта QTcpSocket, чтобы отослать этот блок на сервер.
01 void TripPlanner::updateTableWidget()
02 {
03 QDataStream in(&tcpSocket);
04 in.setVersion(QDataStream::Qt_4_1);
05 forever {
06 int row = tableWidget->rowCount();
07 if (nextBlockSize == 0) {
08 if (tcpSocket.bytesAvailable() < sizeof(quint16))
09 break;
10 in >> nextBlockSize;
11 }
12 if (nextBlockSize == 0xFFFF) {
13 closeConnection();
14 statusLabel->setText(tr("Found %1 trip(s)").arg(row));
15 break;
16 }
17 if (tcpSocket.bytesAvailable() < nextBlockSize)
18 break;
19 QDate date;
20 QTime departureTime;
21 QTime arrivalTime;
22 quint16 duration;
23 quint8 changes;
24 QString trainType;
25 in >> date >> departureTime >> duration >> changes >> trainType;
26 arrivalTime = departureTime.addSecs(duration * 60);
27 tableWidget->setRowCount(row + 1);
28 QStringList fields;
29 fields << date.toString(Qt::LocalDate)
30 << departureTime.toString(tr("hh:mm"))
31 << arrivalTime.toString(tr("hh:mm"))
32 << tr("%1 hr %2 min").arg(duration / 60).arg(duration % 60)
33 << QString::number(changes) << trainType;
34 for (int i = 0; i < fields.count(); ++i)
35 tableWidget->setItem(row, i, new QTableWidgetItem(fields[i]));
36 nextBlockSize = 0;
37 }
38 }
Слот updateTableWidget() подсоединяется к сигналу readyRead() класса QTcpSocket, который генерируется всякий раз при получении QTcpSocket новых данных от сервера.
Сервер пересылает нам список возможных железнодорожных рейсов, которые удовлетворяют критерию пользователя. Каждый рейс передается в виде одного блока, и каждый блок начинается с поля размера блока. Цикл forever необходим, потому что мы не обязательно получаем от сервера блоки по одному. Мы можем получить целый блок или только его часть или полтора блока либо даже все блоки сразу.
Рис. 14.2. Блоки приложения Trip Server.
Итак, как действует цикл forever? Если переменная nextBlockSize равна 0, это означает, что мы не прочитали размер следующего блока. Мы пытаемся прочитать его (предполагается, что имеется по крайней мере 2 байта). Сервер использует значение 0xFFFF в поле размера блока для указания на то, что все данные переданы, и поэтому, если мы обнаруживаем это значение, мы знаем, что достигнут конец.
Если размер блока не равен 0xFFFF, мы пытаемся считать следующий блок. Во-первых, мы проверяем наличие блока байтов необходимого размера. Если его нет, мы прерываем цикл. Сигнал readyRead() будет вновь сгенерирован, когда станет доступно больше данных, и мы попытаемся повторить процедуру.
Если мы уверены, что получен целый блок, мы можем спокойно использовать оператор >> для QDataStream для извлечения относящейся к поездкам информации, и мы создаем элементы QTableWidgetItem с этой информацией. Полученный от сервера блок имеет следующий формат:
• quint16 — размер блока в байтах (не учитывая данное поле),
• QDate — дата отправления,
• QTime — время отправления,
• quint16 — длительность поездки (в минутах),
• quint8 — количество пересадок,
• QString — тип поезда.
В конце мы вновь устанавливаем переменную nextBlockSize на 0 для указания того, что размер следующего блока неизвестен и его необходимо считать.
01 void TripPlanner::closeConnection()
02 {
03 tcpSocket.close();
04 searchButton->setEnabled(true);
05 stopButton->setEnabled(false);
06 progressBar->hide();
07 }
Закрытая функция closeConnection() закрывает соединение сервера TCP и обновляет интерфейс пользователя. Она вызывается из функции updateTableWidget(), когда считывается значение 0xFFFF, и из нескольких других слотов, которые мы вскоре рассмотрим.
01 void TripPlanner::stopSearch()
02 {
03 statusLabel->setText(tr("Search stopped"));
04 closeConnection();
05 }
Слот stopSearch() подсоединяется к сигналу clicked() кнопки Stop. По существу, он просто вызывает функцию closeConnection().
01 void TripPlanner::connectionClosedByServer()
02 {
03 if (nextBlockSize != 0xFFFF)
04 statusLabel->setText(tr("Error: Connection closed by server" ));
05 closeConnection();
06 }
Слот connectionClosedByServer() подсоединяется к сигналу disconnected() объекта QTcpSocket. Если сервер закрывает соединение и мы еще не получили маркер конца, мы уведомляем пользователя о возникновении ошибки. И как обычно, мы вызываем функцию closeConnection() для обновления интерфейса пользователя.
01 void TripPlanner::error()
02 {
03 statusLabel->setText(tcpSocket.errorString());
04 closeConnection();
05 }
Слот error() подсоединяется к сигналу error(QAbstractSocket::SocketError) объекта QTcpSocket. Мы игнорируем код ошибки и используем функцию QTcpSocket::errorString(), которая возвращает понятное человеку сообщение о последней возникшей ошибке.
На этом завершается рассмотрение класса TripPlanner. Функция main() приложения Trip Planner выглядит обычным образом:
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 TripPlanner tripPlanner;
05 tripPlanner.show();
06 return app.exec();
07 }
Теперь давайте реализуем сервер. Сервер состоит из двух классов: TripServer и ClientSocket. Класс TripServer наследует QTcpServer — класс, который позволяет нам принимать входящие соединения TCP. Класс ClientSocket переопределяет QTcpSocket и обслуживает одно соединение. В каждый момент времени в памяти имеется ровно столько объектов типа ClientSocket, сколько обслуживается клиентов.
01 class TripServer : public QTcpServer
02 {
03 Q_OBJECT
04 public:
05 TripServer(QObject *parent = 0);
06 private:
07 void incomingConnection(int socketId);
08 };
Класс TripServer переопределяет функцию incomingConnection() из класса QTcpServer. Данная функция вызывается всякий раз, когда клиент пытается подсоединиться к порту, который прослушивает сервер.
01 TripServer::TripServer(QObject *parent)
02 : QTcpServer (parent)
03 {
04 }
Конструктор TripServer тривиален.
01 void TripServer::incomingConnection(int socketId)
02 {
03 ClientSocket *socket = new ClientSocket(this);
04 socket->setSocketDescriptor(socketId);
05 }
В функции incomingConnection() мы создаем объект ClientSocket в качестве дочернего по отношению к объекту TripServer, и мы устанавливаем дескриптор его coкета на переданное нам значение. Объект ClientSocket автоматически удалит сам себя при прекращении соединения.
01 class ClientSocket : public QTcpSocket
02 {
03 Q_OBJECT
04 public:
05 ClientSocket(QObject *parent = 0);
06 private slots:
07 void readClient();
08 private:
09 void generateRandomTrip(const QString &from, const QString &to,
10 const QDate &date, const QTime &time);
11 quint16 nextBlockSize;
12 };
Класс ClientSocket наследует QTcpSocket и инкапсулирует состояние одного клиента.
01 ClientSocket::ClientSocket(QObject *parent)
02 : QTcpSocket(parent)
03 {
04 connect(this, SIGNAL(readyRead()), this, SLOT(readClient()));
05 connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater()));
06 nextBlockSize = 0;
07 }
В конструкторе мы устанавливаем необходимые соединения сигнал—слот и задаем переменной nextBlockSize значение 0, свидетельствующее о том, что мы еще не знаем размер посланного клиентом блока.
Сигнал disconnected() подсоединяется к функции deleteLater(), которая наследуется от класса QObject, и удаляет объект после возврата управления в цикл обработки событий Qt. Это обеспечивает удаление объекта ClientSocket после закрытия сокетного соединения.
01 void ClientSocket::readClient()
02 {
03 QDataStream in(this);
04 in.setVersion(QDataStream::Qt_4_1);
05 if (nextBlockSize == 0) {
06 if (bytesAvailable() < sizeof(quint16))
07 return;
08 in >> nextBlockSize;
09 }
10 if (bytesAvailable() < nextBlockSize)
11 return;
12 quint8 requestType;
13 QString from;
14 QString to;
15 QDate date;
16 QTime time;
17 quint8 flag;
18 in >> requestType;
19 if (requestType == 'S') {
20 in >> from >> to >> date >> time >> flag;
21 srand(from.length() * 3600 + to.length() * 60 + time.hour());
22 int numTrips = rand() % 8;
23 for (int i = 0; i < numTrips; ++i)
24 generateRandomTrip(from, to, date, time);
25 QDataStream out(this);
26 out << quint16(0xFFFF);
27 }
28 close();
29 }
Слот readClient() подсоединяется к сигналу readyRead() класса QTcpSocket. Если nextBlockSize равен 0, мы начинаем считывать размер блока; в противном случае он уже считан нами, и тогда мы проверяем поступление целого блока. Если это целый блок, мы считываем его за один шаг. Мы используем QDataStream непосредственно для QTcpSocket (объект this) и считываем поля, используя оператор >>.
После чтения запроса клиента мы готовы сформировать ответ. В реальном приложении мы осуществляли бы поиск информации в базе данных расписания железнодорожных рейсов и попытались бы найти подходящие рейсы. Но здесь мы воспользуемся функцией generateRandomTrip(), которая случайным образом генерирует произвольный рейс. Мы вызываем эту функцию произвольное число раз и затем посылаем 0xFFFF для обозначения конца данных. В конце мы закрываем соединение.
01 void ClientSocket::generateRandomTrip(const QString & /* откуда */,
02 const QString & /* куда */, const QDate &date, const QTime &time)
03 {
04 QByteArray block;
05 QDataStream out(&block, QIODevice::WriteOnly);
06 out.setVersion(QDataStream::Qt_4_1);
07 quint16 duration = rand() % 200;
08 out << quint16(0) << date << time << duration << quint8(1)
09 << QString("InterCity");
10 out.device()->seek(0);
11 out << quint16(block.size() - sizeof(quint16));
12 write(block);
13 }
Функция generateRandomTrip() демонстрирует способ пересылки блока данных через соединение TCP. Это очень напоминает то, что мы делали в клиенте в функции sendRequest(). И вновь мы записываем блок в массив QByteArray таким образом, что мы можем определять его размер до того, как мы его отошлем с помощью функции write().
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 TripServer server;
05 if (!server.listen(QHostAddress::Any, 6178)) {
06 cerr << "Failed to bind to port" << endl;
07 return 1;
08 }
09 QPushButton quitButton(QObject::tr("&Quit"));
10 quitButton.setWindowTitle(QObject::tr("Trip Server"));
11 QObject::connect(&quitButton, SIGNAL(clicked()),
12 &app, SLOT(quit()));
13 quitButton.show();
14 return app.exec();
15 }
В функции main() мы создаем объект TripServer и кнопку QPushButton, которая позволяет пользователю остановить сервер. Работа сервера начинается с вызова функции QTcpSocket::listen(), принимающей адрес IP и номер порта, по которому мы хотим принимать соединения. Специальный адрес 0.0.0.0 (QHostAddress::Any) соответствует наличию любого интерфейса IP на локальном хосте.
Этим завершается наш пример системы клиент—сервер. В данном случае нами использовался блокоориентированный протокол, позволяющий применять объект типа QDataStream для чтения и записи данных. Если бы мы захотели использовать строкоориентированный протокол, наиболее простым было бы применение функций canReadLine() и readLine() класса QTcpSocket в слоте, подсоединенном к сигналу readyRead():
QStringList lines;
while (tcpSocket.canReadLine())
lines.append(tcpSocket.readLine());
Мы бы затем могли обрабатывать каждую считанную строку. Пересылка данных могла бы выполняться с использованием QTextStream для QTcpSocket.
Представленная здесь реализация сервера не очень эффективна в случае, когда соединений много. Это объясняется тем, что при обработке нами одного запроса мы не обслуживаем другие соединения. Более эффективным был бы запуск нового процесса для каждого соединения. Пример Threaded Fortune Server (многопоточный сервер, передающий клиентам интересные изречения, называемые «fortunes»), расположенный в каталоге Qt examples/network/threadedfortuneserver, демонстрирует, как это можно сделать.
Передача и прием дейтаграмм UDP
Класс QUdpSocket может использоваться для отправки и приема дейтаграмм UDP. UDP — это ненадежный, ориентированный на дейтаграммы протокол. Некоторые приложения применяют протокол UDP, поскольку с ним легче работать, чем с протоколом TCP. По протоколу UDP данные передаются пакетами (дейтаграммами) от одного хоста к другому. Для него не существует понятия соединения, и если доставка пакета UDP в пункт назначения завершается неудачей, никакого сообщения об ошибке не передается отправителю.
Рис. 14.3. Приложение Weather Station.
Мы рассмотрим способы применения UDP в приложении Qt на примере приложений Weather Balloon (метеозонд) и Weather Station (метеостанция). Приложение Weather Balloon является приложением без графического интерфейса, которое посылает каждые 2 секунды дейтаграммы UDP с параметрами текущего атмосферного состояния. Приложение Weather Station получает эти дейтаграммы и выводит их на экран. Мы начнем с рассмотрения программного кода приложения Weather Balloon.
01 class WeatherBalloon : public QPushButton
02 {
03 Q_OBJECT
04 public:
05 WeatherBalloon(QWidget *parent = 0);
06 double temperature() const;
07 double humidity() const;
08 double altitude() const;
09 private slots:
10 void sendDatagram();
11 private:
12 QUdpSocket udpSocket;
13 QTimer timer;
14 };
Класс WeatherBalloon наследует QPushButton. Он использует свою закрытую переменную типа QUdpSocket для обеспечения связи с приложением Weather Station.
01 WeatherBalloon::WeatherBalloon(QWidget *parent)
02 : QPushButton(tr("Quit"), parent)
03 {
03 connect(this, SIGNAL(clicked()), this, SLOT(close()));
04 connect(&timer, SIGNAL(timeout()), this, SLOT(sendDatagram()));
05 timer.start(2 * 1000);
06 setWindowTitle(tr("Weather Balloon"));
07 }
В конструкторе мы запускаем QTimer для вызова sendDatagram() через каждые 2 секунды.
01 void WeatherBalloon::sendDatagram()
02 {
03 QByteArray datagram;
04 QDataStream out(&datagram, QIODevice::WriteOnly);
05 out.setVersion(QDataStream::Qt_4_1);
06 out << QDateTime::currentDateTime() << temperature()
07 << humidity() << altitude();
08 udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, 5824);
09 }
В sendDatagram() мы формируем и отсылаем дейтаграмму, содержащую текущую дату, время, температуру, влажность и высоту над уровнем моря.
• QDateTime — дата и время измерений,
• double — температура по Цельсию,
• double — влажность в процентах,
• double — высота над уровнем моря в метрах.
Эта дейтаграмма отсылается функцией QUdpSocket::writeBlock() (в коде "writeDatagram". wtf?). Вторым и третьим аргументами функции writeBlock() являются адрес IP и номер порта партнера (приложения Weather Station). В данном примере мы предполагаем, что приложение Weather Station выполняется на той же машине, на которой работает приложение Weather Balloon, и поэтому мы используем адрес IP 127.0.0.1 (QHostAddress::LocalHost) — специальный адрес, предназначенный для использования местными хостами.
В отличие от QAbstractSocket, класс QUdpSocket не получает имена хостов, а только их числовые адреса. Если нам нужно определить имя хоста по его адресу IP, мы имеем две возможности. Если мы готовы блокировать работу во время выполнения поиска, мы можем использовать статическую функцию QHostInfo::fromName(). В противном случае мы можем использовать статическую функцию QHostInfo::lookupHost(), которая немедленно возвращает управление и вызывает слот с передачей в качестве аргумента объекта QHostInfo, который будет содержать соответствующие адреса после завершения поиска.
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 WeatherBalloon balloon;
05 balloon.show();
06 return app.exec();
07 }
Функция main() просто создает объект WeatherBalloon, кoтopый являeтcя yчacтником связи по протоколу UDP и одновременно представлен на экране кнопкой QPushButton. Нажимая кнопку QPushButton, пользователь может завершить приложение.
Теперь давайте рассмотрим исходный код клиентского приложения Weather Station.
01 class WeatherStation : public QDialog
02 {
03 Q_OBJECT
04 public:
05 WeatherStation(QWidget *parent = 0);
06 private slots:
07 void processPendingDatagrams();
08 private:
09 QUdpSocket udpSocket;
10 QLabel *dateLabel;
11 QLabel *timeLabel;
12 QLineEdit *altitudeLineEdit;
13 };
Класс WeatherStation наследует QDialog. Он прослушивает определенный порт UDP, выполняет синтаксический разбор поступающих дейтаграмм (от приложения Weather Balloon) и выводит на экран их содержимое в виде пяти строк редактирования QLineEdit, которые используются только для вывода данных. Здесь нас интересует только одна закрытая переменная udpSocket типа QUdpSocket, которая будет использована для приема дейтаграмм.
01 WeatherStation::WeatherStation(QWidget *parent)
02 : QDialog(parent)
03 {
04 udpSocket.bind(5824);
05 connect(&udpSocket, SIGNAL(readyRead()),
06 this, SLOT(processPendingDatagrams()));
07 }
Конструктор мы начинаем с привязки объекта QUdpSocket к порту, на который передает данные метеозонд. Поскольку мы не указали адрес хоста, сокет будет принимать дейтаграммы, посланные на любой адрес IP, принадлежащий машине, на которой работает приложение Weather Station. Затем мы связываем сигнал сокета readyRead() c закрытым слотом processPendingDatagrams(), который извлекает данные и отображает их на экране.
01 void WeatherStation::processPendingDatagrams()
02 {
03 QByteArray datagram;
04 do {
05 datagram.resize(udpSocket.pendingDatagramSize());
06 udpSocket.readDatagram(datagram.data(), datagram.size());
07 } while (udpSocket.hasPendingDatagrams());
08 QDateTime dateTime;
09 double temperature;
10 double humidity;
11 double altitude;
12 QDataStream in(&datagram, QIODevice::ReadOnly);
13 in.setVersion(QDataStream::Qt_4_1);
14 in >> dateTime >> temperature >> humidity >> altitude;
15 dateLineEdit->setText(dateTime.date().toString());
16 timeLineEdit->setText(dateTime.time().toString());
17 temperatureLineEdit->setText(tr("%1° C").arg(temperature));
18 humidityLineEdit->setText(tr("%1%").arg(humidity));
19 altitudeLineEdit->setText(tr("%1 m").arg(altitude));
20 }
Слот processPendingDatagrams() вызывается при получении дейтаграммы. QUdpSocket ставит в очередь поступившие дейтаграммы и позволяет получать к ним доступ последовательно в порядке очереди. Обычно в очереди будет только одна дейтаграмма, однако нельзя исключать возможность передачи отправителем последовательно нескольких дейтаграмм до генерации сигнала readyRead(). В этом случае мы игнорируем все дейтаграммы, кроме последней, поскольку предыдущие дейтаграммы содержат устаревшие параметры атмосферного состояния.
Функция pendingDatagramSize() возвращает размер первой ждущей обработки дейтаграммы. С точки зрения приложения дейтаграммы всегда посылаются и принимаются как один блок данных. Это означает, что при любом количестве байтов дейтаграмма будет считываться целиком. Вызов readDatagram() копирует содержимое первой ждущей обработки дейтаграммы в указанный буфер char * (обрезая данные, если размер буфера оказывается недостаточным) и осуществляет переход к следующей необработанной дейтаграмме. После считывания всех дейтаграмм мы разбиваем последнюю из них (имеющую самые свежие значения параметров атмосферного состояния) на составные части и заполняем строки редактирования QLineEdit новыми данными.
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 WeatherStation station;
05 station.show();
06 return app.exec();
07 }
Наконец, в функции main() мы создаем и показываем объект WeatherStation.
На этом мы завершаем рассмотрение наших примеров по передаче и приему данных с применением протокола UDP. Представленные приложения максимально упрощены, причем приложение Weather Balloon посылает дейтаграммы, а приложение Weather Station получает их. В большинстве реальных приложений в обоих случаях пришлось бы как считывать, так записывать данные на свой сокет. Функциям QUdpSocket::writeDatagram() могут передаваться адрес хоста и номер порта, поэтому QUdpSocket может читать с хоста и порта, с которыми он был связан функцией bind(), и писать на какой-нибудь другой хост и порт.
Глава 15. XML
XML (Extensible Markup Language — расширяемый язык разметки) — это универсальный формат текстовых файлов, который получил широкое распространение при обмене и хранении данных. Qt обеспечивает два различных программных интерфейса для чтения документов XML; эти интерфейсы входят в состав модуля QtXml:
• SAX (Simple API for XML — простой программный интерфейс для документов XML) позволяет обрабатывать «события синтаксического анализа» непосредственно в приложении в соответствующих виртуальных функциях.
• DOM (Document Object Model — объектная модель документа) преобразует документ XML в структуру в виде дерева, которая затем может обрабатываться в приложении.
Существует много факторов, которые необходимо учитывать в каждом конкретном случае при выборе между DOM и SAX. SAX является интерфейсом более низкого уровня и обычно работает быстрее, что делает его особенно пригодным как для решения простых задач (например, для поиска в документе XML всех повторений заданного тега), так и для чтения очень больших файлов, которые не помещаются в оперативной памяти. Но для большинства приложений удобство применения DOM перевешивает потенциальные преимущества более высокого быстродействия и более эффективного использования памяти в SAX.
Создавать файлы XML можно двумя способами: мы можем сгенерировать XML вручную или представить данные в виде дерева DOM, размещенного в памяти, и «попросить» это дерево записать себя в файл.
Чтение документов XML при помощи интерфейса SAX
SAX является фактическим стандартом программного интерфейса с открытым исходным кодом, который обеспечивает чтение документов XML.
Классы Qt для интерфейса SAX моделируют реализацию SAX2 Java с некоторыми отличиями в названиях для обеспечения принятых в Qt правил обозначений названий классов и их членов. Более подробную информацию относительно SAX можно получить в сети Интернет по адресу .
Qt обеспечивает построенный на основе интерфейса SAX парсер документов XML, не предусматривающий проверку их достоверности под названием QXmlSimpleReader. Этот парсер распознает хорошо сформированные документы XML и поддерживает пространства имен XML. Когда парсер обрабатывает документ, он вызывает виртуальные функции в зарегистрированных классах—обработчиках, уведомляющих о возникновении соответствующих событий в ходе синтаксического анализа документа. (Эти события никак не связаны с такими событиями Qt, как события клавиатуры и события мышки.) Например, пусть парсер выполняет анализ следующего документа XML:
Ars longa vita brevis
В этом случае парсер вызовет следующие обработчики событий синтаксического анализа:
startDocument()
startElement("doc")
startElement("quote")
characters("Ars longa vita brevis")
endElement("quote")
endElement("doc")
endDocument()
Все приведенные выше функции объявлены в классе QXmlContentHandler. Для простоты мы не стали указывать некоторые аргументы функций startElement() и endElement().
QXmlContentHandler — это всего лишь один из многих классов—обработчиков, которые могут использоваться совместно с классом QXmlSimpleReader. Другими такими классами являются QXmlEntityResolver, QXmlDTDHandler, QXmlErrorHandler, QXmlDeclHandler и QXmlLexicalHandler. Эти классы только объявляют чистые виртуальные функции и предоставляют информацию о различных событиях синтаксического анализа. Для большинства приложений вполне достаточно использовать лишь классы QXmlContentHandler и QXmlErrorHandler.
Для удобства Qt также предоставляет класс QXmlDefaultHandler, который наследует все классы—обработчики и обеспечивает очень простую реализацию всех функций. Такая конструкция со множеством абстрактных классов—обработчиков и одним подклассом с тривиальной реализацией функций необычна для Qt; она принята для максимального соответствия модели Java—реализации.
Теперь мы рассмотрим пример, который показывает способы применения QXmlSimpleReader и QXmlDefaultHandler для синтаксического анализа файла XML заранее известного формата и для отображения его содержимого в виджете QTreeWidget. Подкласс QXmlDefaultHandler имеет название SaxHandler, и он используется для обработки предметного указателя книги, который содержит элементы и подэлементы.
Рис. 15.1. Дерево наследования для SaxHandler.
Ниже приводится файл предметного указателя книги, который отображается в виджете QTreeWidget и показан на рис. 15.2:
Рис. 15.2. Файл предметного указателя книги, загруженный в виджет QTreeWidget.
Первый этап в реализации парсера заключается в создании подкласса QXmlDefaultHandler:
01 class SaxHandler : public QXmlDefaultHandler
02 {
03 public:
04 SaxHandler(QTreeWidget *tree);
05 bool startElement(const QString &namespaceURI,
06 const QString &localName,
07 const QString &qName,
08 const QXmlAttributes &attributes);
09 bool endElement(const QString &namespaceURI,
10 const QString &localName,
11 const QString &qName);
12 bool characters(const QString &str);
13 bool fatalError(const QXmlParseException &exception);
14 private:
15 QTreeWidget *treeWidget;
16 QTreeWidgetItem *currentItem;
17 QString currentText;
18 };
Класс SaxHandler наследует QXmlDefaultHandler и переопределяет четыре функции: startElement(), endElement(), characters() и fatalError(). Первые четыре функции объявлены в QXmlContentHandler; последняя функция объявлена в QXmlErrorHandler.
01 SaxHandler::SaxHandler(QTreeWidget *tree)
02 {
03 treeWidget = tree;
04 currentItem = 0;
05 }
Конструктор SaxHandler принимает объект типа QTreeWidget, который мы собираемся заполнять информацией, содержащейся в файле XML.
01 bool SaxHandler::startElement(const QString & /* namespaceURI */,
02 const QString & /* localName */,
03 const QString &qName,
04 const QXmlAttributes &attributes)
05 {
06 if (qName == "entry") {
07 if (currentItem) {
08 currentItem = new QTreeWidgetItem(currentItem);
09 } else {
10 currentItem = new QTreeWidgetItem(treeWidget);
11 }
12 currentItem->setText(0, attributes.value("term"));
13 } else if (qName == "page") {
14 currentText.clear();
15 }
16 return true;
17 }
Функция startElement() вызывается, когда обнаруживается новый открывающий тег. Третий параметр представляет собой имя тега (или точнее — «подходящее имя»). В четвертом параметре задается список атрибутов. В этом примере мы игнорируем первый и второй параметры. Они полезны для тех файлов XML, которые используют механизм пространств имен, подробно описанный в справочной документации.
Если обнаружен тег
Если обнаружен тег
В конце мы возвращаем true, указывая SAX на необходимость продолжения синтаксического анализа файла. Если бы нам нужно было сообщить об ошибке из-за обнаружения неизвестного тега, мы возвращали бы в этих случаях false. Нам также потребовалось бы переопределить функцию errorString() класса QXmlDefaultHandler для возврата соответствующего сообщения об ошибке.
01 bool SaxHandler::characters(const QString &str)
02 {
03 currentText += str;
04 return true;
05 }
Функция characters() используется для извлечения символьных данных из документа XML. Мы просто добавляем символы в конец переменной currentText.
01 bool SaxHandler::endElement(const QString & /* namespaceURI */,
02 const QString & /* localName */, const QString &qName)
03 {
04 if (qName == "entry") {
05 currentItem = currentItem->parent();
06 } else if (qName == "page") {
07 if (currentItem) {
08 QString allPages = currentItem->text(1);
09 if (!allPages.isEmpty())
10 allPages += ", ";
11 allPages += currentText;
12 currentItem->setText(1, allPages);
13 }
14 }
15 return true;
16 }
Функция endElement() вызывается при обнаружении закрывающего тега. Так же как и для функции startElement(), ее третий параметр содержит имя тега.
Если обнаружен тег , мы устанавливаем закрытую переменную currentItem на родительский элемент текущего элемента QTreeWidgetItem. Это обеспечивает восстановление переменной currentItem на значение, которое она имела перед чтением соответствующего тега
Если обнаружен тег , мы добавляем указанный номер страницы или диапазон страниц в разделяемый запятыми список в столбце 1 текущего элемента.
01 bool SaxHandler::fatalError(const QXmlParseException &exception)
02 {
03 QMessageBox::warning(0, QObject::tr("SAX Handler"),
04 QObject::tr("Parse error at line %1, column %2:\n%3.")
05 .arg(exception.lineNumber())
06 .arg(exception.columnNumber())
07 .arg(exception.message()));
08 return false;
09 }
Функция fatalError() вызывается, когда синтаксический анализ файла XML завершается неудачей. В этом случае мы просто выводим на экран сообщение, указывая номер строки, номер столбца и текст об ошибке синтаксического анализа.
Этим мы завершаем реализацию класса SaxHandler. Теперь давайте посмотрим, как можно использовать этот класс:
01 bool parseFile(const QString &fileName)
02 {
03 QStringList labels;
04 labels << QObject::tr("Terms") << QObject::tr("Pages");
05 QTreeWidget *treeWidget = new QTreeWidget;
06 treeWidget->setHeaderLabels(labels);
07 treeWidget->setWindowTitle(QObject::tr("SAX Handler"));
08 treeWidget->show();
09 QFile file(fileName);
10 QXmlInputSource inputSource(&file);
11 QXmlSimpleReader reader;
12 SaxHandler handler(treeWidget);
13 reader.setContentHandler(&handler);
14 reader.setErrorHandler(&handler);
15 return reader.parse(inputSource);
16 }
Мы задаем два столбца в виджете QTreeWidget. Затем мы создаем объект типа QFile для считываемого файла и объект типа QXmlSimpleReader для синтаксического анализа файла. Нам не требуется самим открывать QFile; QXmlInputSource делает это автоматически.
Наконец, мы создаем объект типа SaxHandler, который используется для объекта reader одновременно в качестве обработчика содержимого файла и обработчика ошибок, и мы вызываем функцию parse() для выполнения синтаксического анализа.
Вместо простого объекта файла мы передаем функции parse() объект QXmlInputSource. Этот класс открывает заданный файл, читает его (учитывая кодировку символов в объявлении ) и предоставляет интерфейс для чтения файла парсером.
В классе SaxHandler мы всего лишь переопределили функции, унаследованные от классов QXmlContentHandler и QXmlErrorHandler. Если бы мы стали переопределять функции других классов—обработчиков, нам пришлось бы вызывать соответствующие функции—установщики для объекта reader.
Для сборки приложения с библиотекой QtXml в файл .pro необходимо добавить следующую строку:
QT += xml
Чтение документов XML при помощи интерфейса DOM
DOM является стандартным программным интерфейсом синтаксического анализа документов XML, который разработан Консорциумом всемирной паутины (W3C). Qt обеспечивает уровень 2 интерфейса DOM для чтения, обработки и записи документов XML без проверки их достоверности.
DOM представляет файл XML в памяти в виде дерева. Мы можем просматривать дерево DOM столько раз, сколько нам нужно, и мы можем модифицировать и записывать его на диск в виде файла XML.
Давайте рассмотрим следующий документ XML:
Ars longa vita brevis
Ему соответствует следующее дерево DOM:
Дерево DOM содержит узлы разных типов. Например, узел Element соответствует открывающему тегу и связанному с ним закрывающему тегу. Все, что располагается между этими тегами, представляется в виде дочерних узлов данного элемента Element.
В Qt различные типы таких узлов (как и все другие связанные с DOM классы) имеют префикс QDom. Так, QDomElement представляет узел Element, a QDomText представляет узел Text.
Различные узлы могут иметь дочерние узлы разных типов. Например, узел Element может содержать другие узлы Element, а также узлы EntityReference, Text, CDATASection, ProcessingInstruction и Comment. Рис. 15.3 показывает, какие типы дочерних узлов допустимы для соответствующих родительских узлов. Узлы, показанные серым, не могут иметь дочерних узлов.
Рис. 15.3. Родственные связи между узлами DOM.
Для иллюстрации применения DOM при чтении файлов XML мы напишем парсер для файла предметного указателя книги, описанного в предыдущем разделе.
01 class DomParser
02 {
03 public:
04 DomParser(QIODevice *device, QTreeWidget *tree);
05 private:
06 void parseEntry(const QDomElement &element,
07 QTreeWidgetItem *parent);
08 QTreeWidget *treeWidget;
09 };
Мы определяем класс с названием DomParser, который выполняет синтаксический анализ предметного указателя книги, представленного в виде документа XML, и отображает результат в виджете QTreeWidget. Этот класс не наследует никакой другой класс.
01 DomParser::DomParser(QIODevice *device, QTreeWidget *tree)
02 {
03 treeWidget = tree;
04 QString errorStr;
05 int errorLine;
06 int errorColumn;
07 QDomDocument doc;
08 if (!doc.setContent(device, true, &errorStr,
09 &errorLine, &errorColumn)) {
10 QMessageBox::warning(0, QObject::tr("DOM Parser"),
11 QObject::tr("Parse error at line %1, column %2:\n%3")
12 .arg(errorLine).arg(errorColumn).arg(errorStr));
13 return;
14 }
15 QDomElement root = doc.documentElement();
16 if (root.tagName() != "bookindex")
17 return;
18 QDomNode node = root.firstChild();
19 while (!node.isNull()) {
20 if (node.toElement().tagName() == "entry")
21 parseEntry(node.toElement(), 0);
22 node = node.nextSibling();
23 }
24 }
В конструкторе мы создаем объект QDomDocument и вызываем для него функцию setContent(), чтобы с его помощью прочесть документ XML с устройства QIODevice. Функция setContent() автоматически открывает устройство, если оно еще не открыто. Затем мы вызываем функцию documentElement() для объекта QDomDocument, чтобы получить его одиночный дочерний элемент QDomElement, после чего мы проверяем, является ли данный элемент
Класс QDomNode может хранить узлы любого типа. Если мы хотим продолжить обработку узла, мы должны сначала преобразовать его в правильный тип данных. В нашем примере нас интересуют только узлы Element, и поэтому мы вызываем функцию toElement() объекта QDomNode для преобразования его в объект QDomElement и затем вызова функции tagName() для получения имени тега элемента. Если данный узел не имеет тип Element, функция toElement() возвращает нулевой объект типа QDomElement, содержащий пустое имя тега.
01 void DomParser::parseEntry(const QDomElement &element,
02 QTreeWidgetItem *parent)
03 {
04 QTreeWidgetItem *item;
05 if (parent) {
06 item = new QTreeWidgetTtem(parent);
07 } else {
08 item = new QTreeWidgetItem(treeWidget);
09 }
10 item->setText(0, element.attribute("term"));
11 QDomNode node = element.firstChild();
12 while (!node.isNull()) {
13 if (node.toElement().tagName() == "entry") {
14 parseEntry(node.toElement(), item);
15 } else if (node.toElement().tagName() == "page") {
16 QDomNode childNode = node.firstChild();
17 while (!childNode.isNull()) {
18 if (childNode.nodeType() == QDomNode::TextNode) {
19 QString page = childNode.toText().data();
20 QString allPages = item->text(1);
21 if (!allPages.isEmpty())
22 allPages += ", ";
23 allPages += page;
24 item->setText(1, allPages);
25 break;
26 }
27 childNode = childNode.nextSibling();
28 }
29 }
30 node = node.nextSibling();
31 }
32 }
В функции parseEntry() мы создаем элемент объекта QTreeWidget. Если тег вложен в другой
После инициализации нами элемента QTreeWidgetItem мы выполняем цикл по дочерним узлам элемента QDomElement, который соответствует текущему тегу
Если элементом является
Если элементом является
Давайте теперь посмотрим, как мы можем использовать класс DomParser для синтаксического анализа файла:
01 void parseFile(const QString &fileName)
02 {
03 QStringList labels;
04 labels << QObject::tr("Terms") << QObject::tr("Pages");
05 QTreeWidget *treeWidget = new QTreeWidget;
06 treeWidget->setHeaderLabels(labels);
07 treeWidget->setWindowTitle(QObject::tr("DOM Parser"));
08 treeWidget->show();
09 QFile file(fileName);
10 DomParser(&file, treeWidget);
11 }
Мы начинаем с настройки QTreeWidget. Затем мы создаем объекты QFile и DomParser. При выполнении конструктора DomParser осуществляется синтаксический анализ файла и пополняется виджет дерева.
Как и в предыдущем примере, для сборки приложения с библиотекой QtXml в файл .pro необходимо добавить следующую строку:
QT += xml
Как показывает наш пример, проход по дереву DOM может быть достаточно непростым делом. Простая операция по извлечению текста между тегами
Запись документов XML
Существует два основных подхода к формированию файлов XML в приложениях Qt:
• мы можем построить дерево DOM и вызвать для него функцию save();
• мы можем сформировать файл XML вручную.
Выбор между этими подходами часто не зависит от типа используемого нами интерфейса для чтения документов XML: SAX или DOM.
Ниже приводится фрагмент программного кода, который иллюстрирует способ создания дерева DOM и его записи при помощи QTextStream:
const int Indent = 4;
QDomDocument doc;
QDomElement root = doc.createElement("doc");
QDomElement quote = doc.createElement("quote");
QDomElement translation = doc.createElement("translation");
QDomText latin = doc.createTextNode("Ars longa vita brevis");
QDomText english = doc.createTextNode("Art is long, life is short");
doc.appendChild(root);
root.appendChild(quote);
root.appendChild(translation);
quote.appendChild(latin);
translation.appendChild(english);
QTextStream out(&file);
doc.save(out, Indent);
Второй аргумент функции save() задает размер отступа. При ненулевом его значении читаемость сформированного файла будет лучше. Ниже приводится полученный на выходе файл XML:
....Ars longa vita brevis
....
Порядок действий будет другим, если в приложении дерево DOM используется в качестве главной структуры данных. В таких случаях приложения обычно считывают документы XML, применяя интерфейс DOM, затем модифицируют в памяти дерево DOM и, наконец, вызывают функцию save() для обратного преобразования дерева в документ XML.
По умолчанию функция QDomDocument::save() использует для генерации файла кодировку UTF-8. Мы можем применить другую кодировку, если добавить XML—объявление, например такое, как
в начало дерева DOM. Следующий фрагмент программного кода показывает, как это делать:
QTextStream out(&file);
QDomNode xmlNode = doc.createProcessingInstruction("xml",
"version=\"1.0\" encoding=\"ISO-8859-1\"");
doc.insertBefore(xmlNode, doc.firstChild());
doc.save(out, Indent);
Формирование файлов XML вручную выполняется не намного сложнее, чем при помощи DOM. Мы можем использовать QTextStream и писать строки, как мы бы делали с любым другим текстовым файлом. Наиболее сложным является вставка специальных символов в текст и значения атрибутов. Функция Qt::escape() заменяет символы '<', '>' и '&'. Ниже приводится пример ее использования:
QTextStream out(&file);
out.setCodec("UTF-8");
out << "
<< " " << Qt::escape(quoteText) << "
\n"
<< "
<< "\n";
В статье «Generating XML» (Формирование документов XML) в журнале «Qt Quarterly», доступной в сети Интернет по адресу , рассматривается очень простой класс, позволяющий легко формировать файлы XML. Этот класс решает вопросы, связанные со специальными символами, отступами и кодировкой, позволяя нам полностью сконцентрироваться на документе XML, который мы собираемся формировать. Он предназначен для работы с Qt 3, но его очень легко перенести на Qt 4.
Глава 16. Обеспечение интерактивной помощи
Большинство приложений предоставляют своим пользователям систему помощи, работающую в интерактивном режиме. В некоторых случаях эта помощь носит форму коротких сообщений, например, в виде всплывающих подсказок, комментариев в строке состояния и справок «что это такое?». Все это, естественно, поддерживается в Qt. В других случаях система помощи может быть значительно сложнее и может содержать много страниц текста. Для такого рода систем вы можете воспользоваться классом QTextBrowser в качестве простого браузера системы помощи, а также вы можете вызывать из вашего приложения Qt Assistant или другой браузер файлов HTML.
Всплывающие подсказки, комментарии в строке состояния и справки «что это такое?»
Всплывающая подсказка (tooltip) представляет собой небольшое текстовое сообщение, которое появляется при нахождении курсора мышки на виджете в течение определенного времени. Всплывающие подсказки отображаются на желтом фоне черными буквами. В основном они предназначены для пояснения назначения кнопок на панели инструментов.
Мы можем добавлять всплывающие подсказки к любым виджетам путем включения в программный код вызова функции QWidget::setToolTip(). Например:
findButton->setToolTip(tr("Find next"));
Для установки всплывающей подсказки для объекта QAction, который может быть добавлен к меню или панели инструментов, мы можем просто вызвать функцию setToolTip() для этой команды. Например:
newAction = new QAction(tr("&New"), this);
newAction->setToolTip(tr("New document"));
Если мы явно не устанавливаем всплывающую подсказку, QAction автоматически сформирует ее на основе текста команды.
Комментарии в строке состояния (status tip) также представляют собой короткие текстовые сообщения, причем они обычно немного длиннее всплывающих подсказок. При нахождении курсора мышки на кнопке панели инструментов или на строке меню такой комментарий появляется в строке состояния. Для добавления к команде или к виджету отображаемого в строке состояния комментария необходимо вызвать функцию setStatusTip():
newAction->setStatusTip(tr("Create a new document"));
Рис. 16.1. В этом приложении отображаются всплывающая подсказка и комментарий в строке состояния.
В некоторых ситуациях желательно обеспечить больше информации о виджете, чем это возможно сделать с помощью всплывающих подсказок или комментариев в строке состояния. Например, у нас может возникнуть потребность в обеспечении сложного диалогового окна с пояснительным текстом для каждого поля без принуждения пользователя к вызову отдельного окна системы помощи. Режим «что это такое?» идеально подходит для этого. Когда окно находится в режиме «что это такое?», курсор приобретает форму «?» и пользователь может щелкнуть по любому компоненту интерфейса пользователя для получения текста помощи. Для входа в режим «что это такое?» пользователь может либо нажать на кнопку «?» в строке заголовка диалогового окна (в системе Windows и KDE), либо нажать сочетание клавиш Shift+F1.
Ниже приводится пример установки для диалогового окна текста справки «что это такое?»:
dialog->setWhatsThis(tr(""
" The meaning of the Source field depends "
"on the Type field:"
"
- "
- Books have a Publisher"
"
- Articles have a Journal name with "
"volume and issue number"
"
- Theses have an Institution name "
"and a Department name"
"
"
Мы можем применять теги HTML для форматирования текста справки «что это такое?». В нашем примере мы используем изображение (которое указано в файле ресурсов приложения), маркированный список и жирный текст в некоторых местах. Теги и атрибуты, которые поддерживаются в Qt, приведены на веб-странице .
Рис. 16.2. Диалоговое окно с отображением текста справки «что это такое?»
Кроме того, мы можем задавать текст справки «что это такое?» для команды:
openAct->setWhatsThis(tr(" "
"Click this option to open an existing file."));
При задании для команды текста справки «что это такое?» он будет отображаться, когда пользователь в режиме справки «что это такое?» выбирает пункт меню, нажимает кнопку на панели инструментов или клавишу быстрого вызова команды. Когда компоненты пользовательского интерфейса главного окна приложения предусматривают вывод справки «что это такое?», обычно в меню Help (справка) содержится пункт What's This? (что это такое?) и панель инструментов содержит соответствующую кнопку. Это можно сделать путем создания команды What's This? при помощи статической функции QWhatsThis::createAction() и добавления возвращаемой ею команды в меню Help и в панель инструментов. Класс QWhatsThis предоставляет также статические функции для программного входа и выхода из режима справки «что это такое?».
Использование
QTextBrowser
в качестве простого браузера системы помощи
Для больших приложений может потребоваться более сложная система помощи в отличие от той, которую обычно обеспечивают всплывающие подсказки, комментарии в строке состояния и справки «что это такое?». Простое решение состоит в применении браузера системы помощи. Приложения, которые включают в себя браузер системы помощи, обычно имеют подпункт меню Help в меню Help главного окна и кнопку Help в каждом диалоговом окне.
В данном разделе мы представим простой браузер системы помощи, показанный на рис. 16.3, и покажем, как его можно использовать в приложении. Окно приложения применяет QTextBrowser для вывода на экран страниц справки, представленных в формате HTML. QTextBrowser может обрабатывать много тегов HTML, и поэтому он идеально подходит для этих целей.
Мы начинаем с заголовочного файла:
01 #include
02 class QPushButton;
03 class QTextBrowser;
04 class HelpBrowser : public QWidget
05 {
06 Q_OBJECT
07 public:
08 HelpBrowser(const QString &path,
09 const QString &page, QWidget *parent = 0);
10 static void showPage(const QString &page);
11 private slots:
12 void updateWindowTitle();
13 private:
14 QTextBrowser *textBrowser;
15 QPushButton *homeButton;
16 QPushButton *backButton;
17 QPushButton *closeButton;
18 };
Класс HelpBrowser содержит статическую функцию, которую можно вызывать в любом месте в приложении. Данная функция создает окно HelpBrowser и выводит на экран заданную страницу.
Рис. 16.3. Виджет HelpBrowser.
Ниже приводится начало реализации:
01 #include
02 #include "helpbrowser.h"
03 HelpBrowser::HelpBrowser(const QString &path,
04 const QString &page, QWidget *parent)
05 : QWidget(parent)
06 {
07 setAttribute(Qt::WA_DeleteOnClose);
08 setAttribute(Qt::WA_GroupLeader);
09 textBrowser = new QTextBrowser;
10 homeButton = new QPushButton(tr("&Home"));
11 backButton = new QPushButton(tr("&Back"));
12 closeButton = new QPushButton(tr("Close"));
13 closeButton->setShortcut(tr("Esc"));
14 QHBoxLayout *buttonLayout = new QHBoxLayout;
15 buttonLayout->addWidget(homeButton);
16 buttonLayout->addWidget(backButton);
17 buttonLayout->addStretch();
18 buttonLayout->addWidget(closeButton);
19 QVBoxLayout *mainLayout = new QVBoxLayout;
20 mainLayout->addLayout(buttonLayout);
21 mainLayout->addWidget(textBrowser);
22 setLayout(mainLayout);
23 connect(homeButton, SIGNAL(clicked()),
24 textBrowser, SLOT(home()));
25 connect(backButton, SIGNAL(clicked()),
26 textBrowser, SLOT(backward()));
27 connect(closeButton, SIGNAL(clicked()),
28 this, SLOT(close()));
29 connect(textBrowser, SIGNAL(sourceChanged(const QUrl &)),
30 this, SLOT(updateWindowTitle()));
31 textBrowser->setSearchPaths(QStringList() << path << ":/images");
32 textBrowser->setSource(page);
33 }
Мы устанавливаем атрибут Qt::WA_GroupLeader, потому что хотим выдавать окна HelpBrowser не только из главного окна, но также из модальных диалоговых окон. Обычно модальные диалоговые окна не позволяют пользователям работать с другими окнами приложения. Однако очевидно, что после запроса помощи пользователь должен иметь возможность работать как с модальным диалоговым окном, так и с браузером системы помощи. Установка атрибута Qt::WA_GroupLeader делает возможным такой режим работы.
Мы обеспечиваем два пути поиска: первый определяет путь в файловой системе к документации приложения, а второй определяет расположение ресурсов изображений. HTML может содержать обычные ссылки на изображения в файловой системе и ссылки на ресурсы изображений, пути которых начинаются с символов :/ (двоеточие и слеш). Параметр page содержит имя файла документации с возможным указанием метки HTML (anchor).
01 void HelpBrowser::updateWindowTitle()
02 {
03 setWindowTitle(tr("Help: %1")
04 .arg(textBrowser->documentTitle()));
05 }
При всяком изменении исходной страницы вызывается слот updateWindowTitle(). Функция documentTitle() возвращает текст, содержащийся в теге
01 void HelpBrowser::showPage(const QString &page)
02 {
03 QString path = QApplication::applicationDirPath() + "/doc";
04 HelpBrowser *browser = new HelpBrowser(path, page);
05 browser->resize(500, 400);
06 browser->show();
07 }
В статической функции showPage() мы создаем окно HelpBrowser и затем выдаем его на экран. Это окно будет удалено автоматически, когда пользователь закроет его, поскольку мы установили в конструкторе HelpBrowser атрибут Qt::WA_DeleteOnClose.
В этом примере мы предполагаем, что документация располагается в подкаталоге doc того каталога, где находится исполняемый модуль приложения. Все страницы, передаваемые функции showPage(), будут браться из этого подкаталога.
Теперь мы можем вызывать браузер системы помощи из приложения. В главном окне приложения мы могли бы создать команду Help и подсоединить ее к слоту help(), который может иметь следующий вид:
01 void MainWindow::help()
02 {
03 HelpBrowser::showPage("index.html");
04 }
Здесь предполагается, что главный файл системы помощи имеет имя index.html. Для диалоговых окон мы могли бы подсоединить кнопку Help к слоту help(), который может иметь следующий вид:
01 void EntryDialog::help()
02 {
03 HelpBrowser::showPage("forms.html#editing");
04 }
Здесь мы выводим на экран другой справочный файл, forms.html, и позиционируем браузер QTextBrowser нa метку editing.
Использование
Qt Assistant
для мощной интерактивной системы помощи
Qt Assistant является свободно распространяемой интерактивной системой помощи, поддерживаемой фирмой «Trolltech». Основным ее достоинством является поддержка индексации и поиск по всему тексту, а также возможность ее работы с наборами документации нескольких приложений.
Для применения Qt Assistant мы должны включить в наше приложение соответствующий программный код и указать Qt Assistant место расположения нашей документации.
Связь между приложением Qt и QtAssistant обеспечивается классом QAssistantClient, который располагается в отдельной библиотеке. Для сборки этой библиотеки с нашим приложением мы должны добавить следующую строку к файлу приложения .pro:
CONFIG += assistant
Теперь мы рассмотрим программный код нового класса HelpBrowser, который использует Qt Assistant.
01 #ifndef HELPBROWSER_H
02 #define HELPBROWSER_H
03 class QAssistantClient;
04 class QString;
05 class HelpBrowser
06 {
07 public:
08 static void showPage(const QString &page);
09 private:
10 static QAssistantClient *assistant;
11 };
12 #endif
Ниже приводится новый файл helpbrowser.cpp:
01 #include
02 #include
03 #include "helpbrowser.h"
04 QAssistantClient *HelpBrowser::assistant = 0;
05 void HelpBrowser::showPage(const QString &page)
06 {
07 QString path = QApplication::applicationDirPath() + "/doc/" + page;
08 if (!assistant)
09 assistant = new QAssistantClient("");
10 assistant->showPage(path);
11 }
Конструктор QAssistantClient принимает в качестве своего первого аргумента строку пути, который используется для определения места нахождения исполняемого модуля Qt Assistant. Передавая пустой путь, мы указываем на необходимость QAssistantClient поиска исполняемого модуля в путях переменной среды PATH. QAssistantClient имеет функцию showPage(), которая принимает имя файла страницы HTML с необязательным указанием метки позиции.
На следующем этапе необходимо подготовить оглавление и предметный указатель документации. Это выполняется путем создания профиля Qt Assistant и файла .dcf, который содержит сведения о документации. Все это объясняется в документации по Qt Assistant, и поэтому мы не станем здесь повторять эти сведения.
В качестве альтернативы QTextBrowser или Qt Assistant можно спользовать зависящие от платформы методы обеспечения интерактивной помощи. Для приложений Windows можно создать файлы системы помощи Windows HTML Help и обеспечить доступ к ним при помощи Internet Explorer компании Microsoft. Вы могли бы использовать для этого класс Qt QProcess или рабочую среду ActiveQt. Для приложений X11 подходящий метод мог бы состоять в создании файлов HTML и запуске веб-браузера, с использованием QProcess. В Mac OS X подсистема Apple Help предоставляет аналогичные функциональные возможности для Qt Assistant.
На этом мы завершаем часть II. В рассматриваются более продвинутые и специализированные средства разработки Qt. Их применение при программировании на С++ вызывает не больше трудностей, чем программирование того, что мы видели в части II, однако некоторые концепции и идеи могут вызвать дополнительные сложности в новых для вас областях.