如何使用贝塞尔曲线和QPainter在C++中获取圆角形状

本文概述

  • 介绍
  • 贝塞尔曲线
  • 使用QPainter的C++/Qt实现
  • 总结
介绍 图形设计的当前趋势是在各种形状中使用大量的圆角。我们可以在许多网页, 移动设备和桌面应用程序上观察到这一事实。最著名的例子是应用程序按钮, 单击该按钮可触发某些操作。通常, 它们不是圆角成90度角的严格矩形形状, 而是绘制有圆角。圆角使用户界面更流畅, 更美观。我对此并不完全确信, 但我的设计师朋友告诉了我。
如何使用贝塞尔曲线和QPainter在C++中获取圆角形状

文章图片
圆角使用户界面更流畅, 更美观。
用户界面的视觉元素是由设计人员创建的, 程序员只需要将它们放在正确的位置即可。但是, 当我们不得不动态生成一个带有圆角的形状而又无法对其进行预加载时, 会发生什么呢?一些编程库提供有限的功能来创建带有圆角的预定义形状, 但是通常, 它们不能用于更复杂的情况。例如, Qt框架具有QPainter类, 该类用于绘制从QPaintDevice派生的所有类, 包括小部件, 像素图和图像。它有一个名为drawRoundedRect的方法, 顾名思义, 该方法绘制一个带有圆角的矩形。但是, 如果我们需要更复杂的形状, 则必须自己实现。我们如何用多边形(由一组直线段界定边界的平面形状)做到这一点?如果我们在纸上有一个用铅笔绘制的多边形, 我的第一个想法是使用橡皮擦并删除每个角的一小部分线, 然后将其余的线段末端连接成圆弧。下图说明了整个过程。
如何使用贝塞尔曲线和QPainter在C++中获取圆角形状

文章图片
QPainter类有一些重载的方法, 称为drawArc, 可以绘制圆弧。它们都需要参数, 这些参数定义了弧的中心和大小, 起始角度和弧长。尽管很容易确定非旋转矩形的这些参数的必要值, 但是当我们处理更复杂的多边形时, 情况就完全不同了。另外, 我们将不得不为每个多边形顶点重复此计算。这种计算是一项冗长而繁琐的任务, 并且在此过程中, 人类容易出现各种计算错误。但是, 让计算机为人类服务是软件开发人员的工作, 反之亦然。因此, 在这里我将展示如何开发一个简单的类, 该类可以将一个复杂的多边形变成带有圆角的形状。此类的用户将仅需附加多边形顶点, 其余的将由类完成。我用于此任务的基本数学工具是Bezier曲线。
贝塞尔曲线 有许多描述Bezier曲线理论的数学书籍和网络资源, 因此我将简要概述其相关属性。
根据定义, 贝塞尔曲线是二维表面上两个点之间的曲线, 其轨迹由一个或多个控制点控制。严格来说, 两点之间没有附加控制点的曲线也是贝塞尔曲线。但是, 由于这导致了两点之间的直线, 因此它不是特别有趣, 也没有用。
二次贝塞尔曲线
二次贝塞尔曲线具有一个控制点。该理论说, 控制点为P1的点P0和P2之间的二次Bezier曲线定义如下:
B(t)=(1-t)2P0 + 2t(1-t)P1 + t2P2, 其中0≤t≤1(1)
因此, 当t等于0时, B(t)将产生P0, 当t等于1时, B(t)将产生P2, 但在其他每种情况下, B(t)的值也将取决于P1。由于表达式2t(1-t)在t = 0.5时具有最大值, 因此P1对B(t)的影响最大。我们可以将P1视为虚构的引力源, 这会将函数轨迹拉向自身。下图显示了一些二次Bezier曲线的示例, 包括其起点, 终点和控制点。
如何使用贝塞尔曲线和QPainter在C++中获取圆角形状

文章图片
那么, 如何使用贝塞尔曲线解决问题呢?下图提供了解释。
如何使用贝塞尔曲线和QPainter在C++中获取圆角形状

