'How to add a arrow head to my line in pyqt4?

I got this code:

from PyQt4 import QtGui, QtCore

class MyFrame(QtGui.QGraphicsView):
    def __init__( self, parent = None ):
        super(MyFrame, self).__init__(parent)

        scene = QtGui.QGraphicsScene()
        self.setScene(scene)
        self.resize( 400, 240 )

        # http://pyqt.sourceforge.net/Docs/PyQt4/qpen.html
        pencil = QtGui.QPen( QtCore.Qt.black, 2)
        pencil.setStyle( QtCore.Qt.SolidLine )

        # pencil.setStyle( QtCore.Qt.UpArrow )
        scene.addLine( QtCore.QLineF( 0, 0, 100, 100 ), pencil )

if ( __name__ == '__main__' ):
    app = QtGui.QApplication([])
    f = MyFrame()
    f.show()
    app.exec_()

Which draw this window:

enter image description here

How to add a arrow to one of the ends of the line as these I draw over the last image with a image editor:

enter image description here

I found this tutorial for C++ http://www.codeproject.com/Articles/3274/Drawing-Arrows with this pseudocode:

// ARROWSTRUCT
//
// Defines the attributes of an arrow.
typedef struct tARROWSTRUCT {
    int nWidth;     // width (in pixels) of the full base of the arrowhead
    float fTheta;   // angle (in radians) at the arrow tip between the two
                    //  sides of the arrowhead
    bool bFill;     // flag indicating whether or not the arrowhead should be
                    //  filled
} ARROWSTRUCT;

// ArrowTo()
//
// Draws an arrow, using the current pen and brush, from the current position
//  to the passed point using the attributes defined in the ARROWSTRUCT.
void ArrowTo(HDC hDC, int x, int y, ARROWSTRUCT *pArrow);
void ArrowTo(HDC hDC, const POINT *lpTo, ARROWSTRUCT *pArrow);

Simply fill an ARROWSTRUCT with the desired attributes, make sure the current DC position is correct (MoveTo(), etc.), and call one of the two ArrowTo() functions. The size parameters (nWidth and fTheta) are defined as follows:

Technique

This goes back to high-school algebra and trigonometry. The ArrowTo() function first builds a vector of the full line. Then it calculates the points for the sides of the arrowhead based on the nWidth and fTheta attributes you pass. Badda-boom-badda-bing, you got your arrowhead.

Here's some pseudo-pseudocode:

lineVector = toPoint - fromPoint
lineLength = length of lineVector

// calculate point at base of arrowhead
tPointOnLine = nWidth / (2 * (tanf(fTheta) / 2) * lineLength);
pointOnLine = toPoint + -tPointOnLine * lineVector

// calculate left and right points of arrowhead
normalVector = (-lineVector.y, lineVector.x)
tNormal = nWidth / (2 * lineLength)
leftPoint = pointOnLine + tNormal * normalVector
rightPoint = pointOnLine + -tNormal * normalVector

Moreover I could also find this other question Drawing a polygon in PyQt but it is for qt5. Therefore is it a better way to draw the arrows with polygons in pyqt4?



Solution 1:[1]

I had the same problem so after some work I came up with this.

import math, sys
from PyQt5 import QtWidgets, QtCore, QtGui


class Path(QtWidgets.QGraphicsPathItem):
    def __init__(self, source: QtCore.QPointF = None, destination: QtCore.QPointF = None, *args, **kwargs):
        super(Path, self).__init__(*args, **kwargs)

        self._sourcePoint = source
        self._destinationPoint = destination

        self._arrow_height = 5
        self._arrow_width = 4

    def setSource(self, point: QtCore.QPointF):
        self._sourcePoint = point

    def setDestination(self, point: QtCore.QPointF):
        self._destinationPoint = point

    def directPath(self):
        path = QtGui.QPainterPath(self._sourcePoint)
        path.lineTo(self._destinationPoint)
        return path

    def arrowCalc(self, start_point=None, end_point=None):  # calculates the point where the arrow should be drawn

        try:
            startPoint, endPoint = start_point, end_point

            if start_point is None:
                startPoint = self._sourcePoint

            if endPoint is None:
                endPoint = self._destinationPoint

            dx, dy = startPoint.x() - endPoint.x(), startPoint.y() - endPoint.y()

            leng = math.sqrt(dx ** 2 + dy ** 2)
            normX, normY = dx / leng, dy / leng  # normalize

            # perpendicular vector
            perpX = -normY
            perpY = normX

            leftX = endPoint.x() + self._arrow_height * normX + self._arrow_width * perpX
            leftY = endPoint.y() + self._arrow_height * normY + self._arrow_width * perpY

            rightX = endPoint.x() + self._arrow_height * normX - self._arrow_width * perpX
            rightY = endPoint.y() + self._arrow_height * normY - self._arrow_width * perpY

            point2 = QtCore.QPointF(leftX, leftY)
            point3 = QtCore.QPointF(rightX, rightY)

            return QtGui.QPolygonF([point2, endPoint, point3])

        except (ZeroDivisionError, Exception):
            return None

    def paint(self, painter: QtGui.QPainter, option, widget=None) -> None:

        painter.setRenderHint(painter.Antialiasing)

        painter.pen().setWidth(2)
        painter.setBrush(QtCore.Qt.NoBrush)

        path = self.directPath()
        painter.drawPath(path)
        self.setPath(path)

        triangle_source = self.arrowCalc(path.pointAtPercent(0.1), self._sourcePoint)  # change path.PointAtPercent() value to move arrow on the line

        if triangle_source is not None:
            painter.drawPolyline(triangle_source)


class ViewPort(QtWidgets.QGraphicsView):

    def __init__(self):
        super(ViewPort, self).__init__()

        self.setViewportUpdateMode(self.FullViewportUpdate)

        self._isdrawingPath = False
        self._current_path = None

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:

        if event.button() == QtCore.Qt.LeftButton:

            pos = self.mapToScene(event.pos())
            self._isdrawingPath = True
            self._current_path = Path(source=pos, destination=pos)
            self.scene().addItem(self._current_path)

            return

        super(ViewPort, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):

        pos = self.mapToScene(event.pos())

        if self._isdrawingPath:
            self._current_path.setDestination(pos)
            self.scene().update(self.sceneRect())
            return

        super(ViewPort, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:

        pos = self.mapToScene(event.pos())

        if self._isdrawingPath:
            self._current_path.setDestination(pos)
            self._isdrawingPath = False
            self._current_path = None
            self.scene().update(self.sceneRect())
            return

        super(ViewPort, self).mouseReleaseEvent(event)


def main():
    app = QtWidgets.QApplication(sys.argv)

    window = ViewPort()
    scene = QtWidgets.QGraphicsScene()
    window.setScene(scene)
    window.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

This code will work with any kind of path including bezier, square etc. If you want to change the arrow position you should change the path.PointAtPercent() value to anywhere between 0 and 1. For example if you want to draw arrow in the middle of the line use self.arrowCalc(path.pointAtPercent(0.5), path.pointAtPercent(0.51)). Also, when you pass points to arrowCalc make sure that source and destination points are close.

Extra:

If you want to test square and bezier path (replace the direct path method with below methods):

     def squarePath(self):
        s = self._sourcePoint
        d = self._destinationPoint

        mid_x = s.x() + ((d.x() - s.x()) * 0.5)

        path = QtGui.QPainterPath(QtCore.QPointF(s.x(), s.y()))
        path.lineTo(mid_x, s.y())
        path.lineTo(mid_x, d.y())
        path.lineTo(d.x(), d.y())

        return path

    def bezierPath(self):
        s = self._sourcePoint
        d = self._destinationPoint

        source_x, source_y = s.x(), s.y()
        destination_x, destination_y = d.x(), d.y()

        dist = (d.x() - s.x()) * 0.5

        cpx_s = +dist
        cpx_d = -dist
        cpy_s = 0
        cpy_d = 0

        if (s.x() > d.x()) or (s.x() < d.x()):
            cpx_d *= -1
            cpx_s *= -1

            cpy_d = (
                            (source_y - destination_y) / math.fabs(
                        (source_y - destination_y) if (source_y - destination_y) != 0 else 0.00001
                    )
                    ) * 150

            cpy_s = (
                            (destination_y - source_y) / math.fabs(
                        (destination_y - source_y) if (destination_y - source_y) != 0 else 0.00001
                    )
                    ) * 150

        path = QtGui.QPainterPath(self._sourcePoint)

        path.cubicTo(destination_x + cpx_d, destination_y + cpy_d, source_x + cpx_s, source_y + cpy_s,
                     destination_x, destination_y)

        return path

Output:

enter image description here

Solution 2:[2]

The answer by @Art provide a solution to draw arrows like -->, inspired by his code, I find a work around to draw arrows like this ?. Hope this is helpful to you.

from PyQt5 import QtWidgets, QtCore, QtGui
import math

#  draw an arrow like this
#                           |\
#                ___   _____| \
#    length_width |   |        \  _____
#                _|_  |_____   /    |
#                           | /     | arrow_width
#                           |/    __|__
#
#                           |<->|
#                        arrow_height    
class Arrow(QtWidgets.QGraphicsPathItem):
    def __init__(self, source: QtCore.QPointF, destination: QtCore.QPointF, arrow_height, arrow_width, length_width, *args, **kwargs):
        super(Arrow, self).__init__(*args, **kwargs)

        self._sourcePoint = source
        self._destinationPoint = destination

        self._arrow_height = arrow_height
        self._arrow_width = arrow_width
        self._length_width = length_width

    def arrowCalc(self, start_point=None, end_point=None):  # calculates the point where the arrow should be drawn

        try:
            startPoint, endPoint = start_point, end_point

            if start_point is None:
                startPoint = self._sourcePoint

            if endPoint is None:
                endPoint = self._destinationPoint

            dx, dy = startPoint.x() - endPoint.x(), startPoint.y() - endPoint.y()

            leng = math.sqrt(dx ** 2 + dy ** 2)
            normX, normY = dx / leng, dy / leng  # normalize
            
            # parallel vector (normX, normY)
            # perpendicular vector (perpX, perpY)
            perpX = -normY
            perpY = normX
            
            
            #           p2
            #           |\
            #    p4____p5 \
            #     |        \ endpoint
            #    p7____p6  /
            #           | /
            #           |/
            #          p3
            point2 = endPoint + QtCore.QPointF(normX, normY) * self._arrow_height + QtCore.QPointF(perpX, perpY) * self._arrow_width
            point3 = endPoint + QtCore.QPointF(normX, normY) * self._arrow_height - QtCore.QPointF(perpX, perpY) * self._arrow_width

            point4 = startPoint + QtCore.QPointF(perpX, perpY) * self._length_width
            point5 = endPoint + QtCore.QPointF(normX, normY) * self._arrow_height + QtCore.QPointF(perpX, perpY) * self._length_width
            point6 = endPoint + QtCore.QPointF(normX, normY) * self._arrow_height - QtCore.QPointF(perpX, perpY) * self._length_width
            point7 = startPoint - QtCore.QPointF(perpX, perpY) * self._length_width

            return QtGui.QPolygonF([point4, point5, point2, endPoint, point3, point6, point7])

        except (ZeroDivisionError, Exception):
            return None

    def paint(self, painter: QtGui.QPainter, option, widget=None) -> None:
        painter.setRenderHint(painter.Antialiasing)

        my_pen = QtGui.QPen()
        my_pen.setWidth(1)
        my_pen.setCosmetic(False)
        my_pen.setColor(QtGui.QColor(255, 0, 0, 100))
        painter.setPen(my_pen)

        arrow_polygon = self.arrowCalc()
        if arrow_polygon is not None:
            # painter.drawPolyline(arrow_polygon)
            painter.drawPolygon(arrow_polygon)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2