О ресурре... Главная
The Belarus Internet the Resource about Linux
 
   
Ж. Бланшетт, М. Саммерфильд, "Глава 5. Создание собственных виджетов" - 01/12/2004

Предыдущая глава | Содержание | Следующая глава

Глава 5. Создание собственных виджетов.

В этой главе мы расскажем -- как создаются визуальные компоненты (виджеты) в Qt. Визуальные компоненты могут создаваться путем наследования существующих виджетов Qt или напрямую -- от QWidget. Мы продемонстрируем оба варианта, а так же рассмотрим -- как можно интегрировать свои компоненты в Qt Designer. И в завершение главы представим компонент, который использует прием двойной буферизации для устранения эффекта мерцания во время перерисовки.


5.1. Переделка существующих виджетов Qt.

Иногда возникает необходимость в расширении функциональных возможностей стандартных виджетов. Самое простое решение -- это создать класс потомок от соответствующего виджета Qt и наделить его необходимыми свойствами.

Рисунок 5.1. Виджет HexSpinBox.


В этом разделе мы продемонстрируем виджет шестнадцатиричного счетчика. Стандартный виджет QSpinBox поддерживает только десятичный формат представления чисел, но, за счет создания дочернего класса, его можно "заставить" принимать и обрабатывать шестнадцатиричный формат.
#ifndef HEXSPINBOX_H 
#define HEXSPINBOX_H 

#include <qspinbox.h> 

class HexSpinBox : public QSpinBox {      
public: 
  HexSpinBox(QWidget *parent, const char *name = 0); 
  
protected: 
  QString mapValueToText(int value); 
  int mapTextToValue(bool *ok); 
}; 
#endif
      
Большую часть своих функциональных возможнойстей, виджет HexSpinBox наследует от QSpinBox. Он имеет типичный конструктор и перекрывает две виртуальные функции своего предка. Поскольку класс HexSpinBox не определяет своих собственных сигналов и слотов, то он не нуждается в макроопределении Q_OBJECT.
#include <qvalidator.h> 

#include "hexspinbox.h" 

HexSpinBox::HexSpinBox(QWidget *parent, const char *name) 
    : QSpinBox(parent, name) 
{ 
  QRegExp regExp("[0-9A-Fa-f]+"); 
  setValidator(new QRegExpValidator(regExp, this)); 
  setRange(0, 255); 
}      
      
Пользователь может изменять значение счетчика либо щелкая по кнопкам со стрелками, либо вводя числа в окошко редактора. В последнем случае мы должны ограничить набор допустимых символов шестнадцатиричными цифрами. Для этого используется QRegExpValidator, который пропускает только символы из диапазонов (0..9), (A..F) и (a..f). Дополнительно задается диапазон изменения чисел -- от 0 по 255 (от 0x00 по 0xFF), который больше подходит для шестнадцатиричных чисел, чем диапазон (0..99), устанавливаемый QSpinBox по-умолчанию .
QString HexSpinBox::mapValueToText(int value) 
{ 
  return QString::number(value, 16).upper(); 
}      
      
Функция mapValueToText() преобразует число в строку. Она используется для обновления окошка редактора, когда пользователь изменяет число нажатием на кнопки "вверх" и "вниз". Собственно преобразование выполняется функцией QString::number(), которой вторым аргументом передается число 16 -- основание системы счисления. Она возвращает шестнадцатиричное представление числа с символами в нижнем регистре, а вызов QString::upper() переводит их в верхний регистр.
int HexSpinBox::mapTextToValue(bool *ok) 
{ 
  return text().toInt(ok, 16); 
}      
      
Функция mapTextToValue() выполняет обратное преобразование -- из строки в число. Она вызывается, когда пользователь вводит число с клавиатуры и завершает его нажатием на клавишу Enter. Собственно преобразование выполняется функцией QString::toInt(), которая принимает строку (возвращаемую вызовом QString::toInt()) и число 16 -- основание системы счисления.

Если преобразование было выполнено успешно, то QString::toInt() запишет в аргумент *ok значение true и false -- в противном случае. Это полностью соответствует тому, чего ожидает QSpinBox.

