5.4. Двойная буферизация.

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

Когда Qt генерирует событие paint, виджет сначала "стирается" -- т.е. все пиксели окрашиваются цветом фона. Затем, в функции paintEvent() виджету остается окрасить только те пиксели, цвет которых отличается от цвета фона. Такой двухшаговый алгоритм довольно удобен, поскольку мы перерисовываем только то что нужно, нимало не беспокоясь о других писелях.

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

Рисунок 5.7. Порядок перерисовки виджета, при изменении размеров.


Флаг WStaticContents, с которым мы создавали виджет IconEditor, может рассматриваться как одно из возможных решений проблемы мерцания, но оно применимо только к виджетам, содержимое которых не зависит от их размера. Такие виджеты -- довольно редкое явление. В большинстве своем они стремятся растягивать свое содержимое, чтобы полностью занять отводимое им пространство. Они требуют полной перерисовки, при изменении размеров. В этом случае тоже можно устранить эффект мерцания, но решение проблемы немного сложнее.

Первое правило, на пути к устранению мерцания -- конструировать виджет с флагом WNoAutoErase. Этот флаг предотвращает стирание виджета перед передачей событие paint.

Рисунок 5.8. Порядок перерисовки виджета, созданного с флагом WNoAutoErase.


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

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

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

void MyWidget::paintEvent(QPaintEvent *) { QPainter painter(this); drawMyStuff(&painter); } Тогда версия обработчика, использующего технику двойной буферизации, могла бы выглядеть как то так: void MyWidget::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); painter.translate(-rect.x(), -rect.y()); drawMyStuff(&painter); bitBlt(this, rect.x(), rect.y(), &pixmap, 0, 0, rect.width(), rect.height()); } Сначала устанавливаются размеры QPixmap такими, чтобы они были не меньше размеров прямоугольника, описывающего область перерисовки. (Чаще всего область перерисовки имеет прямоугольную или Г-образную форму, но может иметь и более сложный вид.) Экземпляр QPixmap объявлен статическим, чтобы избежать постоянных операций по его созданию/удалению. По тем же причинам мы никогда не уменьшаем его размер -- вызовы QSize::expandedTo() и QPixmap::resize() приводят к тому, что в течение всей своей "жизни" QPixmap будет только расти. Далее, QPixmap заполняется цветом фона виджета. Второй аргумент функции fill() указывает -- в какой позиции виджета будет находиться верхний левый угол QPixmap. (Это важно в том случае, когда виджет имеет фоновое изображение и процесс "стирания" заключается не в заполнении виджета однородным цветом, а в рисовании фонового изображения.)

Класс QPixmap очень напоминает QImage и QWidget. Подобно QImage, он хранит изображение, но глубина цвета и цветовая палитра зависят от настроек дисплея, подобно QWidget. Если оконная система работает с 8-ми битным цветом, все QWidget и QPixmap ограничиваются 256-ю цветами, а Qt автоматически переводит 24-х битный цвет в 8-ми битное представление.

Затем создается QPainter. Передавая указатель this конструктору, мы заставляем QPainter взять некоторые настройки, например шрифт, из виджета. Вызовом translate() осуществляется переход к системе координат виджета.

В завершение, изображение копируется в виджет с помощью глобальной функции bitBlt() (от англ. "bit-block transfer" -- "перемещение битового блока").

Двойная буферизация бывает полезной не только для устранения эффекта мерцания. Выгода от ее использования особенно заметна в тех случаях, когда формирование изображения -- довольно сложный процесс, и оно должно периодически обновляться на экране. Такие изображения могут сохраняться в памяти, вместе с виджетом, и копироваться в виджет при наступлении события paint. Это особенно полезно, когда необходимо внести в рисунок лишь незначительные изменения, например -- нарисовать границы прямоугольника выделения.

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

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

Рисунок 5.9. Изменение масштаба в компоненте Plotter.


Пользователь может неоднократно изменять масштаб таким образом. Откат на шаг назад выполняется нажатием на кнопку "Zoom Out", а возврат, после выполнения отката -- кнопкой "Zoom In". Эти кнопки видны только тогда, когда пользователь хотя бы раз изменял масштаб отображения.

Компонент может хранить данные любого числа кривых. Он так же имеет стек из экземпляров класса PlotSettings, на котором хранится история изменения масштаба пользователем.

Начнем с файла заголовка:

#ifndef PLOTTER_H #define PLOTTER_H #include <qpixmap.h> #include <qwidget.h> #include <map> #include <vector> class QToolButton; class PlotSettings; typedef std::vector<double> CurveData; Мы подключили стандартные заголовки <map> и <vector>. Мы не импортировали символы из пространства имен std -- для заголовочных файлов это считается дурным тоном.

