Многие пользовательские виджеты являются простой комбинацией существующих виджетов, либо встроенных в Qt, либо других пользовательских виджетов (таких, как
HexSpinBox). Если пользовательские виджеты строятся на основе существующих виджетов, то они, как правило, могут разрабатываться в
Qt Designer. • создайте новую форму, используя шаблон «Widget» (виджет);
• добавьте в эту форму необходимые виджеты и затем расположите их соответствующим образом;
• установите соединения сигналов и слотов;
• если необходима функциональность, которую нельзя обеспечить с помощью механизма сигналов и слотов, необходимый программный код следует писать в рамках класса, который наследует как класс
QWidget, так и класс, сгенерированный компилятором
uic. Естественно, комбинация существующих виджетов может быть также полностью запрограммирована вручную. При любом подходе полученный класс наследует непосредственно
QWidget. Если виджет не имеет своих собственных сигналов и слотов и не переопределяет никакую виртуальную функцию, можно просто собрать виджет из существующих виджетов, не создавая подкласс. Этим методом мы пользовались в
главе 1 для создания приложения Age с применением
QWidget, QSpinBox и
QSlider. Даже в этом случае мы могли бы легко определить подкласс
QWidget и в его конструкторе создать
QSpinBox и
QSlider. Когда под рукой нет подходящих виджетов Qt и когда нельзя получить желаемый результат, комбинируя и адаптируя существующие виджеты, мы можем все же создать требуемый виджет. Это достигается путем создания подкласса
QWidget и переопределением обработчиков некоторых событий, связанных с рисованием виджета и реагированием на щелчки мышки. При таком подходе мы свободно можем определять и управлять как внешним видом, так и режимом работы нашего виджета. Такие встроенные в Qt виджеты, как
QLabel, QPushButton и
QTableWidget, реализованы именно так. Если бы их не было в Qt, все же можно было бы создать их самостоятельно при помощи предусмотренных в классе
QWidget открытых функций, обеспечивающих полную независимость от платформы.
Для демонстрации данного подхода при написании пользовательского виджета мы создадим виджет
IconEditor, показанный на рис. 5.2. Виджет
IconEditor может использоваться в программе редактирования пиктограмм.
Рис. 5.2. Виджет IconEditor. Сначала рассмотрим заголовочный файл.
01 #ifndef ICONEDITOR_H
02 #define ICONEDITOR_H
03 #include <QColor>
04 #include <QImage>
05 #include <QWidget>
06 class IconEditor : public QWidget
07 {
08 Q_OBJECT
09 Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor)
10 Q_PROPERTY(QImage iconImage READ iconImage WRITE setIconImage)
11 Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor)
12 public:
13 IconEditor(QWidget *parent = 0);
14 void setPenColor(const QColor &newColor);
15 QColor penColor() const { return curColor; }
16 void setIconImage(const QImage &newImage);
17 QImage iconImage() const { return image; }
18 QSize sizeHint() const;
19 void setZoomFactor(int newZoom);
20 int zoomFactor() const { return zoom; }
Класс
IconEditor использует макрос
Q_PROPERTY() для объявления трех пользовательских свойств:
penColor, iconImage и
zoomFactor. Каждое свойство имеет тип данных, функцию «чтения» и необязательную функцию «записи». Например, свойство
penColor имеет тип
QColor и может считываться и записываться при помощи функций
penColor() и
setPenColor(). Когда мы используем виджет в
Qt Designer, пользовательские свойства появляются в редакторе свойств
Qt Designer ниже свойств, унаследованных от
QWidget. Свойства могут иметь любой тип, поддерживаемый
QVariant. Макрос
Q_OBJECT необходим для классов, в которых определяются свойства.
21 protected:
22 void mousePressEvent(QMouseEvent *event);
23 void mouseMoveEvent(QMouseEvent *event);
24 void paintEvent(QPaintEvent *event);
25 private:
26 void setImagePixel(const QPoint &pos, bool opaque);
27 QRect pixelRect(int i, int j) const;
28 QColor curColor;
29 QImage image;
30 int zoom;
31 };
32 #endif
IconEditor переопределяет три защищенные функции
QWidget и имеет несколько закрытых функций и переменных. В трех закрытых переменных содержатся значения трех свойств.
Файл реализации класса начинается с конструктора
IconEditor: 01 #include <QtGui>
02 #include "iconeditor.h"
03 IconEditor::IconEditor(QWidget *parent)
04 : QWidget(parent)
05 {
06 setAttribute(Qt::WA_StaticContents);
07 setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
08 curColor = Qt::black;
09 zoom = 8;
10 image = QImage(16, 16, QImage::Format_ARGB32);
11 image.fill(qRgba(0, 0, 0, 0));
12 }
В конструкторе имеется несколько тонких моментов, связанных с применением атрибута
Qt::WA_StaticContents и вызовом функции
setSizePolicy(). Вскоре мы обсудим их.
Устанавливается черный цвет пера. Коэффициент масштабирования изображения (zoom factor) устанавливается на 8, то есть каждый пиксель пиктограммы представляется квадратом 8 × 8.
Данные пиктограммы хранятся в переменной—члене
image, и доступ к ним может осуществляться при помощи функций
setIconImage() и
iconImage(). Программа редактирования пиктограмм обычно вызывает функцию
setIconImage() при открытии пользователем файла пиктограммы и функцию
iconImage() для считывания пиктограммы из памяти, когда пользователь хочет ее сохранить. Переменная
image имеет тип
QImage. Мы инициализируем ее областью в 16 × 16 пикселей и на 32-битовый формат ARGB, который поддерживает полупрозрачность. Мы очищаем данные изображения, устанавливая признак прозрачности.
Способ хранения изображения в классе
QImage не зависит от оборудования. При этом его глубина может устанавливаться на 1, 8 или 32 бита. Изображения с 32-битовой глубиной используют по 8 бит на красный, зеленый и синий компоненты пикселя. В остальных 8 битах хранится альфа—компонент пикселя (уровень его прозрачности). Например, компоненты красный, зеленый и синий «чистого» красного цвета и альфа—компонент имеют значения 255, 0, 0 и 255. В Qt этот цвет можно задавать так:
QRgb red = qRgba(255, 0, 0, 255);
или так (поскольку этот цвет непрозрачен):
QRgb red = qRgb(255, 0, 0);
Тип
QRgb просто синоним типа
unsigned int, созданный с помощью директивы
typedef, a
qRgb() и
qRgba() являются встроенными функциями (то есть со спецификатором
inline), которые преобразуют свои аргументы в 32-битовое целое число. Допускается также запись
QRgb red = 0xFFFF0000;
где первые FF соответствуют альфа—компоненту, а вторые FF — красному компоненту. В конструкторе класса
IconEditor мы делаем
QImage прозрачным, используя 0 в качестве значения альфа—компонента.
В Qt для хранения цветов предусмотрено два типа:
QRgb и
QColor. В то время как
QRgb всего лишь определяется в
QImage ключевым словом
typedef для представления пикселей 32-битовым значением,
QColor является классом, который имеет много полезных функций и широко используется в Qt для хранения цветов. В виджете
IconEditor мы используем
QRgb только при работе с
QImage; мы применяем
QColor во всех остальных случаях, включая свойство цвет пера
penColor. 13 QSize IconEditor::sizeHint() const
14 {
15 QSize size = zoom * image.size();
16 if (zoom >= 3)
17 size += QSize(1, 1);
18 return size;
19 }
Функция
sizeHint() класса
QWidget переопределяется и возвращает «идеальный» размер виджета. Здесь мы размер изображения умножаем на масштабный коэффициент и в случае, когда масштабный коэффициент равен или больше 3, добавляем еще один пиксель по каждому направлению для размещения сетки. (Мы не показываем сетку при масштабном коэффициенте 1 или 2, поскольку в этом случае едва ли найдется место для пикселей пиктограммы.)
Идеальный размер виджета играет очень заметную роль при размещении виджетов. Менеджеры компоновки Qt стараются максимально учесть идеальный размер виджета при размещении дочерних виджетов. Для того чтобы
IconEditor был удобен для менеджера компоновки, он должен сообщить свой правдоподобный идеальный размер.
Кроме идеального размера виджет имеет «политику размера», которая говорит системе компоновки о желательности или нежелательности его растяжения или сжатия. Вызывая в конструкторе функцию
setSizePolicy() со значением
QSizePolicy::Minimum в качестве горизонтальной и вертикальной политики, мы указываем менеджеру компоновки, который отвечает за размещение этого виджета, на то, что идеальный размер является фактически его минимальным размером. Другими словами, при необходимости виджет может растягиваться, но он никогда не должен сжиматься до размеров меньших, чем идеальный. Политику размера можно изменять в
Qt Designer путем установки свойства виджета
sizePolicy. Смысл различной политики размеров объясняется в
главе 6 («Управление компоновкой»).
20 void IconEditor::setPenColor(const QColor &newColor)
21 {
22 curColor = newColor;
23 }
Функция
setPenColor() устанавливает текущий цвет пера. Этот цвет будет использоваться при выводе на экран новых пикселей.
24 void IconEditor::setIconImage(const QImage &newImage)
25 {
26 if (newImage != image) {
27 image = newImage.convertToFormat(QImage::Format_ARGB32);
28 update();
29 updateGeometry();
30 }
31 }
Функция
setIconImage() задает изображение для редактирования. Мы вызываем
convertToFormat() для установки 32-битовой глубины изображения с альфа—буфером, если это еще не сделано. В дальнейшем везде мы будем предполагать, что изображение хранится в 32-битовых элементах типа ARGB.
После установки переменной
image мы вызываем функцию
QWidget::update() для принудительной перерисовки виджета с новым изображением. Затем мы вызываем
QWidget::updateGeometry(), чтобы сообщить всем содержащим этот виджет менеджерам компоновки об изменении идеального размера виджета. Размещение виджета затем будет автоматически адаптировано к его новому идеальному размеру.
32 void IconEditor::setZoomFactor(int newZoom)
33 {
34 if (newZoom < 1)
35 newZoom = 1;
36 if (newZoom != zoom) {
37 zoom = newZoom;
38 update();
39 updateGeometry();
40 }
41 }
Функция
setZoomFactor() устанавливает масштабный коэффициент изображения. Для предотвращения деления на нуль мы корректируем всякое значение, меньшее, чем 1. Мы опять вызываем функции
update() и
updateGeometry() для перерисовки виджета и уведомления всех менеджеров компоновки об изменении идеального размера.
Функции
penColor(), iconImage() и
zoomFactor() реализуются в заголовочном файле как встроенные функции.
Теперь мы рассмотрим программный код функции
paintEvent(). Эта функция играет очень важную роль в классе
IconEditor. Она вызывается всякий раз, когда требуется перерисовать виджет. Используемая по умолчанию ее реализация в
QWidget ничего не делает, оставляя виджет пустым.
Так же как рассмотренная нами в
главе 3 функция
closeEvent(), функция
paintEvent() является обработчиком события. В Qt предусмотрено много других обработчиков событий, каждый из которых относится к определенному типу события. Обработка событий подробно рассматривается в
главе 7.
Существует множество ситуаций, когда генерируется событие рисования
(paint) и вызывается функция
paintEvent(): • при первоначальном выводе на экран виджета система автоматически генерирует событие рисования, чтобы виджет нарисовал сам себя;
• при изменении размеров виджета система генерирует событие рисования;
• если виджет перекрывается другим окном и затем вновь оказывается видимым, генерируется событие рисования для областей, которые закрывались (если только система управления окнами не сохранит закрытую область).
Мы можем также принудительно сгенерировать событие рисования путем вызова функции
QWidget::update() или
QWidget::repaint(). Различие между этими функциями следующее:
repaint() приводит к немедленной перерисовке, а функция
update() просто передает событие рисования в очередь событий, обрабатываемых Qt. (Обе функции ничего не будут делать, если виджет невидим на экране.) Если
update() вызывается несколько раз, Qt из нескольких следующих друг за другом событий рисования делает одно событие для предотвращения мерцания. В классе
IconEditor мы всегда используем функцию
update(). Ниже приводится программный код:
42 void IconEditor::paintEvent(QPaintEvent *event)
43 {
44 QPainter painter(this);
45 if (zoom >= 3) {
46 painter.setPen(palette().foreground().color());
47 for (int i = 0; i <= image.width(); ++i)
48 painter.drawLine(zoom * i, 0,
49 zoom * i, zoom * image.height());
50 for (int j = 0; j <= image.height(); ++j)
51 painter.drawLine(0, zoom * j,
52 zoom * image.width(), zoom * j);
53 }
54 for (int i = 0; i < image.width(); ++i) {
55 for (int j = 0; j < image.height(); ++j) {
56 QRect rect = pixelRect(i, j);
57 if (!event->region().intersect(rect).isEmpty()) {
58 QColor color = QColor::fromRgba(image.pixel(i, j));
59 painter.fillRect(rect, color);
60 }
61 }
62 }
63 }
Мы начинаем с построения объекта
QPainter нашего виджета. Если масштабный коэффициент равен или больше 3, мы вычерчиваем с помощью функции
QPainter::drawLine() горизонтальные и вертикальные линии сетки.
Вызов функции
QPainter::drawLine() имеетследующий формат:
painter.drawLine(x1, y1, x2, y2);
где
(x1, y1) задает положение одного конца линии и
(x2, y2) задает положение другого конца линии. Существует перегруженный вариант функции, которая принимает два объекта типа
QPoint вместо четырех целых чисел.
Пиксель в верхнем левом углу виджета Qt имеет координаты (0, 0), а пиксель в нижнем правом углу имеет координаты (
width() — 1,
height() — 1). Это напоминает обычную декартовскую систему координат, но только перевернутую сверху вниз. Мы можем изменить систему координат в
QPainter, трансформируя ее такими способами, как смещение, масштабирование, вращение и отсечение. Эти вопросы рассматриваются в
главе 8 («Графика 2D и 3D»).
Рис. 5.3. Вычерчивание линии при помощи QPainter. Перед вызовом в
QPainter функции
drawLine() мы устанавливаем цвет линии, используя функцию
setPen(). Мы могли бы жестко запрограммировать цвет (например, черный или серый), но лучше использовать палитру виджета.
Каждый виджет имеет палитру, которая определяет назначение цветов. Например, предусмотрен цвет фона виджетов (обычно светло—серый) и цвет текста на этом фоне (обычно черный). По умолчанию палитра виджета адаптирована под схему цветов оконной системы. Используя цвета из палитры, мы обеспечим в
IconEditor учет пользовательских настроек.
Палитра виджета состоит из трех цветовых групп: активной, неактивной и нерабочей. Цветовая группа выбирается в зависимости от текущего состояния виджета:
• группа
Active используется для виджетов текущего активного окна;
• группа
Inactive используется виджетами других окон;
• группа
Disabled используется отключенными виджетами любого окна.
Функция
QWidget::palette() возвращает палитру виджета в виде объекта
QPalette. Цветовые группы определяются как элементы перечисления типа
QPalette::QColorGroup. Удобная функция
QWidget::colorGroup() возвращает правильную цветовую группу текущего состояния виджета, и поэтому нам редко придется выбирать цвет непосредственно из палитры.
Когда нам нужно получить соответствующую кисть или цвет для рисования, правильный подход связан с применением текущей палитры, полученной функцией
QWidget::palette(), и соответствующей ролевой функции, например
QPalette::foreground(). Каждая ролевая функция возвращает кисть, что обычно и требуется, однако если нам нужен только цвет, его можно извлечь из кисти, как мы это делали в
paintEvent(). По умолчанию возвращаемые кисти соответствуют состоянию виджета, поэтому нам не надо указывать цветовую группу.
Функция
paintEvent() завершает рисование изображения. Вызов
IconEditor::pixelRect() возвращает
QRect, который определяет область перерисовки. Мы не выдаем пиксели, которые попадают за пределы данной области, обеспечивая простую оптимизацию.
Рис. 5.4. Вычерчивание прямоугольника при помощи QPainter. Мы вызываем
QPainter::fillRect() для вывода на экран масштабируемого пикселя.
QPainter::fillRect() принимает
QRect и
QBrush. Передавая
QColor в качестве кисти, мы обеспечиваем равномерное заполнение области.
64 QRect IconEditor::pixelRect(int i, int j) const
65 {
66 if (zoom >= 3) {
67 return QRect(zoom * i + 1, zoom * j + 1, zoom - 1, zoom - 1);
68 } else {
69 return QRect(zoom * i, zoom * j, zoom, zoom);
70 }
71 }
Функция
pixelRect() возвращает объект
QRect, который может использоваться функцией
QPainter::fillRect(). Параметры
i и
j являются координатами пикселя в
QImage, а не в виджете. Если коэффициент масштабирования равен 1, обе системы координат будут полностью совпадать.
Конструктор
QRect имеет синтаксис
QRect(x, у, width, height), где
(x, у) являются координатами верхнего левого угла прямоугольника, a
width и
height являются размерами прямоугольника (шириной и высотой). Если коэффициент масштабирования равен не менее 3, мы уменьшаем размеры прямоугольника на один пиксель по горизонтали и по вертикали, чтобы не загораживать линии сетки.
72 void IconEditor::mousePressEvent(QMouseEvent *event)
73 {
74 if (event->button() == Qt::LeftButton) {
75 setImagePixel(event->pos(), true);
76 } else if (event->button() == Qt::RightButton) {
77 setImagePixel(event->pos(), false);
78 }
79 }
Когда пользователь нажимает кнопку мышки, система генерирует событие «клавиша мышки нажата» (mouse press). Путем переопределения функции
QWidget::mousePressEvent() мы можем обработать это событие и установить или стереть пиксель изображения, находящийся под курсором мышки.
Если пользователь нажал левую кнопку мышки, мы вызываем закрытую функцию
setImagePixel() c
true в качестве второго аргумента, указывая на необходимость установки цвета пикселя на текущий цвет пера. Если пользователь нажал правую кнопку мышки, мы также вызываем функцию
setImagePixel(), но передаем
false для стирания пикселя.
80 void IconEditor::mouseMoveEvent(QMouseEvent *event)
81 {
82 if (event->buttons() & Qt::LeftButton) {
83 setImagePixel(event->pos(), true);
84 } else if (event->buttons() & Qt::RightButton) {
85 setImagePixel(event->pos(), false);
86 }
87 }
Функция
mouseMoveEvent() обрабатывает события «перемещение мышки». По умолчанию эти события генерируются только при нажатой пользователем кнопки мышки. Можно изменить этот режим работы с помощью вызова функции
QWidget::setMouseTracking(), но нам не нужно это делать в нашем примере.
Как при нажатии левой или правой кнопки мышки устанавливается или стирается пиксель, так и при удерживании нажатой кнопки над пикселем тоже будет устанавливаться или стираться пиксель. Поскольку допускается удерживать нажатыми одновременно несколько кнопок, возвращаемое функцией
QMouseEvent::buttons() значение представляет собой результат логической операции поразрядного ИЛИ для кнопок. Мы проверяем нажатие определенной кнопки при помощи оператора & и при наличии соответствующего состояния вызываем функцию
setImagePixel(). 88 void IconEditor::setImagePixel(const QPoint &pos, bool opaque)
89 {
90 int i = pos.x() / zoom;
91 int j = pos.y() / zoom;
92 if (image.rect().contains(i, j)) {
93 if (opaque) {
94 image.setPixel(i, j, penColor().rgba());
95 } else {
96 image.setPixel(i, j, qRgba(0, 0, 0, 0));
97 }
98 update(pixelRect(i, j));
99 }
100 }
Функция
setImagePixel() вызывается из
mousePressEvent() и
mouseMoveEvent() для установки или стирания пикселя. Параметр
pos определяет положение мышки на виджете.
На первом этапе надо преобразовать положение мышки из системы координат виджета в систему координат изображения. Это достигается путем деления координат положения мышки
x() и
y() на коэффициент масштабирования. Затем мы проверяем попадание точки в нужную область. Это легко сделать при помощи функций
QImage::rect() и
QRect::contains(); фактически здесь проверяется попадание значения переменной i в промежуток между 0 и значением
image.width() — 1, а переменной j — в промежуток между 0 и значением
image.height() — 1. В зависимости от значения параметра
opaque мы устанавливаем или стираем пиксель в изображении. При стирании пиксель фактически становится прозрачным. Для вызова
QImage::setPixel() мы должны преобразовать перо
QColor в 32-битовое значение ARGB. В конце мы вызываем функцию
update() с передачей объекта
QRect, задающего область перерисовки.
Теперь, когда уже рассмотрены функции—члены, мы вернемся к используемому в конструкторе атрибуту
Qt::WA_StaticContents. Этот атрибут указывает Qt на то, что содержимое виджета не изменяется при изменении его размеров и что его верхний левый угол остается на прежнем месте. Qt использует эту информацию, чтобы лишний раз не перерисовывать при изменении размеров виджета уже видимые его области.
Обычно при изменении размеров виджета Qt генерирует событие рисования для всей видимой области виджета. Но если виджет создается с установленным флажком
Qt::WA_StaticContents, область рисования ограничивается не показанными ранее пикселями. Это подразумевает, что, если размеры виджета уменьшаются, событие рисования вообще не будет сгенерировано.
Рис. 5.5. Изменение размеров виджета Qt::WA_StaticContents. Теперь виджет
IconEditor полностью построен. На основе применения приводимых в предыдущих главах сведений и примеров мы можем написать программу, в которой виджет
IconEditor будет сам являться окном, использоваться в качестве центрального виджета в главном окне
QMainWindow, в качестве дочернего виджета менеджера компоновки или в качестве дочернего виджета объекта
QScrollArea. В следующем разделе мы рассмотрим способы его интеграции в
Qt Designer.
Прежде чем мы сможем использовать пользовательские виджеты в
Qt Designer, мы должны сделать так, что
Qt Designer будет знать о них. Для этого существует два способа: метод «продвижения» (promotion) и метод подключения (plugin).
Метод продвижения является самым быстрым и самым простым. Он заключается в выборе некоторого встроенного виджета Qt, программный интерфейс которого похож на программный интерфейс пользовательского виджета, и заполнении полей диалогового окна в
Qt Designer некоторыми данными о пользовательском виджете. Впоследствии этот виджет может использоваться в формах, разработанных с помощью
Qt Designer, но при редактировании или просмотре он отображается просто в виде выбранного встроенного виджета Qt.
Ниже приводится порядок действий при интеграции данным методом виджета
HexSpinBox: 1. Создайте наборный счетчик
QSpinBox, перетаскивая его с панели виджетов
Qt Designer на форму.
2. Щелкните правой клавишей мышки по наборному счетчику и выберите пункт контекстного меню Promote to Custom Widget (Преобразовать в пользовательский виджет).
3. Заполните в появившемся диалоговом окне поле названия класса значением
«HexSpinBox» и поле заголовочного файла значением
«hexspinbox.h». Вот и все! Сгенерированный компилятором
uic программный код будет содержать оператор
#include hexspinbox.h вместо
<QSpinBox> и будет инстанцировать
HexSpinBox. В
Qt Designer виджет
HexSpinBox будет представлен виджетом
QSpinBox, позволяя нам устанавливать любые свойства
QSpinBox (например, допустимый диапазон значений и текущее значение).
Рис. 5.6. Диалоговое окно для создания пользовательских виджетов Qt Designer. Недостатками метода продвижения являются недоступность в
Qt Designer свойств, характерных для пользовательского виджета, и то, что пользовательский виджет представляется в
QtDesigner не своим изображением. Обе эти проблемы могут быть решены при применении метода подключения.
Метод подключения требует создания библиотеки подключаемых модулей, которую
Qt Designer может загружать во время выполнения и использовать для создания экземпляров виджетов. В этом случае при редактировании формы и ее просмотре в
Qt Designer будет использован реальный виджет, и благодаря мета—объектной системе Qt можно динамически получать список его свойств в
Qt Designer. Для демонстрации этого метода мы с его помощью выполним интеграцию редактора пиктограмм
IconEditor, описанного в предыдущем разделе.
Во-первых, мы должны создать подкласс
QDesignerCustomWidgetInterface и переопределить несколько виртуальных функций. Мы предположим, что исходный файл подключаемого модуля расположен в каталоге с именем
iconeditorplugin, а исходный текст программы
IconEditor расположен в параллельном каталоге с именем
iconeditor. Ниже приводится определение класса:
01 #include <QDesignerCustomWidgetInterface>
02 class IconEditorPlugin : public QObject,
03 public QDesignerCustomWidgetInterface
04 {
05 Q_OBJECT
06 Q_INTERFACES(QDesignerCustomWidgetInterface)
07 public:
08 IconEditorPlugin(QObject *parent = 0);
09 QString name() const;
10 QString includeFile() const;
11 QString group() const;
12 QIcon icon() const;
13 QString toolTip() const;
14 QString whatsThis() const;
15 bool isContainer() const;
16 QWidget *createWidget(QWidget *parent);
17 };
Подкласс
IconEditorPlugin является фабрикой класса (factory class), который инкапсулирует виджет
IconEditor. Он является наследником классов
QObject и
QDesignerCustomWidgetIterface и использует макрос
Q_INTERFACES(), указывая компилятору
moc на то, что второй базовый класс представляет собой подключаемый интерфейс. Его функции применяются
Qt Designer для создания экземпляров класса и получения информации о нем.
01 IconEditorPlugin::IconEditorPlugin(QObject *parent)
02 : QObject(parent)
03 {
04 }
IconEditorPlugin имеет тривиальный конструктор.
05 QString IconEditorPlugin::name() const
06 {
07 return "IconEditor";
08 }
Функция
name() возвращает имя подключаемого виджета.
09 QString IconEditorPlugin::includeFile() const
10 {
11 return "iconeditor.h";
12 }
Функция
includeFile() возвращает имя заголовочного файла для заданного виджета, который инкапсулирован в подключаемом модуле. Заголовочный файл включается в программный код, сгенерированный компилятором
uic. 13 QString IconEditorPlugin::group() const
14 {
15 return tr("Image Manipulation Widgets");
16 }
Функция
group() возвращает имя группы на панели виджетов, к которой принадлежит пользовательский виджет. Если это имя еще не используется,
Qt Designer coздаст новую группу для виджета.
17 QIcon IconEditorPlugin::icon() const
18 {
19 return QIcon(":/images/iconeditor.png");
20 }
Функция
icon() возвращает пиктограмму которая будет использоваться для представления пользовательского виджета на панели виджетов
Qt Designer. В нашем случае мы предполагаем, что
IconEditorPlugin имеет ресурсный файл Qt, содержащий соответствующий элемент для изображения редактора пиктограмм.
21 QString IconEditorPlugin::toolTip() const
22 {
23 return tr("An icon editor widget");
24 }
Функция
toolTip() возвращает всплывающую подсказку, которая появляется, когда мышка находится на пользовательском виджете в панели виджетов
Qt Designer. 25 QString IconEditorPlugin::whatsThis() const
26 {
27 return tr("This widget is presented in Chapter 5 of <i>C++ GUI "
28 "Programming with Qt 4</i> as an example of a custom Qt "
29 "widget.");
30 }
Функция
whatsThis() возвращает текст «What's This?» (что это?) для отображения в
Qt Designer. 31 bool IconEditorPlugin::isContainer() const
32 {
33 return false;
34 }
Функция
isContainer() возвращает
true, если данный виджет может содержать другие виджеты; в противном случае он возвращает
false. Например,
QFrame представляет собой виджет, который может содержать другие виджеты. В целом любой виджет может содержать другие виджеты, но
Qt Designer не позволяет это делать, если
isContainer() возвращает
false. 35 QWidget *IconEditorPlugin::createWidget(QWidget *parent)
36 {
37 return new IconEditor(parent);
38 }
Функция
createWidget() вызывается
Qt Designer для создания экземпляра класса виджета для указанного родительского виджета.
39 Q_EXPORT_PLUGIN2(iconeditorplugin, IconEditorPlugin)
В конце исходного файла реализации класса подключаемого модуля мы должны использовать макрос
Q_EXPORT_PLUGIN2(), чтобы сделать его доступным для
Qt Designer. Первый аргумент — назначаемое нами имя подключаемого модуля, второй аргумент — имя класса, который его реализует.
Используемый для построения подключаемого модуля файл
.pго выглядит следующим образом:
TEMPLATE = lib
CONFIG += designer plugin release
HEADERS = ../iconeditor/iconeditor.h \
iconeditorplugin.h
SOURCES = ../iconeditor/iconeditor.cpp \
iconeditorplugin.cpp
RESOURCES = iconeditorplugin.qrc
DESTDIR = $(QTDIR)/plugins/designer
Файл
.pro предполагает, что переменная окружения
QTDIR установлена на каталог, где располагается Qt. Когда вы вводите команду
make или
nmake для построения подключаемого модуля, он автоматически устанавливается в каталог
plugins Qt Designer. Поле построения подключаемого модуля виджет
IconEditor мoжeт использоваться в
Qt Designer таким же образом как, любые встроенные виджеты Qt.
Если требуется интегрировать в
Qt Designer несколько пользовательских виджетов, вы можете либо создать отдельный подключаемый модуль для каждого из них, либо объединить все в один подключаемый модуль, реализуя интерфейс
QDesignerCustomWidgetCollectionInterface.
Двойная буферизация является методом программирования графического пользовательского интерфейса, при котором изображение виджета формируется вне экрана в виде пиксельной карты, и затем эта пиксельная карта выводится на экран. В ранних версиях Qt этот метод часто использовался для предотвращения мерцания изображения и для построения более быстрого пользовательского интерфейса.
В Qt 4 класс
QWidget это делает автоматически, поэтому нам редко приходится беспокоиться о мерцании виджетов. Все же явная двойная буферизация оказывается полезной, если виджет воспроизводится сложным образом и это приходится делать постоянно. Мы можем постоянно хранить с виджетом пиксельную карту, которая всегда будет готова отреагировать на следующее событие рисования, и копировать пиксельную карту в виджет при получении нами любого события рисования. Она особенно полезна в тех случаях, когда мы хотим выполнить небольшие модификации, например начертить резиновую ленту без необходимости постоянной перерисовки виджета.
Мы закончим данную главу рассмотрением пользовательского виджета
Plotter (построитель графиков). Этот виджет использует двойную буферизацию и также демонстрирует некоторые другие аспекты Qt—программирования, в том числе обработку событий клавиатуры, ручную компоновку виджетов и координатные системы.
Виджет
Plotter выводит на экран одну или несколько кривых, задаваемых вектором ее координат. Пользователь может начертить на изображении резиновую ленту, и
Plotter отобразит крупным планом заключенную в ней область. Пользователь вычерчивает резиновую ленту, делая сначала щелчок в некоторой точке изображения, перетаскивая затем мышку с нажатой левой кнопкой в другую позицию и освобождая клавишу мышки.
Рис. 5.7. Увеличение изображения виджета Plotter. Пользователь может увеличивать изображение, несколько раз используя резиновую ленту, уменьшить изображение при помощи кнопки Zoom Out (уменьшить изображение) и затем вновь его увеличить с помощью кнопки Zoom In (увеличить изображение). Кнопки Zoom In и Zoom Out появляются при первом изменении масштаба изображения, и поэтому они не будут заслонять экран, если пользователь не изменяет масштаб представления диаграммы.
Виджет
Plotter может содержать данные любого количества кривых. Он также содержит стек параметров графика
PlotSettings, каждое значение которого соответствует конкретному масштабу изображения.
Давайте рассмотрим этот класс, начиная с заголовочного файла
plotter.h: 01 #ifndef PLOTTER_H
02 #define PLOTTER_H
03 #include <QMap>
04 #include <QPixmap>
05 #include <QVector>
06 #include <QWidget>
07 class QToolButton;
08 class PlotSettings;
09 class Plotter : public QWidget
10 {
11 Q_OBJECT
12 public:
13 Plotter(QWidget *parent = 0);
14 void setPlotSettings(const PlotSettings &settings);
15 void setCurveData(int id, const QVector<QPointF> &data);
16 void clearCurve(int id);
17 QSize minimumSizeHint() const;
18 QSize sizeHint() const;
19 public slots:
20 void zoomIn();
21 void zoomOut();
Сначала мы включаем заголовочные файлы для Qt—классов, используемых в заголовочном файле построителя графиков, и предварительно объявляем классы, на которые имеются указатели или ссылки в заголовочном файле.
В классе
Plotter мы предоставляем три открытые функции для настройки графика и два открытых слота для увеличения и уменьшения масштаба изображения. Мы также переопределяем функции
minimumSizeHint() и
sizeHint() класса
QWidget. Мы храним точки кривой в векторе
QVector<QPointF>, где
QPointF — версия
QPoint для значений с плавающей точкой.
22 protected:
23 void paintEvent(QPaintEvent *event);
24 void resizeEvent(QResizeEvent *event);
25 void mousePressEvent(QMouseEvent *event);
26 void mouseMoveEvent(QMouseEvent *event);
27 void mouseReleaseEvent(QMouseEvent *event);
28 void keyPressEvent(QKeyEvent *event);
29 void wheelEvent(QWheelEvent *event);
В защищенной секции класса мы объявляем все обработчики событий
QWidget, которые хотим переопределить.
30 private:
31 void updateRubberBandRegion();
32 void refreshPixmap();
33 void drawGrid(QPainter *painter);
34 void drawCurves(QPainter *painter);
35 enum { Margin = 50 };
36 QToolButton *zoomInButton;
37 QToolButton *zoomOutButton;
38 QMap<int, QVector<QPointF> > curveMap;
39 QVector<PlotSettings> zoomStack;
40 int curZoom;
41 bool rubberBandIsShown;
42 QRect rubberBandRect;
43 QPixmap pixmap;
44 };
В закрытой секции класса мы объявляем несколько функций для рисования виджета, константу и несколько переменных—членов. Константа
Margin применяется для обеспечения некоторого свободного пространства вокруг диаграммы.
Среди переменных—членов находится
pixmap, которая имеет тип
QPixmap. Эта переменная содержит копию всего виджета, идентичную его изображению на экране. График всегда сначала строится вне экрана на пиксельной карте, и затем пиксельная карта помещается на виджет.
45 class PlotSettings
46 {
47 public:
48 PlotSettings();
49 void scroll(int dx, int dy);
50 void adjust();
51 double spanX() const { return maxX - minX; }
52 double spanY() const { return maxY - minY; }
53 double minX;
54 double maxX;
55 int numXTicks;
56 double minY;
57 double maxY;
58 int numYTicks;
59 private:
60 static void adjustAxis(double &min, double &max, int &numTicks);
61 };
62 #endif
Класс
PlotSettings задает диапазон значений по осям
x и
y и количество отметок на этих осях. На рис. 5.8 показано соответствие между объектом
PlotSettings и виджетом
Plotter. По условному соглашению значение в
numXTicks и
numYTicks задается на единицу меньше; если
numXTicks равно 5,
Plotter будет на самом деле выводить 6 отметок по оси
x. Это упростит расчеты в будущем.
Рис. 5.8. Переменные—члены настроек графика PlotSettings. Теперь давайте рассмотрим файл реализации:
001 #include <QtGui>
002 #include <cmath>
003 #include "plotter.h"
Мы включаем необходимые заголовочные файлы и импортируем все символы пространства имен
std в глобальное пространство имен. Это позволяет нам получать доступ к функциям, объявленным в
<cmath>, без указания префикса
std:: (например,
floor() вместо
std::floor()).
004 Plotter::Plotter(QWidget *parent)
005 : QWidget(parent)
006 {
007 setBackgroundRole(QPalette::Dark);
008 setAutoFillBackground(true);
009 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
010 setFocusPolicy(Qt::StrongFocus);
011 rubberBandIsShown = false;
012 zoomInButton = new QToolButton(this);
013 zoomInButton->setIcon(QIcon(":/images/zoomin.png"));
014 zoomInButton->adjustSize();
015 connect(zoomInButton, SIGNAL(clicked()), this, SLOT(zoomIn()));
016 zoomOutButton = new QToolButton(this);
017 zoomOutButton->setIcon(QIcon(":/images/zoomout.png"));
018 zoomOutButton->adjustSize();
019 connect(zoomOutButton, SIGNAL(clicked()), this, SLOT(zoomOut()));
020 setPlotSettings(PlotSettings());
021 }
Вызов
setBackgroundRole() указывает
QWidget на необходимость использования для цвета стирания виджета «темного» компонента палитры вместо компонента «window» (окно). Этим мы определяем цвет, который будет использоваться в Qt по умолчанию для заполнения любых вновь появившихся пикселей при увеличении размеров виджета прежде, чем
paintEvent() получит возможность рисования нового пикселя. Для включения этого механизма необходимо также вызвать
setAutoFillBackground(true). (По умолчанию дочерние виджеты наследуют фон своего родительского виджета.)
Вызов
setSizePolicy() устанавливает политику размера виджета по обоим направлениям на значение
QSizePolicy::Expanding. Это подсказывает любому менеджеру компоновки, который ответственен за виджет, что он прежде всего склонен к росту, но может также сжиматься. Такая настройка параметров типична для виджетов, которые занимают много места на экране. По умолчанию в обоих направлениях устанавливается политика
QSizePolicy::Preferred, означающая, что для виджета предпочтительно устанавливать размер на основе его идеального размера, но он может сжиматься до своего минимального идеального размера или расширяться в любых пределах при необходимости.
Вызов
setFocusPolicy(Qt::StrongFocus) заставляет виджет получать фокус при нажатии клавиши табуляции Tab. Когда
Plotter получает фокус, он будет реагировать на события нажития клавиш. Виджет
Plotter понимает несколько клавиш: «+» для увеличения изображения, «—» для уменьшения изображения и клавиш стрелок для прокрутки вверх, вниз, влево и вправо.
Рис. 5.9. Скроллинг виджета Plotter. Также в конструкторе мы создаем две кнопки
QToolButtons, каждая из которых имеет пиктограмму. Эти кнопки дают возможность пользователю увеличивать и уменьшать масштаб изображения. Пиктограммы кнопок хранятся в файле ресурсов, поэтому любое приложение, использующее виджет
Plotter, должно иметь следующую строку в файле
.pro: RESOURCES = plotter.qrc
Этот файл ресурсов похож на файл, который мы использовали для приложения Электронная таблица:
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>images/zoomin.png</file>
<file>images/zoomout.png</file>
</qresource>
</RCC>
Вызовы функции
adjustSize() устанавливают для кнопок их идеальные размеры. Кнопки не размещаются в менеджере компоновки; вместо этого мы задаем их положение вручную при обработке события изменения размеров виджета
Plotter. Поскольку мы не пользуемся никакими менеджерами компоновки, необходимо явно задавать родительский виджет кнопок, передавая
this конструктору
QPushButton. Вызов в конце функции
setPlotSettings() завершает инициализацию.
022 void Plotter::setPlotSettings(const PlotSettings &settings)
023 {
024 zoomStack.clear();
025 zoomStack.append(settings);
026 curZoom = 0;
027 zoomInButton->hide();
028 zoomOutButton->hide();
029 refreshPixmap();
030 }
Функция
setPlotSettings() устанавливает настройки
PlotSettings для отображения графика. Ее вызывает конструктор
Plotter, и она может также вызываться пользователями класса. Построитель кривых начинает работу с принятого по умолчанию масштаба изображения. Каждый раз, когда пользователь увеличивает изображение, создается новый экземпляр
PlotSettings, который затем помещается в стек масштабов изображения. Этот стек масштабов изображений представлен двумя переменными—членами:
•
zoomStack содержит настройки для различных масштабов изображения в объекте
QVector<PlotSettings>; •
curZoom содержит индекс текущего элемента
PlotSettings стека
zoomStack. После вызова функции
setPlotSettings() в стеке масштабов изображений будет находиться только один элемент, а кнопки Zoom In и Zoom Out будут скрыты. Эти кнопки не будут видны на экране до тех пор, пока мы не вызовем для них функцию
show() в слотах
zoomIn() и
zoomOut(). (Обычно для показа всех дочерних виджетов достаточно вызвать функцию
show() для виджета верхнего уровня. Но когда мы явным образом вызываем для дочернего виджета функцию
hide(), этот виджет будет скрыт до вызова для него функции
show().)
Вызов функции
refreshPixmap() необходим для обновления изображения на экране. Обычно мы вызываем функцию
update(), но здесь мы поступаем немного по-другому, потому что хотим иметь пиксельную карту
QPixmap постоянно в обновленном состоянии. После регенерации пиксельной карты функция
refreshPixmap() вызывает
update() для помещения пиксельной карты на виджет.
031 void Plotter::zoomOut()
032 {
033 if (curZoom > 0) {
034 --curZoom;
035 zoomOutButton->setEnabled(curZoom > 0);
036 zoomInButton->setEnabled(true);
037 zoomInButton->show();
038 refreshPixmap();
039 }
040 }
Слот
zoomOut() уменьшает масштаб диаграммы, если она отображена крупным планом. Он уменьшает на единицу текущий масштаб изображения и включает или выключает кнопку
ZoomOut, в зависимости от возможности дальнейшего уменьшения диаграммы. Кнопка Zoom In включается и отображается на экране, а изображение диаграммы обновляется посредством вызова функции
refreshPixmap(). 041 void Plotter::zoomIn()
042 {
043 zoomInButton->setEnabled(curZoom< zoomStack.count() - 1);
044 if (curZoom < zoomStack.count() - 1) {
045 ++curZoom;
046 zoomOutButton->setEnabled(true);
047 zoomOutButton->show();
048 refreshPixmap();
049 }
050 }
Если пользователь сначала увеличил изображение, а затем вновь его уменьшил, настройки
PlotSettings для следующего масштаба изображения уже будут в стеке масштабов изображения, и мы можем увеличить его. (В противном случае можно все же увеличить изображение при помощи резиновой ленты.)
Слот увеличивает на единицу значение
curZoom для перехода на один уровень вглубь стека масштабов изображения, включает или выключает кнопку Zoom In взависимости от возможности дальнейшего увеличения изображения и включает и показывает кнопку Zoom Out. И вновь мы вызываем
refreshPixmap() для использования построителем графиков настроек самого последнего масштаба изображения.
051 void Plotter::setCurveData(int id, const QVector<QPointF> &data)
052 {
053 curveMap[id] = data;
054 refreshPixmap();
055 }
Функция
setCurveData() устанавливает данные для кривой с заданным идентификатором. Если в
curveMap уже имеется кривая с таким идентификатором, ее данные заменяются новыми значениями; в противном случае просто добавляется новая кривая. Переменная—член
curveMap имеет тип
QMap<int, QVector<QPointF> >. 056 void Plotter::clearCurve(int id)
057 {
058 curveMap.remove(id);
059 refreshPixmap();
060 }
Функция
clearCurve() удаляет заданную кривую из
curveMap. 061 QSize Plotter::minimumSizeHint() const
062 {
063 return QSize(6 * Margin, 4 * Margin);
064 }
Функция
minimumSizeHint() напоминает
sizeHint(); в то время как функция
sizeHint() устанавливает идеальный размер виджета,
minimumSizeHint() задает идеальный минимальный размер виджета. Менеджер компоновки никогда не станет задавать виджету размеры ниже идеального минимального размера.
Мы возвращаем значение 300 × 200 (поскольку
Margin равен 50) для того, чтобы можно было разместить окаймляющую кромку по всем четырем сторонам и обеспечить некоторое пространство для самого графика. При меньших размерах считается, что график будет слишком мал и бесполезен.
065 QSize Plotter::sizeHint() const
066 {
067 return QSize(12 * Margin, 8 * Margin);
068 }
В функции
sizeHint() мы возвращаем «идеальный» размер относительно константы
Margin, причем горизонтальный и вертикальный компоненты этого размера составляют ту же самую приятную для глаза пропорцию 3:2, которую мы использовали для
minimumSizeHint(). Мы завершаем рассмотрение открытых функций и слотов построителя графиков
Plotter. Теперь давайте рассмотрим защищенные обработчики событий.
069 void Plotter::paintEvent(QPaintEvent * /* event */)
070 {
071 QStylePainter painter(this);
072 painter.drawPixmap(0, 0, pixmap);
073 if (rubberBandIsShown) {
074 painter.setPen(palette().light().color());
075 painter.drawRect(rubberBandRect.normalized()
076 .adjusted(0, 0, -1, -1));
077 }
078 if (hasFocus()) {
079 QStyleOptionFocusRect option;
080 option.initFrom(this);
081 option.backgroundColor = palette().dark().color();
082 painter.drawPrimitive(QStyle::PE_FrameFocusRect, option);
083 }
084 }
Обычно все действия по рисованию выполняются функцией
paintEvent(). Но в данном случае вся диаграмма уже нарисована функцией
refreshPixmap(), и поэтому мы можем воспроизвести весь график, просто копируя пиксельную карту в виджет в позицию (0, 0).
Если резиновая лента должна быть видимой, мы рисуем ее поверх графика. Мы используем светлый («light») компонент из текущей цветовой группы виджета в качестве цвета пера для обеспечения хорошего контраста с темным («dark») фоном. Следует отметить, что мы рисуем непосредственно на виджете, оставляя нетронутым внеэкранное изображение на пиксельной карте. Вызов
QRect::normalized() гарантирует наличие положительных значений ширины и высоты прямоугольника резиновой ленты (выполняя обмен значений координат при необходимости), а вызов
adjusted() уменьшает размер прямоугольника на один пиксель, позволяя вывести на экран его контур шириной в один пиксель.
Если
Plotter получает фокус, вывод фокусного прямоугольника выполняется с использованием функции
drawPrimitive(), задающей стиль виджета, с передачей
QStyle::PE_FrameFocusRect в качестве первого аргумента и объекта
QStyleOptionFocusRect в качестве второго аргумента. Опции рисования фокусного прямоугольника наследуются от виджета
Plotter (путем вызова
initFrom()). Цвет фона должен задаваться явно.
Если при рисовании требуется использовать текущий стиль, мы можем либо непосредственно вызвать функцию
QStyle, например
style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
либо использовать
QStylePainter вместо обычного
QPainter (как мы это делали в
Plotter), что делает рисование более удобным.
Функция
QWidget::style() возвращает стиль, который будет использован для рисования виджета. В Qt стиль виджета является подклассом
QStyle. Встроенными являются стили
QWindowsStyle, QWindowsXPStyle, QMotifStyle, QCDEStyle, QMacStyle и
OPlastiqueStyle. Все эти стили переопределяют виртуальные функции класса
QStyle, чтобы обеспечить корректное рисование в стиле имитируемой платформы. Функция
drawPrimitive() класса
QStylePainter вызывает функцию класса
QStyle с тем именем, которое используется для рисования таких «примитивов», как панели, кнопки и фокусные прямоугольники. Обычно все виджеты используют стиль приложения
(QApplication::style()), но в любом виджете стиль может переопределяться с помощью функции
QWidget::setStyle(). Путем создания подкласса
QStyle можно определить пользовательский стиль. Это можно делать с целью придания отличительных стилевых особенностей одному какому-то приложению или группе из нескольких приложений. Хотя рекомендуется в целом придерживаться «родного» стиля выбранной платформы, Qt предлагает достаточно гибкие средства по управлению стилем тем, у кого большая фантазия.
Встроенные в Qt виджеты при рисовании самих себя почти полностью зависят от
QStyle. Именно поэтому они выглядят естественно на всех платформах, поддерживаемых Qt. Пользовательские виджеты могут создаваться чувствительными к стилю либо путем применения
QStyle (через
QStylePainter) при рисовании самих себя, либо используя встроенные виджеты Qt в качестве дочерних. В
Plotter мы используем оба подхода: фокусный прямоугольник рисуется с применением
QStyle, а кнопки Zoom In и Zoom Out являются встроенными виджетами Qt.
085 void Plotter::resizeEvent(QResizeEvent * /* event */ )
086 {
087 int x= width() - (zoomInButton->width()
088 + zoomOutButton->width() + 10);
089 zoomInButton->move(x, 5);
090 zoomOutButton->move(x + zoomInButton->width() + 5, 5);
091 refreshPixmap();
092 }
При всяком изменении размера виджета
Plotter Qt генерирует событие «изменение размера». Здесь мы переопределяем функцию
resizeEvent() для размещения кнопок Zoom In и Zoom Out в верхнем правом углу виджета
Plotter. Мы располагаем кнопки Zoom In и Zoom Out рядом, отделяя их 5-пиксельным промежутком от верхнего и правого краев родительского виджета.
Если бы нам захотелось оставить эти кнопки в верхнем левом углу, который имеет координаты (0, 0), мы бы просто переместили их туда в конструкторе
Plotter. Но мы хотим, чтобы они находились в верхнем правом углу, координаты которого зависят от размеров виджета. По этой причине необходимо переопределить функцию
resizeEvent() и в ней устанавливать положение кнопок.
Мы не устанавливали положение каких-либо кнопок в конструкторе
Plotter. Это сделано из-за того, что Qt всегда генерирует событие изменения размера до первого появления на экране виджета.
В качестве альтернативы переопределению функции
resizeEvent() и размещению дочерних виджетов «вручную» можно использовать менеджер компоновки (например,
QGridLayout). При применении менеджеров компоновки это выполнить немного сложнее и такой подход потребовал бы больше ресурсов; с другой стороны, это дало бы элегантное решение компоновки справа налево, что необходимо для таких языков, как арабский и еврейский.
В конце мы вызываем функцию
refreshPixmap() для перерисовки пиксельной карты с новым размером.
093 void Plotter::mousePressEvent(QMouseEvent *event)
094 {
095 QRect rect(Margin, Margin,
096 width() - 2 * Margin, height() - 2 * Margin);
097 if (event->button() == Qt::LeftButton) {
098 if (rect.contains(event->pos())) {
099 rubberBandIsShown = true;
100 rubberBandRect.setTopLeft(event->pos());
101 rubberBandRect.setBottomRight(event->pos());
102 updateRubberBandRegion();
103 setCursor(Qt::CrossCursor);
104 }
105 }
106 }
Когда пользователь нажимает левую кнопку мышки, мы начинаем отображать на экране резиновую ленту. Для этого необходимо установить флажок
rubberBandIsShown на значение
true, инициализировать переменную—член
rubberBandRect на значение текущей позиции курсора мышки, поставить в очередь событие рисования для вычерчивания резиновой ленты и изменить изображение курсора мышки на перекрестие.
Переменная
rubberBandRect имеет тип
QRect. Объект
QRect может задаваться либо четырьмя параметрами
(x, у, w, h), где
(x, у) является позицией верхнего левого угла и
w × h определяет размеры четырехугольника, либо парой точек верхнего левого и нижнего правого углов. Здесь мы используем формат с парой точек. То место, где пользователь первый раз щелкнул мышкой, становится верхним левым углом, а текущая позиция курсора определяет позицию нижнего правого угла. Затем мы вызываем
updateRubberBandRegion() для принудительной перерисовки (небольшой) области, покрываемой резиновой лентой.
В Qt предусмотрено два способа управления формой курсора мышки:
•
QWidget::setCursor() устанавливает форму курсора, которая используется при его нахождении на конкретном виджете. Если для виджета курсор не задан, используется курсор родительского виджета. По умолчанию для виджета верхнего уровня назначается курсор в виде стрелки;
•
QApplication::setOverrideCursor() устанавливает форму курсора для всего приложения, отменяя формы курсоров отдельных виджетов до вызова функции
restoreOverrideCursor(). В
главе 4 мы вызывали функцию
QApplication::setOverrideCursor() с параметром
Qt::WaitCursor для установки курсора приложения на стандартный курсор ожидания.
107 void Plotter::mouseMoveEvent(QMouseEvent *event)
108 {
109 if (rubberBandIsShown) {
110 updateRubberBandRegion();
111 rubberBandRect.setBottomRight(event->pos());
112 updateRubberBandRegion();
113 }
114 }
Когда пользователь перемещает курсор мышки с нажатой левой кнопкой, мы сначала вызываем функцию
updateRubberBandRegion() для постановки в очередь события рисования для перерисовки области, занятой резиновой лентой, затем пересчитываем значение переменной
rubberBandRect для учета перемещения курсора и, наконец, второй раз вызываем функцию
updateRubberBandRegion() для перерисовки области, в которую переместилась резиновая лента. Это фактически приводит к стиранию резиновой ленты и ее вычерчиванию с новыми координатами.
Если пользователь перемещает мышку вверх или влево, может оказаться, что номинальный нижний правый угол резиновой ленты
rubberBandRect выше или левее верхнего левого угла. В этом случае
QRect будет иметь отрицательную ширину или высоту. В
paintEvent() нами использована функция
QRect::normalized(), которая настраивает координаты верхнего левого и нижнего правого углов для получения положительного значения ширины и высоты.
115 void Plotter::mouseReleaseEvent(QMouseEvent *event)
116 {
117 if ((event->button() == Qt::LeftButton) &&
118 rubberBandIsShown) {
119 rubberBandIsShown = false;
120 updateRubberBandRegion();
121 unsetCursor();
122 QRect rect = rubberBandRect.normalized();
123 if (rect.width() < 4 || rect.height() < 4)
124 return;
125 rect.translate(-Margin, -Margin);
126 PlotSettings prevSettings = zoomStack[curZoom];
127 PlotSettings settings;
128 double dx = prevSettings.spanX() / (width() - 2 * Margin);
130 double dy = prevSettings.spanY() / (height() - 2 * Margin);
131 settings.minX = prevSettings.minX + dx * rect.left();
132 settings.maxX = prevSettings.minX + dx * rect.right();
133 settings.minY = prevSettings.maxY - dy * rect.bottom();
134 settings.maxY = prevSettings.maxY - dy * rect.top();
135 settings.adjust();
136 zoomStack.resize(curZoom + 1);
137 zoomStack.append(settings);
138 zoomIn();
139 }
140 }
Когда пользователь отпускает левую кнопку мышки, мы стираем резиновую ленту и восстанавливаем стандартный курсор в виде стрелки. Если резиновая лента ограничивает прямоугольник, по крайней мере размером 4 × 4, мы изменяем масштаб изображения. Если резиновая лента выделяет прямоугольник меньшего размера, то, по-видимому, пользователь сделал щелчок мышкой по ошибке или просто перевел фокус, и поэтому мы ничего не делаем.
Программный код по изменению масштаба изображения немного сложен. Это вызвано тем, что мы работаем сразу с двумя системами координат: виджета и построителя графиков. Большинство выполняемых здесь действий связано с преобразованием координат объекта
rubberBandRect (прямоугольник резиновой ленты) из системы координат виджета в систему координат построителя графиков. После выполнения преобразований мы вызываем функцию
PlotSettings::adjust() для округления чисел и определения разумного количества отметок по обеим осям. Эта ситуация отражена на рис. 5.10 и 5.11.
Рис. 5.10. Преобразование прямоугольника резиновой ленты из системы координат виджета в систему координат построителя графиков. Рис. 5.11. Настройка прямоугольника резиновой ленты в системе координат построителя графиков и увеличение изображения. Затем мы изменяем масштаб изображения. Это достигается путем помещения новых, только что рассчитанных настроек
PlotSettings в вершину стека масштабов изображения и вызова функции
zoomIn(), которая выполняет всю остальную работу.
141 void Plotter::keyPressEvent(QKeyEvent *event)
142 {
143 switch (event->key()) {
144 case Qt::Key_Plus:
145 zoomIn();
146 break;
147 case Qt::Key_Minus:
148 zoomOut();
149 break;
150 case Qt::Key_Left:
151 zoomStack[curZoom].scroll(-1, 0);
152 refreshPixmap();
153 break;
154 case Qt::Key_Right:
155 zoomStack[сurZoom].scrol1(+1, 0);
156 refreshPixmap();
157 break;
158 case Qt::Key_Down:
159 zoomStack[curZoom].scroll(0, -1);
160 refreshPixmap();
161 break;
162 case Qt::Key_Up:
163 zoomStack[curZoom].scroll(0, +1);
164 refreshPixmap();
165 break;
166 default:
167 QWidget::keyPressEvent(event);
168 }
169 }
Когда пользователь нажимает на клавиатуре какую-нибудь клавишу и фокус имеет построитель графиков
Plotter, вызывается функция
keyPressEvent(). Мы ее переопределяем здесь, чтобы она реагировала на шесть клавиш: +, —, Up (вверх), Down (вниз), Left (влево) и Right (вправо). Если пользователь нажимает другую клавишу, мы вызываем реализацию этой функции из базового класса. Для простоты мы не учитываем ключи модификаторов Shift, Ctrl и Alt, доступ к которым осуществляется с помощью функции
QKeyEvent::modifiers(). 170 void Plotter::wheelEvent(QWheelEvent *event)
171 {
172 int numDegrees= event->delta() / 8;
173 int numTicks = numDegrees / 15;
174 if (event->orientation() == Qt::Horizontal) {
175 zoomStack[curZoom].scroll(numTicks, 0);
176 } else {
177 zoomStack[curZoom].scroll(0, numTicks);
178 }
179 refreshPixmap();
180 }
События колесика мышки возникают при повороте колесика мышки. В большинстве мышек предусматривается колесико для перемещения по вертикали, но некоторые мышки имеют также колесико для перемещения по горизонтали. Qt поддерживает оба вида колесиков. События колесика мышки передаются виджету, на котором находится фокус. Функция
delta() возвращает перемещение колесика, выраженное в восьмых долях градуса. Обычно шаг работы колесика мышки составляет 15 градусов. Здесь мы перемещаемся на заданное количество отметок, модифицируя верхний элемент стека масштабов изображений, и обновляем изображение, используя
refreshPixmap(). Наиболее распространенное применение колесико мышки получило для продвижения по полосе прокрутки. При использовании нами
QScrollArea (рассматривается в
главе 6) с полосами прокрутки
QScrollArea автоматически управляет событиями колесика мышки и нам не приходится самим переопределять функцию
wheelEvent(). Этим завершается реализация обработчиков событий. Теперь давайте рассмотрим закрытые функции.
181 void Plotter::updateRubberBandRegion()
182 {
183 QRect rect = rubberBandRect.normalized();
184 update(rect.left(), rect.top(), rect.width(), 1);
185 update(rect.left(), rect.top(), 1, rect.height());
186 update(rect.left(), rect.bottom(), rect.width(), 1);
187 update(rect.right(), rect.top(), 1, rect.height());
188 }
Функция
updateRubberBand() вызывается из
mousePressEvent(), mouseMoveEvent() и
mouseReleaseEvent() для стирания или перерисовки резиновой ленты. Она состоит из четырех вызовов функции
update(), которая устанавливает в очередь событие рисования для четырех небольших прямоугольных областей, составляющих изображение резиновой ленты (две вертикальные и две горизонтальные линии). Для рисования резиновой ленты в Qt предусмотрен класс
QRubberBand, однако в нашем случае ручное кодирование обеспечило более тонкое управление.
189 void Plotter::refreshPixmap()
190 {
191 pixmap = QPixmap(size());
192 pixmap.fill(this, 0, 0);
193 QPainter painter(&pixmap);
194 painter.initFrom(this);
195 drawGrid(&painter);
196 drawCurves(&painter);
197 update();
198 }
Функция
refreshPixmap() перерисовывает график на внеэкранной пиксельной карте и обновляет изображение на экране. Мы изменяем размеры пиксельной карты на размеры виджета и заполняем ее цветом стертого виджета. Этот цвет является «темным» компонентом палитры из-за вызова функции
setBackgroundRole() в конструкторе
Plotter. Если фон задается неоднородной кистью, в функции
QPixmap::fill() необходимо указать смещение в виджете, где будет заканчиваться пиксельная карта, чтобы правильно выравнить образец кисти. Здесь пиксельная карта соответствует всему виджету, поэтому мы задаем позицию (0, 0).
Затем мы создаем
QPainter для вычерчивания диаграммы на пиксельной карте. Вызов
initFrom() устанавливает в рисовальщике перо, фон и шрифт такими же, как для виджета
Plotter. Затем мы вызываем функции
drawGrid() и
drawCurves(), которые рисуют диаграмму. В конце мы вызываем функцию
update() для инициации события рисования всего виджета. Пиксельная карта копируется в виджет функцией
paintEvent(). 199 void Plotter::drawGrid(QPainter *painter)
200 {
201 QRect rect(Margin, Margin,
202 width() - 2 * Margin, height() - 2 * Margin);
203 if (!rect.isValid())
204 return;
205 PlotSettings settings = zoomStack[curZoom];
206 QPen quiteDark = palette().dark().color().light();
207 QPen light = palette().light().color();
208 for (int i = 0; i <= settings.numXTicks; ++i) {
209 int x = rect.left() + (i * (rect.width() - 1)
210 / settings.numXTicks);
211 double label = settings.minX + (i * settings.spanX()
212 / settings.numXTicks);
213 painter->setPen(quiteDark);
214 painter->drawLine(x, rect.top(), x, rect.bottom());
215 painter->setPen(light);
216 painter->drawLine(x, rect.bottom(), x, rect.bottom() + 5);
217 painter->drawText(x - 50, rect.bottom() + 5, 100, 15,
218 Qt::AlignHCenter | Qt::AlignTop,
219 QString::number(label));
220 }
221 for (int j = 0; j <= settings.numVTicks; ++j) {
222 int y = rect.bottom() - (j * (rect.height() - 1)
223 / settings.numYTicks);
224 double label = settings.minY + (j * settings.spanY()
225 / settings.numYTicks);
226 painter->setPen(quiteDark);
227 painter->drawLine(rect.left(), у, rect.right(), у);
228 painter->setPen(light);
229 painter->drawLine(rect.left() - 5, y, rect.left(), у);
230 painter->drawText(rect.left() - Margin, у - 10, Margin - 5, 20,
231 Qt::AlignRight | Qt::AlignVCenter,
232 QString::number(label));
233 }
234 painter->drawRect(rect.adjusted(0, 0, -1, -1));
235 }
Функция
drawGrid() чертит сетку под кривыми и осями. Область для вычерчивания сетки задается прямоугольником
rect. Если размеры виджета недостаточны для размещения графика, мы сразу возвращаем управление.
Первый цикл
for проводит вертикальные линии сетки и отметки по оси
x. Второй цикл
for выводит горизонтальные линии и отметки по оси
y. В конце мы рисуем прямоугольники по окаймляющей кромке. Функция
drawText() применяется для вывода числовых значений для отметок обеиз осей.
Вызовы функции
drawText() имеют следующий формат:
painter.drawText(
x, у, ширина, высота, смещение, текст); где
(x, у, ширина, высота) определяют прямоугольник,
смещение задает позицию текста в этом прямоугольнике и
текст представляет собой выводимый текст.
236 void Plotter::drawCurves(QPainter *painter)
237 {
238 static const QColor colorForIds[6] = {
239 Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta, Qt::yellow };
240 PlotSettings settings = zoomStack[curZoom];
241 QRect rect(Margin, Margin,
242 width() - 2 * Margin, height() - 2 * Margin);
243 if (!rect.isValid())
244 return;
245 painter->setClipRect(rect.adjusted(+1, +1, -1, -1));
246 QMapIterator<int, QVector<QPointF> > i(curveMap);
247 while (i.hasNext()) {
248 i.next();
249 int id = i.key();
250 const QVector<QPointF> &data = i.value();
251 QPolygonF polyline(data.count());
252 for (int j = 0; j < data.count(); ++j) {
253 double dx = data[j].x() - settings.minX;
254 double dy = data[j].y() - settings.minY;
255 double x = rect.left() + (dx * (rect.width() - 1)
256 / settings.spanX());
257 double у = rect.bottom() - (dy * (rect.height() - 1)
258 / settings.spanY());
259 polyline[j] = QPointF(x, у);
260 }
261 painter->setPen(colorForIds[uint(id) % 6]);
262 painter->drawPolyline(polyline);
263 }
264 }
Функция
drawCurves() рисует кривые поверх сетки. Мы начинаем с вызова функции
setClipRect для ограничения области отображения
QPainter прямоугольником, содержащим кривые (без окаймляющей кромки и рамки вокруг графика). После этого
QPainter будет игнорировать вывод пикселей вне этой области.
Затем мы выполняем цикл по всем кривым, используя итератор в стиле Java, и для каждой кривой мы выполняем цикл по ее точкам
QPointF. Функция
key() позволяет получить идентификатор кривой, а функция
value() — данные соответствующей кривой в виде вектора
QVector<QPointF>. Внутри цикла
for производятся преобразование всех точек
QPointF из системы координат построителя графика в систему координат виджета и сохранение их в переменной
polyline. После преобразования всех точек кривой в систему координат виджета мы устанавливаем цвет пера для кривой (используя один из наборов заранее определенных цветов) и вызываем
drawPolyline() для вычерчивания линии, которая проходит по всем точкам кривой.
Этим мы завершаем построение класса
Plotter. Остается только рассмотреть несколько функций настроек графика
PlotSettings. 265 PlotSettings::PlotSettings()
266 {
267 minX = 0.0;
268 maxX = 10.0;
269 numXTicks = 5;
270 minY = 0.0;
271 maxY = 10.0;
272 numYTicks = 5;
273 }
Конструктор
PlotSettings инициализирует обе оси координат диапазоном от 0 до 10 с пятью отметками.
274 void PlotSettings::scroll(int dx, int dy)
275 {
276 double stepX = spanX() / numXTicks;
277 minX += dx * stepX;
278 maxX += dx * stepX;
279 double stepY = spanY() / numYTicks;
280 minY += dy * stepY;
281 maxY += dy *stepY;
282 }
Функция
scroll() увеличивает (или уменьшает)
minX, maxX, minY и
maxY на интервал между двух отметок, помноженный на заданное число. Данная функция применяется для реализации скроллинга в функции
Plotter::keyPressEvent(). 283 void PlotSettings::adjust()
284 {
285 adjustAxis(minX, maxX, numXTicks);
286 adjustAxis(minY, maxY, numYTicks);
287 }
Функция
adjust() вызывается из
mouseReleaseEvent() для округления значений
minX, maxX, minY и
maxY, чтобы получить «удобные» значения, и определения количества меток на каждой оси. Закрытая фyнкция
adjustAxis() выполняет эти действия отдельно для каждой оси.
288 void PlotSettings::adjustAxis(double &min, double &max, int &numTiсks)
289 {
290 const int MinTicks = 4;
291 double grossStep = (max - min) / MinTicks;
292 double step = pow(10.0, floor(log10(grossStep)));
293 if (5 * step < grossStep) {
294 step *= 5;
295 } else if (2* step < grossStep) {
296 step *= 2;
297 }
298 numTicks = int (ceil(max / step) - floor(min / step));
299 if (numTicks < MinTicks)
300 numTicks = MinTicks;
301 min = floor(min / step) * step;
302 max = ceil(max / step) * step;
303 }
Функция
adjustAxis() преобразует свои параметры
min и
max в «удобные» числа и устанавливает свой параметр
numTicks на количество меток, которое, по ее расчету, подходит для заданного диапазона
[min, max]. Поскольку в функции
adjustAxis() фактически требуется модифицировать переменные (
minX, maxX, numXTicks и так далее), а не просто копировать их, для этих параметров не используется модификатор
const. Большая часть программного кода в
adjustAxis() предназначена просто для определения соответствующего значения интервала между двумя метками (переменная
step — шаг). Для получения на оси удобных чисел мы должнытщательно выбирать этот шаг. Например, значение шага 3.8 привело бы к появлению на оси чисел, кратных 3.8, что затрудняет восприятие диаграммы человеком. Для осей с десятичной системой обозначения «удобными» значениями шага являются числа вида 10
n, 2 • 10
n или 5 • 10
n.
Мы начинаем расчет с «крупного шага», то есть с определенного максимального значения шага. Затем мы находим число вида 10
n, меньшее или равное крупному шагу. Мы его получаем путем взятия десятичного логарифма от крупного шага, затем округляем полученное значение до целого числа, после чего возводим 10 в степень, равную этому округленному значению. Например, если крупный шаг равен 236, мы вычисляем log 236 = 2.37291…; затем мы округляем это значение до 2 и получаем 10
2 = 100 в качестве кандидата на значениешага в форме числа 10
n.
После получения первого кандидата на значение шага мы можем его использовать для расчета двух других кандидатов: 2 • 10
n и 5 • 10
n. Для нашего примера два других кандидата являются числами 200 и 500. Кандидат 500 имеет значение большее, чем крупный шаг, и поэтому мы не можем использовать его. Но 200 меньше, чем 236, и поэтому мы можем использовать 200 в качестве размера шага в нашем примере.
Достаточно легко получить
numTicks, min и
max из значения шага. Новое значение
min получается путем округления снизу первоначального
min до ближайшего числа, кратного этому шагу, а новое значение
max получается путем округления сверху до ближайшего числа, кратного этому шагу. Новое значение
numTicks представляет собой количество интервалов между округленными значениями
min и
max. Например, если при входе в функцию
min равно 240, а
max равно 1184, то новый диапазон будет равен [200, 1200] с пятью отметками.
Этот алгоритм в некоторых случаях дает почти оптимальный результат. Более изощренный алгоритм описан в статье Поля С. Хекберта (Paul S. Heckbert) «Nice Numbers for Graph Labels» (удобные числа для меток графа), опубликованной в
Graphics Gems (ISBN 0—12—286166—3).
Данная глава является последней в части I. В ней объяснены способы настройки существующего виджета Qt и способы построения виджета с использованием в качестве основы базового класса виджетов
QWidget. В
главе 2 мы уже узнали, как можно построить виджет на основе существующих виджетов, и мы еще вернемся к этой теме в
главе 6.
К этому моменту у нас достаточно знаний для написания законченных приложений с графическим интерфейсом с помощью средств разработки Qt. В частях II и III мы проведем более глубокое исследование Qt, чтобы можно было в полной мере использовать возможности Qt.