Это собственно все, что мы хотели рассказать о HexSpinBox. Расширение возможностей других виджетов Qt выполняется аналогичным образом: выбирается необходимый виджет, создается класс-потомок и перекрываются некоторые виртуальные функции, изменяющие поведение класса-предка. Это общепринятая в Qt техника программирования. Фактически мы с ней уже сталкивались в Главе 4, когда создавали класс-потомок от QTable и перекрывали методы createEditor() и endEdit().


5.2. Создание класса-потомка от QWidget.

В большинстве своем, нестандартные виджеты образуются за счет комбинирования существующих компонентов, как встроенных виджетов Qt, так и других нестандартных виджетов, таких как HexSpinBox. Нестандартные визуальные компоненты, которые состоят из существующих виджетов, как правило могут разрабатываться в среде Qt Designer. Для этого:

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

Если виджет не имеет собственных сигналов и слотов, и не перекрывает методов родителя, то возможна простая сборка виджета путем аггрегирования существующих виджетов, без создания класса-потомка. Такой подход использовался нами в Главе 1, при создании приложения "Age", когда мы просто "собрали" его из трех компонентов: QHBox, QSpinBox и QSlider. Но даже в этом случае можно было бы породить дочерний класс от QHBox и в его конструкторе создать виджеты QSpinBox и QSlider.

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

С целью демонстрации этого подхода, мы создадим свой виджет IconEditor, который может использоваться в программе редактирования иконок.

Как обычно, начнем с файла заголовка:

#ifndef ICONEDITOR_H 
#define ICONEDITOR_H 

#include <qimage.h> 
#include <qwidget.h> 

class IconEditor : public QWidget 
{ 
  Q_OBJECT 
  Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor) 
  Q_PROPERTY(QImage iconImage READ iconImage WRITE setIconImage) 
  Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor) 
  
public: 
  IconEditor(QWidget *parent = 0, const char *name = 0); 
  void setPenColor(const QColor &newColor); 
  QColor penColor() const { return curColor; } 
  void setZoomFactor(int newZoom); 
  int zoomFactor() const { return zoom; } 
  void setIconImage(const QImage &newImage); const 
  QImage &iconImage() const { return image; } 
  QSize sizeHint() const;      
      
Класс IconEditor использует макрос Q_PROPERTY, для объявления свойств penColor, iconImage и zoomFactor. Каждое из свойств имеет свой тип и функции "чтения" и "записи" ("read" и "write"). Например, свойство penColor имеет тип QColor и функции "чтения"/"записи" -- penColor() и setPenColor(), соответственно.

Рисунок 5.2. Виджет IconEditor.


Когда мы будем работать с виджетом в Qt Designer, то эти свойства появятся в инспекторе свойств, сразу же после свойств, унаследованных от QWidget. Свойства могут иметь любой тип, который поддерживает QVariant. Макроопределение Q_PROPERTY необходимо вставлять в классы, которые определяют свойства.
protected: 
  void mousePressEvent(QMouseEvent *event); 
  void mouseMoveEvent(QMouseEvent *event); 
  void paintEvent(QPaintEvent *event);      

private: 
  void drawImagePixel(QPainter *painter, int i, int j); 
  void setImagePixel(const QPoint &pos, bool opaque); 
  
  QColor curColor; 
  QImage image; 
  int zoom; 
}; 
#endif 
      
Наш виджет перекрывает три защищенные функции своего предка и добавляет несколько приватных функций и переменных. Эти три приватные переменные хранят значения трех свойств, которые были определены чуть выше.

Файл реализации начинается с директив подключения заголовочных файлов и конструктора класса IconEditor:

#include <qpainter.h> 

#include "iconeditor.h" 

IconEditor::IconEditor(QWidget *parent, const char *name) 
    : QWidget(parent, name, WStaticContents) 
{ 
  setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); 
  curColor = black; 
  zoom = 8; 
  image.create(16, 16, 32); 
  image.fill(qRgba(0, 0, 0, 0)); 
  image.setAlphaBuffer(true); 
}
      
В конструкторе имеется ряд моментов, такие как -- вызов setSizePolicy() и передача флага WStaticContents унаследованному конструктору, к которым мы вскоре вернемся.