文章图片
如果我们想象删除多边形顶点和周围的连接线段的一小部分, 我们可以将一个线段的末端设为P0, 将另一个线段的末端设为P2, 将删除的顶点设为P1。我们对这组点应用二次贝塞尔曲线, 瞧, 有所需的圆角。
使用QPainter的C++/Qt实现 QPainter类无法绘制二次Bezier曲线。虽然很容易按照等式(1)从头开始实现它, 但Qt库确实提供了更好的解决方案。还有一个用于2D绘图的强大类:QPainterPath。 QPainterPath类是直线和曲线的集合, 可以将其添加和以后与QPainter对象一起使用。有一些重载方法可将Bezier曲线添加到当前集合。特别是, 方法quadTo将添加二次Bezier曲线。曲线将从当前的QPainterPath点(P0)开始, 而P1和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是所有计算发生的地方。它将返回从添加到RoundedPolygon的多边形点生成的QPainterPath对象。
私有部分中的方法只是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的位置。更确切地说, 它将返回一个点, 该点是在朝第(i + 1)个顶点的方向上从第i个顶点离开m_uiRadius像素。访问第(i + 1)个顶点时, 我们必须记住, 在多边形中, 最后一个顶点与第一个顶点之间还有一个线段, 使其成为闭合形状, 因此表达式(i + 1) %计数()。这也可以防止方法超出范围, 而是访问第一个点。变量fRat保持半径与第i个线段长度之间的比率。还有一项检查可以防止fRat的值超过0.5。如果fRat的值超过0.5, 则两个连续的圆角将重叠, 这将导致较差的视觉效果。
当从点P1到点P2直线行驶并完成距离的30%时, 我们可以使用公式0.7?P1 + 0.3?P2确定位置。通常, 如果我们获得完整距离的一小部分, 并且α= 1表示完整距离, 则当前位置为(1-α)?P1 +α?P2。
这就是GetLineStart方法确定在第(i + 1)方向上距离第i个顶点m_uiRadius像素的点的位置的方式。
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非常相似。它为第(i + 1)个顶点而不是第i个顶点计算点P0的位置。换句话说, 如果我们为介于0和n-1之间的每个i绘制一条从GetLineStart(i)到GetLineEnd(i)的线, 其中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形状, 在这种情况下, 该方法将发出警告并返回空路径。当有足够的点可用时, 我们遍历多边形的所有直线段(直线段的数量当然等于顶点的数量), 计算四舍五入之间的每个直线段的起点和终点角落。我们使用当前顶点的位置作为控制点, 在这两个点之间放置一条直线, 并在上一条线段的末端和当前起点之间放置一条二次贝塞尔曲线。循环之后, 我们必须使用最后一条线段与第一条线段之间的Bezier曲线封闭路径, 因为在循环中, 我们绘制的直线比Bezier曲线多。
类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");

这段源代码非常简单。初始化两个QPixmap及其QPainters之后, 我们创建一个RoundedPolygon对象, 并用点填充它。 Painter P1绘制常规多边形, 而P2绘制具有从多边形生成的圆角的QPainterPath。两个结果像素图都保存到它们的文件中, 结果如下:
如何使用贝塞尔曲线和QPainter在C++中获取圆角形状

文章图片
总结 我们已经看到, 从多边形生成带有圆角的形状毕竟不是那么困难, 特别是如果我们使用良好的编程框架(例如Qt)的话。我在本博客中描述为概念证明的课程可以使该过程自动化。但是, 仍有很多改进的空间, 例如:
  • 仅在选定的顶点处而不是在所有顶点处均形成圆角。
  • 在不同的顶点制作具有不同半径的圆角。
  • 实现一种方法, 该方法生成带有圆角的折线(Qt术语中的折线与多边形类似, 但它不是闭合形状, 因为它缺少最后一个顶点与第一个顶点之间的线段, 所以它不是闭合的形状)。
  • 使用RoundedPolygon生成位图, 可以将其用作背景小部件蒙版以生成疯狂的形状的小部件。
  • RoundedPolygon类未针对执行速度进行优化。我将其保留下来是为了更容易理解该概念。优化可能包括在将新顶点附加到多边形后计算大量中间值。同样, 当GetPath将要返回对生成的QPainterPath的引用时, 它可以设置一个标志, 指示该对象是最新的。下次调用GetPath将导致仅返回相同的QPainterPath对象, 而无需重新计算任何内容。但是, 开发人员必须确保在任何多边形顶点的每个更改以及每个新顶点上都清除此标志, 这使我认为优化的类最好从头开始开发而不是派生。来自QPolygon。好消息是, 这并不像听起来那样困难。
总而言之, RoundedPolygon类可以随时用作我们想要在GUI上实时添加设计者风格的工具, 而无需事先准备像素图或形状。
【如何使用贝塞尔曲线和QPainter在C++中获取圆角形状】相关:如何学习C和C++语言:最终列表

    推荐阅读