Search code examples
pythonpyqtpyqt5qgraphicsviewqgraphicsscene

create a Qt display


I'm making an project that will use an arrow in a field as shown bellow.

enter image description here

I want the object to move as it receives some data for movement - such as coordinates from a satellite.

Questions are:

  • How do I create the object arrow?
  • What libraries should I use? should I use the QGraphicsScene Class to move it?.

I've never worked exactly with this type of Qt object. I'd started coding in Python with PyQt5, but with the lack of examples using this library in this language - I started studying C++ to understand Qt5 library better.


Solution

  • The advantage of Qt is that it allows you to create visual elements using low-level tools (like QPainter) to high-level tools (like QGraphicsItem, QML items). In this case, the easiest thing is to use QGraphicsItem based on a QGraphicsPathItem and override the paint method for custom painting. For the case of movement you just have to use the setPos() method, but to obtain a smooth movement then a QVariantAnimation must be used.

    import random
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class ArrowItem(QtWidgets.QGraphicsPathItem):
        def __init__(self, parent=None):
            super().__init__(parent)
            self._length = -1
            self._points = QtCore.QPointF(), QtCore.QPointF(), QtCore.QPointF()
    
            self.length = 40.0
    
        @property
        def length(self):
            return self._length
    
        @length.setter
        def length(self, l):
            self._length = l
    
            pos_top = QtCore.QPointF(0, l * 4 / 5)
            pos_left = QtCore.QPointF(-l * 3 / 5, -l / 5)
            pos_right = QtCore.QPointF(
                l * 3 / 5,
                -l / 5,
            )
    
            path = QtGui.QPainterPath()
            path.moveTo(pos_top)
            path.lineTo(pos_right)
            path.lineTo(pos_left)
    
            self.setPath(path)
    
            self._points = pos_top, pos_left, pos_right
    
        def paint(self, painter, option, widget):
    
            pos_top, pos_left, pos_right = self._points
    
            left_color = QtGui.QColor("#cc0000")
            right_color = QtGui.QColor("#ff0000")
            bottom_color = QtGui.QColor("#661900")
    
            path_left = QtGui.QPainterPath()
            path_left.lineTo(pos_top)
            path_left.lineTo(pos_left)
    
            path_right = QtGui.QPainterPath()
            path_right.lineTo(pos_top)
            path_right.lineTo(pos_right)
    
            path_bottom = QtGui.QPainterPath()
            path_bottom.lineTo(pos_left)
            path_bottom.lineTo(pos_right)
    
            painter.setPen(QtGui.QColor("black"))
            painter.setBrush(left_color)
            painter.drawPath(path_left)
            painter.setBrush(right_color)
            painter.drawPath(path_right)
            painter.setBrush(bottom_color)
            painter.drawPath(path_bottom)
    
        def moveTo(self, next_position, duration=100):
            self._animation = QtCore.QVariantAnimation()
            self._animation.setStartValue(self.pos())
            self._animation.setEndValue(next_position)
            self._animation.setDuration(duration)
            self._animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
            self._animation.valueChanged.connect(self.setPos)
    
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self.scene = QtWidgets.QGraphicsScene()
            view = QtWidgets.QGraphicsView()
            view.setRenderHints(QtGui.QPainter.Antialiasing)
            view.setScene(self.scene)
            view.scale(1, -1)
            button = QtWidgets.QPushButton("Click me")
    
            central_widget = QtWidgets.QWidget()
            lay = QtWidgets.QVBoxLayout(central_widget)
            lay.addWidget(view)
            lay.addWidget(button)
            self.setCentralWidget(central_widget)
            self.resize(640, 480)
    
            self.arrow_item = ArrowItem()
            self.scene.addItem(self.arrow_item)
    
            button.clicked.connect(self.handle_clicked)
    
        def handle_clicked(self):
            x, y = random.sample(range(300), 2)
            print("next position:", x, y)
            self.arrow_item.moveTo(QtCore.QPointF(x, y))
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication([])
    
        w = MainWindow()
        w.show()
    
        app.exec_()
    

    Note: PyQt5 (and also PySide2) offer many examples written in python that are the translation of the examples in C++ so I recommend you download the source code(PyQt5 and PySide2) and study them. I also have a repo with translations of some examples.