В переменную zoom записывается число 8. Это означает, что каждый пиксель иконки будет отображаться в виде квадрата 8 X 8. Устанавливается черный цвет "чернил", символ black -- это предопределенная константа в Qt. Сама иконка хранится в переменной image, доступ к которой осуществлен посредством функций setIconImage() и iconImage(). Программа-редактор должна вызывать setIconImage(), когда пользователь открывает файл с иконкой, и iconImage() -- когда пользователь сохраняет иконку в файл.

Переменная image имеет тип QImage. При инициализации мы задаем ей размер 16 X 16 и глубину цвета -- 32 бита, затем очищаем ее и разрешаем альфа-буфер.

Класс QImage хранит изображения в платформо-независимом виде. Глубина цвета может быть выбрана одной из следующих: 1 бит, 8 бит или 32 бита. Изображения с 32-х битной глубиной цвета используют по 8 бит на каждый цветовой канал -- красный, зеленый и синий, для каждого пикселя. Оставшиеся 8 бит определяют значение альфа-составляющей пикселя -- степень прозрачности. Например, пиксель чистого красного цвета должен иметь значения цветовых (красный, зеленый, синий) и альфа каналов -- 255, 0, 0, 255. В Qt этот цвет может быть задан как:

QRgb red = qRgba(255, 0, 0, 255);      
      
или как:
QRgb red = qRgb(255, 0, 0);      
      
Тип QRgb определен как unsigned int, а QRgb() и QRgba() -- это inline-функции, которые составляют 32-х битное значение цвета из своих аргументов. Допустимо определять цвет таким образом:
QRgb red = 0xFFFF0000;      
      
где первая пара символов FF соответствует альфа-составляющей, а вторая пара FF -- красной составляющей цвета. В конструкторе IconEditor мы заполнили QImage прозрачным цветом, т.е. в качестве значения альфа-составляющей указали число 0.

В Qt имеется два типа для хранения значения цвета -- QRgb и QColor. QRgb -- это лишь тип, определенный через typedef, который используется QImage для хранения значения цвета, а QColor -- это полноценный класс, со множеством полезных функций, который широко используется в Qt. В нашем случае, мы будем использовать QRgb, когда будем иметь дело с QImage и QColor во всех остальных случаях, включая свойство penColor.

QSize IconEditor::sizeHint() const 
{ 
  QSize size = zoom * image.size(); 
  if (zoom >= 3) 
    size += QSize(1, 1); 
  return size; 
}
      
Функция sizeHint() перекрывает метод класса-родителя и возвращает "идеальный" размер виджета. Она умножает размер изображения на масштабный коэффициент (zoom). Если масштабный коэффициент больше 3, то добавляется по одному пикселу, в каждой из координатных осей, чтобы имелась возможность разместить координатную сетку (Сетка не отображается, если коэффициент равен 2 или 1).

Идеальный размер виджета главным образом используется в целях размещения компонента на форме. Менеджеры размещения в Qt всегда пытаются выделить виджету тот объем площади на форме, который наиболее близко соответствует идельному размеру виджета.

В дополнение к идеальному размеру, виджет имеет политику изменения размера, которая сообщает менеджеру размещения -- может ли виджет растягиваться или сжиматься. Вызовом setSizePolicy() мы указали политику изменения размеров в обоих направлениях, как QSizePolicy::Minimum. Тем самым, виджет сообщает менеджерам размещения о том, что идеальный размер является минимально возможным или, говоря другими словами, виджет может быть растянут, но никогда не должен сжиматься меньше идеальных размеров. Это поведение может быть изменено в Qt Designer, установкой свойства sizePolicy виджета. Смысл и назначение различных политик управления размерами будут обсуждаться в Главе 6.

void IconEditor::setPenColor(const QColor &newColor) 
{ 
  curColor = newColor; 
}
      
Функция setPenColor() устанавливает текущий цвет "чернил", который используется для "закрашивания" пикселей.
void IconEditor::setIconImage(const QImage &newImage) 
{ 
  if (newImage != image) { 
    image = newImage.convertDepth(32); 
    image.detach(); 
    update(); 
    updateGeometry(); 
  } 
}
      
Функция setIconImage() подготавливает новое изображение к редактированию. Вызов convertDepth() устанвливает глубину цвета равной 32-м битам, поскольку мы везде исходим из предположения, что изображение имеет 32-х битную глубину цвета.

Затем вызывется detach(), для получения полной копии изображения. Это совершенно необходимо, поскольку QImage пытается сэкономить память и время, копируя изображение только в том случае, когда его явно попросят об этом. Такая оптимизация называется явное совместное использование. Она будет подробно обсуждаться в разделе Контейнеры указателей, Главы 11 .

После того, как изображение будет скопировано, мы вызываем QWidget::update(), чтобы перерисовать виджет. Затем вызывается QWidget::updateGeometry(), чтобы сообщить менеджеру размещения о том, что идеальный размер виджета изменился. После чего будет выполнена автоматическая перекомпоновка виджетов, с учетом нового идеального размера.

void IconEditor::setZoomFactor(int newZoom) 
{ 
  if (newZoom < 1) 
    newZoom = 1; 
  if (newZoom != zoom) { 
    zoom = newZoom; 
    update(); 
    updateGeometry(); 
  } 
}
      
Функция setZoomFactor() устанавливает масштабный коэффициент изображения. Для предотвращения деления на ноль, все значения меньше 1 корректируются. Если масштабный коэффициент действительно изменился, то вызываются update() и updateGeometry(), чтобы перерисовать виджет и известить менеджеров размещения об изменении идеального размера.

Функции penColor(), iconImage() и zoomFactor() реализованы в виде inline-функций в файле заголовка.

Теперь перейдем к функции paintEvent(). Это самая важная функция. Она вызывается, когда необходимо перерисовать виджет. Ее реализация в QWidget фактически ничего не делает, оставляя на месте виджета пустое пространство.

Аналогично функциям contextMenuEvent() и closeEvent(), с которыми мы сталкивались в Главе 3, функция paintEvent() является обработчиком события. В Qt, для обработки любого вида события, предусматривается своя функция-обработчик. Обработка событий более подробно будет обсуждаться в Главе 7.

Существует несколько ситуаций, когда возникает событие paint и вызывается paintEvent():

Событие так же порождается в результате вызова QWidget::update() или QWidget::repaint(). Отличия между ними заключаются в том, что repaint() вызывает немедленную перерисовку, а update() просто ставит событие paint в очередь, которая обрабатывается библиотекой Qt. (Обе функции ничего не делают, если виджет невидим на экране.) Если update() вызывается несколько раз, то Qt помещает в очередь только одно событие paint. В виджете IconEditor мы всегда будем использовать только функцию update().
void IconEditor::paintEvent(QPaintEvent *) 
{ 
  QPainter painter(this); 
  
  if (zoom >= 3) { 
    painter.setPen(colorGroup().foreground()); 
    for (int i = 0; i <= image.width(); ++i) 
      painter.drawLine(zoom * i, 0, 
                       zoom * i, zoom * image.height()); 
    for (int j = 0; j <= image.height(); ++j) 
      painter.drawLine(0, zoom * j, 
                       zoom * image.width(), zoom * j); 
  } 
  
  for (int i = 0; i < image.width(); ++i) { 
    for (int j = 0; j < image.height(); ++j) 
      drawImagePixel(&painter, i, j); 
  } 
}      
      
Обработка события начинается с создания объекта QPainter. Если масштабный коэффициент больше 2, то рисуются вертикальная и горизонтальная линии, формирующие сетку, с помощью функции QPainter::drawLine().

Функция QPainter::drawLine() имеет следующий синтаксис вызова:

painter.drawLine(x1, y1, x2, y2);     
      
где (x1, y1) -- это координаты начала, а (x2, y2) -- координаты конца линии. Имеется перегруженная версия этой функции, которая принимает координаты в виде двух QPoint.

Верхний левый пиксель виджета, в Qt, имеет координаты (0, 0), правый нижний пиксель -- (width()-1, height-1). То есть, по сути, обычная Декартова система координат, с небольшим отличием -- ось OY направлена вниз, что имеет определенный смысл при программировании графического интерфейса. Система координат в QPainter может быть подвергнута таким трансформациям, как трансляция, масштабирование, вращение и сдвиг. Более подробно мы обсудим эту тему в Главе 8.

Рисунок 5.3. Пример рисования линии с помощью QPainter.


Прежде чем нарисовать линию, устанавливается цвет "чернил", вызовом setPen(). Можно было бы жестко "зашить" цвет в исходном коде, например black или gray, но лучше использовать палитру виджета.

