Search code examples
pythonpyqt5qgraphicsviewqgraphicssceneqgraphicspathitem

How can I draw a transparent line continues inside the QGraphicsview without overlap?


I'm trying to draw a path made up of lines in two ways: the first, the path is made of straight lines, which have their extremities in common. The problem with using this methodology is that the lines overlap in their extremities causing an undesirable effect as can be seen in the following figure:

enter image description here

And here is the code:

from PyQt5 import QtWidgets, QtCore, QtGui
import typing
import random


class Track(QtWidgets.QGraphicsPathItem):
    def __init__(self, parent=None, offset: float = 50):
        super(Track, self).__init__(parent)  # initiate the parent class
        self.__points: list = [QtCore.QPointF(0, 0)]
        self.__pen: QtGui.QPen = QtGui.QPen()
        self.setPen()

    def getPoints(self) -> list:
        return self.__points

    def append(self, point: QtCore.QPointF):
        self.__points.append(point)

    def getPen(self) -> QtGui.QPen:
        return self.__pen

    def setPen(self, pen: QtGui.QPen = None, width: int = 10, color: QtGui.QColor = QtGui.QColor(0, 24, 128, 100),
               cap: QtCore.Qt.PenCapStyle = QtCore.Qt.SquareCap, line_style: QtCore.Qt.PenStyle = QtCore.Qt.SolidLine,
               join: QtCore.Qt.PenJoinStyle = QtCore.Qt.RoundJoin) -> None:
        """
        Set the pen that will be used to paint the implement.
        :param pen : set the pen or its arguments
        :param width: the pen width.
        :param color: the pen color.
        :param cap: the cap style: rounded, flatted and squared.
        :param line_style: dashed, solid ...
        :param join: miter , rounded ...
        :return: None
        """
        if pen == None:
            self.__pen.setWidth(width)  # set the pen width
            self.__pen.setColor(color)  # define your color from QtCore, it is safer to use the statement:
            self.__pen.setCapStyle(cap)  # set the cap style of the line
            self.__pen.setStyle(line_style)  # set the line style for instance: solid, dash... whatever
            self.__pen.setJoinStyle(join)  # set how the lines will be connected.
        else:
            self.__pen = pen

    def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
              widget: typing.Optional[QtWidgets.QWidget] = ...) -> None:
        painter.setPen(self.getPen())
        painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)

        try:
            path = QtGui.QPainterPath()
            path.moveTo(self.getPoints()[0])
            for point in self.getPoints():
                path.lineTo(point)
            painter.drawPath(path)
        except IndexError:
            self.append(QtCore.QPointF(0, 0))


class Producer(QtCore.QObject):

    def __init__(self, parent=None):
        super(Producer, self).__init__(parent)
        self.__last_point = QtCore.QPointF(10, 0)
        self.__point = QtCore.QPointF(10, 0)
        self.upper = 10
        self.bottom = 0

    def setPoint(self) -> None:
        self.setLastpoint(self.getPoint())
        x = random.randint(self.bottom, self.upper)
        y = random.randint(self.bottom, self.upper)
        self.upper += 50  # increases the range of probability at the upper limit
        self.__point = QtCore.QPointF(x, y)  # produce a new random point

    def getPoint(self) -> QtCore.QPointF:
        return self.__point

    def setLastpoint(self, point: QtCore.QPointF):
        self.__last_point = point

    def getLastPoint(self) -> QtCore.QPointF:
        return self.__last_point



class Window2(QtWidgets.QMainWindow):
    def __init__(self):
        super(Window2, self).__init__()
        central_widget = QtWidgets.QWidget()
        self.__pen = QtGui.QPen()
        self.setMinimumHeight(500)
        self.setMinimumWidth(500)
        self.scene = QtWidgets.QGraphicsScene(self)
        self.view = QtWidgets.QGraphicsView(self.scene)
        self.view.setSceneRect(self.view.mapToScene(self.view.viewport().rect()).boundingRect())
        self.btn = QtWidgets.QPushButton('Get Track')
        self.btn.clicked.connect(self.getTrack)
        self.producer = Producer()
        hbox = QtWidgets.QHBoxLayout(central_widget)
        hbox.addWidget(self.view)
        hbox.addWidget(self.btn)
        self.setCentralWidget(central_widget)
        self.setPen()

    def getPen(self) -> QtGui.QPen:
        return self.__pen

    def getTrack(self):
        print('run')
        self.producer.setPoint()
        line = QtCore.QLineF(self.producer.getPoint(), self.producer.getLastPoint())
        self.scene.addLine(line, pen = self.getPen())
        dx = self.producer.getPoint().x() - self.producer.getLastPoint().x()
        dy = self.producer.getPoint().y() - self.producer.getLastPoint().y()
        print(dx, dy)
        self.view.setSceneRect(self.view.sceneRect().translated(dx, dy))

    def setPen(self, pen: QtGui.QPen = None, width: int = 10, color: QtGui.QColor = QtGui.QColor(0, 24, 128, 100),
               cap: QtCore.Qt.PenCapStyle = QtCore.Qt.SquareCap, line_style: QtCore.Qt.PenStyle = QtCore.Qt.SolidLine,
               join: QtCore.Qt.PenJoinStyle = QtCore.Qt.RoundJoin) -> None:
        """
        Set the pen that will be used to paint the implement.
        :param pen : set the pen or its arguments
        :param width: the pen width.
        :param color: the pen color.
        :param cap: the cap style: rounded, flatted and squared.
        :param line_style: dashed, solid ...
        :param join: miter , rounded ...
        :return: None
        """
        if pen == None:
            self.__pen.setWidth(width)  # set the pen width
            self.__pen.setColor(color)  # define your color from QtCore, it is safer to use the statement:
            self.__pen.setCapStyle(cap)  # set the cap style of the line
            self.__pen.setStyle(line_style)  # set the line style for instance: solid, dash... whatever
            self.__pen.setJoinStyle(join)  # set how the lines will be connected.
        else:
            self.__pen = pen


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    # w = Window()
    w = Window2()
    w.show()
    sys.exit(app.exec_())

