Qt: рисование по мотивам векторной графики
Qt предоставляет программисту очень богатые возможности, однако набор виджетов ограничен. Если ничего из имеющегося в наличии не подходит, приходится рисовать что-то свое. Простейший способ — использовать готовые картинки — имеет серьезные недостатки: необходимость хранения изображений в файле или ресурсах, проблемы с масштабируемостью, с переносимостью форматов изображений. Ниже описывается вариант использования принципов векторной графики без использования собственно векторных изображений.
Преамбула
Началось все с того, что понадобилась однажды индикация одноразрядных признаков. Некоторое приложение получает по некоторому порту некоторые данные, пакет надо разобрать и отобразить на экране. Хорошо бы при этом как-то имитировать привычную приборную лицевую панель. Для отображения цифровых данных Qt предлагает «из коробки» класс QLCDNumber, похожий на знакомые семисегментные индикаторы, а вот одиночных лампочек что-то не видно.
Использование флажков (они же check boxes) и переключателей (они же radio buttons) для этих целей плохо, и вот список причин:
- Это неправильно семантически. Кнопки — они и есть кнопки, и предназначены для ввода пользователем, а не для показа ему чего-либо.
- Отсюда вытекает второе: пользователь так и норовит тыкнуть в такие кнопки. Если при этом обновление информации не особенно быстрое, индикация будет врать, а пользователь — сообщать о неправильной работе программы, мерзко хихикая.
- Если заблокировать кнопку для нажатия (setEnabled (false)), то она становится некрасиво серой. Помнится, в Delphi, в районе версии 6, был такой финт ушами: можно было положить флажок на панель и отключить доступность панели, а не флажка, тогда флажок не был ни серым, ни активным. Тут такой фокус не проходит.
- Кнопки имеют фокус ввода. Соответственно, если в окне есть элементы ввода, и пользователь гуляет по ним с помощью клавиши «Tab», ему придется погулять и по элементам вывода, это неудобно и некрасиво.
- В конце концов, такие кнопки просто неэстетично смотрятся, особенно рядом с семисегментниками.
Вывод: надо рисовать лампочку самому.
Муки выбора
Сначала поискал готовые решения. В ту далекую пору, когда использовал Delphi, можно было найти просто гигантское количество готовых компонентов, как от серьезных фирм, так и любительского изготовления. В Qt с этим напряженка. У QWT есть кое-какие элементы, но не то. Любительщины вообще не видел. Наверное, если грамотно рыть на Github`е, то можно что-то найти, но я, пожалуй, быстрее сам сделаю.
Первое, что напрашивалось из самодельного — использовать два файла-картинки с изображениями включенной и выключенной лампочки. Плохо:
- Надо найти хорошие картинки (или нарисовать, но художник я никакой);
- Принципиальный вопрос: тырить нехорошо, даже картинки, даже валяющиеся под ногами;
- Надо их хранить где-то. В файлах совсем плохо: случайно сотрется — и нету кнопок. В ресурсах получше, но тоже не хочется, если можно обойтись;
- Масштабируемость никакая;
- Настраиваемость (цвета, например) достигается только добавлением файлов. То есть, ресурсоемко и негибко.
Второе, что вытекает из первого — вместо картинок использовать векторные изображения. Тем более, что Qt умеет рендерить SVG. Тут уже чуть проще с поиском собственно изображения: в сети много уроков по векторной графике, можно найти что-то более-менее подходящее и адаптировать под свои нужды. Но остается вопрос по хранению и настраиваемости, да и рендеринг не бесплатен по ресурсам. Копейки, конечно, но все же…
И третье вытекает из второго: можно же воспользоваться принципами векторной графики при самостоятельной прорисовке изображения! Файл векторной картинки в текстовом виде указывает, что и как рисовать. Я могу кодом указать то же самое, используя векторные туториалы. Благо, у объекта QPainter имеются в наличии необходимые инструменты: перо, кисть, градиент и рисование примитивов, даже заливка текстурой. Да, инструменты далеко не все: нет масок, режимов наложения, но совсем уж фотореалистичности не требуется.
Поискал немного примеры в сети. Взял первый попавшийся урок: «Рисуем кнопку в графическом редакторе Inkscape» с сайта «Рисовать легко». Кнопка из этого урока гораздо больше похожа на лампочку, чем на кнопку, что меня вполне устраивает. Делаю заготовку: вместо Inkscape — проект в Qt.
Проба пера
Создаю новый проект. Выбираю название проекта rgbled (потому что хочу сделать что-то вроде RGB-светодиода) и путь к нему. Выбираю базовый класс QWidget и название RgbLed, отказываюсь создавать файл формы. Проект по умолчанию после запуска делает пустое окно, оно пока неинтересное.
Подготовка к рисованию
Заготовка есть. Теперь надо завести закрытые члены класса, которые будут определять геометрию рисунка. Существенным плюсом векторной графики является ее масштабируемость, поэтому константных чисел должно быть по минимуму, и те лишь задавать пропорции. Размеры будут пересчитываться в событии resizeEvent (), которое надо будет переопределить.
В используемом уроке по рисованию размеры задаются в пикселах по ходу действия. Мне же нужно заранее определить, что я буду использовать и как пересчитывать.
Рисуемая картинка состоит из таких элементов:
- внешнее кольцо (с наклоном наружу, часть выпуклого ободка)
- внутреннее кольцо (с наклоном внутрь)
- корпус лампочки-светодиода, «стекло»
- тень по краю стекла
- верхний блик
- нижний блик
Концентрические круги, то есть, всё, кроме бликов, определяется позицией центра и радиусом. Блики определяются центром, шириной и высотой, причем позиция X центров бликов совпадает с позицией X центра всего рисунка.
Для расчетов элементов геометрии понадобится определить, что больше — ширина или высота, потому что лампочка круглая и должна вписываться в квадрат со стороной, равной меньшему из двух измерений. Итак, добавляю соответствующие закрытые члены в заголовочный файл.
private:
int height;
int width;
int minDim;
int half;
int centerX;
int centerY;
QRect drawingRect;
int outerBorderWidth;
int innerBorderWidth;
int outerBorderRadius;
int innerBorderRadius;
int topReflexY;
int bottomReflexY;
int topReflexWidth;
int topReflexHeight;
int bottomReflexWidth;
int bottomReflexHeight;
Затем переопределяю защищенную функцию, вызываемую при изменении размеров виджета.
protected:
void resizeEvent(QResizeEvent *event);
void RgbLed::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
this->height = this->size().height();
this->width = this->size().width();
this->minDim = (height > width) ? width : height;
this->half = minDim / 2;
this->centerX = width / 2;
this->centerY = height / 2;
this->outerBorderWidth = minDim / 10;
this->innerBorderWidth = minDim / 14;
this->outerBorderRadius = half - outerBorderWidth;
this->innerBorderRadius = half - (outerBorderWidth + innerBorderWidth);
this->topReflexY = centerY
- (half - outerBorderWidth - innerBorderWidth) / 2;
this->bottomReflexY = centerY
+ (half - outerBorderWidth - innerBorderWidth) / 2;
this->topReflexHeight = half / 5;
this->topReflexWidth = half / 3;
this->bottomReflexHeight = half / 5;
this->bottomReflexWidth = half / 3;
drawingRect.setTop((height - minDim) / 2);
drawingRect.setLeft((width - minDim) / 2);
drawingRect.setHeight(minDim);
drawingRect.setWidth(minDim);
}
Здесь вычисляется сторона квадрата, в который вписана лампочка, центр этого квадрата, радиус ободка, занимающего максимально возможную площадь, ширина ободка, внешняя часть которого пусть будет 1/10 от диаметра, а внутренняя — 1/14. Затем вычисляется положение бликов, которые находятся в серединах верхнего и нижнего радиусов, ширина и высота подбираются на глазок.
Кроме того, в защищенные поля сразу добавлю набор цветов, которые будут использоваться.
QColor ledColor;
QColor lightColor;
QColor shadowColor;
QColor ringShadowDarkColor;
QColor ringShadowMedColor;
QColor ringShadowLightColor;
QColor topReflexUpColor;
QColor topReflexDownColor;
QColor bottomReflexCenterColor;
QColor bottomReflexSideColor;
По названиям примерно понятно, что это цвета лампочки, светлой части тени, темной части тени, три цвета кольцевой тени вокруг лампочки и цвета градиентов бликов.
Цвета надо бы инициализировать, поэтому дополню заготовку конструктора.
RgbLed::RgbLed(QWidget *parent) : QWidget(parent),
ledColor(Qt::green),
lightColor(QColor(0xE0, 0xE0, 0xE0)),
shadowColor(QColor(0x70, 0x70, 0x70)),
ringShadowDarkColor(QColor(0x50, 0x50, 0x50, 0xFF)),
ringShadowMedColor(QColor(0x50, 0x50, 0x50, 0x20)),
ringShadowLightColor(QColor(0xEE, 0xEE, 0xEE, 0x00)),
topReflexUpColor(QColor(0xFF, 0xFF, 0xFF, 0xA0)),
topReflexDownColor(QColor(0xFF, 0xFF, 0xFF, 0x00)),
bottomReflexCenterColor(QColor(0xFF, 0xFF, 0xFF, 0x00)),
bottomReflexSideColor(QColor(0xFF, 0xFF, 0xFF, 0x70))
{
}
Еще надо не забыть вставить в заголовочный файл инклуды классов, которые понадобятся при рисовании.
#include
#include
#include
#include
#include
Этот код компилируется успешно, но в окне виджета ничего не изменилось. Пора начинать рисовать.
Рисование
Ввожу закрытую функцию
void drawLed(const QColor &color);
и переопределяю защищенную функцию
void paintEvent(QPaintEvent *event);
Событие перерисовки будет вызывать собственно рисование, которому в качестве параметра передается цвет «стекла».
void RgbLed::paintEvent(QPaintEvent *event)
{
QWidget::paintEvent(event);
this->drawLed(ledColor);
}
Пока так. А функцию рисования начинаем понемногу заполнять.
void RgbLed::drawLed(const QColor &color)
{
QPainter p(this);
QPen pen;
pen.setStyle(Qt::NoPen);
p.setPen(pen);
}
Сперва создается объект-художник, который и будет заниматься рисованием. Затем создается карандаш, который нужен для того, чтобы карандаша не было: в данном изображении обводка по контуру не просто не нужна, а вообще не нужна.
Затем рисуется первый круг в примерном соответствии с уроком по векторной графике: большой круг, залитый радиальным градиентом. У градиента светлая опорная точка вверху, но не на самом краю, а темная — внизу, но тоже не на самом краю. На основе градиента создается кисть, этой кистью художник painter закрашивает круг (то есть, эллипс, вписанный в квадрат). Получается такой код
QRadialGradient outerRingGradient(QPoint(centerX,
centerY - outerBorderRadius - (outerBorderWidth / 2)),
minDim - (outerBorderWidth / 2));
outerRingGradient.setColorAt(0, lightColor);
outerRingGradient.setColorAt(1, shadowColor);
QBrush outerRingBrush(outerRingGradient);
p.setBrush(outerRingBrush);
p.drawEllipse(this->drawingRect);
qDebug() << "draw";
Среда подчеркивает параметр color функции drawLed, потому что он не используется. Пусть потерпит, он пока не нужен, но скоро понадобится. Запущенный проект выдает такой результат:
Добавляем еще порцию кода.
QRadialGradient innerRingGradient(QPoint(centerX,
centerY + innerBorderRadius + (innerBorderWidth / 2)),
minDim - (innerBorderWidth / 2));
innerRingGradient.setColorAt(0, lightColor);
innerRingGradient.setColorAt(1, shadowColor);
QBrush innerRingBrush(innerRingGradient);
p.setBrush(innerRingBrush);
p.drawEllipse(QPoint(centerX, centerY),
outerBorderRadius, outerBorderRadius);
Почти тот же самый круг, только меньше размером и вверх ногами. Получаем такую картинку:
Дальше наконец-то понадобится цвет стекла:
QColor dark(color.darker(120));
QRadialGradient glassGradient(QPoint(centerX, centerY),
innerBorderRadius);
glassGradient.setColorAt(0, color);
glassGradient.setColorAt(1, dark);
QBrush glassBrush(glassGradient);
p.setBrush(glassBrush);
p.drawEllipse(QPoint(centerX, centerY),
innerBorderRadius,
innerBorderRadius);
Здесь при помощи функции darker из переданного цвета получается такой же цвет, но потемнее, для организации градиента. Коэффициент 120 подобран на глазок. Вот результат:
Добавляю кольцевую тень вокруг стекла. Так сделано в уроке по векторной графике, и это должно добавить объему и реалистичности:
QRadialGradient shadowGradient(QPoint(centerX, centerY),
innerBorderRadius);
shadowGradient.setColorAt(0, ringShadowLightColor);
shadowGradient.setColorAt(0.85, ringShadowMedColor);
shadowGradient.setColorAt(1, ringShadowDarkColor);
QBrush shadowBrush(shadowGradient);
p.setBrush(shadowBrush);
p.drawEllipse(QPoint(centerX, centerY),
innerBorderRadius,
innerBorderRadius);
Тут градиент трехступенчатый, чтобы тень была гуще к краю и бледнела к центру. Получается так:
Добавляю блики, сразу оба. Верхний блик в отличие от нижнего (и всех остальных элементов) сделан линейным градиентом. Художник из меня так себе, поверю на слово автору урока. Возможно, в этом есть какая-то правда, экспериментировать с разными видами градиентов не буду.
QLinearGradient topTeflexGradient(QPoint(centerX,
(innerBorderWidth + outerBorderWidth)),
QPoint(centerX, centerY));
topTeflexGradient.setColorAt(0, topReflexUpColor);
topTeflexGradient.setColorAt(1, topReflexDownColor);
QBrush topReflexbrush(topTeflexGradient);
p.setBrush(topReflexbrush);
p.drawEllipse(QPoint(centerX, topReflexY), topReflexWidth, topReflexHeight);
QRadialGradient bottomReflexGradient(QPoint(centerX,
bottomReflexY + (bottomReflexHeight / 2)),
bottomReflexWidth);
bottomReflexGradient.setColorAt(0, bottomReflexSideColor);
bottomReflexGradient.setColorAt(1, bottomReflexCenterColor);
QBrush bottomReflexBrush(bottomReflexGradient);
p.setBrush(bottomReflexBrush);
p.drawEllipse(QPoint(centerX, bottomReflexY),
bottomReflexWidth,
bottomReflexHeight);
Вот, собственно, и все, готовая лампочка, как на КДПВ.
На заметность бликов и выпуклости стекла влияет цвет, точнее, то, насколько он темный. Возможно, имеет смысл добавить регулировку яркости бликов и коэффициента затемнения в функции darker в зависимости от темности, но это уже перфекционизм, я считаю.
Ниже — пример использования в окне программы.
Баловство
Для интереса можно поиграться с цветами. Например, переопределив защищенное событие клацанья мыши
void mousePressEvent(QMouseEvent *event);
таким образом:
void RgbLed::mousePressEvent(QMouseEvent *event)
{
static int count = 0;
if (event->button() == Qt::LeftButton) {
switch (count) {
case 0:
ledColor = Qt::red;
count++;
break;
case 1:
ledColor = Qt::green;
count++;
break;
case 2:
ledColor = Qt::blue;
count++;
break;
case 3:
ledColor = Qt::gray;
count++;
break;
default:
ledColor = QColor(220, 30, 200);
count = 0;
break;
}
this->repaint();
}
QWidget::mousePressEvent(event);
}
не забыв добавить мышиные события в заголовок:
#include
Теперь щелчок мыши по компоненту будет переключать цвет лампочки: красный, зеленый, синий, серый и какой-то от фонаря наугад подобранный.
Эпилог
Что касается рисования, то на этом все. А виджету следует добавить функциональности. В моем случае было добавлено булево поле «использовать ли состояние», еще одно булево поле, определяющее состояние «Вкл» или «Выкл» и цвета по умолчанию для этих состояний, а также открытые геттеры и сеттеры для всего этого. Эти поля используются в функции paintEvent () для выбора цвета, передаваемого drawLed () в виде параметра. В результате можно отключить использование состояний и задавать «лампочке» любой цвет, а можно включить состояния и зажигать или гасить лампочку по событиям. Особенно удобно сделать сеттер состояния открытым слотом и соединить его с сигналом, который надо отслеживать.
Использование mousePressEvent демонстрирует, что виджет можно сделать не только индикатором, но и кнопкой, делая ее нажатой, отпущенной, гнутой, скрученной, раскрашенной и какой хотите еще по событиям наведения, нажатия и отпускания.
Но это уже не принципиально. Целью было показать, где можно взять образцы для подражания при прорисовке собственных виджетов и как эту прорисовку несложно реализовать без использования картинок растровых или векторных, в ресурсах или файлах.