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 <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 демонстрирует, что виджет можно сделать не только индикатором, но и кнопкой, делая ее нажатой, отпущенной, гнутой, скрученной, раскрашенной и какой хотите еще по событиям наведения, нажатия и отпускания.

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


ссылка на оригинал статьи https://habr.com/post/425547/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *