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.