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:
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:
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_())
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