The second way I am using is creating a path that is continuous and made up of points. This path inherits the class: QGraphicsPathItem. However, when I 'update' my 'scene Rectangle' this path disappears when it has one of its ends outside my border = 'scene Rectangle'. Was there any way to prevent it from disappearing? A second concern with this approach that I am taking is the fact that I need to save these points that make up my path ... for a small amount of points this is not a problem but as my path gets full of points it will have problems with memory management. image:

enter image description here

And the code is here:

class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        central_widget = QtWidgets.QWidget()
        self.setMinimumHeight(500)
        self.setMinimumWidth(500)
        self.scene = QtWidgets.QGraphicsScene(self)
        self.view = QtWidgets.QGraphicsView(self.scene)
        self.view.setSceneRect(self.view.mapToScene(self.view.viewport().rect()).boundingRect())
        self.btn = QtWidgets.QPushButton('Get Track')
        self.btn.clicked.connect(self.getTrack)
        self.producer = Producer()
        hbox = QtWidgets.QHBoxLayout(central_widget)
        hbox.addWidget(self.view)
        hbox.addWidget(self.btn)
        self.track = Track()
        self.scene.addItem(self.track)
        self.setCentralWidget(central_widget)

    def getTrack(self):
        self.producer.setPoint()
        self.track.append(self.producer.getPoint())
        dx = self.producer.getPoint().x() - self.producer.getLastPoint().x()
        dy = self.producer.getPoint().y() - self.producer.getLastPoint().y()
        print(dx, dy)
        self.view.setSceneRect(self.view.sceneRect().translated(dx, dy))

I am traying to simulate a GPS, where the path that I'm traying to paint is the car's displacement. But I don't know what of this two are the better approach and if there is another one.

here a minimal reproducible example:

from PyQt5 import QtWidgets, QtCore, QtGui
import typing
import random

pen = QtGui.QPen()
pen.setColor(QtGui.QColor(0, 24, 128, 100))
pen.setWidth(10)
pen.setStyle(QtCore.Qt.SolidLine)

pen.setCapStyle(QtCore.Qt.SquareCap)


class Track(QtWidgets.QGraphicsPathItem):
    def __init__(self, parent=None, offset: float = 50):
        super(Track, self).__init__(parent)  # initiate the parent class
        self.__points: list = [QtCore.QPointF(0, 0)]

    def getPoints(self) -> list:
        return self.__points

    def append(self, point: QtCore.QPointF):
        self.__points.append(point)

    def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
              widget: typing.Optional[QtWidgets.QWidget] = ...) -> None:
        painter.setPen(pen)
        painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
        path = QtGui.QPainterPath()
        path.moveTo(self.getPoints()[0])
        for point in self.getPoints():
            path.lineTo(point)
        painter.drawPath(path)

class Producer(QtCore.QObject):

    def __init__(self, parent=None):
        super(Producer, self).__init__(parent)
        self.__last_point = QtCore.QPointF(10, 0)
        self.__point = QtCore.QPointF(10, 0)
        self.upper = 10

    def setPoint(self) -> None:
        self.setLastpoint(self.getPoint())
        x = random.randint(0, self.upper)
        y = random.randint(0, self.upper)
        self.upper += 50  # increases the range of probability at the upper limit
        self.__point = QtCore.QPointF(x, y)  # produce a new random point

    def getPoint(self) -> QtCore.QPointF:
        return self.__point

    def setLastpoint(self, point: QtCore.QPointF):
        self.__last_point = point

    def getLastPoint(self) -> QtCore.QPointF:
        return self.__last_point


class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        central_widget = QtWidgets.QWidget()
        self.setMinimumHeight(500)
        self.setMinimumWidth(500)
        self.scene = QtWidgets.QGraphicsScene(self)
        self.view = QtWidgets.QGraphicsView(self.scene)
        self.view.setSceneRect(self.view.mapToScene(self.view.viewport().rect()).boundingRect())
        self.btn = QtWidgets.QPushButton('Get Track')
        self.btn.clicked.connect(self.getTrack)
        self.producer = Producer()
        hbox = QtWidgets.QHBoxLayout(central_widget)
        hbox.addWidget(self.view)
        hbox.addWidget(self.btn)
        self.track = Track()
        self.scene.addItem(self.track)
        self.setCentralWidget(central_widget)

    def getTrack(self):
        self.producer.setPoint()
        self.track.append(self.producer.getPoint())
        dx = self.producer.getPoint().x() - self.producer.getLastPoint().x()
        dy = self.producer.getPoint().y() - self.producer.getLastPoint().y()
        self.view.setSceneRect(self.view.sceneRect().translated(dx, dy))


class Window2(QtWidgets.QMainWindow):
    def __init__(self):
        super(Window2, self).__init__()
        central_widget = QtWidgets.QWidget()
        self.setMinimumHeight(500)
        self.setMinimumWidth(500)
        self.scene = QtWidgets.QGraphicsScene(self)
        self.view = QtWidgets.QGraphicsView(self.scene)
        self.view.setSceneRect(self.view.mapToScene(self.view.viewport().rect()).boundingRect())
        self.btn = QtWidgets.QPushButton('Get Track')
        self.btn.clicked.connect(self.getTrack)
        self.producer = Producer()
        hbox = QtWidgets.QHBoxLayout(central_widget)
        hbox.addWidget(self.view)
        hbox.addWidget(self.btn)
        self.setCentralWidget(central_widget)

    def getTrack(self):
        self.producer.setPoint()
        self.scene.addLine(QtCore.QLineF(self.producer.getPoint(), self.producer.getLastPoint()), pen=pen)
        dx = self.producer.getPoint().x() - self.producer.getLastPoint().x()
        dy = self.producer.getPoint().y() - self.producer.getLastPoint().y()
        self.view.setSceneRect(self.view.sceneRect().translated(dx, dy))


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    # w = Window()
    w = Window2()
    w.show()
    sys.exit(app.exec_())

Solution

  • Using the multiple segments is obviously not the right choice, as lines are considered individual elements, and it's not possible to draw them without the collapsing edges.

    The most important problem about the QPainterPath method is that, while you're using a QGraphicsPathItem, you're not using it at all since you're overriding its paint() method.
    Manually drawing the path makes it completely useless, and you should use its setPath() instead.

    Your implementation is also wrong, because it does not consider the rest of the path, and uses incremental translation. As already suggested in a comment to another question of yours, incremental transformations are usually not a good idea as they are often misleading and usually lead to unexpected behavior (exactly like in this case).

    The solution is to correctly use the QGraphicsPathItem:

    
    class Track(QtWidgets.QGraphicsPathItem):
        def __init__(self, parent=None, offset: float = 50):
            super(Track, self).__init__(parent)  # initiate the parent class
            self.__points = [QtCore.QPointF(0, 0)]
            self.setPen(pen)
    
        def getPoints(self) -> list:
            return self.__points
    
        def append(self, point: QtCore.QPointF):
            self.__points.append(point)
            path = self.path()
            path.lineTo(point)
            self.setPath(path)
    
        # no paint method override!
    

    Then, in order to ensure that the item is always visible, you can use the existing ensureVisible(item) or the centerOn(item) methods; note that I also removed the first setSceneRect() call, and this is because leaving it to the default ensures that the view's sceneRect is always updated to the scene's sceneRect, which defaults to the items' bounding rectangle unless explicitly specified:

    class Window(QtWidgets.QMainWindow):
        def __init__(self):
            # ...
            # remove the following!
            # self.view.setSceneRect(self.view.mapToScene(self.view.viewport().rect()).boundingRect())
            # set the antialiasing for the whole view
            self.view.setRenderHints(QtGui.QPainter.HighQualityAntialiasing)
    
        def getTrack(self):
            self.producer.setPoint()
            self.track.append(self.producer.getPoint())
            self.view.centerOn(self.track)
            # no setSceneRect() even here
    

    Finally, you really shouldn't care that much about memory usage. QPoints have a very small memory footprint, and if you're worried that the system could not support that, then you're out of track: you would need about 20 thousand points only to get just one single megabyte of memory.

    Note unrelated to the question: I'd avoid unnecessary overuse of type hinting. Python will always remain a dynamically typed language, and while it's not considered bad practice to use them, using them too much and everywhere is just distracting, especially when dealing with overridden methods that are internally used by a library/framework; for instance, type hinting in paint() is completely uncalled for, as you can be sure that all arguments will always be called with the correct types