Qt: малювання за мотивами векторної графіки

Qt надає програмісту дуже багаті можливості, проте набір віджетів обмежений. Якщо нічого з наявного в наявності не підходить, доводиться малювати щось своє. Найпростіший спосіб — використовувати готові картинки — має серйозні недоліки: необхідність зберігання зображень у файлі або ресурси, проблеми з масштабністю, з переносимістю форматів зображень. Нижче описується варіант використання принципів векторної графіки без використання власне векторних зображень.

 

Преамбула

 

Почалося все з того, що знадобилася одного разу індикація одноразрядних ознак. Деякий додаток отримує з деякого порту деякі дані, пакет треба розібрати і відобразити на екрані. Добре б при цьому якось імітувати звичну приладову лицьову панель. Для відображення цифрових даних Qt пропонує «з коробки» клас QLCDNumber, схожий на знайомі семисегментні індикатори, а ось одиночних лампочок щось не видно.

Використання прапорців (вони ж check boxes) і перемикачів (вони ж radio buttons) для цих цілей погано, і ось список причин:

  • Це неправильно семантично. Кнопки — вони і є кнопки, призначені для введення користувачем, а не для показу йому чого-небудь.
  • Звідси випливає друге: користувач так і норовить тицьнути в такі кнопки. Якщо при цьому оновлення інформації не особливо швидке, індикація буде брехати, а користувач — повідомляти про неправильної роботи програми, бридко хихикаючи.
  • Якщо заблокувати кнопку для натискання (setEnabled(false), то вона стає негарно сіркою. Пам’ятається, в Delphi, в районі версії 6, був такий фінт вухами: можна було покласти прапорець на панель і відключити доступність панелі, а не прапорця, тоді прапорець не був сірим, ні активним. Тут такий фокус не проходить.
  • Кнопки мають фокус вводу. Відповідно, якщо у вікні є елементи введення, і користувач гуляє з ним за допомогою клавіші «Tab», йому доведеться погуляти і по елементам висновку, це незручно і негарно.
  • Зрештою, такі кнопки просто неестетично виглядають, особливо поруч з семисегментниками.

 

Висновок: треба малювати лампочку самому.

 

Муки вибору

 

Спочатку пошукав готові рішення. В ту далеку пору, коли використовував Delphi, можна було знайти просто гігантську кількість готових компонентів, як від серйозних фірм, так і любительського виготовлення. В Qt з цим сутужно. У QWT є деякі елементи, але не те. Любительщини взагалі не бачив. Напевно, якщо грамотно рити на Github’е, то можна щось знайти, але я, мабуть, швидше сам зроблю.

 

Перше, що напрошувалося з саморобного — використовувати два файла-картинки з зображеннями вимкнутим лампочки. Погано:

  • Треба знайти гарні картинки (або намалювати, але художник я ніякий);
  • Принципове питання: тирити недобре, навіть картинки, навіть валяються під ногами;
  • Треба їх зберігати десь. У файлах зовсім погано: випадково зітреться — і немає кнопок. У ресурсах трохи краще, але теж не хочеться, якщо можна обійтися;
  • Масштабованість ніяка;
  • Настроюваність (кольору, наприклад) досягається додаванням файлів. Тобто, ресурсомісткі і негнучке.

 

Друге, що випливає з першого — замість картинок використовувати векторні зображення. Тим більше, що Qt вміє рендери SVG. Тут вже трохи простіше з пошуком власне зображення: у мережі багато уроків по векторній графіці, можна знайти щось більш-менш підходяще і адаптувати під свої потреби. Але залишається питання по зберіганню і настраиваемости, так і рендеринг не безкоштовний ресурсів. Копійки, звичайно, але все ж…

Читайте також  Typegram — дзен блог-платформа

 

І третє випливає з другого: можна ж скористатися принципами векторної графіки при самостійній промальовуванні зображення! Файл векторної ілюстрації в текстовому вигляді вказує, що і як малювати. Я можу кодом вказати те ж саме, використовуючи векторні туторіали. Благо, у об’єкта 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);
}

 

Читайте також  Огляд безкоштовних 2D САПР

Тут обчислюється сторона квадрата, в який вписана лампочка, центр цього квадрата, радіус обідка, займає максимально можливу площу, ширина обідка, зовнішня частина якого нехай буде 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);

 

Читайте також  Підводний «GPS» на два прийомопередавача

Тут за допомогою функції 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 демонструє, що віджет можна зробити не лише індикатором, але і кнопкою, роблячи її натиснутою, відпущеної, гнутих, скрученої, розфарбованої і якою хочете ще з подій наведення, натискання і відпускання.

Але це вже не принципово. Метою було показати, де можна взяти зразки для наслідування при промальовуванні власних віджетів і як цю промальовування нескладно реалізувати без використання картинок растрових або векторних, ресурсах або файлах.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *