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:
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:
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?
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: