from PyQt5.QtGui import QPen, QPainterPath, QBrush, QPainterPathStroker, QPainter, QCursor from PyQt5.QtWidgets import QGraphicsItem, QGraphicsPathItem from PyQt5.QtCore import Qt, QPointF, QRectF class Grabber(QGraphicsPathItem): """ Extends QGraphicsPathItem to create grabber for line for moving a particular segment """ circle = QPainterPath() circle.addEllipse(QRectF(-5, -5, 10, 10)) def __init__(self, annotation_line, index, direction): super(Grabber, self).__init__() self.m_index = index self.m_annotation_item = annotation_line self._direction = direction self.setPath(Grabber.circle) # set graphical settings for this item self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setAcceptHoverEvents(True) self.pen = QPen(Qt.white, -1, Qt.SolidLine) self.brush = QBrush(Qt.transparent) def itemChange(self, change, value): """ move position of grabber after resize""" if change == QGraphicsItem.ItemPositionChange and self.isEnabled(): p = QPointF(self.pos()) if self._direction == Qt.Horizontal: p.setX(value.x()) elif self._direction == Qt.Vertical: p.setY(value.y()) movement = p - self.pos() self.m_annotation_item.movePoints(self.m_index, movement) return p return super(Grabber, self).itemChange(change, value) def paint(self, painter, option, widget): """paints the path of grabber only if it is selected """ if self.isSelected() and not self.m_annotation_item.isSelected() : # show parent line of grabber self.m_annotation_item.setSelected(True) painter.setBrush(self.brush) painter.setPen(self.pen) painter.drawPath(self.path()) # To paint path of shape # color = Qt.red if self.isSelected() else Qt.black # painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine)) # painter.drawPath(self.shape()) def shape(self): """Overrides shape method and set shape to segment on which grabber is located""" index = self.m_index startPoint = QPointF(self.m_annotation_item.path().elementAt(index)) endPoint = QPointF(self.m_annotation_item.path().elementAt(index + 1)) startPoint = self.mapFromParent(startPoint) endPoint = self.mapFromParent(endPoint) path = QPainterPath(startPoint) path.lineTo(endPoint) # generate outlines for path stroke = QPainterPathStroker() stroke.setWidth(8) return stroke.createStroke(path) def boundingRect(self): return self.shape().boundingRect() def mousePressEvent(self, event): print('grabber clicked', self) super(Grabber, self).mousePressEvent(event) def hoverEnterEvent(self, event): """ Changes cursor to horizontal movement or vertical movement depending on the direction of the grabber on mouse enter """ if self._direction == Qt.Horizontal: self.setCursor(QCursor(Qt.SplitHCursor)) else: self.setCursor(QCursor(Qt.SplitVCursor)) super(Grabber, self).hoverEnterEvent(event) def hoverLeaveEvent(self, event): """ reverts cursor to default on mouse leave """ self.setCursor(QCursor(Qt.ArrowCursor)) super(Grabber, self).hoverLeaveEvent(event) def show(self): self.pen = QPen(Qt.black, 2, Qt.SolidLine) self.brush = QBrush(Qt.cyan) def hide(self): self.pen = QPen(Qt.white, -1, Qt.SolidLine) self.brush = QBrush(Qt.transparent) class Line(QGraphicsPathItem): """ Extends QGraphicsPathItem to draw zig-zag line consisting of multiple points """ penStyle = Qt.SolidLine def __init__(self, startPoint, endPoint, **args): QGraphicsItem.__init__(self, **args) self.startPoint = startPoint self.endPoint = endPoint #stores all points of line self.points = [] self.points.extend([startPoint, endPoint]) self.startGripItem = None self.endGripItem = None self._selected = False self.m_grabbers = [] # stores current pen style of line self.penStyle = Line.penStyle # set graphical settings for this item self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setAcceptHoverEvents(True) # initiates path self.createPath() def createPath(self): """ creates initial path and stores it's points :return: """ offset = 30 x0, y0 = self.startPoint.x(), self.startPoint.y() x1, y1 = self.endPoint.x(), self.endPoint.y() # create line is in process self.points = [self.startPoint, QPointF((x0 + x1) / 2, y0), QPointF((x0 + x1) / 2, y1), self.endPoint] # final path of line if self.startGripItem and self.endGripItem: # determine ns (point next to start) item = self.startGripItem self.startPoint = item.parentItem().mapToScene(item.pos()) if item.m_location == "top": ns = QPointF(self.startPoint.x(), self.startPoint.y() - offset) elif item.m_location == "left": ns = QPointF(self.startPoint.x() - offset, self.startPoint.y()) elif item.m_location == "bottom": ns = QPointF(self.startPoint.x(), self.startPoint.y() + offset) else: ns = QPointF(self.startPoint.x() + offset, self.startPoint.y()) # pe (point previous to end) item = self.endGripItem self.endPoint = item.parentItem().mapToScene(item.pos()) if item.m_location == "top": pe = QPointF(self.endPoint.x(), self.endPoint.y() - offset) elif item.m_location == "left": pe = QPointF(self.endPoint.x() - offset, self.endPoint.y()) elif item.m_location == "bottom": pe = QPointF(self.endPoint.x(), self.endPoint.y() + offset) else: pe = QPointF(self.endPoint.x() + offset, self.endPoint.y()) start = self.startPoint end = self.endPoint sheight = self.startGripItem.m_annotation_item.boundingRect().height() / 2 swidth = self.startGripItem.m_annotation_item.boundingRect().width() / 2 eheight = self.endGripItem.m_annotation_item.boundingRect().height() / 2 ewidth = self.endGripItem.m_annotation_item.boundingRect().width() / 2 if self.startGripItem.m_location in ["right"]: if self.endGripItem.m_location in ["top"]: if start.x() + offset < end.x() - ewidth: if start.y() + offset < end.y(): self.points = [start, QPointF(end.x(), start.y()), end] else: self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] elif start.x() - 2 * swidth > end.x(): if start.y() + sheight + offset < end.y(): self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] elif start.y() - sheight - offset < end.y(): self.points = [start, ns, QPointF(ns.x(), ns.y() - sheight - offset), QPointF(pe.x(), ns.y() - sheight - offset), end] else: self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] else: self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] if start.y() > end.y(): x = max(end.x() + ewidth + offset, ns.x()) self.points = [start, QPointF(x, start.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["bottom"]: if start.x() + offset < end.x() - ewidth: if start.y() + offset < end.y(): self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] else: self.points = [start, QPointF(end.x(), start.y()), end] elif start.x() - 2 * swidth > end.x(): if start.y() + sheight + offset < end.y(): self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] elif start.y() - sheight - offset < end.y(): y = max(pe.y(), start.y() + sheight + offset) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), end] else: self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] else: self.points = [start, ns, QPointF(ns.x(), pe.y()), pe, end] if start.y() < end.y(): x = max(end.x() + ewidth + offset, ns.x()) self.points = [start, QPointF(x, start.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["right"]: x = max(start.x() + offset, pe.x()) self.points = [start, QPointF(x, start.y()), QPointF(x, end.y()), end] if start.x() + offset < end.x() - ewidth: if start.y() + offset > end.y() - eheight and end.y() >= start.y(): self.points = [start, ns, QPointF(ns.x(), pe.y() - eheight), QPointF(pe.x(), pe.y() - eheight), pe, end] elif start.y() - offset < end.y() + eheight and end.y() <= start.y(): self.points = [start, ns, QPointF(ns.x(), pe.y() + eheight), QPointF(pe.x(), pe.y() + eheight), pe, end] elif start.y() - sheight - offset < end.y() < start.y() + sheight + offset: if end.y() < start.y(): self.points = [start, ns, QPointF(ns.x(), ns.y() - sheight), QPointF(pe.x(), ns.y() - sheight), pe, end] else: self.points = [start, ns, QPointF(ns.x(), ns.y() + sheight), QPointF(pe.x(), ns.y() + sheight), pe, end] elif self.endGripItem.m_location in ["left"]: self.points = [start, QPointF((start.x() + end.x()) / 2, start.y()), QPointF((start.x() + end.x()) / 2, end.y()), end] if end.x() < start.x() + offset: if end.y() + eheight <= start.y() - sheight - offset: self.points = [start, ns, QPointF(ns.x(), ns.y() - sheight), QPointF(pe.x(), ns.y() - sheight), pe, end] elif end.y() - eheight >= start.y() + sheight + offset: self.points = [start, ns, QPointF(ns.x(), ns.y() + sheight), QPointF(pe.x(), ns.y() + sheight), pe, end] elif end.y() <= start.y(): y = min(end.y() - eheight, start.y() - sheight) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] else: y = max(end.y() + eheight, start.y() + sheight) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] elif self.startGripItem.m_location in ["left"]: if self.endGripItem.m_location in ["top"]: if start.x() + offset < end.x() - ewidth: if end.y() > start.y() + sheight + offset: self.points = [start, ns, QPointF(ns.x(), ns.y() + sheight), QPointF(pe.x(), ns.y() + sheight), end] else: y = min(start.y() - sheight, pe.y()) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), end] elif end.x() + ewidth >= start.x() - offset: x = min(ns.x(), end.x() - ewidth) self.points = [start, QPointF(x, ns.y()), QPointF(x, pe.y()), pe, end] else: if end.y() >= start.y() + offset: self.points = [start, QPointF(end.x(), start.y()), end] else: x = (start.x() + end.x()) / 2 self.points = [start, QPointF(x, start.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["bottom"]: if start.x() + offset < end.x() - ewidth: if end.y() < start.y() - sheight - offset: self.points = [start, ns, QPointF(ns.x(), ns.y() - sheight), QPointF(pe.x(), ns.y() - sheight), end] else: y = max(start.y() + sheight, pe.y()) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), end] elif end.x() + ewidth >= start.x() - offset: x = min(ns.x(), end.x() - ewidth) self.points = [start, QPointF(x, ns.y()), QPointF(x, pe.y()), pe, end] else: if end.y() <= start.y() - offset: self.points = [start, QPointF(end.x(), start.y()), end] else: x = (start.x() + end.x()) / 2 self.points = [start, QPointF(x, start.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["right"]: self.points = [start, QPointF((start.x() + end.x()) / 2, start.y()), QPointF((start.x() + end.x()) / 2, end.y()), end] if end.x() > start.x() + offset: if end.y() + eheight <= start.y() - sheight - offset: self.points = [start, ns, QPointF(ns.x(), ns.y() - sheight), QPointF(pe.x(), ns.y() - sheight), pe, end] elif end.y() - eheight >= start.y() + sheight + offset: self.points = [start, ns, QPointF(ns.x(), ns.y() + sheight), QPointF(pe.x(), ns.y() + sheight), pe, end] elif end.y() <= start.y(): y = min(end.y() - eheight, start.y() - sheight) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] else: y = max(end.y() + eheight, start.y() + sheight) self.points = [start, ns, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] elif self.endGripItem.m_location in ["left"]: self.points = [start, QPointF(pe.x(), start.y()), pe, end] if start.x() + offset < end.x(): self.points = [start, ns, QPointF(ns.x(), end.y()), end] if start.y() + sheight + offset > end.y() > start.y() - sheight - offset: self.points = [start, ns, QPointF(ns.x(), ns.y() - sheight), QPointF(pe.x(), ns.y() - sheight), pe, end] elif end.y() - eheight - offset < start.y() < end.y() + eheight + offset: if end.y() > start.y(): self.points = [start, ns, QPointF(ns.x(), pe.y() - eheight), QPointF(pe.x(), pe.y() - eheight), pe, end] else: self.points = [start, ns, QPointF(ns.x(), pe.y() + eheight), QPointF(pe.x(), pe.y() + eheight), pe, end] elif self.startGripItem.m_location in ["top"]: if self.endGripItem.m_location in ["top"]: self.points = [self.startPoint, QPointF(start.x(), pe.y()), pe, self.endPoint] if start.y() < end.y(): self.points = [self.startPoint, ns, QPointF(pe.x(), ns.y()), self.endPoint] if start.x() + swidth > end.x() > start.x() - swidth or end.x() + ewidth > start.x() > end.x() - ewidth: x = max(start.x() + swidth, end.x() + ewidth) x += offset if start.x() > end.x(): x = min(start.x() - swidth, end.x() - ewidth) x -= offset self.points = [start, ns, QPointF(x, ns.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["bottom"]: self.points = [self.startPoint, ns, QPointF((x0 + x1) / 2, ns.y()), QPointF((x0 + x1) / 2, pe.y()), pe, self.endPoint] if start.y() - offset > end.y(): self.points = [start, QPointF(start.x(), (y0 + y1) / 2), QPointF(end.x(), (y0 + y1) / 2), self.endPoint] elif start.x() + swidth > end.x() > start.x() - swidth or end.x() + ewidth > start.x() > end.x() - ewidth: x = max(start.x() + swidth, end.x() + ewidth) x += offset if start.x() > end.x(): x = min(start.x() - swidth, end.x() - ewidth) x -= offset self.points = [start, ns, QPointF(x, ns.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["right"]: y = min(ns.y(), end.y() + eheight + offset) self.points = [start, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] if start.x() - swidth - offset < end.x() < start.x() + swidth + offset and end.y() > start.y() + offset: self.points = [start, ns, QPointF(ns.x() + swidth + offset, ns.y()), QPointF(ns.x() + swidth + offset, pe.y()), end] elif end.y() - eheight < start.y() - offset < end.y() + eheight: self.points = [start, ns, QPointF(start.x(), end.y() - eheight), QPointF(pe.x(), end.y() - eheight), pe, end] elif self.endGripItem.m_location in ["left"]: y = min(ns.y(), end.y() + eheight + offset) self.points = [start, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] if start.x() - swidth - offset < end.x() < start.x() + swidth + offset and end.y() > start.y() + offset: self.points = [start, ns, QPointF(ns.x() - swidth - offset, ns.y()), QPointF(ns.x() - swidth - offset, pe.y()), end] elif end.y() - eheight < start.y() - offset < end.y() + eheight: self.points = [start, ns, QPointF(start.x(), end.y() - eheight), QPointF(pe.x(), end.y() - eheight), pe, end] elif self.startGripItem.m_location in ["bottom"]: if self.endGripItem.m_location in ["top"]: self.points = [self.startPoint, ns, QPointF((x0 + x1) / 2, ns.y()), QPointF((x0 + x1) / 2, pe.y()), pe, self.endPoint] if start.y() < end.y(): self.points = [self.startPoint, ns, QPointF(pe.x(), ns.y()), self.endPoint] if start.x() + swidth > end.x() > start.x() - swidth or end.x() + ewidth > start.x() > end.x() - ewidth: x = max(start.x() + swidth, end.x() + ewidth) x += offset self.points = [start, ns, QPointF(x, ns.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["bottom"]: self.points = [self.startPoint, ns, QPointF((x0 + x1) / 2, ns.y()), QPointF((x0 + x1) / 2, pe.y()), pe, self.endPoint] if start.x() + swidth > end.x() > start.x() - swidth or end.x() + ewidth > start.x() > end.x() - ewidth: x = max(start.x() + swidth, end.x() + ewidth) x += offset self.points = [start, ns, QPointF(x, ns.y()), QPointF(x, pe.y()), pe, end] elif self.endGripItem.m_location in ["right"]: y = max(ns.y(), end.y() + eheight + offset) self.points = [start, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] if start.x() - swidth - offset < end.x() < start.x() + swidth + offset: self.points = [start, ns, QPointF(ns.x() + swidth + offset, ns.y()), QPointF(ns.x() + swidth + offset, pe.y()), end] elif self.endGripItem.m_location in ["left"]: y = max(ns.y(), end.y() + eheight + offset) self.points = [start, QPointF(ns.x(), y), QPointF(pe.x(), y), pe, end] if start.x() - swidth - offset < end.x() < start.x() + swidth + offset: self.points = [start, ns, QPointF(ns.x() - swidth - offset, ns.y()), QPointF(ns.x() - swidth - offset, pe.y()), end] # draw line path = QPainterPath(self.startPoint) for i in range(1, len(self.points)): path.lineTo(self.points[i]) self.setPath(path) if self.endGripItem: self.addGrabber() def updatePath(self): """ update path when svg item moves """ path = QPainterPath(self.startPoint) self.updatePoints() for i in range(1, len(self.points) - 1): path.lineTo(self.points[i]) path.lineTo(self.endPoint) self.setPath(path) def updatePoints(self): """ updates points of line when grabber is moved :return: """ if self.startGripItem.m_location in ["left", "right"]: point = self.points[1] self.points[1] = QPointF(point.x(), self.startPoint.y()) if self.endGripItem.m_location in ["left", "right"]: point = self.points[len(self.points) - 2] self.points[len(self.points) - 2] = QPointF(point.x(), self.endPoint.y()) else: point = self.points[len(self.points) - 2] self.points[len(self.points) - 2] = QPointF(self.endPoint.x(), point.y()) else: point = self.points[1] self.points[1] = QPointF(self.startPoint.x(), point.y()) if self.endGripItem.m_location in ["left", "right"]: point = self.points[len(self.points) - 2] self.points[len(self.points) - 2] = QPointF(point.x(), self.endPoint.y()) else: point = self.points[len(self.points) - 2] self.points[len(self.points) - 2] = QPointF(self.endPoint.x(), point.y()) def shape(self): """generates outline for path """ qp = QPainterPathStroker() qp.setWidth(8) path = qp.createStroke(self.path()) return path def paint(self, painter, option, widget): color = Qt.red if self.isSelected() else Qt.black painter.setPen(QPen(color, 2, self.penStyle)) painter.drawPath(self.path()) # To paint path of shape # painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine)) # painter.drawPath(self.shape()) # if self.isSelected(): # self.showGripItem() # self._selected = True # elif self._selected: # self.hideGripItem() # self._selected = False def movePoints(self, index, movement): """move points of line """ for i in [index, index + 1]: point = self.points[i] point += movement self.points[i] = point self.updatePath() self.updateGrabber([index]) def addGrabber(self): """adds grabber when line is moved """ if self.startGripItem.m_location in ["left", "right"]: direction = [Qt.Horizontal, Qt.Vertical] else: direction = [Qt.Vertical, Qt.Horizontal] for i in range(1, len(self.points) - 2): item = Grabber(self, i, direction[(i - 1)%2]) item.setParentItem(self) item.setPos(self.pos()) self.scene().addItem(item) self.m_grabbers.append(item) def updateGrabber(self, index_no_updates=None): """updates all grabber of line when it is moved """ index_no_updates = index_no_updates or [] for grabber in self.m_grabbers: if grabber.m_index in index_no_updates: continue index = grabber.m_index startPoint = self.points[index] endPoint = self.points[index + 1] pos = (startPoint + endPoint) / 2 grabber.setEnabled(False) grabber.setPos(pos) grabber.setEnabled(True) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: if value == 1: self.showGripItem() else: self.hideGripItem() return if change == QGraphicsItem.ItemSceneHasChanged and self.scene(): # self.addGrabber() # self.updateGrabber() return return super(Line, self).itemChange(change, value) def updateLine(self, startPoint=None, endPoint=None): """This function is used to update connecting line when it add on canvas and when it's grip item moves :return: """ self.prepareGeometryChange() if startPoint: self.startPoint = startPoint if endPoint: self.endPoint = endPoint self.createPath() self.updateGrabber() return if self.startGripItem and self.endGripItem: item = self.startGripItem self.startPoint = item.parentItem().mapToScene(item.pos()) item = self.endGripItem self.endPoint = item.parentItem().mapToScene(item.pos()) self.updatePath() self.updateGrabber() def removeFromCanvas(self): """This function is used to remove connecting line from canvas :return: """ if self.scene(): self.scene().removeItem(self) def showGripItem(self): """shows grip items which contains line """ if self.startGripItem: self.startGripItem.show() if self.endGripItem: self.endGripItem.show() for grabber in self.m_grabbers: grabber.show() def hideGripItem(self): """hides grip items which contains line """ if self.startGripItem: self.startGripItem.hide() if self.endGripItem: self.endGripItem.hide() for grabber in self.m_grabbers: grabber.hide() def setStartGripItem(self, item): self.startGripItem = item def setEndGripItem(self, item): self.endGripItem = item def setPenStyle(self, style): """change current pen style for line""" self.penStyle = style