Как нарисовать сплайн c PyQt5.QPainter?

Есть вот такая программа, на данный момент представляющая из себя простой холст, на котором должен отрисовываться сплайн:

import sys

from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen, QPixmap, QColor
from PyQt5.QtWidgets import (QStyleFactory, QWidget, QLabel,
                             QLineEdit, QApplication, QMainWindow, QVBoxLayout)

class Canvas(QLabel):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.pixmap = QPixmap()
        self.setPixmap(self.pixmap)
        self.arr = []
        self.flag = False
        self.installEventFilter(self)


    def paintEvent(self, event):
        super().paintEvent(event)
        self.qp = QPainter(self.pixmap)
        self.qp.setRenderHint(QPainter.Antialiasing)
        self.qp.begin(self)
        pen = QPen(Qt.black, 1, Qt.SolidLine)
        #self.qp.drawPoint(event.pos().x(), event.pos().y())
        self.qp.setPen(pen)
        if self.flag:
            self.drawSpline(self.qp, self.arr)
        self.qp.end()

    def drawSpline(self, qp, array):
        L = [[0, 0], [0, 0], [0, 0], [0, 0]]
        dt = 0.004
        t = 0
        term = 1 + dt / 2

        Ppred = array[0]
        Pt = array[0]

        Pv1X = 4 * (array[1][0] - array[0][0])
        Pv1Y = 4 * (array[1][1] - array[0][1])
        Pv2X = 4 * (array[3][0] - array[2][0])
        Pv2Y = 4 * (array[3][1] - array[2][1])

        L[0][0] = 2 * array[0][0] - 2 * array[3][0] + Pv1X + Pv2X
        L[0][1] = 2 * array[0][1] - 2 * array[3][1] + Pv1Y + Pv2Y
        L[1][0] = -3 * array[0][0] + 3 * array[3][0] - 2 * Pv1X - Pv2X
        L[1][1] = -3 * array[0][1] + 3 * array[3][1] - 2 * Pv1Y - Pv2Y
        L[2][0] = Pv1X
        L[2][1] = Pv1Y
        L[3][0] = array[0][0]
        L[3][1] = array[0][1]

        while t < term:
            xt = ((L[0][0] * t + L[1][0]) * t + L[2][0]) * t + L[3][0]
            yt = ((L[0][1] * t + L[1][1]) * t + L[2][1]) * t + L[3][1]
            Pt[0] = round(xt)
            Pt[1] = round(yt)
            print(f'{xt}, {yt}') # после первой же итерации, числа перестают округляться
            qp.drawLine(Ppred[0], Ppred[1], Pt[0], Pt[1])
            Ppred = Pt
            t = t + dt


    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.MouseButtonPress:
            if event.button() == QtCore.Qt.LeftButton:
                pointPosition = [event.pos().x(), event.pos().y()]
                self.arr.append(pointPosition)
            elif event.button() == QtCore.Qt.RightButton:
                self.flag = True
        return super().eventFilter(source, event)


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.initUI()

    def initUI(self):
        self.resize(1000, 950)
        self.canvas = Canvas()

        w = QWidget()
        l = QVBoxLayout()
        w.setLayout(l)
        l.addWidget(self.canvas)
        self.setCentralWidget(w)


if __name__ == '__main__':
    app = QApplication([])
    app.setStyle(QStyleFactory.create('Fusion'))
    application = MainWindow()
    application.setWindowTitle("Drawning")
    application.show()

    sys.exit(app.exec_())

Есть несколько проблем в этом коде, которые я никак не могу решить:

  • сплайн рисуется только тогда, когда я как-либо изменяю размер окна;
  • сплайн рисуется прерывистой линией, а по идее должен был сплошной.

В чем суть метода drawSpline:

происходит расчет координат точек, которые в последствии соединяются и образовывают сплайн. Итоговая форма сплайн получается верной, однако выглядит не очень)

введите сюда описание изображения

