В этой главе будут рассмотрены графические возможности Qt. Краеугольным камнем движка двухмерной графики в Qt является QPainter. Он может использоваться для рисования на поверхности виджета (на экране), во внутреннем буфере (pixmap) и на принтере. Кроме того, в состав Qt входит класс QCanvas, который позволяет создавать изображения из графических примитивов.
В качестве альтернативы QPainter и QCanvas, можно рассматривать библиотеку OpenGL. Она предоставляет механизмы создания трехмерной графики, но может использоваться и для рисования двухмерных изображений. Код, использующий OpenGL очень легко интегрируется в приложения Qt, мы продемонстрируем это на конкретных примерах.
Класс QPainter используется для создания изображений на "графических устройствах", таких как виджеты или карты пикселей (pixmap). Чаще всего он используется при создании нестандартных виджетов, для придания им уникального, ни на что не похожего, внешнего вида. Однако этот класс может использоваться и для вывода графики на принтер, более подробно мы коснемся этого вопроса немного ниже.
QPainter может рисовать простые геометрические фигуры: точки, линии, прямоугольники, эллипсы, дуги, сегменты круга, замкнутые ломаные (многоугольники) и кривые Безье. Он так же может отображать карты пикселей, рисунки и текст.
Когда конструктору QPainter передается устройство для рисования, он получает часть настроек от заданного устройства, оставшиеся параметры настройки заполняет значениями по-умолчанию. Эти настройки определяют способ рисования. Тремя наиболее важными характеристиками QPainter являются перо (pen), кисть (brush) и шрифт (font).
Перо используется для рисования линий и границ геометрических фигур. Оно характеризуется такими параметрами, как: цвет, толщина, стиль рисования линий, стиль оформления концов линий и стиль оформления углов.
Рисунок 8.1. Методы класса QPainter, для рисования геометрических фигур.
Рисунок 8.2. Стили пера.
Кисть -- это шаблон, которым заполняются геометрические фигуры. Кисти характеризуются цветом и стилем.
Шрифт используется для рисования текста. Шрифт может иметь огромное количество атрибутов, среди них: название и размер.
Рисунок 8.3. Стили оформления концов линий и углов.
Рисунок 8.4. Стили кисти.
QPainter painter(this);
painter.setPen(QPen(black, 3, DashDotLine));
painter.setBrush(QBrush(red, SolidPattern));
painter.drawEllipse(20, 20, 100, 60);
Следующий код рисует сегмент круга, показанный на рисунке 8.5(б):
QPainter painter(this);
painter.setPen(QPen(black, 5, SolidLine));
painter.setBrush(QBrush(red, DiagCrossPattern));
painter.drawPie(20, 20, 100, 60, 60 * 16, 270 * 16);
Последние два аргумента drawPie()
выражаются в 1/16 долях градуса.
И наконец код, который рисует кривую Безье, показанную на
рисунке 8.5(в):
QPainter painter(this);
QPointArray points(4);
points[0] = QPoint(20, 80);
points[1] = QPoint(50, 20);
points[2] = QPoint(80, 20);
points[3] = QPoint(120, 80);
painter.setPen(QPen(black, 3, SolidLine));
painter.drawCubicBezier(points);
Текущее состояние QPainter может быть
сохранено на стеке, вызовом save() и
восстановлено со стека, вызовом restore().
Это может потребоваться в том случае, когда необходимо на время
изменить какие либо настройки, а затем восстановить их прежние
значения.Кроме перечисленных выше характеристик (перо, кисть и шрифт), QPainter имеет еще целый ряд параметров настройки:
Цвет фона (background color), который используется для заливки геометрических фигур (под шаблоном, наносимым кистью), текста или рисунков, когда background mode имеет значение OpaqueMode (по-умолчанию: TransparentMode).
Растровые операции (raster operation) определяют, как новое изображение должно накладываться на существующее. По-умолчанию: CopyROP, т.е. новое изображение (пиксели) просто копируется на устройство рисования, ранее находившееся там изображение игнорируется. В список растровых операций так же входят: XorROP, NotROP, AndROP и NotAndROP.
Начальные координаты кисти (brush origin) задают начальную точку рисования шаблона кисти, обычно это левый верхний угол виджета.
Врезка (clip region) определяет область устройства, на которой может производиться рисование. Операции рисования за пределами этой области -- игнорируются.
Область просмотра (viewport), окно (window) и матрица преобразования (world matrix) определяют отношения между логической системой координат QPainter и системой координат физического устройства. Значения по-умолчанию принимаются таковыми, что эти две системы координат совпадают.
Понятия область просмотра и окно тесно связаны между собой. Область просмотра -- это произвольный прямоугольник, заданный физическими координатами. Окно -- описывает тот же самый прямоугольник, но уже в логических координатах. Когда выполняется рисование, то указываются логические координаты, которые затем преобразуются в физические.
По-умолчанию координаты области просмотра и окна совпадают с системой координат физического устройства. Например, если устройство отображения представляет из себя виджет, с размерами 320 X 200, то и область просмотра и окно имеют те же самые размеры. В данном случае логическая и физическая системы координат совпадают.
Подобный механизм дает возможность писать код, который не зависит от размера или разрешения устройства. Конечно же, мы и сами можем выполнять отображение логических координат в физические, но проще доверить эту работу классу QPainter. Например, представим, что нам необходимо работать в системе координат, ограниченной прямоугольником от (-50, -50) до (+50, +50), когда точка с координатами (0, 0) находится в центре прямоугольника. В этом случае можно установить параетры окна следующим образом:
painter.setWindow(QRect(-50, -50, 100, 100));
где первые два аргумента задают координаты верхнего левого угла
(-50, -50), последние два аргумента (100, 100)-- ширину и высоту
прямоугольника, соответственно. В данном случае, это означает, что
логические координаты (-50, -50) соответствуют физическим координатам
(0, 0), а логические координаты (+50, +50) -- физическим (320, 200).
Изменять параметры области просмотра нет необходимости.
Рисунок 8.6. Преобразование логических координат в физические.
QWMatrix matrix;
matrix.rotate(45.0);
painter.setWorldMatrix(matrix);
painter.drawText(rect, AlignCenter, tr("Revenue"));
Здесь логические координаты, передаваемые в drawText(), сначала подвергаются трансформации, а
затем отображаются в физические координаты.Если указывается несколько трансформаций, то они применяются в порядке следования в исходном коде программы. Например, допустим, что необходимо повернуть изображение относительно точки с координатами (10, 20). Для этого можно задать следующий порядок трансформаций: сдвинуть окно так, чтобы центр вращения переместился в координаты (0, 0), повернуть изображение и затем выполнить обратный сдвиг:
QWMatrix matrix;
matrix.translate(-10.0, -20.0);
matrix.rotate(45.0);
matrix.translate(+10.0, +20.0);
painter.setWorldMatrix(matrix);
painter.drawText(rect, 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, AlignCenter, tr("Revenue"));
Но если необходимо воспользоваться одним и тем же набором
трансформаций несколько раз подряд, то вариант с
QWMatrix даст значительный выигрыш по времени.При необходимости, матрицу преобразований можно сохранить вызовом saveWorldMatrix() и затем восстановить вызовом restoreWorldMatrix().
Рисунок 8.7. Внешний вид виджета OvenTimer.
class OvenTimer : public QWidget
{
Q_OBJECT
public:
OvenTimer(QWidget *parent, const char *name = 0);
void setDuration(int secs);
int duration() const;
void draw(QPainter *painter);
signals:
void timeout();
protected:
void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent *event);
private:
QDateTime finishTime;
QTimer *updateTimer;
QTimer *finishTimer;
};
Класс OvenTimer порожден от класса
QWidget и перекрывает два виртуальных метода
предка: paintEvent() и mousePressEvent().
#include <qpainter.h>
#include <qpixmap.h>
#include <qtimer.h>
#include <cmath>
using namespace std;
#include "oventimer.h"
const double DegreesPerMinute = 7.0;
const double DegreesPerSecond = DegreesPerMinute / 60;
const int MaxMinutes = 45;
const int MaxSeconds = MaxMinutes * 60;
const int UpdateInterval = 10;
OvenTimer::OvenTimer(QWidget *parent, const char *name)
: QWidget(parent, name)
{
finishTime = QDateTime::currentDateTime();
updateTimer = new QTimer(this);
finishTimer = new QTimer(this);
connect(updateTimer, SIGNAL(timeout()), this, SLOT(update()));
connect(finishTimer, SIGNAL(timeout()), this, SIGNAL(timeout()));
}
В конструкторе создаются два объекта QTimer: updateTimer -- для
обновления изображения виджета, и finishTimer -- для выдачи сигнала timeout(), по достижении нулевой отметки.
void OvenTimer::setDuration(int secs)
{
if (secs > MaxSeconds)
secs = MaxSeconds;
finishTime = QDateTime::currentDateTime().addSecs(secs);
updateTimer->start(UpdateInterval * 1000, false);
finishTimer->start(secs * 1000, true);
update();
}
Функция setDuration() устанавливает
продолжительность действия таймера в секундах. Аргумент
false, передаваемый в функцию
dateTimer->start() сообщает Qt о
том, что это таймер с многократным срабатыванием. Период
срабатывания таймера равен 10 секундам. Таймер finishTimer должен сработать всего один раз,
поэтому в функцию start(), этого
объекта, передается аргумент true. Конечное время работы таймера
вычисляется сложением текущего времени, которое мы получаем
вызовом QDateTime::currentDateTime()
и времени ожидания.Переменная finishTime имеет тип QDateTime, который в Qt отвечает за хранение даты и времени. Объекты этого типа становятся просто незаменимы в ситуациях, когда в отмеряемый интервал времени попадает граница суток.
int OvenTimer::duration() const
{
int secs = QDateTime::currentDateTime().secsTo(finishTime);
if (secs < 0)
secs = 0;
return secs;
}
Функция duration() возвращает число
секунд, оставшихся до конца работы таймера.
void OvenTimer::mousePressEvent(QMouseEvent *event)
{
QPoint point = event->pos() - rect().center();
double theta = atan2(-(double)point.x(), -(double)point.y())
* 180 / 3.14159265359;
setDuration((int)(duration() + theta / DegreesPerSecond));
update();
}
Когда пользователь щелкает по лимбу таймера, вычисляется новый
интервал действия таймера. Затем в очередь ставится событие
"paint". Теперь, на вершине будет находиться выбранная
пользователем риска.
void OvenTimer::paintEvent(QPaintEvent *)
{
QPainter painter(this);
int side = QMIN(width(), height());
painter.setViewport((width() - side) / 2, (height() - side) / 2,
side, side);
painter.setWindow(-50, -50, 100, 100);
draw(&painter);
}
В обработчике paintEvent()
устанавливается область просмотра (viewport), которая по своим размерам
является наибольшей квадратной областью, которую можно разместить в
виджете, а затем настраивается окно -- прямоугольник (-50, -50, 100,
100), с размерами 100 X 100. Макрос QMIN()
возвращает наименьшее из двух аргументов.
Рисунок 8.8. Внешний вид виджета OvenTimer с различными размерами.
Размеры окна (-50, -50, 100, 100) выбирались из следующих соображений:
Функции рисования в QPainter, принимают значения координат в виде целых чисел. Если выбрать размер окна слишком маленьким, то координаты некоторых точек не смогут быть указаны достаточно точно, из-за возникающей проблемы округления.
Если выбрать размер окна слишком большим, то при необходимости рисования текста функцией drawText(), нам придется выбирать шрифт большого размера.
Теперь перейдем к функции draw():
void OvenTimer::draw(QPainter *painter)
{
static const QCOORD triangle[3][2] = {
{ -2, -49 }, { +2, -49 }, { 0, -47 }
};
QPen thickPen(colorGroup().foreground(), 2);
QPen thinPen(colorGroup().foreground(), 1);
painter->setPen(thinPen);
painter->setBrush(colorGroup().foreground());
painter->drawConvexPolygon(QPointArray(3, &triangle[0][0]));
Рисование виджета начинается с маленького треугольника, который
обозначает нулевую позицию вверху. Треугольник задается тремя, жестко
зашитыми парами координат. Собственно рисование производится функцией
drawConvexPolygon(). Треугольник можно было
бы нарисовать функцией drawPolygon(), но
если заранее известно, что многоугольник выпуклый, то вы можете
сэкономить несколько микросекунд, за счет использования функции
drawConvexPolygon().Одна из замечательных сторон механизма перехода от логических координат к физическим состоит в том, что мы можем жестко зашивать координаты точек в исходный код и при этом получать неплохие результаты, при изменении размеров виджета.
painter->setPen(thickPen);
painter->setBrush(colorGroup().light());
painter->drawEllipse(-46, -46, 92, 92);
painter->setBrush(colorGroup().mid());
painter->drawEllipse(-20, -20, 40, 40);
painter->drawEllipse(-15, -15, 30, 30);
Далее рисуются внешний и два внутренних круга. Внешний круг
заполняется цветом "light" (обычно -- белый), Внутренние
круги заполняются цветом "mid" (обычно -- серый).
int secs = duration();
painter->rotate(secs * DegreesPerSecond);
painter->drawRect(-8, -25, 16, 50);
for (int i = 0; i <= MaxMinutes; ++i) {
if (i % 5 == 0) {
painter->setPen(thickPen);
painter->drawLine(0, -41, 0, -44);
painter->drawText(-15, -41, 30, 25,
AlignHCenter | AlignTop,
QString::number(i));
} else {
painter->setPen(thinPen);
painter->drawLine(0, -42, 0, -44);
}
painter->rotate(-DegreesPerMinute);
}
}
Затем рисуются рукоятка и риски на лимбе. Напротив каждой пятой
риски рисуется число, обозначающее количество минут. Функция
rotate() вызывается для того, чтобы
повернуть систему координат. В начальный момент, риска с отметкой
"0" находилась вверху, теперь же она переместилась в
точку, координаты которой зависят от оставшегося до срабатывания
времени. Рукоятка рисуется после выполнения поворота, поскольку
ее ориентация зависит от угла поворота.В цикле for, по краю внешнего круга рисуются риски, а под ними -- числа, обозначающие количество минут, с шагом 5. В конце каждой итерации выполняется поворот системы координат по часовой стрелке на 7 градусов, что соответствует одной минуте. Таким образом, каждая следующая риска будет рисоваться на своем месте, хотя координаты в drawLine() и drawText() задаются одни и те же.
Тут есть еще одна проблема, которую мы не учли -- мерцание. Виджет перерисовывается целиком каждые 10 секунд, что становится причиной появления эффекта подмаргивания изображения. Чтобы избавиться от нее, добавим двойную буферизацию. Для этого нужно передать родительскому конструктору флаг WNoAutoErase и изменить paintEvent() следующим образом:
void OvenTimer::paintEvent(QPaintEvent *event)
{
static QPixmap pixmap;
QRect rect = event->rect();
QSize newSize = rect.size().expandedTo(pixmap.size());
pixmap.resize(newSize);
pixmap.fill(this, rect.topLeft());
QPainter painter(&pixmap, this);
int side = QMIN(width(), height());
painter.setViewport((width() - side) / 2 - event->rect().x(),
(height() - side) / 2 - event->rect().y(),
side, side);
painter.setWindow(-50, -50, 100, 100);
draw(&painter);
bitBlt(this, event->rect().topLeft(), &pixmap);
}
На этот раз все рисование производится в буфере. Сначала
устанавливается размер будущего изображения, в соответствии с размером
области, которую необходимо перерисовать. Затем настраиваются область
просмотра и окно таким образом, что сам процесс рисования проходит
точно так же, как и раньше. Благодаря этому нам не надо вносить
изменения в функцию draw(). В завершение
обработки события "paint", готовый буфер переносится на
поверхность виджета, функцией bitBlt().Очень похоже на то, что мы описывали в разделе Двойная буферизация, но с одним важным отличием: в Главе 5, для выполнения сдвига, мы пользовались функцией translate(), теперь же, мы вычитаем координаты левого верхнего угла прямоугольника, требующего перерисовки, при настройке области просмотра. Использование translate() здесь было бы не очень удобным, поскольку преобразование должно быть выражено в логических координатах, в то время как событие поставляется с координатами физическими.
Пред. | В начало | След. |
Сокращение времени отклика при длительной обработке данных. | На уровень выше | Рисование средствами QCanvas. |