Любой виджет снабжается своей собственной палитрой цветов, которая определяет -- какой цвет для каких целей используется. Например, в палитре есть запись, которая определяет цвет фона (обычно светло-серый), есть запись, которая определяет цвет текста (обычно черный). Как правило, палитра содержит цвета, соответствующие системной цветовой схеме. Используя палитру виджета, можно быть уверенным, что учитываются цветовые предпочтения пользователя.

Палитра содержит в себе три основные группы цветов: активные, неактивные и запрещенные. Решение о том, какую группу цветов использовать, зависит от текущего состояния виджета:

Функция QWidget::palette() возвращает палитру виджета в виде экземпляра класса QPalette. Доступ к отдельным цветовым группам, имеющим тип QColorGroup, осуществляется через функции active(), inactive() и disabled(). Для удобства, в класс QWidget была введена функция colorGroup(), которая возвращает ту или иную цветовую группу, в зависимости от состояния виджета, благодаря этому, вам довольно редко придется напрямую обращаться к палитре.

Функция paintEvent() завершается перерисовкой самого изображения, вызовом IconEditor::drawImagePixel(), которая отрисовывает каждый пиксель иконки в виде закрашенного квадрата.

void IconEditor::drawImagePixel(QPainter *painter, int i, int j) 
{ 
  QColor color; 
  QRgb rgb = image.pixel(i, j); 
  
  if (qAlpha(rgb) == 0) 
    color = colorGroup().base(); 
  else 
    color.setRgb(rgb); 
  
  if (zoom >= 3) { 
    painter->fillRect(zoom * i + 1, zoom * j + 1, 
                      zoom - 1, zoom - 1, color); 
  } else { 
    painter->fillRect(zoom * i, zoom * j, 
                      zoom, zoom, color); 
  } 
}
      
Функция drawImagePixel() рисует пиксели средствами QPainter, с учетом масштабного коэффициента. Параметры i и j -- это координаты пикселя в системе координат QImage, но не в системе координат виджета (если масштабный коэффициент равен 1, то эти две системы координат полностью совпадают). Если пиксель прозрачен (альфа-составляющая равна 0), то для рисования пикселя используется цвет "base" текущей группы (обычно -- белый). В противном случае -- используется цвет пикселя в QImage. Затем вызывается QPainter::fillRect(), которая рисует закрашенный квадрат. Если поверх изображения рисуется координатная сетка, то размер квадрата уменьшается на 1 по обеим осям.

Рисунок 5.4. Пример рисования прямоугольника с помощью QPainter.


Функция QPainter::fillRect() имеет следующий синтаксис:
painter->fillRect(x, y, w, h, brush);      
      
где (x, y) -- координаты левого верхнего угла прямоугольника, w x h -- его размеры, а brush задает цвет заполнения и шаблон заполнения. Передавая QColor, в качестве аргумента brush, мы задаем сплошной режим закрашивания.
void IconEditor::mousePressEvent(QMouseEvent *event) 
{ 
  if (event->button() == LeftButton) 
    setImagePixel(event->pos(), true); 
  else if (event->button() == RightButton)
    setImagePixel(event->pos(), false); 
}
      
Когда пользователь нажимает кнопку мыши, система генерирует событие "mouse press". За счет перекрытия метода родителя QWidget::mousePressEvent(), мы получаем возможность перехватывать и обрабатывать это событие, закрашивая или очищая пиксель в изображении, находящийся под указателем мыши.

Когда пользователь щелкает левой кнопкой мыши, вызывается приватная функция setImagePixel() с аргументом true, сообщая о том, что пиксель должен быть закрашен текущим цветом "чернил". Если пользователь щелкает правой кнопкой мыши, то в функцию setImagePixel() передается аргумент false и пиксель очищается.

void IconEditor::mouseMoveEvent(QMouseEvent *event) 
{ 
  if (event->state() & LeftButton) 
    setImagePixel(event->pos(), true); 
  else if (event->state() & RightButton) 
    setImagePixel(event->pos(), false); 
}
      