После ресайза окна по каким-то причинам видимо paintEvent вызывается снова и рисует уже вот это:

введите сюда описание изображения

Как изменить поведение paintEvent и как добиться более красивого отображения сплайна?


Ответы (1 шт):

Автор решения: Alexander Chernin

Это не точки, а ваши отрезки сплайна, но обо всем по-порядку:

import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen, QColor
from PyQt5.QtWidgets import (QStyleFactory, QWidget, QApplication, QMainWindow, QVBoxLayout)

# Класс Canvas наследуем от QWidget 
# на котором можно прекрасно порисовать.
# Не нужны QLabel и QPixmap
class Canvas(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        # Массив точек
        self.arr = []

    def paintEvent(self, event):
        # не нужно делать паинтер объектом класса (self.qp)
        qp = QPainter()
        qp.begin(self)
        qp.setRenderHint(QPainter.Antialiasing)
        
        # По умолчанию ручка черная, давайте установим синию
        pen = QPen(Qt.blue)
        qp.setPen(pen)

        # Если массив содержит достаточно точек рассчитываем и рисуем сплайн
        if len(self.arr) >= 4:
            self.drawSpline(qp)
        qp.end()

    def drawSpline(self, qp):
        # Создаем ссылку array на наш массив self.array
        array = self.arr
        L = [[0, 0], [0, 0], [0, 0], [0, 0]]
        dt = 0.004
        t = 0
        term = 1 + dt / 2

        Ppred = array[0]
        Pt = [0, 0]

        Pv1X = 4 * (array[1][0] - array[0][0])
        Pv1Y = 4 * (array[1][1] - array[0][1])
        Pv2X = 4 * (array[3][0] - array[2][0])
        Pv2Y = 4 * (array[3][1] - array[2][1])

        L[0][0] = 2 * array[0][0] - 2 * array[3][0] + Pv1X + Pv2X
        L[0][1] = 2 * array[0][1] - 2 * array[3][1] + Pv1Y + Pv2Y
        L[1][0] = -3 * array[0][0] + 3 * array[3][0] - 2 * Pv1X - Pv2X
        L[1][1] = -3 * array[0][1] + 3 * array[3][1] - 2 * Pv1Y - Pv2Y
        L[2][0] = Pv1X
        L[2][1] = Pv1Y
        L[3][0] = array[0][0]
        L[3][1] = array[0][1]

        while t < term:
            xt = ((L[0][0] * t + L[1][0]) * t + L[2][0]) * t + L[3][0]
            yt = ((L[0][1] * t + L[1][1]) * t + L[2][1]) * t + L[3][1]
            Pt[0] = round(xt)
            Pt[1] = round(yt)
            print(f'{Ppred} - {Pt}') # Не xt и yt надо выводить :)
            qp.drawLine(Ppred[0], Ppred[1], Pt[0], Pt[1])

            # Если просто присвоить Ppred = Pt вы создаете ссылку на Pt
            # а не копию (и когда внесете новые данные в Pt - там где вы округляете, 
            # то и Ppred будет работать с ними же), 
            # поэтому у вас получаются точки на графике, а не линии,
            # и поэтому, чтобы этого не было, надо создать 
            # копию текущей точки
            Ppred = Pt.copy()
            t = t + dt

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        # Нам нужны четыре точки, поэтому если длина массива уже равна 4, 
        # то вынимаем из него первую
        if len(self.arr) == 4:
            self.arr.pop(0)

        # добавляем новую точку в массив
        # и, в соответствии, с предыдущим условием длина 
        # массива всегда будет <= 4-м
        self.arr.append([event.pos().x(), event.pos().y()])

        # вызываем перерисовку канвы
        self.update()


if __name__ == '__main__':
    app = QApplication([])
    app.setStyle(QStyleFactory.create('Fusion'))
    application = Canvas()
    application.setWindowTitle("Drawning")
    application.show()

    sys.exit(app.exec_())

Пример рабочий. Скорее запускайте красотень!

→ Ссылка