Мы определили CurveData, как синоним std::vector<double>. Координаты точек, определяющих кривую на графике, предполагается хранить в виде массива пар координат x и y. Например, кривая задана тремя точками, с координатами (0, 24), (1, 44), (2, 89), что соответствует массиву значений [0, 24, 1, 44, 2, 89].

class Plotter : public QWidget { Q_OBJECT public: Plotter(QWidget *parent = 0, const char *name = 0, WFlags flags = 0); void setPlotSettings(const PlotSettings &settings); void setCurveData(int id, const CurveData &data); void clearCurve(int id); QSize minimumSizeHint() const; QSize sizeHint() const; public slots: void zoomIn(); void zoomOut(); Компонент имеет три публичных метода для его настройки, и два публичных слота изменяющих масштаб отображения. Кроме того, перекрыты методы предка minimumSizeHint() и sizeHint(). protected: void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void keyPressEvent(QKeyEvent *event); void wheelEvent(QWheelEvent *event); В защищенной секции класса объявлены функции-обработчики событий, которые мы должны реализовать. private: void updateRubberBandRegion(); void refreshPixmap(); void drawGrid(QPainter *painter); void drawCurves(QPainter *painter); enum { Margin = 40 }; QToolButton *zoomInButton; QToolButton *zoomOutButton; std::map<int, CurveData> curveMap; std::vector<PlotSettings> zoomStack; int curZoom; bool rubberBandIsShown; QRect rubberBandRect; QPixmap pixmap; }; В приватной секции объявлены константа, несколько функций, связанных с рисованием, и несколько переменных-членов. Константа Margin определяет ширину пустого пространства вокруг графика. Среди переменных присутствует QPixmap, которая хранит копию изображения виджета, идентичного тому, что отображается на экране. График с кривыми всегда сначала рисуется в этой переменной, а затем копируется в виджет. class PlotSettings { public: PlotSettings(); void scroll(int dx, int dy); void adjust(); double spanX() const { return maxX - minX; } double spanY() const { return maxY - minY; } double minX; double maxX; int numXTicks; double minY; double maxY; int numYTicks; private: void adjustAxis(double &min, double &max, int &numTicks); }; #endif Класс PlotSettings определяет дипазоны изменения аргументов по осям x и y, а так же количество рисок, отображаемых на каждой из осей. На рисунке 5.10 показано соответствие между объектом PlotSettings и масштабом отображения виджета Plotter.

Строго говоря, переменные numXTicks и numYTicks хранят не число рисок, а число интервалов между рисками, т.е. если, например, в переменной numXTicks хранится число 5, то фактически, на оси x будет нарисовано 6 рисок. Такой подход упрощает расчеты, которые мы будем рассматривать чуть ниже.

Рисунок 5.10. Переменные-члены класса PlotSettings.


Перейдем к файлу ревлизации: #include <qpainter.h> #include <qstyle.h> #include <qtoolbutton.h> #include <cmath> using namespace std; #include "plotter.h" Мы подключили все необходимые заголовочные файлы и импортировали все имена из пространства имен std. Plotter::Plotter(QWidget *parent, const char *name, WFlags flags) : QWidget(parent, name, flags | WNoAutoErase) { setBackgroundMode(PaletteDark); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setFocusPolicy(StrongFocus); rubberBandIsShown = false; zoomInButton = new QToolButton(this); zoomInButton->setIconSet(QPixmap::fromMimeSource("zoomin.png")); zoomInButton->adjustSize(); connect(zoomInButton, SIGNAL(clicked()), this, SLOT(zoomIn())); zoomOutButton = new QToolButton(this); zoomOutButton->setIconSet( QPixmap::fromMimeSource("zoomout.png")); zoomOutButton->adjustSize(); connect(zoomOutButton, SIGNAL(clicked()), this, SLOT(zoomOut())); setPlotSettings(PlotSettings()); } Третьим аргументом, конструктор Plotter принимает набор флагов. Этот аргумент просто передается родительскому конструктору, правда, при этом попутно включается флаг WNoAutoErase. Этот параметр имеет особое значение для виджетов, которые могут использоваться как автономные окна, поскольку позволяет пользователю класса сконфигурировать рамку окна и полосу заголовка.

