【问题标题】:Drawing multi-point curve with PyQt5用 PyQt5 绘制多点曲线
【发布时间】:2020-11-10 22:24:59
【问题描述】:

如何使用 PyQt5 将多个点与流动曲线连接起来?例如,我尝试使用 quadTo() 对 8 个点执行此操作,使用交替点作为控制点,但弧线不接触控制点(参见下面的代码和图表)。我也尝试使用cubicTo(),但这也导致了一条奇怪的曲线。是否有任何其他我应该使用的函数调用,或者自定义的方法来做到这一点?

from PyQt5 import QtGui, QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.title = "PyQt5 Drawing Tutorial"
        self.top= 150
        self.left= 150
        self.width = 500
        self.height = 500
        self.InitWindow()
    def InitWindow(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.top, self.left, self.width, self.height)
        self.show()
    def paintEvent(self, event):
        painter = QPainter(self)
        path = QPainterPath()
        points = [
            QPoint(20,40),
            QPoint(60,10),
            QPoint(100,50),
            QPoint(80,200),
            QPoint(200,300),
            QPoint(150,400),
            QPoint(350,450),
            QPoint(400,350),
            ]

        # draw small red dots on each point
        painter.setPen(QtCore.Qt.red)
        painter.setBrush(QBrush(Qt.red))
        for i in range(len(points)):
            painter.drawEllipse(points[i], 3, 3)

        painter.setPen(QtCore.Qt.blue)
        painter.setBrush(QBrush(Qt.red, Qt.NoBrush)) #reset the brush
        path.moveTo(points[0])

        # connect the points with blue straight lines
        #for i in range(len(points)-1):  # 1 less than length
        #    path.lineTo(points[i+1])

        # connect points with curve
        for i in range(0,len(points),2):
            path.quadTo(points[i], points[i+1])

        painter.drawPath(path)

App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

【问题讨论】:

  • 您能否阐明您想要实现的目标,可能使用显示预期结果的图像?我的印象是您想绘制一条连接所有点的“平滑”曲线,但在这种情况下: 1. 这是一个数学问题; 2. 有无数种方法可以做到这一点;
  • 第 2 项 - 任何连接所有点的平滑曲线现在都可以。关于第 1 项 - 是否有可以在 pyqt5 框架内调用的函数?

标签: python pyqt pyqt5


【解决方案1】:

使用quadTocubicTo 之类的函数将不起作用,因为它们使用控制点 来创建贝塞尔曲线,而这些点通常不是曲线的一部分。

更新

我意识到我之前的回答不仅不准确,而且是错误的。出于文档/历史目的,我将其留在此答案的底部。

精确的“样条”插值必须使用与可能曲线相切的线段;为了找到您需要的细分数据:

  1. 上一点和下一点
  2. 找到由上一个/当前点和当前/下一个点创建的线段之间的角平分线
  3. 创建两个垂直于该角度的线段,从当前点开始,长度与每个线段成比例
  4. 使用这些段的末端作为控制点

在下图中,您可以看到所有重要信息:

  • 红色点:参考点;
  • 浅灰色线条:线段
  • 天蓝色线:角平分线
  • 红色行:目标行的参考(从当前点到下一个点)
  • 绿色行:参考源行(从前一点到当前)
  • 橙色方块:控制点

请注意,第一条和最后一条曲线只是二次曲线(不是三次曲线),因为只有一个控制点:第一个点的目标线参考,最后一个点的源线参考。

代码使用从第二个到倒数第二个点循环的 for 循环,并且还使用 previous 循环中设置的控制点。

我建议你使用factor = .25,它应该创建一个足够平滑的路径。较低的值会导致“更小”的曲线,而较高的值会为您提供更“圆润”的路径。

class Window(QWidget):
    # ...

    def buildPath(self):
        factor = 
        self.path = QtGui.QPainterPath(points[0])
        for p, current in enumerate(points[1:-1], 1):
            # previous segment
            source = QtCore.QLineF(points[p - 1], current)
            # next segment
            target = QtCore.QLineF(current, points[p + 1])
            targetAngle = target.angleTo(source)
            if targetAngle > 180:
                angle = (source.angle() + source.angleTo(target) / 2) % 360
            else:
                angle = (target.angle() + target.angleTo(source) / 2) % 360

            revTarget = QtCore.QLineF.fromPolar(source.length() * factor, angle + 180).translated(current)
            cp2 = revTarget.p2()

            if p == 1:
                self.path.quadTo(cp2, current)
            else:
                # use the control point "cp1" set in the *previous* cycle
                self.path.cubicTo(cp1, cp2, current)

            revSource = QtCore.QLineF.fromPolar(target.length() * factor, angle).translated(current)
            cp1 = revSource.p2()

        # the final curve, that joins to the last point
        self.path.quadTo(cp1, points[-1])

上一个答案

有一些算法允许为插值构建“样条曲线”,但您需要一些数学技能来理解它们并创建一个可以创建平滑曲线的良好系统。同时,一个可能的(但不是完美的)解决方案是创建从现有段的扩展计算的控制点(这类似于矢量图形编辑器所做的):

每个扩展的末端都用作贝塞尔曲线的控制点:对于第一段和最后一段,我使用二次曲线(一个控制点),而所有其他曲线都是三次曲线(两个控制点);这会产生可接受的结果:

不幸的是,它远非完美,尤其是对于角度和长度的某些组合:

我建议您仅在需要时构建路径(例如,点发生​​变化),而不是在paintEvent中。

class Window(QWidget):
    # ...

    def buildPath(self):
        self.path = QtGui.QPainterPath()
        self.path.moveTo(points[0])
        factor = .1412
        for p in range(len(points) - 2):
            p2 = points[p + 1]
            target = QtCore.QLineF(p2, points[p + 2])
            reverseTarget = QtCore.QLineF.fromPolar(
                target.length() * factor, target.angle() + 180).translated(p2)
            if not p:
                self.path.quadTo(reverseTarget.p2(), p2)
            else:
                p0 = points[p - 1]
                p1 = points[p]
                source = QtCore.QLineF(p0, p1)
                current = QtCore.QLineF(p1, p2)
                targetAngle = target.angleTo(current)
                if 90 < targetAngle < 270:
                    ratio = abs(sin(radians(targetAngle)))
                    reverseTarget.setLength(reverseTarget.length() * ratio)
                reverseSource = QtCore.QLineF.fromPolar(
                    source.length() * factor, source.angle()).translated(p1)
                sourceAngle = current.angleTo(source)
                if 90 < sourceAngle < 270:
                    ratio = abs(sin(radians(sourceAngle)))
                    reverseSource.setLength(reverseSource.length() * ratio)
                self.path.cubicTo(reverseSource.p2(), reverseTarget.p2(), p2)

        final = QtCore.QLineF(points[-3], points[-2])
        reverseFinal = QtCore.QLineF.fromPolar(
            final.length() * factor, final.angle()).translated(final.p2())
        self.path.quadTo(reverseFinal.p2(), points[-1])

【讨论】:

  • 优秀。非常感谢您的解决方案!顺便说一句,你能推荐一些教程材料来阅读这个理论吗?我想了解制作图表的数学和替代解决方案(例如,对于您上面提到的问题)。
  • @R71 我刚刚对数学做了一些研究(包括here on StackExchange)。无论如何,我更新了我的答案,因为我意识到我以前的算法只是错误
猜你喜欢
  • 2019-08-31
  • 2021-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多