The съвременна тенденция в графичния дизайн е да се използват много заоблени ъгли във всякакви форми. Можем да наблюдаваме този факт на много уеб страници, мобилни устройства и настолни приложения. Най-забележителните примери са бутоните за приложения, които се използват за задействане на някакво действие при щракване. Вместо строго правоъгълна форма с ъгли от 90 градуса в ъглите, те често се рисуват със заоблени ъгли. Заоблените ъгли правят потребителския интерфейс по-гладък и приятен. Не съм напълно убеден в това, но моят приятел дизайнер ми го казва.
Визуалните елементи на потребителските интерфейси са създадени от дизайнери и програмистът трябва само да ги постави на правилните места. Но какво се случва, когато трябва да генерираме форма със заоблени ъгли в движение и не можем да я заредим предварително? Някои библиотеки за програмиране предлагат ограничени възможности за създаване на предварително дефинирани фигури със заоблени ъгли, но обикновено те не могат да се използват в по-сложни случаи. Например, Qt рамка има клас QPainter
, който се използва за извличане на всички класове, получени от QPaintDevice
, включително джаджи, pixmaps и изображения. Той има метод, наречен drawRoundedRect
, който, както подсказва името, чертае правоъгълник със заоблени ъгли. Но ако имаме нужда от малко по-сложна форма, трябва да я приложим сами. Как бихме могли да направим това с многоъгълник, равнинна форма, ограничена от група от отсечки с права линия? Ако имаме многоъгълник, нарисуван с молив върху лист хартия, първата ми идея би била да използвам гума и да изтрия малка част от линиите на всеки ъгъл и след това да свържа останалите краища на сегмента с кръгла дъга. Целият процес може да бъде илюстриран на фигурата по-долу.
Клас QPainter
има някои претоварени методи с име drawArc
, които могат да нарисуват кръгови дъги. Всички те изискват параметри, които определят центъра и размера на дъгата, началния ъгъл и дължината на дъгата. Въпреки че е лесно да се определят необходимите стойности на тези параметри за не завъртян правоъгълник, съвсем различен е въпросът, когато имаме работа с по-сложни полигони. Плюс това би трябвало да повторим това изчисление за всеки многоъгълен връх. Това изчисление е дълга и досадна задача и хората са склонни към всякакви грешки в изчисленията в процеса. Работата на разработчиците на софтуер обаче е да накарат компютрите да работят за хората, а не обратното. И така, тук ще покажа как да се развие прост клас, който може да превърне сложен многоъгълник във форма със заоблени ъгли. Потребителите от този клас ще трябва само да добавят върхове на многоъгълници, а класът ще свърши останалото. Основният математически инструмент, който използвам за тази задача, е Крива на Безие .
Има много математически книги и интернет ресурси, описващи теорията на кривите на Безие, така че накратко ще очертая съответните свойства.
По дефиниция кривата на Безие е крива между две точки на двумерна повърхност, чиято траектория се управлява от една или повече контролни точки. Строго погледнато, крива между две точки без допълнителни контролни точки също е крива на Безие. Тъй като обаче това води до права линия между двете точки, това не е особено интересно, нито полезно.
Квадратичните криви на Безие имат една контролна точка. Теорията казва, че квадратна крива на Безие между точките P0 и P2 с контролна точка Pедин се определя, както следва:
B (t) = (1 - t)2P0+ 2t (1 - t) Pедин+ t2P2, където 0 ≤ t ≤ 1 (един)
Така че, когато т е равно на 0 , B (t) ще даде P0 , кога т е равно на един , B (t) ще даде P2 , но във всеки друг случай стойността на B (t) също ще зависи от Pедин . Тъй като изразът 2t (1 - t) има максимална стойност при t = 0,5 , там влиянието на Pедин На B (t) ще бъде най-големият. Можем да се сетим Pедин като на въображаем източник на гравитация, който привлича траекторията на функцията към себе си. Фигурата по-долу показва няколко примера за квадратни криви на Бези с техните начални, крайни и контролни точки.
И така, как да решим проблема си, използвайки криви на Безие? Фигурата по-долу предлага обяснение.
Ако си представим как изтриваме многоъгълник връх и кратка част от свързани сегменти от права в околността му, можем да мислим за края на един сегмент от линия като за P0 , другият сегмент на линията завършва от P2 и изтрития връх към Pедин . Прилагаме квадратна крива на Безие към този набор от точки и voila, има желаният заоблен ъгъл.
Клас QPainter
няма начин да начертае квадратни криви на Безие. Въпреки че е доста лесно да се приложи от нулата, следвайки уравнението (1), библиотеката Qt предлага по-добро решение. Има още един мощен клас за 2D рисуване: QPainterPath
. Клас QPainterPath
е колекция от линии и криви, които могат да бъдат добавени и използвани по-късно с QPainter
обект. Има някои претоварени методи, които добавят криви на Безие към текущата колекция. По-специално методи quadTo
ще добави квадратни криви на Безие. Кривата ще започне от текущия QPainterPath
точка ( P0 ), докато Pедин и P2 трябва да бъдат предадени на quadTo
като параметри.
QPainter
Метод drawPath
се използва за изчертаване на колекция от линии и криви от QPainterPath
обект, който трябва да бъде даден като параметър, с активна писалка и четка.
Така че нека видим декларацията на класа:
class RoundedPolygon : public QPolygon { public: RoundedPolygon() { SetRadius(10); } void SetRadius(unsigned int iRadius) { m_iRadius = iRadius; } const QPainterPath& GetPath(); private: QPointF GetLineStart(int i) const; QPointF GetLineEnd(int i) const; float GetDistance(QPoint pt1, QPoint pt2) const; private: QPainterPath m_path; unsigned int m_iRadius; };
Реших да подкласирам QPolygon
така че да не се налага да прилагам добавяне на върхове и други неща от себе си. Освен конструктора, който просто задава радиуса на някаква разумна начална стойност, този клас има два други публични метода:
SetRadius
метод задава радиуса на дадена стойност. Радиусът е дължината на права линия (в пиксели) близо до всеки връх, която ще бъде изтрита (или по-точно не нарисувана) за заобления ъгъл.GetPath
е мястото, където се извършват всички изчисления. Ще върне QPainterPath
обект, генериран от многоъгълни точки, добавени към RoundedPolygon
.Методите от частната част са просто спомагателни методи, използвани от GetPath
.
Нека да видим изпълнението и ще започна с частните методи:
float RoundedPolygon::GetDistance(QPoint pt1, QPoint pt2) const { float fD = (pt1.x() - pt2.x())*(pt1.x() - pt2.x()) + (pt1.y() - pt2.y()) * (pt1.y() - pt2.y()); return sqrtf(fD); }
Не е много за обяснение тук, методът връща евклидовото разстояние между дадените две точки.
QPointF RoundedPolygon::GetLineStart(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX((1.0f-fRat)*pt1.x() + fRat*pt2.x()); pt.setY((1.0f-fRat)*pt1.y() + fRat*pt2.y()); return pt; }
Метод GetLineStart
изчислява местоположението на точката P2 от последната фигура, ако точките се добавят към многоъгълника по посока на часовниковата стрелка. По-точно ще върне точка, която е m_uiRadius
пиксели от i
-ти връх в посока към (i+1)
-ти връх. При достъп до (i+1)
-тия връх трябва да помним, че в многоъгълника има и отсечка от права между последния и първия връх, което го прави затворена форма, като по този начин изразът (i+1)%count()
. Това също така предотвратява излизането на метода извън обхвата и вместо това получава достъп до първата точка. Променлива fRat
съдържа съотношението между радиуса и i
дължината на сегмента от третия ред. Има и проверка, която предотвратява fRat
от стойност над 0.5
. Ако fRat
има стойност над 0.5
, тогава двата последователни заоблени ъгъла ще се припокрият, което би довело до лош визуален резултат.
Когато пътувате от точка Pедин да се P2 по права линия и като попълним 30 процента от разстоянието, можем да определим местоположението си, използвайки формулата 0,7 • Редин+ 0,3 • P2 . Като цяло, ако постигнем част от пълното разстояние, и α = 1 обозначава пълно разстояние, текущото местоположение е на (1 - α) • P1 + α • P2 .
Ето как GetLineStart
метод определя местоположението на точката, която е m_uiRadius
пиксели от i
-ти връх в посока (i+1)
-ти.
QPointF RoundedPolygon::GetLineEnd(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX(fRat*pt1.x() + (1.0f - fRat)*pt2.x()); pt.setY(fRat*pt1.y() + (1.0f - fRat)*pt2.y()); return pt; }
Този метод е много подобен на GetLineStart
. Той изчислява местоположението на точката P0 за (i+1)
-тия връх, а не i
-ти. С други думи, ако начертаем линия от GetLineStart(i)
до GetLineEnd(i)
за всеки i
между 0
и n-1
, където n
е броят на върховете в многоъгълника, ще получим полигона със изтрити върхове и близките им околности.
И сега, основният метод на класа:
const QPainterPath& RoundedPolygon::GetPath() { m_path = QPainterPath(); if (count() <3) { qWarning() << 'Polygon should have at least 3 points!'; return m_path; } QPointF pt1; QPointF pt2; for (int i = 0; i < count(); i++) { pt1 = GetLineStart(i); if (i == 0) m_path.moveTo(pt1); else m_path.quadTo(at(i), pt1); pt2 = GetLineEnd(i); m_path.lineTo(pt2); } // close the last corner pt1 = GetLineStart(0); m_path.quadTo(at(0), pt1); return m_path; }
В този метод ние изграждаме QPainterPath
обект. Ако полигонът няма поне три върха, вече нямаме работа с 2D фигура и в този случай методът издава предупреждение и връща празния път. Когато са налични достатъчно точки, ние се придвижваме по всички отсечки на прави линии на многоъгълника (броят на отсечките от линии е, разбира се, равен на броя на върховете), като изчисляваме началото и края на всеки отсек с права линия между заоблените ъгли. Поставяме права линия между тези две точки и квадратна крива на Безие между края на предходния сегмент на линията и началото на тока, като използваме местоположението на текущия връх като контролна точка. След цикъла трябва да затворим пътеката с крива на Безие между последния и първия сегмент на реда, тъй като в цикъла нарисувахме една права линия повече от кривите на Безие.
RoundedPolygon
използване и резултатиСега е време да видим как да използваме този клас на практика.
QPixmap pix1(300, 200); QPixmap pix2(300, 200); pix1.fill(Qt::white); pix2.fill(Qt::white); QPainter P1(&pix1); QPainter P2(&pix2); P1.setRenderHints(QPainter::Antialiasing); P2.setRenderHints(QPainter::Antialiasing); P1.setPen(QPen(Qt::blue, 2)); P1.setBrush(Qt::red); P2.setPen(QPen(Qt::blue, 2)); P2.setBrush(Qt::red); RoundedPolygon poly; poly << QPoint(147, 187) << QPoint(95, 187) << QPoint(100, 175) << QPoint(145, 165) << QPoint(140, 95) << QPoint(5, 85) << QPoint(5, 70) << QPoint(140, 70) << QPoint(135, 45) << QPoint(138, 25) << QPoint(145, 5) << QPoint(155, 5) << QPoint(162, 25) << QPoint(165, 45) << QPoint(160, 70) << QPoint(295, 70) << QPoint(295, 85) << QPoint(160, 95) << QPoint(155, 165) << QPoint(200, 175) << QPoint(205, 187) << QPoint(153, 187) << QPoint(150, 199); P1.drawPolygon(poly); P2.drawPath(poly.GetPath()); pix1.save('1.png'); pix2.save('2.png');
Този парче изходен код е доста ясен. След инициализиране на две QPixmaps
и техните QPainters
, ние създаваме RoundedPolygon
обект и го запълнете с точки. Художник P1
изчертава правилния многоъгълник, докато P2
изчертава QPainterPath
със заоблени ъгли, генерирани от многоъгълника. И двете получени pixmaps се записват в техните файлове и резултатите са както следва:
Видяхме, че генерирането на форма със заоблени ъгли от многоъгълник в края на краищата не е толкова трудно, особено ако използваме добра рамка за програмиране като Qt. Този процес може да бъде автоматизиран от класа, който описах в този блог като доказателство за концепцията. Все още обаче има много място за подобрения, като например:
RoundedPolygon
за генериране на растерни изображения, които могат да бъдат използвани като маска на приспособления за фон, за да се получат джаджи с луда форма.RoundedPolygon
класът не е оптимизиран за скорост на изпълнение; Оставих го такъв, какъвто е за по-лесно разбиране на концепцията. Оптимизацията може да включва изчисляване на много междинни стойности при добавяне на нов връх към полигона. Също така, когато GetPath
е на път да върне препратка към генерирания QPainterPath
, той може да зададе флаг, указващ, че обектът е актуален. Следващото обаждане до GetPath
би довело само до връщане на същото QPainterPath
обект, без да преизчислява нищо. Разработчикът обаче трябва да се увери, че този флаг е изчистен при всяка промяна във всеки от върховете на многоъгълници, както и при всеки нов връх, което ме кара да мисля, че оптимизираният клас ще бъде по-добре разработен от нулата, а не изведен от QPolygon
. Добрата новина е, че това не е толкова трудно, колкото звучи.Като цяло, RoundedPolygon
клас, такъв какъвто е, може да се използва като инструмент по всяко време, когато искаме да добавим a дизайнерски щрих към нашия GUI в движение, без предварително да подготвяте карти или пиксели.