Вызов setBackgroundMode() устанавливает в качестве фонового, вместо элемента палитры "background", элемент палитры -- "dark" (темный). Хотя в конструктор базового класса и передается флаг WNoAutoErase, тем не менее, по-прежнему необходимо иметь какой нибудь цвет в качестве фонового, которым будут закрашиваться пиксели, появляющиеся при увеличении размеров виджета, до того, как сработает обработчик paintEvent(). Поскольку фон виджета Plotter будет темным, то определенно имеет смысл окрашивать новые пиксели именно в темный цвет.

Затем, вызовом setSizePolicy(), устанавливается политика изменения размеров виджета. В данном случае, виджет может свободно изменять свои размеры по обеим осям. Такая политика изменения размеров характерна для виджетов, которые могут занимать значительную часть площади экрана. По-умолчанию, политика изменения размеров, для обеих осей, имеет значение QSizePolicy::Preferred, т.е. -- виджет "предпочитает" иметь размеры, равные "идеальным" значениям, но допускает и сжатие до минимально возможного размера (minimumSizeHint()), и растягивание до неопределенного предела.

Вызов setFocusPolicy() указывает виджету, что он может принимать фокус по щелчку мыши или по клавише Tab. Когда Plotter владеет фокусом, он может принимать и обрабатывать события от клавиатуры. Он реагирует на нажатия клавиш: "+" -- увеличить изображение, "-" -- уменьшить изображение и клавиши со стрелками -- для перемещения графика вверх, вниз, влево и вправо.

Рисунок 5.11. Перемещение графика клавишами управления курсором.


Остальной код конструктора создает две кнопки QToolButton с иконками. С помощью этих кнопок пользователь сможет перемещаться, взад и вперед, по стеку истории изменения масштаба. Иконки для кнопок хранятся в коллекции изображений, поэтому в файл .pro мы добавили следующие строки: IMAGES += images/zoomin.png \ images/zoomout.png Вызовы методов adjustSize() кнопок, устанавливают размеры кнопок равные их "идеальным" размерам.

И, наконец, вызов setPlotSettings() завершает инициализацию виджета.

void Plotter::setPlotSettings(const PlotSettings &settings) { zoomStack.resize(1); zoomStack[0] = settings; curZoom = 0; zoomInButton->hide(); zoomOutButton->hide(); refreshPixmap(); } Функция setPlotSettings() используется для того, чтобы указать PlotSettings, который должен использоваться для отображения графика. Она вызывается из конструктора и может вызываться пользователем класса. Каждый раз, когда пользователь изменяет масштаб отображения, создается новый экземпляр PlotSettings и помещается на стек истории изменения масштаба.

Стек представляют две переменные:

После вызова setPlotSettings(), стек содержит только одну запись и обе кнопки, Zoom In и Zoom Out, скрыты. Они останутся невидимыми до тех пор, пока мы не вызовем их методы show() в слотах zoomIn() и zoomOut(). (Обычно, для того, чтобы сделать подчиненные виджеты видимыми, достаточно вызвать метод show() владельца, но в данном случае, мы явно вызывали hide() у подчиненных виджетов, поэтому они останутся скрытыми до тех пор, пока мы явно не вызовем методы show().)

Вызов refreshPixmap() обновляет изображение. Как правило, в таких случаях, мы вызываем update(), но в данной ситуации все делается несколько иначе, поскольку необходимо, чтобы QPixmap хранил самую последнюю версию картинки, отображаемой на экране. После регенерации картинки, refreshPixmap() вызывает update(), чтобы скопировать полученное изображение в виджет.

void Plotter::zoomOut() { if (curZoom > 0) { --curZoom; zoomOutButton->setEnabled(curZoom > 0); zoomInButton->setEnabled(true); zoomInButton->show(); refreshPixmap(); } } Слот zoomOut() уменьшает изображение, если оно перед этим было увеличено. Индекс текущего элемента на стеке уменьшается, и разрешается или запрещается кнопка Zoom Out, в зависимости от того -- возможно ли дальнейшее перемещение к началу истории. Кнопка Zoom In разрешается и делается видимой. В конце, изображение обновляется вызовом refreshPixmap(). void Plotter::zoomIn() { if (curZoom < (int)zoomStack.size() - 1) { ++curZoom; zoomInButton->setEnabled( curZoom < (int)zoomStack.size() - 1); zoomOutButton->setEnabled(true); zoomOutButton->show(); refreshPixmap(); } } Если пользователь сначала увеличил изображение, а затем опять уменьшил, PlotSettings положит предыдущее значение масштаба на стек и мы сможем опять увеличить изображение нажатием на кнопку. (По прежнему остается возможность увеличить размер изображения, выделив мышью требуемый участок графика)

