如何在 pyqt4 中的行中添加箭头?

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

我得到了这个代码:

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

画这个的window:

如何将箭头添加到线的一端,因为这些我用图像编辑器在最后一张图像上绘制:

我找到了这个 C++ 教程 http://www.codeproject.com/Articles/3274/Drawing-Arrows 和这个伪代码:

// 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

此外,我还可以找到另一个问题 ,但它是针对 qt5 的。因此在pyqt4中用多边形绘制箭头是不是更好的方法?

我遇到了同样的问题,所以经过一些工作我想到了这个。

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

此代码适用于任何类型的路径,包括贝塞尔曲线、正方形等。如果您想更改箭头位置,您应该将 path.PointAtPercent() 值更改为 0 和 [=14 之间的任何值=].例如,如果你想在线条的中间绘制箭头,使用 self.arrowCalc(path.pointAtPercent(0.5), path.pointAtPercent(0.51))。此外,当您将点传递给 arrowCalc 时,请确保源点和目标点接近。

额外:

如果你想测试方形和贝塞尔曲线路径(用下面的方法替换直接路径方法):

     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

输出:

@Art 的回答提供了一个绘制箭头的解决方案 --> 受他的代码启发,我找到了一个解决方法来绘制像这样的箭头 ⇨。希望对您有所帮助。

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)