Функция mouseMoveEvent() обрабатывает событие "mouse move" (перемещение указателя мыши). По-умолчанию это событие возникает только в том случае, когда пользователь перемещает указатель мыши при нажатой, и удерживаемой в нажатом состоянии, кнопке. Но имеется возможность изменить это поведение, вызовом QWidget::setMouseTracking(), однако в данном примере нам этого не требуется. Аналогично предыдущему обработчику, в зависимости от того, какая кнопка мыши нажата, пиксели либо закрашиваются, либо очищаются. Поскольку возможна ситуация, когда пользователь нажал и удерживает сразу две кнопки -- значение, возвращаемое QMouseEvent::state(), представляет собой битовую карту, в которой каждой из кнопок мыши соответствует свой бит (в этой карте так же есть биты, определяющие состояние клавиш Shift и Ctrl на клавиатуре). Проверка факта нажатия на ту или иную клавишу, выполняется с помощью оператора &. Если клавиша нажата, то вызывается setImagePixel()..
void IconEditor::setImagePixel(const QPoint &pos, bool opaque) 
{ 
  int i = pos.x() / zoom; 
  int j = pos.y() / zoom; 
  
  if (image.rect().contains(i, j)) { 
    if (opaque) 
      image.setPixel(i, j, penColor().rgb()); 
    else 
      image.setPixel(i, j, qRgba(0, 0, 0, 0)); 
    
    QPainter painter(this); 
    drawImagePixel(&painter, i, j); 
  } 
}
      
Функция setImagePixel() вызывается из обработчиков mousePressEvent() и mouseMoveEvent() для закрашивания или очистки пикселя. Параметр pos определяет позицию указателя мыши в системе координат виджета.

На первом этапе выполняется переход от системы координат виджета к системе координат изображения. Переход осуществляется делением координат указателя мыши x и y на коэффициент масштабирования. Затем проверяется -- находятся ли координаты точки в допустимом диапазоне. Проверка выполняется с помощью QImage::rect() и QRect::contains(), которые проверяют попадание i в диапазон 0..image.width()-1 и попадание j в диапазон 0..image.height()-1.

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

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

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

Рисунок 5.5. Изменение размеров виджета, созданного с флагом WStaticContents.


На этом, работу по созданию виджета IconEditor можно считать законченной. Используя знания, полученные в первых главах книги, вы без труда напишете код, который будет использовать IconEditor в качестве центрального виджета в QMainWindow, как подчиненный виджет внутри области компоновки или внутри QScrollView. В следующем разделе мы покажем, как интегрировать его в Qt Designer.


5.3. Интеграция виджета в Qt Designer.

Прежде, чем мы сможем использовать наш виджет в Qt Designer, мы должны известить его об этом. Существует два подхода: подключение как "простого виджета" и как плагина.

Методика "простого виджета" заключается в заполнении полей диалога Qt Designer. После этого виджет может вставляться в формы, разрабатываемые в среде Qt Designer, но отображаться на форме, во время редактирования и предварительного просмотра, он будет в виде черного прямоугольника. Ниже приводится последовательность действий по интеграции HexSpinBox таким способом:

  1. Выберите пункт меню Tools|Custom|Edit Custom Widget. Перед вами появится диалоговое окно "Edit Custom Widgets".

  2. Щелкните по кнопке "New Widget".

  3. Измените имя класса MyCustomWidget на HexSpinBox и имя заголовочного файла mycustomwidget.h на hexspinbox.h.

  4. Измените "Size Hint" на (60, 20).

  5. Измените "Size Policy" на (Minimum, Fixed).

После этого виджет появится в секции "Custom Widgets" в палитре компонентов Qt Designer.

Рисунок 5.6. Диалог "Edit Custom Widgets".


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

Прежде всего, необходимо создать класс-потомок от QWidgetPlugin и перекрыть некоторые виртуальные функции. Весь код можно разместить в тех же самых файлах с исходными текстами, но мы создадим файлы плагина отдельно. Допустим, что файлы, с исходным кодом плагина, находятся в каталоге iconeditorplugin, а с исходным кодом самого компонента -- в параллельном каталоге iconeditor.

Заголовочный файл плагина:

#include <qwidgetplugin.h> 

#include "../iconeditor/iconeditor.h" 

class IconEditorPlugin : public QWidgetPlugin 
{ 
public: 
  QStringList keys() const; 
  QWidget *create(const QString &key, QWidget *parent, 
                  const char *name);      
  QString includeFile(const QString &key) const; 
  QString group(const QString &key) const; 
  QIconSet iconSet(const QString &key) const; 
  QString toolTip(const QString &key) const; 
  QString whatsThis(const QString &key) const; 
  bool isContainer(const QString &key) const; 
};                  
      