Слот увеличивает значение переменной curZoom, для перемещения на очередной уровень в стеке масштабов. Разрешает или запрещает кнопку Zoom In в зависимости от того -- достигнуто ли дно стека. И разрешает кнопку Zoom Out. Напоследок вызывается refreshPixmap(), чтобы обновить изображение на экране.

void Plotter::setCurveData(int id, const CurveData &data) { curveMap[id] = data; refreshPixmap(); } Функция setCurveData() заносит массив координат для заданной кривой. Если кривая с таким ID уже существует, то она заменяется новыми данными, в противном случае в график вставляется новая кривая. Координаты точек кривых хранятся в переменной curveMap, имеющей тип map<int, CurveData>.

И опять же, для обновления отображения на экране, вместо update(), вызывается refreshPixmap().

void Plotter::clearCurve(int id) { curveMap.erase(id); refreshPixmap(); } Функция clearCurve() удаляет кривую из curveMap. QSize Plotter::minimumSizeHint() const { return QSize(4 * Margin, 4 * Margin); } Функция minimumSizeHint() очень похожа на sizeHint(), с тем лишь отличием, что последняя возвращает "идеальные" размеры виджета, а minimumSizeHint() -- минимальные "идеальные" размеры. Менеджеры размещения никогда не будут пытаться уменьшить размеры виджета меньше этих пределов.

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

QSize Plotter::sizeHint() const { return QSize(8 * Margin, 6 * Margin); } Функция sizeHint() возвращет "идеальные" размеры виджета, устанавливая его пропорции как 4:3.

На этом мы завершаем обзор публичных методов и слотов класса Plotter и переходим к защищенным обработчикам событий.

void Plotter::paintEvent(QPaintEvent *event) { QMemArray<QRect> rects = event->region().rects(); for (int i = 0; i < (int)rects.size(); ++i) bitBlt(this, rects[i].topLeft(), &pixmap, rects[i]); QPainter painter(this); if (rubberBandIsShown) { painter.setPen(colorGroup().light()); painter.drawRect(rubberBandRect.normalize()); } if (hasFocus()) { style().drawPrimitive(QStyle::PE_FocusRect, &painter, rect(), colorGroup(), QStyle::Style_FocusAtBorder, colorGroup().dark()); } } Как правило, в paintEvent() сосредотачивается весь код, который отвечает за рисование виджета на экране. Но в нашем случае, рисование выполняет функция refreshPixmap(), поэтому здесь мы просто переносим буфер с рисунком в виджет.

Вызов QRegion::rect() возвращает массив из QRect, который задает область перерисовки. Для копирования каждой подобласти, из буфера с изображением в виджет, используется функция bitBlt(). Это функция с глобальной областью видимости. Она имеет следующий синтаксис:

bitBlt(dest, destPos, source, sourceRect); где source -- это виджет-источник (в нашем случае -- буфер с картинкой), dest -- виджет-приемник (или pixmap) и destPos -- координаты верхнего левого угла области в приемнике, в которую будет выполняться копирование.

Рисунок 5.12. Копирование некоторой прямоугольной области из буфера в виджет.


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

Как только перенос картинки из буфера будет завершен, мы приступаем к рисованию границ области выделения. Рамка рисуется цветом группы "light", чтобы обеспечить приемлемую контрастность рамки и фона. Обратите внимание: рамка рисуется прямо на виджете, оставляя буфер с рисунком в неприкосновенности. Собственно рисование выполняется функцией drawPrimitive().

Функция QWidget::style() возвращает стиль рисования виджета. В Qt стиль рисования виджета -- это подкласс QStyle. В список встроенных стилей входят QWindowsStyle, QWindowsXPStyle, QMotifStyle и QMacStyle.. Каждый из них предоставляет свою реализации виртуальных методов. Функция drawPrimitive() -- одна из них. Она рисует графические примитивы, такие как панели, кнопки и границы областей выделения, в соответствии с выбранным стилем. Как правило, для всех виджетов приложения устанавливается единый стиль отображения (QApplication::style()), но он может быть изменен для каждого из виджетов, вызовом QWidget::setStyle().

Создавая дочерние классы от QStyle, вы можете опредлять свои собственные стили отображения. Делается это обычно для того, чтобы подчеркнуть индивидуальность приложения (или группы приложений). Тем не менее, считается хорошим тоном соблюдать единый стиль отображения, выбранный пользователем при настройке рабочего окружения.

Стандартные виджеты Qt, практически всегда отрисовывают себя, основываясь на QStyle. Именно по этой причине они похожи на "родные" графические элементы самой операционной системы. Свои виджеты вы можете отрисовывать либо используя QStyle, либо собирая их из стандартных виджетов Qt. В случае с Plotter мы использовали оба подхода: прямоугольная рамка выделения рисуется с помощью QStyle, а кнопки Zoom In и Zoom Out -- это стандартные виджеты.

void Plotter::resizeEvent(QResizeEvent *) { int x = width() - (zoomInButton->width() + zoomOutButton->width() + 10); zoomInButton->move(x, 5); zoomOutButton->move(x + zoomInButton->width() + 5, 5); refreshPixmap(); } Когда необходимо изменить размеры Plotter, Qt генерирует событие "resize". Здесь мы реализуем обработку этого события. Кнопки Zoom In и Zoom Out размещаются в правом верхнем углу виджета, с небольшим (5 пикселей) промежутком между ними.

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

Нам не нужно изначально устанавливать кнопки в конструкторе, поскольку перед тем как виджет впервые появится на экране, Qt сгенерирует событие "resize".

В качестве альтернативы, можно было бы вставить в наш виджет менеджер размещения (например QGridLayout), который управлял бы расположением кнопок. Однако, это усложнило бы реализацию виджета и он стал бы более ресурсоемким. Когда виджет создается "с нуля", как в данном лучае, то правильнее будет отказаться от услуг менеджеров размещения и устанавливать все подчиненные компоненты вручную.

В конце обработчика, для перерисовки графика с новыми размерами, вызывается refreshPixmap().

void Plotter::mousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) { rubberBandIsShown = true; rubberBandRect.setTopLeft(event->pos()); rubberBandRect.setBottomRight(event->pos()); updateRubberBandRegion(); setCursor(crossCursor); } } Когда пользователь нажимает левую кнопку мыши, мы начинаем показывать рамку выделяемой области. Для этого, в переменную rubberBandIsShown, записывается значение true, переменная rubberBandRect инициализируется текущими координатами указателя мыши, затем планируются события "paint", для отрисовки рамки, и наконец изменяется вид указателя мыши -- теперь он представляется в виде крестика.

Qt предоставляет два основных механизма управления внешним видом указателя мыши:

В Главе 4 мы уже пользовались функцией QApplication::setOverrideCursor(), с аргументом waitCursor, чтобы показать занятость приложения. void Plotter::mouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) { updateRubberBandRegion(); rubberBandRect.setBottomRight(event->pos()); updateRubberBandRegion(); } } Когда пользователь перемещает указатель мыши, удерживая при этом левую кнопку в нажатом состоянии, вызывается updateRubberBandRegion(). Она ставит в очередь планировщика событие "paint", чтобы перерисовать области, где находилась рамка области выделения, затем записывает новые координаты в rubberBandRect и вторично выполняет перерисовку рамки выделения. В результате прежняя рамка стирается и рисуется новая, в соответствии с изменившимися координатами указателя мыши.

Переменная rubberBandRect имеет тип QRect. Экземпляры этого класса могут поставлять значения в виде (x, y, w, h), где (x, y) --это координаты левого верхнего угла, а w, h -- ширина и высота прямоугольника либо в виде пар координат верхнего левого и правого нижнего углов. В нашем случае мы используем представление в виде пар координат. В качестве координат верхнего левого угла устанавливаются координаты указателя мыши в момент нажатия на кнопку, а текущее положение курсора мыши принимается за правый нижний угол рамки выделения.

Если пользователь переместит указатель влево или вверх, то может получиться ситуация, когда то, что мы считаем правым нижним углом, окажется левее и/или выше левого верхнего угла. В этом случае QRect будет представлять высоту и ширину прямоугольника отрицательными числами. Чтобы избежать сложностей с отрицательными числами, в QRect предусмотрена функция normalize(), которая возвращает нормализованные координаты прямоугольника.

void Plotter::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == LeftButton) { rubberBandIsShown = false; updateRubberBandRegion(); unsetCursor(); QRect rect = rubberBandRect.normalize(); if (rect.width() < 4 || rect.height() < 4) return; rect.moveBy(-Margin, -Margin); PlotSettings prevSettings = zoomStack[curZoom]; PlotSettings settings; double dx = prevSettings.spanX() / (width() - 2 * Margin); double dy = prevSettings.spanY() / (height() - 2 * Margin); settings.minX = prevSettings.minX + dx * rect.left(); settings.maxX = prevSettings.minX + dx * rect.right(); settings.minY = prevSettings.maxY - dy * rect.bottom(); settings.maxY = prevSettings.maxY - dy * rect.top(); settings.adjust(); zoomStack.resize(curZoom + 1); zoomStack.push_back(settings); zoomIn(); } } Когда левая кнопка мыши отпускается, производится стирание рамки области выделения и восстанавливается прежний вид указателя мыши. Если размер выделенной области не менее, чем 4 X 4, выполняется изменение масштаба отображения графика. Если меньше -- скорее всего пользователь щелкнул по виджету по ошибке или хотел передать ему фокус. В этом случае ничего не делается.

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

После выполнения преобразований, вызывается PlotSettings::adjust(), которая округляет размеры и находит наиболее разумные значения для рисок, наносимых на оси графика.

Рисунок 5.13. Преобразование координат рамки выделения из системы координат виджета, в систему координат графика.


Рисунок 5.14. Округление и переход к новому масштабу отображения.


Затем выполняется масштабирование. Сначала на стек добавляется новый экземпляр PlotSettings, а затем вызывается zoomIn(). void Plotter::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Key_Plus: zoomIn(); break; case Key_Minus: zoomOut(); break; case Key_Left: zoomStack[curZoom].scroll(-1, 0); refreshPixmap(); break; case Key_Right: zoomStack[curZoom].scroll(+1, 0); refreshPixmap(); break; case Key_Down: zoomStack[curZoom].scroll(0, -1); refreshPixmap(); break; case Key_Up: zoomStack[curZoom].scroll(0, +1); refreshPixmap(); break; default: QWidget::keyPressEvent(event); } } Когда виджет Plotter владеет фокусом ввода, нажатие клавиш на клавиатуре приводит к вызову функции keyPressEvent(). Наша реализация обработчика обслуживает шесть клавиш: "+", "-" и клавиши управления курсором ("вверх", "вниз", "влево" и "вправо"). Если нажата клавиша, которую мы не обрабатываем, вызывается обработчик класса-предка. Для простоты мы игнорируем состояние клавиш-модификаторв: Ctrl, Shift и Alt. Состояние этих клавиш может быть получено через QKeyEvent::state(). void Plotter::wheelEvent(QWheelEvent *event) { int numDegrees = event->delta() / 8; int numTicks = numDegrees / 15; if (event->orientation() == Horizontal) zoomStack[curZoom].scroll(numTicks, 0); else zoomStack[curZoom].scroll(0, numTicks); refreshPixmap(); } Событие "wheel" возникает, когда выполняется вращение колесика мыши. Чаще всего встречаются мыши, имеющие только одно колесико -- колесико вертикальной прокрутки, но есть и такие, которые имеют дополнительное колесико горизонтальной прокрутки. Qt поддерживает оба типа колесиков. Событие "wheel" передается виджету, если он владеет фокусом ввода. Функция delta() возвращает угол поворота колесика в восьмых долях градуса. В большинстве случаев, один "шаг" колесика мыши равен 15 градусам.

На этом мы завершаем обзор обработчиков событий и переходим к приватным функциям:

void Plotter::updateRubberBandRegion() { QRect rect = rubberBandRect.normalize(); update(rect.left(), rect.top(), rect.width(), 1); update(rect.left(), rect.top(), 1, rect.height()); update(rect.left(), rect.bottom(), rect.width(), 1); update(rect.right(), rect.top(), 1, rect.height()); } Функция updateRubberBand() вызывается из обработчиков событий mousePressEvent(), mouseMoveEvent() и mouseReleaseEvent(), чтобы стереть и вновь нарисовать рамку области выделения. Она содержит четыре вызова update(), которые ставят в очередь события "paint" для четырех небольших прямоугольников, в которых отображаются стороны рамки.

Использование логической операции NOT (НЕ), при рисовании рамки выделенной области

Один из самых распространенных способов рисование рамки области выделения состоит в использовании логической операции NOT (или XOR), которая замещает значение цвета каждого пикселя рамки на обратное. Ниже приводится альтернативная версия updateRubberBandRegion(), которая использует такую методику рисования:

void Plotter::updateRubberBandRegion() { QPainter painter(this); painter.setRasterOp(NotROP); painter.drawRect(rubberBandRect.normalize()); } Вызовом setRasterOp() задается операция наложения NotROP. В оригинальной версии используется значение по-умолчанию -- CopyROP, которая означает простое копирование нового изображения поверх имеющегося.

Кода функция updateRubberBandRegion() вызывается вторично, для тех же самых координат, то восстанавливается начальное значение цвета пикселей, поскольку вторая логическая операция NOT отменяет действие первой.

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

