用PyQt5绘制多点曲线

Drawing multi-point curve with PyQt5

如何使用 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())

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

更新

我意识到我之前的回答不仅不准确,而且是错误的。出于 documentation/historical 的目的,我将它留在这个答案的底部。

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

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

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

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

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

该代码使用 for 循环,从第二个点循环到倒数第二个点,还使用从 上一个 循环设置的控制点。

我建议你使用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])