|
||
![]() |
||
В этой главе будут рассмотрены графические возможности 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 (Canvas -- холст, полотно, канва. прим. перев.) предоставляет более высокоуровневый интерфейс, чем QPainter. Он может включать в себя элементы любой формы и имеет внутреннюю реализацию двойной буферизации. Для приложений, которые занимаются визуализацией информации или двухмерных игр, выбор QCanvas может оказаться лучшим решением.
Элементы, которые может отображать QCanvas, являются экземплярами класса QCanvasItem или его потомков. Qt содержит неплохой набор предопределенных графических элементов: QCanvasLine, QCanvasRectangle, QCanvasPolygon, QCanvasPolygonalItem, QCanvasEllipse, QCanvasSpline, QCanvasSprite и QCanvasText..
Классы QCanvas и QCanvasItem -- просто данные, они не имеют визуального представления. Для отображения QCanvas и его элементов мы должны использовать виджет QCanvasView. Такое разделение данных и средств их отображения, позволяет отображать один и тот же QCanvas в нескольких QCanvasView, причем каждый из них может визуализировать свою собственную часть QCanvas, причем с применением различных матриц преобразования.
Класс QCanvas оптимизирован для работы с большим количеством элементов. Когда изменяется какой либо элемент, то перерисовывается только та часть, которая действительно изменилась. В нем так же заложен эффективный алгоритм проверки на пересечение. Поэтому, QCanvas можно смело рассматривать как неплохую альтернативу подходам, связанным с перекрытием родительских методов paintEvent() и QScrollView::drawContents().
Рисунок 8.9. Внешний вид виджета DiagramView.
class DiagramView : public QCanvasView { Q_OBJECT public: DiagramView(QCanvas *canvas, QWidget *parent = 0, const char *name = 0); public slots: void cut(); void copy(); void paste(); void del(); void properties(); void addBox(); void addLine(); void bringToFront(); void sendToBack();Класс DiagramView порожден от класса QCanvasView, который в свою очередь ведет родословную от класса QScrollView. Он предоставляет массу публичных слотов, через которые возможно взаимодействие с приложением. Эти слоты так же используются и самим виджетом, для обслуживания контекстного меню.
protected: void contentsContextMenuEvent(QContextMenuEvent *event); void contentsMousePressEvent(QMouseEvent *event); void contentsMouseMoveEvent(QMouseEvent *event); void contentsMouseDoubleClickEvent(QMouseEvent *event); private: void createActions(); void addItem(QCanvasItem *item); void setActiveItem(QCanvasItem *item); void showNewItem(QCanvasItem *item); QCanvasItem *pendingItem; QCanvasItem *activeItem; QPoint lastPos; int minZ; int maxZ; QAction *cutAct; QAction *copyAct; ... QAction *sendToBackAct; };Приватные и защищенные члены класса мы будем описывать очень коротко.
Рисунок 8.10. Элементы DiagramBox и DiagramLine.
class DiagramBox : public QCanvasRectangle { public: enum { RTTI = 1001 }; DiagramBox(QCanvas *canvas); ~DiagramBox(); void setText(const QString &newText); QString text() const { return str; } void drawShape(QPainter &painter); QRect boundingRect() const; int rtti() const { return RTTI; } private: QString str; };Элемент диаграммы DiagramBox отображается в виде прямоугольника, с текстом внутри. Он наследует значительную часть функциональности от своего предка -- класса QCanvasRectangle, в который добавлена возможность рисования дополнительного текста и маленьких квадратиков по углам, для индикации активности элемента. В реальном приложении, эти квадратики можно было бы использовать для того, чтобы изменять размеры прямоугольника, но в данном случае, для упрощения примера, мы не будем этого делать.
Функция rtti() перекрывает родительский метод. Имя этой функции происходит от английского "run-time type identification" -- "идентификация типа во время исполнения". Возвращаемый ею результат будет сравниваться с константой RTTI, чтобы узнать -- является ли тот или иной элемент объектом класса DiagramBox. Эту же проверку можно было бы выполнить с использованием механизма C++ dynamic_cast<T>(), но это ограничило бы нас в выборе компилятора C++.
Число 1001 выбрано случайным образом. Приемлемо любое значение, большее 1000, единственное ограничение: в одном и том же приложении не должны использоваться разные классы с одинаковым значением RTTI.
class DiagramLine : public QCanvasLine { public: enum { RTTI = 1002 }; DiagramLine(QCanvas *canvas); ~DiagramLine(); QPoint offset() const { return QPoint((int)x(), (int)y()); } void drawShape(QPainter &painter); QPointArray areaPoints() const; int rtti() const { return RTTI; } };Элемент диаграммы DiagramLine отображается в виде линии. Этот класс наследует функциональность класса QCanvasLine, в который добавлена возможность отображения маленьких квадратиков на концах линии, для индикации активности элемента.
Перейдем к обзору реализации этих трех классов:
DiagramView::DiagramView(QCanvas *canvas, QWidget *parent, const char *name) : QCanvasView(canvas, parent, name) { pendingItem = 0; activeItem = 0; minZ = 0; maxZ = 0; createActions(); }Конструктор DiagramView в первом аргументе получает указатель на QCanvas и передает его унаследованному конструктору.
В приватной функции createActions() создаются экземпляры QAction. Мы уже рассматривали подобные функции в примерах ранее, поэтому реализацию этой функции мы опустим.
void DiagramView::contentsContextMenuEvent(QContextMenuEvent *event) { QPopupMenu contextMenu(this); if (activeItem) { cutAct->addTo(&contextMenu); copyAct->addTo(&contextMenu); deleteAct->addTo(&contextMenu); contextMenu.insertSeparator(); bringToFrontAct->addTo(&contextMenu); sendToBackAct->addTo(&contextMenu); contextMenu.insertSeparator(); propertiesAct->addTo(&contextMenu); } else { pasteAct->addTo(&contextMenu); contextMenu.insertSeparator(); addBoxAct->addTo(&contextMenu); addLineAct->addTo(&contextMenu); } contextMenu.exec(event->globalPos()); }Чтобы создать контекстное меню, мы перекрыли обработчик contentsContextMenuEvent() родительского класса QScrollView.
Рисунок 8.11. Контекстное меню виджета DiagramView.
void DiagramView::addBox() { addItem(new DiagramBox(canvas())); } void DiagramView::addLine() { addItem(new DiagramLine(canvas())); }Слоты addBox() и addLine() создают элементы диаграммы DiagramBox или DiagramLine, соответственно, которые затем добавляются в виджет, с помощью addItem().
void DiagramView::addItem(QCanvasItem *item) { delete pendingItem; pendingItem = item; setActiveItem(0); setCursor(crossCursor); }Приватная функция addItem() изменяет внешний вид указателя мыши на крестик и записывает в переменную pendingItem указатель на вновь созданный элемент. Этот элемент не будет видим на экране до тех пор, пока не будет вызван его метод show().
Когда пользователь выбирает пункт контекстного меню Add Box или Add Line, изменяется внешний вид указателя мыши, но элемент будет добавлен только когда он щелкнет по канве.
void DiagramView::contentsMousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton << pendingItem) { pendingItem->move(event->pos().x(), event->pos().y()); showNewItem(pendingItem); pendingItem = 0; unsetCursor(); } else { QCanvasItemList items = canvas()->collisions(event->pos()); if (items.empty()) setActiveItem(0); else setActiveItem(*items.begin()); } lastPos = event->pos(); }Когда пользователь нажимает левую кнопку мыши и при этом курсор отображается в виде крестика, то вставляемый элемент диаграммы уже создан. Поэтому нам остается только вставить его в позицию курсора мыши, сделать видимым и вернуть внешний виж курсовра в первоначальное состояние.
Любой другой щелчок по канве интерпретируется как попытка выделить какой либо из элементов или наоборот, снять выделение. Функция collisions() возвращает список всех элементов, находящихся под указателем мыши. Первый из этого списка активизируется. Если список содержит несколько элементов, то первым в нем всегда будет стоять тот элемент, который отображается поверх других.
void DiagramView::contentsMouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) { if (activeItem) { activeItem->moveBy(event->pos().x() - lastPos.x(), event->pos().y() - lastPos.y()); lastPos = event->pos(); canvas()->update(); } } }Пользователь может перемещать элементы диаграммы, удерживая их левой кнопкой мыши. Каждый раз, когда виджет получает событие, извещающее о перемещении мыши, мы сдвигаем элемент по горизонтали и вертикали, на полученные расстояния и вызываем update() канвы. Всякий раз, когда изменяется содержимое канвы, мы должны вызывать метод update(), чтобы перерисовать виджет.
void DiagramView::contentsMouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == LeftButton && activeItem && activeItem->rtti() == DiagramBox::RTTI) { DiagramBox *box = (DiagramBox *)activeItem; bool ok; QString newText = QInputDialog::getText( tr("Diagram"), tr("Enter new text:"), QLineEdit::Normal, box->text(), &ok, this); if (ok) { box->setText(newText); canvas()->update(); } } }Когда пользователь выполняет двойной щелчок по элементу диаграммы, вызывается функция rtti(), а полученное от нее значение сравнивается с DiagramBox::RTTI (1001).
Рисунок 8.12. Диалог изменения текста в элементе DiagramBox.
void DiagramView::bringToFront() { if (activeItem) { ++maxZ; activeItem->setZ(maxZ); canvas()->update(); } }Слот bringToFront() перемещает выбранный элемент поверх других элементов диаграммы. Это достигается за счет записи значания, в координату z компонента, большего, чем у других. Если на канве, в тех же самых координатах, находятся два или более компонентов, то тот, который имеет большее значение координаты z будет отображаться поверх остальных.
void DiagramView::sendToBack() { if (activeItem) { --minZ; activeItem->setZ(minZ); canvas()->update(); } }Слот sendToBack() перемещает выбранный элемент ниже других. Это достигается за счет записи значания, в координату z компонента, меньшего, чем у других.
void DiagramView::cut() { copy(); del(); }Реализация слота cut() достаточно проста, и мы не будем его подробно описывать.
void DiagramView::copy() { if (activeItem) { QString str; if (activeItem->rtti() == DiagramBox::RTTI) { DiagramBox *box = (DiagramBox *)activeItem; str = QString("DiagramBox %1 %2 %3 %4 %5") .arg(box->width()) .arg(box->height()) .arg(box->pen().color().name()) .arg(box->brush().color().name()) .arg(box->text()); } else if (activeItem->rtti() == DiagramLine::RTTI) { DiagramLine *line = (DiagramLine *)activeItem; QPoint delta = line->endPoint() - line->startPoint(); str = QString("DiagramLine %1 %2 %3") .arg(delta.x()) .arg(delta.y()) .arg(line->pen().color().name()); } QApplication::clipboard()->setText(str); } }Слот copy() преобразует информацию об элементе в строку и копирует ее в буфер обмена. Строка содержит все необходимые сведения, чтобы потом можно было опять воссоздать элемент. Например, прямоугольник черного цвета, с текстом "My Left Foot" белого цвета, будет представлен в виде строки:
DiagramBox 320 40 #000000 #ffffff My Left FootНет необходимости беспокоиться о сохранении координат элемента. Когда элемент вынимается из буфера обмена, он просто вставляется в левый верхний угол канвы. Представление объекта в виде строки -- это самый простой способ добавить поддержку буфера обмена. Безусловно, буфер обмена может хранить и двоичные данные в произвольном формате, но об этом мы поговорим в Главе 9.
void DiagramView::paste() { QString str = QApplication::clipboard()->text(); QTextIStream in(&str); QString tag; in >> tag; if (tag == "DiagramBox") { int width; int height; QString lineColor; QString fillColor; QString text; in >> width >> height >> lineColor >> fillColor; text = in.read(); DiagramBox *box = new DiagramBox(canvas()); box->move(20, 20); box->setSize(width, height); box->setText(text); box->setPen(QColor(lineColor)); box->setBrush(QColor(fillColor)); showNewItem(box); } else if (tag == "DiagramLine") { int deltaX; int deltaY; QString lineColor; in >> deltaX >> deltaY >> lineColor; DiagramLine *line = new DiagramLine(canvas()); line->move(20, 20); line->setPoints(0, 0, deltaX, deltaY); line->setPen(QColor(lineColor)); showNewItem(line); } }Слот paste() пользуется услугами QTextIStream, для разбора содержимого строки из буфера обмена. QTextIStream отделяет поля в строке по символу пробела, точно так же, как и cin. Поля считываются оператором ">>", за исключением последнего, которое может содержать пробелы. Чтобы прочитать последнее поле используется метод QTextStream::read(), который возвращает остаток строки.
void DiagramView::del() { if (activeItem) { QCanvasItem *item = activeItem; setActiveItem(0); delete item; canvas()->update(); } }Слот del() удаляет активный элемент и перерисовывает канву.
void DiagramView::properties() { if (activeItem) { PropertiesDialog dialog; dialog.exec(activeItem); } }Слот properties() запускает диалог изменения свойств активного элемента. Класс PropertiesDialog получает только указатель на элемент, и сам определяет -- какого типа элемент он получил, после чего выполняет все необходимые действия.
Рисунок 8.13. Два варианта отображения диалога PropertiesDialog.
void DiagramView::showNewItem(QCanvasItem *item) { setActiveItem(item); bringToFront(); item->show(); canvas()->update(); }Функция showNewItem() активизирует элемент диаграммы и делает его видимым.
void DiagramView::setActiveItem(QCanvasItem *item) { if (item != activeItem) { if (activeItem) activeItem->setActive(false); activeItem = item; if (activeItem) activeItem->setActive(true); canvas()->update(); } }Последняя функция setActiveItem() сбрасывает признак активности у предыдущего активного элемента, запоминает указатель на новый активный элемент и активизирует его. Признак активности элемента хранится в классе QCanvasItem. Qt не использует его, но предоставляет такую возможность для удобства разработчика. Мы используем этот признак, поскольку в нашем случае активные элементы рисуются несколько иначе, чем неактивные.
Перейдем к рассмотрению реализации классов DiagramBox и DiagramLine.
const int Margin = 2; void drawActiveHandle(QPainter &painter, const QPoint ¢er) { painter.setPen(Qt::black); painter.setBrush(Qt::gray); painter.drawRect(center.x() - Margin, center.y() - Margin, 2 * Margin + 1, 2 * Margin + 1); }Функция drawActiveHandle() рисует маленькие квадратики, для индикации активности элемента диаграммы.
DiagramBox::DiagramBox(QCanvas *canvas) : QCanvasRectangle(canvas) { setSize(100, 60); setPen(black); setBrush(white); str = "Text"; }В конструкторе задаются начальные размеры прямоугольника 100 X 60, цвет пера (черный) и цвет кисти (белый). Цветом пера отображаются границы прямоугольника и текст, цветом кисти заливается внутреннее пространство прямоугольника.
DiagramBox::~DiagramBox() { hide(); }Деструктор скрывает элемент диаграммы, вызовом метода hide(). Это необходимо для любых классов, порожденных от QCanvasPolygonalItem (базовый класс для QCanvasRectangle).
void DiagramBox::setText(const QString &newText) { str = newText; update(); }Функция setText() записывает текст, который должен отображаться в прямоугольнике, и вызывает QCanvasItem::update(), чтобы отобразить изменения на экране.
void DiagramBox::drawShape(QPainter &painter) { QCanvasRectangle::drawShape(painter); painter.drawText(rect(), AlignCenter, text()); if (isActive()) { drawActiveHandle(painter, rect().topLeft()); drawActiveHandle(painter, rect().topRight()); drawActiveHandle(painter, rect().bottomLeft()); drawActiveHandle(painter, rect().bottomRight()); } }Функция drawShape() перекрывает метод класса QCanvasPolygonalItem, чтобы нарисовать текст и маленькие квадратики по углам, если данный элемент диаграммы активен. Сам прямоугольник рисуется родительским методом.
QRect DiagramBox::boundingRect() const { return QRect((int)x() - Margin, (int)y() - Margin, width() + 2 * Margin, height() + 2 * Margin); }Функция boundingRect() перекрывает метод класса QCanvasItem. Она вызывается классом QCanvas, для проверки наложения одних элементов на другие и оптимизации перерисовки. Возвращаемые размеры должны быть не меньше тех, которые получает drawShape().
Значение, которое возвращает родительский метод, нас не устраивает потому, что он не учитывает размеры маленьких квадратиков, рисуемых по углам активного элемента.
DiagramLine::DiagramLine(QCanvas *canvas) : QCanvasLine(canvas) { setPoints(0, 0, 0, 99); }Конструктор DiagramLine задает координаты точек, между которыми будет нарисована линия: (0, 0) и (0, 99). В результате получается вертикальная линия, длиной в 100 пикселей.
DiagramLine::~DiagramLine() { hide(); }Опять же, в деструкторе необходимо скрыть элемент.
void DiagramLine::drawShape(QPainter &painter) { QCanvasLine::drawShape(painter); if (isActive()) { drawActiveHandle(painter, startPoint() + offset()); drawActiveHandle(painter, endPoint() + offset()); } }Функция drawShape() перекрывает родительский метод, чтобы нарисовать маленькие квадратики на концах линии, если элемент активен. Сама линия рисуется средствами родительского класса. Реализация функции offset() находится внутри определения класса DiagramLine. Она возвращает положение элемента на канве.
QPointArray DiagramLine::areaPoints() const { const int Extra = Margin + 1; QPointArray points(6); QPoint pointA = startPoint() + offset(); QPoint pointB = endPoint() + offset(); if (pointA.x() > pointB.x()) swap(pointA, pointB); points[0] = pointA + QPoint(-Extra, -Extra); points[1] = pointA + QPoint(-Extra, +Extra); points[3] = pointB + QPoint(+Extra, +Extra); points[4] = pointB + QPoint(+Extra, -Extra); if (pointA.y() > pointB.y()) { points[2] = pointA + QPoint(+Extra, +Extra); points[5] = pointB + QPoint(-Extra, -Extra); } else { points[2] = pointB + QPoint(-Extra, +Extra); points[5] = pointA + QPoint(+Extra, -Extra); } return points; }Функция areaPoints() играет роль, аналогичную boundingRect() класса DiagramBox. Аппроксимация области, принадлежащей диагональной линии, прямоугольником будет слишком грубым приближением. Потому необходимо перекрыть родительский метод и вернуть более точные границы области рисования элемента. В принципе, реализация метода в классе QCanvasLine уже возвращает приемлемые границы, но она не учитывает маленькие квадратики, которые рисуются у активных элементов.
Первое, что делает функция -- сохраняет координаты точек во временных переменных pointA и pointB, а затем проверяет -- находится ли точка pointA левее точки pointB и меняет их местами, если это необходимо, с помощью функции swap() (определена в <algorithm>). После этого она выполняет различные действия для ниспадающих и восстающих линий.
Границы области рисования линии всегда представляются в виде 6 точек, но их координаты существенно зависят от того -- ниспадающая линия или восстающая. Однако, координаты 4-х точек из 6-ти (0, 1, 3 и 4) всегда одинаковы для обоих случаев. Например, точки 0 и 1 всегда определяют левый верхний и левый нижний углы конца A, а точка 2 задает правый нижний угол для восстающих линий на конце A и левый нижний угол для ниспадающих линий на конце B.
Рисунок 8.14. Границы области рисования линий DiagramLine.
Одна деталь, которую мы опустили -- пользователь не может изменить размеры элемента, манипулируя маленькими квадратиками. Если бы мы хотели добавить такую возможность, то скорее всего нам пришлось бы сделать все немного иначе. Вместо того, чтобы рисовать квадратики в drawShape(), нам скорее всего пришлось бы сделать их самостоятельными элементами канвы. И изменять внешний вид указателя мыши, вызовом setCursor(), когда он находится над квадратиком, но для этого, сначала потребовалось бы вызвать setMouseTracking(true), потому что обычно Qt передает события перемещения мыши только тогда, когда какая либо кнопка мыши удерживается в нажатом состоянии.
Кроме того, можно было бы расширить набор элементов диаграмм, сделать возможным выделение нескольких элементов диаграммы одновременно и добавить возможность объединения элементов в группы. Статья "Canvas Item Groupies", в ежеквартальнике Qt Quarterly ( http://doc.trolltech.com/qq/qq05-canvasitemgrouping.html), описывает один из приемов реализации подобных возможностей.
В этом разделе мы предоставили пример работающего кода, использующего функциональность классов QCanvas и QCanvasView, но не раскрыли всех возможностей класса QCanvas. Например, элементы могут перемещаться по канве, если им указать скорость перемещения вызовом метода setVelocity(). За подробной информацией обращайтесь к сопроводительной документации.
Процедура вывода изображений на печать в Qt очень похожа на рисование по поверхности виджетов. Вкратце, процесс печати можно представить следующими шагами:
Создается экземпляр класса QPrinter, который будет представлять "устройство для рисования".
Вызывается функция QPrinter::setup(), которая покажет пользователю диалог выбора принтера.
Создается экземпляр класса QPainter, который будет взаимодействовать с объектом QPrinter.
Средствами QPainter рисуется изображение на странице.
Вызывается метод QPrinter::newPage(), чтобы прокрутить страницу.
Повторять действия, описанные в пунктах 4 и 5, пока не будут отпечатаны все страницы.
Рисунок 8.15. Пример вывода на печать виджетов OvenTimer, QCanvas и QImage.
void PrintWindow::printOvenTimer(OvenTimer *ovenTimer) { if (printer.setup(this)) { QPainter painter(&printer); QRect rect = painter.viewport(); int side = QMIN(rect.width(), rect.height()); painter.setViewport(0, 0, side, side); painter.setWindow(-50, -50, 100, 100); ovenTimer->draw(&painter); } }Здесь мы исходим из того, что класс PrintWindow содержит переменную-член printer, класса QPrinter. В противном случае можно было бы создать экземпляр QPrinter на стеке, но в этом случае у нас отсутствовала бы возможность сохранить пользовательские настройки принтера.
Мы вызываем setup(), чтобы запустить диалог выбора принтера. Она возвращает true, если пользователь нажал на кнопку OK. После вызова setup(), объект QPrinter готов к работе.
Далее создается QPainter, который будет рисовать на QPrinter. Потом настраивается область просмотра (viewport) и назначается система координат окна (-50, -50, 100, 100) -- прямоугольник, который ожидает получить OvenTimer, и в завершение выполняется рисование виджета, вызовом функции draw(). Если не установить размеры области просмотра, то виджет OvenTimer будет вытянут на всю высоту страницы.
По-умолчанию QPainter устанавливает размеры окна такими, чтобы они соответствовали разрешению экрана (обычно где-то между 72 и 100 точками на дюйм), но в данном случае это не имеет большого значения, так как мы сами установили систему координат окна.
Пример вывода на печать виджета OvenTimer не имеет особой практической ценности, потому что он предназначен, в первую очередь, для вывода на экран и взаимодействия с пользователем. Но для других виджетов, таких как Plotter, который был разработан нами в Главе 5, этот пример приобретает определенный смысл.
Более практичный пример -- вывод на печать QCanvas. Приложения, которые его используют, очень часто нуждаются в возможности вывода на печать того, что нарисует пользователь.
void PrintWindow::printCanvas(QCanvas *canvas) { if (printer.setup(this)) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = canvas->size(); size.scale(rect.size(), QSize::ScaleMin); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(canvas->rect()); painter.drawRect(painter.window()); painter.setClipRect(painter.viewport()); QCanvasItemList items = canvas->collisions(canvas->rect()); QCanvasItemList::const_iterator it = items.end(); while (it != items.begin()) { --it; (*it)->draw(painter); } } }На этот раз мы установили систему координат окна в соответствии с размерами канвы и ограничили область просмотра тем же самым соотношением сторон. Для этого мы использовали функцию QSize::scale(), задав в качестве второго аргумента ScaleMin. Например, если канва имела размер 640 X 480, а область просмотра QPainter -- 5000 X 5000, в результате получится область просмотра с размерами 5000 X 3750.
Функция collisions() возвратит список видимых элементов канвы, отсортированный по значению координаты z. Список просматривается в цикле, начиная с конца, и выполняется рисование элементов списка вызовом QCanvasItem::draw(). Таким образом, чем выше в списке стоит элемент, тем позднее он будет нарисован.
Третий пример -- печать картинки из QImage.
void PrintWindow::printImage(const QImage &image) { if (printer.setup(this)) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = image.size(); size.scale(rect.size(), QSize::ScaleMin); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(image.rect()); painter.drawImage(0, 0, image); } }Мы установили размеры окна в соответствии с размерами изображения и размеры области просмотра (viewport), чтобы соблюсти отношения сторон, после чего нарисовали изображение, начиная с позиции (0, 0).
Печать компонентов, которые занимают не более одной страницы, достаточно проста. Но нередко приходится сталкиваться с необходимостью вывода на печать многостраничных документов. В таких случаях нужно вывести на печать одну страницу, затем вызвать функцию newPage() и напечатать следующую страницу. Однако здесь возникает проблема определения окончания каждой из страниц.
Qt предлагает два варианта вывода на печать многостраничных документов:
Можно "перегнать" документ в формат HTML и вывести его средствами QSimpleRichText.
Можно выполнять перевод страниц вручную.
В качестве примера напечатаем справочник цветовода, который содержит названия цветов и их краткое описание. Каждая статья справочника хранится в виде "название: описание", например:
Miltonopsis santanae: Самая опасная разновидность орхидеи.Поскольку каждая статья представлена одной строкой, то весь справочник можно представить как список строк -- QStringList.
Следующий фрагмент кода выводит на печать содержимое справочника, предварительно "перегнав" его в формат HTML:
void PrintWindow::printFlowerGuide(const QStringList &entries) { QString str; QStringList::const_iterator it = entries.begin(); while (it != entries.end()) { QStringList fields = QStringList::split(": ", *it); QString title = QStyleSheet::escape(fields[0]); QString body = QStyleSheet::escape(fields[1]); str += "<table width=\"100%\" border=1 cellspacing=0>\n" "<tr><td bgcolor=\"lightgray\"><font size=\"+1\">" "<b><i>" + title + "</i></b></font>\n<tr><td>" + body + "\n</table>\n<br>\n"; ++it; } printRichText(str); }
Рисунок 8.16. Пример вывода на печать справочника цветовода, с помощью QSimpleRichText.
const int LargeGap = 48; void PrintWindow::printRichText(const QString &str) { if (printer.setup(this)) { QPainter painter(&printer); int pageHeight = painter.window().height() - 2 * LargeGap; QSimpleRichText richText(str, bodyFont, "", 0, 0, pageHeight); richText.setWidth(&painter, painter.window().width()); int numPages = (int)ceil((double)richText.height() / pageHeight); int index; for (int i = 0; i < (int)printer.numCopies(); ++i) { for (int j = 0; j < numPages; ++j) { if (i > 0 || j > 0) printer.newPage(); if (printer.pageOrder() == QPrinter::LastPageFirst) { index = numPages - j - 1; } else { index = j; } printPage(&painter, richText, pageHeight, index); } } } }Сначала мы рассчитываем высоту одной страницы, отталкиваясь от размера окна и размера пространства, которое резервируется под нижний и верхний колонтитулы. Затем создается объект класса QSimpleRichText, содержащий HTML текст. Последний аргумент, в конструкторе QSimpleRichText -- это высота страницы. Класс QSimpleRichText использует эту величину, чтобы вставить разрывы страниц.
Рисунок 8.17. Раскладка страницы справочника цветовода.
Внутренний цикл for отсчитывет страницы. если страница не является первой, то вызывается функция newPage(). Для вывода очередной страницы на печать вызывается функция printPage().
Диалог выбора принтера позволяет пользователю заказать печать страниц в обратном порядке, мы так же соблюдаем и это требование.
В данном примере предполагается, что printer, bodyFont и footerFont -- это переменные-члены класса PrintWindow.
void PrintWindow::printPage(QPainter *painter, const QSimpleRichText &richText, int pageHeight, int index) { QRect rect(0, index * pageHeight + LargeGap, richText.width(), pageHeight); painter->saveWorldMatrix(); painter->translate(0, -rect.y()); richText.draw(painter, 0, LargeGap, rect, colorGroup()); painter->restoreWorldMatrix(); painter->setFont(footerFont); painter->drawText(painter->window(), AlignHCenter | AlignBottom, QString::number(index + 1)); }Функция printPage() выводит на печать (index + 1)-ую страницу. Она содержит HTML-код и номер страницы в нижнем колонтитуле.
Мы выполняем смещение системы координат и вызываем draw(), чтобы нарисовать текст, с нужной позиции. После этого, в нижнем колонтитуле, по центру страницы, выводится ее номер. Если бы нам потребовалось выводить что нибудь в верхнем колонтитуле, то мы добавили бы еще один вызов drawText().
Константа LargeGap равна числу 48. Если исходить из предположения, что разрешение экрана срставляет 96 точек на дюйм, то число 48 соответствует половине дюйма (12.7 мм). Чтобы найти точное значение для константы, в каждом конкретном случае, можно воспользоваться услугами класса QPaintDeviceMetrics:
QPaintDeviceMetrics metrics(&printer); int LargeGap = metrics.logicalDpiY() / 2;Ниже приводится один из вариантов инициализации bodyFont и footerFont в конструкторе PrintWindow:
bodyFont = QFont("Helvetica", 14); footerFont = bodyFont;А теперь покажем, как напечатать справочник с помощью QPainter. Ниже приводится измененный вариант функции printFlowerGuide():
void PrintWindow::printFlowerGuide(const QStringList &entries) { if (printer.setup(this)) { QPainter painter(&printer); vector<QStringList> pages; int index; paginate(&painter, &pages, entries); for (int i = 0; i < (int)printer.numCopies(); ++i) { for (int j = 0; j < (int)pages.size(); ++j) { if (i > 0 || j > 0) printer.newPage(); if (printer.pageOrder() == QPrinter::LastPageFirst) { index = pages.size() - j - 1; } else { index = j; } printPage(&painter, pages, index); } } } }Первое, что нужно сделать после настройки принтера и QPainter -- это вызвать вспомогательную функцию paginate(), чтобы определить разбивку справочника по страницам. Результат работы функции -- массив QStringList, в котором каждый из элементов хранит статьи справочника для одной страницы.
Например, допустим, что справочник содержит всего 6 статей, которые мы обозначим как A, B, C, D, E и F. Теперь предположим, что статьи A и B располагаются на первой странице, C, D и E -- на второй, а F -- на третьей. Таким образом, массив pages, в элементе с индексом 0, будет содержать статьи A и B, статьи C, D и E -- в элементе с индексом 1 и статью F -- в элементе с индексом 2.
В остальном, функция printFlowerGuide() практически идентична приведенному ранее варианту. Однако, функция printPage() имеет существенные отличия, но об этом немного позже.
void PrintWindow::paginate(QPainter *painter, vector<QStringList> *pages, const QStringList &entries) { QStringList currentPage; int pageHeight = painter->window().height() - 2 * LargeGap; int y = 0; QStringList::const_iterator it = entries.begin(); while (it != entries.end()) { int height = entryHeight(painter, *it); if (y + height > pageHeight && !currentPage.empty()) { pages->push_back(currentPage); currentPage.clear(); y = 0; } currentPage.push_back(*it); y += height + MediumGap; ++it; } if (!currentPage.empty()) pages->push_back(currentPage); }Функция paginate() распределяет статьи справочника по страницам, основываясь на результатах функции entryHeight(), которая вычисляет высоту одной статьи.
Рисунок 8.18. Вывод справочника цветовода с помощью QPainter.
Рисунок 8.19. Раскладка одной статьи справочника.
int PrintWindow::entryHeight(QPainter *painter, const QString &entry) { QStringList fields = QStringList::split(": ", entry); QString title = fields[0]; QString body = fields[1]; int textWidth = painter->window().width() - 2 * SmallGap; int maxHeight = painter->window().height(); painter->setFont(titleFont); QRect titleRect = painter->boundingRect(0, 0, textWidth, maxHeight, WordBreak, title); painter->setFont(bodyFont); QRect bodyRect = painter->boundingRect(0, 0, textWidth, maxHeight, WordBreak, body); return titleRect.height() + bodyRect.height() + 4 * SmallGap; }Функция entryHeight(), с помощью QPainter::boundingRect(), вычисляет высоту статьи на странице. На рисунке 8.19 показана раскладка статьи справочника и назначение констант SmallGap и MediumGap.
void PrintWindow::printPage(QPainter *painter, const vector<QStringList> &pages, int index) { painter->saveWorldMatrix(); painter->translate(0, LargeGap); QStringList::const_iterator it = pages[index].begin(); while (it != pages[index].end()) { QStringList fields = QStringList::split(": ", *it); QString title = fields[0]; QString body = fields[1]; printBox(painter, titleFont, title, lightGray); printBox(painter, bodyFont, body, white); painter->translate(0, MediumGap); ++it; } painter->restoreWorldMatrix(); painter->setFont(footerFont); painter->drawText(painter->window(), AlignHCenter | AlignBottom, QString::number(index + 1)); }Функция printPage() обходит в цикле все статьи справочника и печатает их в два приема: первый раз функция printBox() вызывается для печати заголовка статьи (название цветка) и второй раз -- для печати описания (тела статьи). В заключение печатается номер страницы, внизу по центру.
void PrintWindow::printBox(QPainter *painter, const QFont &font, const QString &str, const QBrush &brush) { painter->setFont(font); int boxWidth = painter->window().width(); int textWidth = boxWidth - 2 * SmallGap; int maxHeight = painter->window().height(); QRect textRect = painter->boundingRect(SmallGap, SmallGap, textWidth, maxHeight, WordBreak, str); int boxHeight = textRect.height() + 2 * SmallGap; painter->setPen(QPen(black, 2, SolidLine)); painter->setBrush(brush); painter->drawRect(0, 0, boxWidth, boxHeight); painter->drawText(textRect, WordBreak, str); painter->translate(0, boxHeight); }Функция printBox() рисует прямоугольник, а затем внутри него -- текст.
Если на печать выводится большой документ, или пользователь заказал несколько копий одного документа, то неплохо было бы показать индикатор хода выполнения задания -- QProgressDialog. Ниже приводится модифицированный вариант функции printFlowerGuide(), которая выводит перед пользователем индикатор хода выполнения задания:
void PrintWindow::printFlowerGuide(const QStringList &entries) { if (printer.setup(this)) { QPainter painter(&printer); vector<QStringList> pages; int index; paginate(&painter, &pages, entries); int numSteps = printer.numCopies() * pages.size(); int step = 0; QProgressDialog progress(tr("Printing file..."), tr("Cancel"), numSteps, this); progress.setModal(true); for (int i = 0; i < (int)printer.numCopies(); ++i) { for (int j = 0; j < (int)pages.size(); ++j) { progress.setProgress(step); qApp->processEvents(); if (progress.wasCanceled()) { printer.abort(); return; } ++step; if (i > 0 || j > 0) printer.newPage(); if (printer.pageOrder() == QPrinter::LastPageFirst) { index = pages.size() - j - 1; } else { index = j; } printPage(&painter, pages, index); } } } }Когда пользователь нажимает на кнопку Cancel -- вызывается QPrinter::abort(), которая останавливает процесс печати.
OpenGL -- это стандарт API, для отображения двух- и трехмерной графики. Приложения Qt могут использовать OpenGL, посредством модуля QGL. Мы полагаем, что вы уже имеете некоторое знакомство с OpenGL. Если это не так, то рекомендуем начать изучение с посещения сайта http://www.opengl.org/.
Рисование трехмерных объектов, с помощью OpenGL, не так сложно, как может показаться на первый взгляд. Все что вам нужно сделать -- создать дочерний класс от QGLWidget, перекрыть некоторые виртуальные методы предка и связать приложение с модулем QGL и библиотекой OpenGL. Поскольку QGLWidget ведет свою родословную от QWidget, то здесь вполне применимы знания, которые вы уже получили. Основное отличие здесь состоит в том, что теперь, вместо QPainter, вам придется использовать стандартные функции рисования из OpenGL.
Рисунок 8.20. Приложение Cube.
class Cube : public QGLWidget { public: Cube(QWidget *parent = 0, const char *name = 0); protected: void initializeGL(); void resizeGL(int width, int height); void paintGL(); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseDoubleClickEvent(QMouseEvent *event); private: void draw(); int faceAtPosition(const QPoint &pos); GLfloat rotationX; GLfloat rotationY; GLfloat rotationZ; QColor faceColors[6]; QPoint lastPos; };Класс Cube порожден от QGLWidget. Функции initializeGL(), resizeGL() и paintGL() перекрывают методы родительского класса QGLWidget. Обработчики событий от мыши перекрывают обработчики, унаследованные от QWidget. Определение класса QGLWidget находится в заголовке <qgl.h>.
Cube::Cube(QWidget *parent, const char *name) : QGLWidget(parent, name) { setFormat(QGLFormat(DoubleBuffer | DepthBuffer)); rotationX = 0; rotationY = 0; rotationZ = 0; faceColors[0] = red; faceColors[1] = green; faceColors[2] = blue; faceColors[3] = cyan; faceColors[4] = yellow; faceColors[5] = magenta; }В конструкторе вызывается QGLWidget::setFormat(), чтобы задать контекст устройства отображения OpenGL, и инициализируются приватные переменные-члены класса.
void Cube::initializeGL() { qglClearColor(black); glShadeModel(GL_FLAT); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); }Функция initializeGL() вызывается один раз, перед вызовом paintGL(). Здесь выполняется настройка контекста отображения.
Все функции являются стандартными вызовами из библиотеки OpenGL, за исключением qglClearColor() -- метода класса QGLWidget. Если задаться целью, до конца следовать стандарту OpenGL, то мы могли бы вызвать функцию glClearColor(), в режиме RGBA, или glClearIndex(), в режиме индексированных цветов.
void Cube::resizeGL(int width, int height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); GLfloat x = (GLfloat)width / height; glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0); glMatrixMode(GL_MODELVIEW); }Функция resizeGL() вызывается один раз, перед paintGL(), но после того, как будет вызвана функция initializeGL(). Здесь настраивается область просмотра (viewport), проекция и прочие настройки, которые зависят от размера виджета.
void Cube::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); draw(); }Функция paintGL() вызывается всякий раз, когда возникает необходимость перерисовать содержимое виджета. Она напоминает обработчик события QWidget::paintEvent(), только вместо QPainter здесь используются обращения к функциям OpenGL. Собственно рисование выполняется внутри приватной функции draw():
void Cube::draw() { static const GLfloat coords[6][4][3] = { { { +1.0, -1.0, +1.0 }, { +1.0, -1.0, -1.0 }, { +1.0, +1.0, -1.0 }, { +1.0, +1.0, +1.0 } }, { { -1.0, -1.0, -1.0 }, { -1.0, -1.0, +1.0 }, { -1.0, +1.0, +1.0 }, { -1.0, +1.0, -1.0 } }, { { +1.0, -1.0, -1.0 }, { -1.0, -1.0, -1.0 }, { -1.0, +1.0, -1.0 }, { +1.0, +1.0, -1.0 } }, { { -1.0, -1.0, +1.0 }, { +1.0, -1.0, +1.0 }, { +1.0, +1.0, +1.0 }, { -1.0, +1.0, +1.0 } }, { { -1.0, -1.0, -1.0 }, { +1.0, -1.0, -1.0 }, { +1.0, -1.0, +1.0 }, { -1.0, -1.0, +1.0 } }, { { -1.0, +1.0, +1.0 }, { +1.0, +1.0, +1.0 }, { +1.0, +1.0, -1.0 }, { -1.0, +1.0, -1.0 } } }; glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -10.0); glRotatef(rotationX, 1.0, 0.0, 0.0); glRotatef(rotationY, 0.0, 1.0, 0.0); glRotatef(rotationZ, 0.0, 0.0, 1.0); for (int i = 0; i < 6; ++i) { glLoadName(i); glBegin(GL_QUADS); qglColor(faceColors[i]); for (int j = 0; j < 4; ++j) { glVertex3f(coords[i][j][0], coords[i][j][1], coords[i][j][2]); } glEnd(); } }Внутри функции draw() выполняется рисование куба, с учетом вращения по осям x, y и z и цветов граней, находящихся в массиве faceColors. Все вызовы являются стандартными для OpenGL, за исключением qglColor(). Мы могли бы использовать вместо нее стандартные функции OpenGL glColor3d() или glIndex(), в зависимости от выбранного режима цветопередачи.
void Cube::mousePressEvent(QMouseEvent *event) { lastPos = event->pos(); } void Cube::mouseMoveEvent(QMouseEvent *event) { GLfloat dx = (GLfloat)(event->x() - lastPos.x()) / width(); GLfloat dy = (GLfloat)(event->y() - lastPos.y()) / height(); if (event->state() & LeftButton) { rotationX += 180 * dy; rotationY += 180 * dx; updateGL(); } else if (event->state() & RightButton) { rotationX += 180 * dy; rotationZ += 180 * dx; updateGL(); } lastPos = event->pos(); }Функции mousePressEvent() и mouseMoveEvent() позволяют пользователю вращать куб и перемещать его по поверхности экрана. Левой кнопкой мыши выполняется вращение по осям x и y, правой -- по осям x и z.
После изменения переменных rotationX и/или rotationY и rotationZ, вызывается функция updateGL(), которая перерисовывает изображение.
void Cube::mouseDoubleClickEvent(QMouseEvent *event) { int face = faceAtPosition(event->pos()); if (face != -1) { QColor color = QColorDialog::getColor(faceColors[face], this); if (color.isValid()) { faceColors[face] = color; updateGL(); } } }Обработчик mouseDoubleClickEvent() позволяет пользователю изменить цвет грани по двойному щелчку мыши. Для определения номера грани вызывается функция faceAtPosition(). Если под указателем мыши действительно находится какая либо грань куба, вызывается QColorDialog::getColor(), чтобы получить от пользователя новый цвет грани. Затем он заносится в массив faceColors и вызывается updateGL(), чтобы перерисовать изображение.
int Cube::faceAtPosition(const QPoint &pos) { const int MaxSize = 512; GLuint buffer[MaxSize]; GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); glSelectBuffer(MaxSize, buffer); glRenderMode(GL_SELECT); glInitNames(); glPushName(0); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); gluPickMatrix((GLdouble)pos.x(), (GLdouble)(viewport[3] - pos.y()), 5.0, 5.0, viewport); GLfloat x = (GLfloat)width() / height(); glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0); draw(); glMatrixMode(GL_PROJECTION); glPopMatrix(); if (!glRenderMode(GL_RENDER)) return -1; return buffer[3]; }Функция faceAtPosition() возвращает либо номер грани, находящейся в заданных координатах, либо -1, если точка с заданными координатами не входит ни в одну из граней. Код, выполняющий проверку, достаточно сложен. По сути -- он переводит сцену в режим GL_SELECT, чтобы мы могли воспользоваться дополнительными возможностями OpenGL, и отыскивает номер грани ("name").
Далее приводится содержимое файла main.cpp:
#include <qapplication.h> #include "cube.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!QGLFormat::hasOpenGL()) qFatal("This system has no OpenGL support"); Cube cube; cube.setCaption(QObject::tr("Cube")); cube.resize(300, 300); app.setMainWidget(&cube); cube.show(); return app.exec(); }Если система не поддерживает OpenGL, то, с помощью вызова qFatal(), приложение выводит сообщение об ошибке и завершает работу.
Чтобы связать приложение Cube с модулем QGL и библиотекой OpenGL, в файл .pro нужно добавить строчку:
CONFIG += openglЗа дополнительной информацией о модуле QGL, обращайтесь к сопроводительной документации по классам QGLWidget, QGLFormat, QGLContext и QGLColormap.
|