Еще один из подходов к рисованию рамок -- создание анимированных пунктирных линий. Он часто используется в программах, занимающихся обработкой изображений, поскольку дает хороший контраст не зависимо от начального цвета пикселей, по которым проходит рамка. Для того, чтобы создать анимированную рамку в Qt, вам придется перекрыть обработчик события QObject::timerEvent(), в котором надо будет стирать рамку и опять рисовать ее, при этом всякий раз начинать рисование точек пунктира с новой позиции, что создаст иллюзию движения точек по линии.

void Plotter::refreshPixmap() { pixmap.resize(size()); pixmap.fill(this, 0, 0); QPainter painter(&pixmap, this); drawGrid(&painter); drawCurves(&painter); update(); } Функция refreshPixmap() перерисовывает кривые графиков нв буфере и затем обновляет изображение на экране. Сначала устанавливается размер буфера, чтобы он соответствовал размерам виджета. Затем он заполняется цветом фона, который был установлен в конструкторе, вызовом setBackgroundMode().

Далее создается QPainter и с его помощью в буфере рисуются координатная сетка и кривые. В заключение вызывается update(), которая планирует событие "paint" для всего виджета в целом. Буфер будет скопирован в виджет -- в обработчике события paintEvent().

void Plotter::drawGrid(QPainter *painter) { QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin); PlotSettings settings = zoomStack[curZoom]; QPen quiteDark = colorGroup().dark().light(); QPen light = colorGroup().light(); for (int i = 0; i <= settings.numXTicks; ++i) { int x = rect.left() + (i * (rect.width() - 1) / settings.numXTicks); double label = settings.minX + (i * settings.spanX() / settings.numXTicks); painter->setPen(quiteDark); painter->drawLine(x, rect.top(), x, rect.bottom()); painter->setPen(light); painter->drawLine(x, rect.bottom(), x, rect.bottom() + 5); painter->drawText(x - 50, rect.bottom() + 5, 100, 15, AlignHCenter | AlignTop, QString::number(label)); } for (int j = 0; j <= settings.numYTicks; ++j) { int y = rect.bottom() - (j * (rect.height() - 1) / settings.numYTicks); double label = settings.minY + (j * settings.spanY() / settings.numYTicks); painter->setPen(quiteDark); painter->drawLine(rect.left(), y, rect.right(), y); painter->setPen(light); painter->drawLine(rect.left() - 5, y, rect.left(), y); painter->drawText(rect.left() - Margin, y - 10, Margin - 5, 20, AlignRight | AlignVCenter, QString::number(label)); } painter->drawRect(rect); } Функция drawGrid() рисует координатную сетку, на фоне которой будут отображаться графики. Первый цикл for рисует вертикальные линии сетки и риски на оси OX. Второй -- горизонтальные линии сетки и риски на оси OY. Для рисования числовых значений, напротив рисок, и обозначений осей -- вызывается функция drawText().

Функция drawText() имеет следующий синтаксис:

painter.drawText(x, y, w, h, alignment, text); где (x, y, w, h) задают область рисования, alignment -- выравнивание текста внутри этой области, text -- собственно текст. void Plotter::drawCurves(QPainter *painter) { static const QColor colorForIds[6] = { red, green, blue, cyan, magenta, yellow }; PlotSettings settings = zoomStack[curZoom]; QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin); painter->setClipRect(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2); map<int, CurveData>::const_iterator it = curveMap.begin(); while (it != curveMap.end()) { int id = (*it).first; const CurveData &data = (*it).second; int numPoints = 0; int maxPoints = data.size() / 2; QPointArray points(maxPoints); for (int i = 0; i < maxPoints; ++i) { double dx = data[2 * i] - settings.minX; double dy = data[2 * i + 1] - settings.minY; double x = rect.left() + (dx * (rect.width() - 1) / settings.spanX()); double y = rect.bottom() - (dy * (rect.height() - 1) / settings.spanY()); if (fabs(x) < 32768 && fabs(y) < 32768) { points[numPoints] = QPoint((int)x, (int)y); ++numPoints; } } points.truncate(numPoints); painter->setPen(colorForIds[(uint)id % 6]); painter->drawPolyline(points); ++it; } } Функция drawCurves() рисует кривые графиков поверх координатной сетки. Начинается она с ограничения области рисования, вызовом setClipRect(). QPainter будет игнорировать попытки рисования за ее пределами.

Затем выполняется проход по всем кривым графика и для каждой из них -- по парам координат (x, y). Элемент итератора first дает нам ID (идентификатор) кривой, а second -- массив координат точек кривой.

Вложенный цикл for выполняет преобразование из системы координат графика в систему координат виджета и сохраняет результат в массив points, при условии, что они находятся в разумных пределах.