Класс IconEditorPlugin является своего рода "фабрикой", которая изготавливает и выпускает экземпляры виджета IconEditor. Функции плагина используются средой Qt Designer для создания экземпляров класса и получения необходимой информации.
QStringList IconEditorPlugin::keys() const 
{ 
  return QStringList() << "IconEditor"; 
}      
      
Функция keys() возвращает список виджетов, "выпускаемых" плагином-фабрикой. Наш плагин "выпускает" только один виджет -- IconEditor.
QWidget *IconEditorPlugin::create(const QString &, QWidget *parent, 
                                  const char *name) 
{ 
  return new IconEditor(parent, name); 
}      
      
Функцию create() вызывает Qt Designer, когда необходимо создать экземпляр виджета. Первый аргумент -- имя класса виджета. В данном примере мы можем игнорировать его, поскольку наш плагин обслуживает только один класс. Все остальные функции так же получают имя класса в первом аргументе.
QString IconEditorPlugin::includeFile(const QString &) const 
{ 
  return "iconeditor.h"; 
}      
      
Функция includeFile() возвращает имя заголовочного файла виджета, который представляет плагин. Имя файла заголовка подключается к коду, создаваемому утилитой uic.
bool IconEditorPlugin::isContainer(const QString &) const 
{ 
  return false; 
}      
      
Функция isContainer() возвращает true, если виджет может содержать в себе другие виджеты, иначе -- false. Например, QFrame может содержать в себе другие виджеты. В нашем случае возвращается false, поскольку нет смысла делать из IconEditor контейнер для других виджетов. Строго говоря, любой виджет может быть площадкой для размещения других виджетов, но Qt Designer отвергает такую возможность, если isContainer() возвращает false.
QString IconEditorPlugin::group(const QString &) const 
{ 
  return "Plugin Widgets"; 
}      
      
Функция group() возвращает имя секции палитры компонентов, в которой будет размещен виджет. Если такой секции пока нет, она будет создана автоматически.
QIconSet IconEditorPlugin::iconSet(const QString &) const 
{ 
  return QIconSet(QPixmap::fromMimeSource("iconeditor.png")); 
}      
      
Функция iconSet() возвращает иконку для палитры компонентов.
QString IconEditorPlugin::toolTip(const QString &) const 
{ 
  return "Icon Editor"; 
}      
      
Функция toolTip() возвращает текст подсказки, которая появляется при наведении указателя мыши на иконку виджета в палитре компонентов.
QString IconEditorPlugin::whatsThis(const QString &) const 
{ 
  return "Widget for creating and editing icons"; 
}
      
Функция whatsThis() возвращает текст, который появляется по запросу "What's This?" Qt Designer-а.
Q_EXPORT_PLUGIN(IconEditorPlugin)      
      
Файл с исходным текстом плагина должен завершаться вызовом макроса Q_EXPORT_PLUGIN().

Файл .pro для сборки плагина выглядит примерно так:

TEMPLATE   = lib 
CONFIG    += plugin 
HEADERS    = ../iconeditor/iconeditor.h 
SOURCES    = iconeditorplugin.cpp \ 
             ../iconeditor/iconeditor.cpp 
IMAGES     = images/iconeditor.png 
DESTDIR    = $(QTDIR)/plugins/designer      
      
Предполагается, что переменная окружения QTDIR содержит путь к каталогу, куда была установлена библиотека Qt. Когда вы собираете плагин командой make или nmake, он автоматически устанавливается в каталог plugins Qt Designer-а.

После сборки плагина вы можете использовать IconEditor в Qt Designer точно так же, как встроенные виджеты Qt.


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, что позволит нам использовать всю мощь этой замечательной библиотеки.


Предыдущая глава | Содержание | Следующая глава


Ж. Бланшетт, М. Саммерфильд, "Глава 5. Создание собственных виджетов" - 01/12/2004

г.ВИТЕБСК
РЕСПУБЛИКА БЕЛАРУСЬ

 
  О ресурре... Главная
[ ГЛАВНАЯ ] [ СОДЕРЖАНИЕ ]

E-mail : БИРЛ "Ombrello".

Hosted by uCoz