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 <QPainter>
#include <QPen>
#include <QBrush>
#include <QColor>
#include <QGradient>
Этот код компилируется успешно, но в окне виджета ничего не изменилось. Пора начинать рисовать.
Рисование
Ввожу закрытую функцию
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 <QMouseEvent>
Теперь щелчок мыши по компоненту будет переключать цвет лампочки: красный, зеленый, синий, серый и какой-то от фонаря наугад подобранный.
Эпилог
Что касается рисования, то на этом все. А виджету следует добавить функциональности. В моем случае было добавлено булево поле «использовать ли состояние»", еще одно булево поле, определяющее состояние «Вкл» или «Выкл» и цвета по умолчанию для этих состояний, а также открытые геттеры и сеттеры для всего этого. Эти поля используются в функции paintEvent() для выбора цвета, передаваемого drawLed() в виде параметра. В результате можно отключить использование состояний и задавать «лампочке» любой цвет, а можно включить состояния и зажигать или гасить лампочку по событиям. Особенно удобно сделать сеттер состояния открытым слотом и соединить его с сигналом, который надо отслеживать.
Использование mousePressEvent демонстрирует, что виджет можно сделать не только индикатором, но и кнопкой, делая ее нажатой, отпущенной, гнутой, скрученной, раскрашенной и какой хотите еще по событиям наведения, нажатия и отпускания.
Но это уже не принципиально. Целью было показать, где можно взять образцы для подражания при прорисовке собственных виджетов и как эту прорисовку несложно реализовать без использования картинок растровых или векторных, в ресурсах или файлах.
Комментарии (13)
Sazonov
09.10.2018 10:15Спасибо, конечно, за статью, но кроме обычного рисования на Qt я тут ничего не увидел. Стоит хотя-бы вынести большинство констант в properties, чтобы можно было настраивать поведение через те же стили. А ещё лучше сделать все тоже самое средствами QtQuick, если нужна интерактивность. Там даже параллакс можно достаточно дёшево прикрутить.
Для эффективного рисования таких вещей всё-таки намного проще использовать картинки + qss + QToolButton (это на случай, если всякие hover эффекты хочется из коробки). В крайнем случае, если нужно эффективное масштабирование, можно взять svg, оверхеда практически не будет. Еще режут глаза статические переменные внутри методов.RomeoGolf Автор
09.10.2018 17:13Кроме рисования на Qt заголовок ничего и не обещал :-)
Нарисовать такую кнопку у меня получилось довольно быстро, около часа на всё, причем, следующий подобный виджет пошел еще быстрее. Найти описание рисования кнопки у меня вышло быстрее, чем найти готовые приличные картинки «бесплатно, без регистрации и СМС» — это к вопросу о SVG и готовых картинках. Стремиться к идеалу надо, конечно, другими путями, тут вариант «просто, но симпатично».Sazonov
09.10.2018 17:41+1Ну да, тут вопрос в масштабах проекта. Если человек 20-30 его делают, то резко возрастает вероятность появления кучи велосипедов с перекрывающийся функциональностью. Я с таким сталкивался. Поэтому считаю, что если вы рассчитываете на реюзабельность виджета, то нужно делать его максимально продуманным и гибким. И крайне желательно, чтобы он реализовывал стандартные механизмы Qt по кастомизации, а именно поддержку стилей.
По поводу картинок — погуглите по запросу “fatcow icons”. Практически полностью покрывает потребности многих проектов.RomeoGolf Автор
09.10.2018 18:07За наводку на иконки — гран мерси, премного благодарен. Хотя по возможности предпочитаю рисовать что-то свое (если время остается).
andi123
09.10.2018 13:24Я конечно понимаю, что тут про принцип и подход к реализации.
Но если кому-то нужен светодиод без лишних велосипедов, то например в кедах есть /usr/include/kled.h.
Причем я его использовал еще как минимум 12 лет назад.RomeoGolf Автор
09.10.2018 17:17Кеды — это очень хорошо. ХРюша — похуже, но выбора нет. Под винду ничего приличного не нашел, оказалось быстрее сделать самому.
A1ien
09.10.2018 17:45А почему не использовать QML?
RomeoGolf Автор
09.10.2018 18:15Честно сказать, он мне чисто субъективно не нравится. Предпочитаю обходиться без.
A1ien
09.10.2018 20:56Полная аналогия с WPF, декларативная векторная графика, с моделью событий, и декларативной анимацией, оооочень сильная штука. На императивной модели создаия интерфейса и близко такого не сделать. Ваша задача решается там практически без кода.
Serge78rus
Красиво, но не очень реалистично. На реальном горящем светодиоде блики от внешнего освещения не видны. То есть блики нужны только для отображения выключенного состояния.
RomeoGolf Автор
Согласен. Причем, как я упомянул в опусе, заметность блика сильно зависит от яркости, блики автоматически «гаснут» на «зажженном» варианте. Причем, этот эффект можно несложно регулировать одновременно с цветом. И я не очень стремился к фотореалистичности.