По окончании выполнения преобразований координат для всех точек кривой, устанавливается цвет "чернил" (используя один из предопределенных цветов) и вызывается drawPolyline(), которая рисует ломаную линию, проходящую через заданные точки.

На этом завершается реализация класса Plotter. И нам остается рассмотреть еще ряд функций-членов класса PlotSettings.

PlotSettings::PlotSettings() { minX = 0.0; maxX = 10.0; numXTicks = 5; minY = 0.0; maxY = 10.0; numYTicks = 5; } Конструктор инициализирует оси координат, с диапазоном измерения от 0 до 10 по каждой из них, и задает количество рисок на каждой из осей, равное 5. void PlotSettings::scroll(int dx, int dy) { double stepX = spanX() / numXTicks; minX += dx * stepX; maxX += dx * stepX; double stepY = spanY() / numYTicks; minY += dy * stepY; maxY += dy * stepY; } Функция scroll() увеличивает (или уменьшает) значения переменных minX, maxX, minY и maxY. Она реализует поддержку скроллинга и вызывается из Plotter::keyPressEvent(). void PlotSettings::adjust() { adjustAxis(minX, maxX, numXTicks); adjustAxis(minY, maxY, numYTicks); } Функция adjust() вызывается из mouseReleaseEvent(). Она округляет значения переменных minX, maxX, minY и maxY до "наилучших" и определяет значения рисок по каждой из осей. Обработка конкретной оси координат выполняется функцией adjustAxis(). void PlotSettings::adjustAxis(double &min, double &max, int &numTicks) { const int MinTicks = 4; double grossStep = (max - min) / MinTicks; double step = pow(10, floor(log10(grossStep))); if (5 * step < grossStep) step *= 5; else if (2 * step < grossStep) step *= 2; numTicks = (int)(ceil(max / step) - floor(min / step)); min = floor(min / step) * step; max = ceil(max / step) * step; } Она округляет аргументы min и max до "наилучших" значений и определяет число рисок (numTicks) на оси, исходя из диапазона [min.. max]. Функция должна изменять фактические параметры (minX, maxX, numXTicks, и т.д.), поэтому они передаются по ссылке, а не по значению.

Большая часть кода функции служит для определения наиболее подходящего "расстояния" между соседними рисками ("шаг"). К выбору шага нужно подходить очень осторожно. Дробные значения шага, например 3.8, сложнее воспринимаются людьми, чем круглые. Для осей, которые имеют метки, записываемые в десятичной нотации, "наилучшими" значениями будут числа 10^n, 2*10^n или 5*10^n.

Поиск начинается с "большого шага", своего рода максимального значения для шага. Затем находится число, ближайшее (меньше или равно) к значению "большого шага", которое можно записать в форме 10^n: берется десятичный логарифм от "большого шага", округляется вниз до ближайшего целого и затем вычисляется степень 10-ти, с найденым числом в качестве показателя. Например, пусть "большой шаг" равен числу 236, в результате получаем: log 236 = 2.37291; округление дает число 2, а 10^2 = 100 -- кандидат для размера "наилучшего" шага.

Как только мы получили значение первого "кандидата" для шага оси, необходимо рассчитать еще два значения -- 2*10^n и 5*10^n. Для примера выше, два других кандидата -- это числа 200 и 500. Но число 500 значительно больше установленного нами максимума (236), а 200 -- меньше, поэтому в качестве шага оси принимается число 200.

Теперь, основываясь на значении шага, очень легко вычислить min, max и numTicks. Значение min получается за счет округления вниз начального min, до ближайшего множителя шага, а значение max -- за счет округления вверх, до ближайшего множителя шага. Величина numTicks -- это количество шагов, укладывающихся в интервал, между min и max. Например, если начальные значения min = 240, max = 1184, то новый диапазон значений оси будет составлять [200..1200], с 5 интервалами-шагами.

Этот алгоритм не всегда дает оптимальные значения. Более изощренный алгоритм вы найдете в статье Пауля Хекберта (Paul S. Heckbert) -- "Nice Numbers for Graph Labels", опубликованной в Graphics Gems (ISBN 0-12-286166-3). Кроме того, в ежеквартальнике Qt Quarterly имеется статья "Fast and Flicker-Free" ( http://doc.trolltech.com/qq/qq06-flicker-free.html), которая рассматривает некоторые идеи по устранению эффекта мерцания.

Эта глава завершает первую часть книги. Здесь мы рассказали как настроить стандартные виджеты Qt и как создать свой виджет, используя в качестве базового класса QWidget. В Главе 2 мы видели, как можно "собрать" виджет из других виджетов, эта тема будет рассматриваться глубже в Главе 6.

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