PyQt issue with resizing and repainting of QGraphicsRectItem - python

I'm trying to create a resizable QGraphicsRectItem with an option to choose different draw styles.
If I create a simple rect with a resize feature only, then it works as expected:
class Rectangle(QtWidgets.QGraphicsRectItem):
def __init__(self, x, y, w, h):
super(Rectangle, self).__init__(0, 0, w, h)
self.setPen(QtGui.QPen(QtCore.Qt.red, 2))
self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemIsFocusable
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
self.setPos(QtCore.QPointF(x, y))
self.rect = rect = QtCore.QRectF(0, 0, 200, 200)
def boundingRect(self):
return self.rect.adjusted(-10, -10, 10, 10)
def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
super(Rectangle, self).mouseMoveEvent(event)
if event.buttons() & QtCore.Qt.RightButton:
self.rect = QtCore.QRectF(QtCore.QPoint(), event.pos()).normalized()
self.prepareGeometryChange()
self.setRect(self.rect)
When i try to modify it to change pen styles and color if active, it becomes unselectable and unfocusable. More than that, the bounding rect disappears unexpectedly. Here's a modified version:
class Rectangle(QtWidgets.QGraphicsRectItem):
def __init__(self, position, scene, style=QtCore.Qt.SolidLine,
rect=None, matrix=QtGui.QMatrix()):
super(Rectangle, self).__init__()
# self.setPen(QtGui.QPen(QtCore.Qt.red, 2))
self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemIsFocusable
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
if rect is None:
rect = QtCore.QRectF(0, 0, 200, 200)
self.size = QtCore.QPointF(200, 200)
self.rect = rect
self.style = style
self.setPos(position)
self.setMatrix(matrix)
scene.clearSelection()
scene.addItem(self)
self.setSelected(True)
self.setFocus()
global RAW
RAW = True
self.pen = QtGui.QPen(self.style)
self.pen.setColor(QtCore.Qt.black)
self.pen.setWidth(1)
def parentWidget(self):
return self.scene().views()[0]
def boundingRect(self):
return self.rect.adjusted(-10, -10, 10, 10)
def paint(self, painter, option, widget):
if option.state & QtWidgets.QStyle.State_Selected:
self.pen.setColor(QtCore.Qt.blue)
painter.setPen(self.pen)
painter.drawRect(self.rect)
def itemChange(self, change, variant):
if change != QtWidgets.QGraphicsItem.ItemSelectedChange:
global RAW
RAW = True
return QtWidgets.QGraphicsItem.itemChange(self, change, variant)
def contextMenuEvent(self, event):
wrapped = []
menu = QtWidgets.QMenu(self.parentWidget())
for text, param in (("&Solid", QtCore.Qt.SolidLine),
("&Dashed", QtCore.Qt.DashLine),
("D&otted", QtCore.Qt.DotLine),
("D&ashDotted", QtCore.Qt.DashDotLine),
("DashDo&tDotten", QtCore.Qt.DashDotDotLine)):
wrapper = functools.partial(self.setStyle, param)
wrapped.append(wrapper)
menu.addAction(text, wrapper)
menu.exec_(event.screenPos())
def setStyle(self, style):
#self.prepareGeometryChange()
self.style = style
self.update()
global RAW
RAW = True
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
super(Rectangle, self).mouseMoveEvent(event)
if event.buttons() & QtCore.Qt.MiddleButton:
if self.isSelected():
self.rect = QtCore.QRectF(QtCore.QPoint(), event.pos()).normalized()
self.prepareGeometryChange()
self.setRect(self.rect)
global RAW
RAW = True
I guess that the main problem lays in reimplemented paint() function but I still haven't come up with any idea where exactly...
Could anyone explain what am I doing wrong?
Where's a mistake and how to make this thing work properly?

I do not have the middle button on my touchpad so I have implemented the logic with the right button but I have given a small edge of 10px where you can change the size of the rectangle.
To change the style you just have to change the QPen of the QGraphicsItem.
import functools
from PyQt5 import QtCore, QtGui, QtWidgets
class Rectangle(QtWidgets.QGraphicsRectItem):
def __init__(self, x, y, w, h):
super(Rectangle, self).__init__(0, 0, w, h)
self.setPen(QtGui.QPen(QtCore.Qt.red, 2))
self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemIsFocusable
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
self.setPos(QtCore.QPointF(x, y))
def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
super(Rectangle, self).mouseMoveEvent(event)
if event.buttons() & QtCore.Qt.RightButton:
rect = QtCore.QRectF(QtCore.QPoint(), event.pos()).normalized()
self.prepareGeometryChange()
self.setRect(rect)
def contextMenuEvent(self, event):
super(Rectangle, self).contextMenuEvent(event)
delta = 10
r = self.boundingRect()
r.adjust(delta, delta, -delta, -delta)
if not r.contains(event.pos()):
return
self.setSelected(True)
wrapped = []
menu = QtWidgets.QMenu(self.parentWidget())
for text, param in (("&Solid", QtCore.Qt.SolidLine),
("&Dashed", QtCore.Qt.DashLine),
("D&otted", QtCore.Qt.DotLine),
("D&ashDotted", QtCore.Qt.DashDotLine),
("DashDo&tDotten", QtCore.Qt.DashDotDotLine)):
wrapper = functools.partial(self.setStyle, param)
wrapped.append(wrapper)
menu.addAction(text, wrapper)
menu.exec_(event.screenPos())
def paint(self, painter, option, widget):
painter.setPen(self.pen())
painter.setBrush(self.brush())
if option.state & QtWidgets.QStyle.State_Selected:
pen = self.pen()
pen.setColor(QtCore.Qt.blue)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.NoBrush)
painter.drawRect(self.boundingRect())
def setStyle(self, style):
pen = self.pen()
pen.setStyle(style)
self.setPen(pen)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene(-400, -400, 800, 800)
w = QtWidgets.QGraphicsView(scene)
scene.addItem(Rectangle(100, 100, 100, 100))
w.show()
sys.exit(app.exec_())

Related

Problem with resizing custom item inside QGraphicsScene (Pyqt)

I have built a custom widget that is placed inside a custom scene. The custom scene rect resizes when the itemBoundingRect crosses the scene rect. In the beginning, the scene rectangle is set to (0, 0, 2000, 2000). I am able to resize my widgets properly inside this rectangle. The problem arises when I try to move the item against the top (i,e when the item moves in negative y-axis), The item altogether decreases the size on the y-axis when I try to increase it.
Here is the demonstration of the problem:
(Note: the scene resizes when the item is placed at any of the edges by a factor of 500. eg:- after first resize the scene rect would be (-500, -500, 2500, 2500) )
Here is the code:
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QGraphicsItem, QStyle, QGraphicsView
# ---------class size grip use to increase widget size----------#
class SizeGrip(QtWidgets.QSizeGrip):
def __init__(self, parent):
super().__init__(parent)
parent.installEventFilter(self)
self.setFixedSize(30, 30)
self.polygon = QtGui.QPolygon([
QtCore.QPoint(10, 20),
QtCore.QPoint(20, 10),
QtCore.QPoint(20, 20),
])
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Resize:
geo = self.rect()
geo.moveBottomRight(source.rect().bottomRight())
self.setGeometry(geo)
return super().eventFilter(source, event)
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setPen(QtCore.Qt.white)
qp.setBrush(QtCore.Qt.gray)
qp.drawPolygon(self.polygon)
class Container(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sizeGrip = SizeGrip(self)
self.startPos = None
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(6, 6, 6, 30)
self.setStyleSheet('''
Container {
background: lightblue;
border: 0px;
border-radius: 4px;
}
''')
def resizeEvent(self, event):
super(Container, self).resizeEvent(event)
# ------------------ Creating custom item to place in scene--------------------#
class GraphicsFrame(QtWidgets.QGraphicsWidget):
def __init__(self):
super().__init__()
graphic_layout = QtWidgets.QGraphicsLinearLayout(Qt.Vertical, self)
self.container = Container()
proxyWidget = QtWidgets.QGraphicsProxyWidget()
proxyWidget.setWidget(self.container)
graphic_layout.addItem(proxyWidget)
self.pen = QtGui.QPen()
self.pen.setColor(Qt.red)
self.container.setMinimumSize(150, 150)
self.container.setMaximumSize(400, 800)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.container.resizeEvent = lambda _: self.resize()
self.container.startPos = None
def addWidget(self, widget):
self.container.layout().addWidget(widget)
def paint(self, qp, opt, widget):
qp.save()
self.pen.setWidth(3)
p = QtGui.QPainterPath()
p.addRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)
if self.isSelected():
self.pen.setColor(Qt.yellow)
qp.setBrush(Qt.transparent)
qp.setPen(self.pen)
qp.drawPath(p)
qp.setClipPath(p)
opt.state &= ~QStyle.State_Selected
super().paint(qp, opt, widget)
qp.restore()
def resize(self):
width = self.container.size().width()
height = self.container.size().height()
rect = QtCore.QRectF(self.pos().x(), self.pos().y(), width + 22, height + 22)
self.setGeometry(rect)
# -------------------- Custom view to hold the items -----------------#
class View(QtWidgets.QGraphicsView):
context_menu_signal = QtCore.pyqtSignal()
def __init__(self, bg_color=Qt.white):
super().__init__()
self.scene = Scene()
self.setRenderHints(QtGui.QPainter.Antialiasing)
self.setDragMode(self.RubberBandDrag)
self._isPanning = False
self._mousePressed = False
self.setCacheMode(self.CacheBackground)
self.setMouseTracking(True)
self.setScene(self.scene)
self.scene.selectionChanged.connect(self.selection_changed)
self._current_selection = []
texture = QtGui.QImage(30, 30, QtGui.QImage.Format_ARGB32)
qp = QtGui.QPainter(texture)
qp.setBrush(bg_color)
qp.setPen(QtGui.QPen(QtGui.QColor(189, 190, 191), 2))
qp.drawRect(texture.rect())
qp.end()
self.scene.setBackgroundBrush(QtGui.QBrush(texture))
self.setViewportUpdateMode(self.FullViewportUpdate) # This will avoid rendering artifacts
testFrame = GraphicsFrame()
newFrame = GraphicsFrame()
testFrame.addWidget(QtWidgets.QLineEdit())
newFrame.addWidget(QtWidgets.QLabel('Bruh'))
self.scene.addItem(testFrame)
self.scene.addItem(newFrame)
def wheelEvent(self, event):
# Save the scene pos
oldPos = self.mapToScene(event.pos())
if event.modifiers() == Qt.ControlModifier:
delta = event.angleDelta().y()
if delta > 0:
self.on_zoom_in()
elif delta < 0:
self.on_zoom_out()
# Get the new position
newPos = self.mapToScene(event.pos())
# Move scene to old position
delta = newPos - oldPos
self.translate(delta.x(), delta.y())
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self._mousePressed = True
if self._isPanning:
self.viewport().setCursor(Qt.ClosedHandCursor)
self._dragPos = event.pos()
event.accept()
else:
super().mousePressEvent(event)
elif event.button() == Qt.MidButton:
self._mousePressed = True
self._isPanning = True
self.viewport().setCursor(Qt.ClosedHandCursor)
self._dragPos = event.pos()
event.accept()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._mousePressed and self._isPanning:
newPos = event.pos()
diff = newPos - self._dragPos
self._dragPos = newPos
self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - diff.x()
)
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - diff.y()
)
event.accept()
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
if self._isPanning:
self.viewport().setCursor(Qt.OpenHandCursor)
else:
self._isPanning = False
self.viewport().unsetCursor()
self._mousePressed = False
zoomed = False if self.transform().m11() == 1.0 else True
self.scene.adjust(zoomed) # adjust the item scene rectangle
elif event.button() == Qt.MiddleButton:
self._isPanning = False
self.viewport().unsetCursor()
self._mousePressed = False
super().mouseReleaseEvent(event)
def select_items(self, items, on):
pen = QtGui.QPen(
QtGui.QColor(245, 228, 0) if on else Qt.white,
0.5,
Qt.SolidLine,
Qt.RoundCap,
Qt.RoundJoin,
)
for item in items:
item.pen = pen
def selection_changed(self):
try:
self.select_items(self._current_selection, False)
self._current_selection = self.scene.selectedItems()
self.select_items(self._current_selection, True)
except RuntimeError:
pass
def on_zoom_in(self):
if self.transform().m11() < 2.25:
self.scale(1.5, 1.5)
def on_zoom_out(self):
if self.transform().m11() > 0.7:
self.scale(1.0 / 1.5, 1.0 / 1.5)
def resizeEvent(self, event):
super().resizeEvent(event)
# ------------Custom scene which resizes the scene rect on when the item boundary hits the scene rect---------#
class Scene(QtWidgets.QGraphicsScene):
def __init__(self):
super(Scene, self).__init__()
self.setSceneRect(0, 0, 2000, 2000)
self.sceneRect().adjust(-20, -20, 20, 20)
self.old_rect = self.itemsBoundingRect()
def adjust(self, zoomed):
w = self.sceneRect().width()
h = self.sceneRect().height()
x = self.sceneRect().x()
y = self.sceneRect().y()
adjust_factor = 500
adjust_factor2 = 200
smaller = self.is_smaller()
self.old_rect = self.itemsBoundingRect()
if not self.sceneRect().contains(self.old_rect):
self.setSceneRect(-adjust_factor + x, -adjust_factor + y, adjust_factor + w, adjust_factor + h)
print(f'sceneRect: {self.sceneRect()}')
if not zoomed and smaller:
print('yes')
self.setSceneRect(adjust_factor2 + x, adjust_factor2 + y, abs(adjust_factor2 - w),
abs(adjust_factor2 - h))
def is_smaller(self):
x = self.old_rect.x()
y = self.old_rect.y()
h = self.old_rect.height()
w = self.old_rect.width()
if ((x <= self.itemsBoundingRect().x()) and (y <= self.itemsBoundingRect().y())
and (h > self.itemsBoundingRect().height()) and (w > self.itemsBoundingRect().width())):
return True
return False
# -----------main---------#
import sys
app = QtWidgets.QApplication(sys.argv)
w = View()
w.show()
sys.exit(app.exec_())
I know the code is lengthy but any help is appreciated.
The problem is a (partial) recursion caused by a wrong implementation of the object structure.
When the widget is resized using the size grip, it receives a resizeEvent that in your code is overridden by the GraphicsFrame.resize(): if you use setGeometry(), it will cause the graphics layout to resize its children, which in turn will call again a resize on the proxy widget. It's just a partial recursion because layout adjustments often require more than an event loop "cycle" (and with proxy widgets it might be even more).
The solution obviously is to avoid this recursion, which could be done by simply adding the proxy widget to the scene. Since the multiple item selection is required, this cannot be possible, as QGraphicsProxyWidgets are also panels (see the ItemIsPanel flag): only one panel graphics item can be active at once, so the only actual solution is to create a QGraphicsItem as a parent for the proxy. From that point, the geometry of the parent is based on the proxy, and the painting can be easily implemented.
class GraphicsFrame(QtWidgets.QGraphicsItem):
def __init__(self):
super().__init__()
self.container = Container()
self.container.setMinimumSize(150, 150)
self.container.setMaximumSize(400, 800)
self.proxy = QtWidgets.QGraphicsProxyWidget(self)
self.proxy.setWidget(self.container)
self.setFlags(self.ItemIsSelectable | self.ItemIsMovable)
def addWidget(self, widget):
self.container.layout().addWidget(widget)
def boundingRect(self):
# use the proxy for the bounding rect, adding the preferred margin
return self.proxy.boundingRect().adjusted(-11, -11, 11, 11)
def paint(self, qp, opt, widget):
qp.save()
qp.setPen(QtGui.QPen(Qt.yellow if self.isSelected() else Qt.red, 3))
qp.drawRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)
qp.restore()

How to show a preview of the line I'm drawing with QPainter in PyQt5

My code is drawing lines on a QImage using mousePressEvent and mouseReleaseEvent. It works fine but I would like a dynamic preview line to appear when I'm drawing the said line (ie on MouseMoveEvent). Right now the line just appears when I release the left mouse button and I can't see what I'm drawing.
I want the preview of the line to appear and update as I move my mouse, and only "fixate" when I release the left mouse button. Exactly like the MS Paint Line tool : https://youtu.be/YIw9ybdoM6o?t=207
Here is my code (it is derived from the Scribble Example):
from PyQt5.QtCore import QPoint, QRect, QSize, Qt
from PyQt5.QtGui import QImage, QPainter, QPen, QColor, qRgb
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow
import sys
class DrawingArea(QWidget):
def __init__(self, parent=None):
super(DrawingArea, self).__init__(parent)
self.setAttribute(Qt.WA_StaticContents)
self.scribbling = False
self.myPenWidth = 1
self.myPenColor = QColor('#000000')
self.image = QImage()
self.startPoint = QPoint()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.startPoint = event.pos()
self.scribbling = True
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton and self.scribbling:
self.drawLineTo(event.pos())
self.scribbling = False
def paintEvent(self, event):
painter = QPainter(self)
dirtyRect = event.rect()
painter.drawImage(dirtyRect, self.image, dirtyRect)
def resizeEvent(self, event):
if self.width() > self.image.width() or self.height() > self.image.height():
newWidth = max(self.width() + 128, self.image.width())
newHeight = max(self.height() + 128, self.image.height())
self.resizeImage(self.image, QSize(newWidth, newHeight))
self.update()
super(DrawingArea, self).resizeEvent(event)
def drawLineTo(self, endPoint):
painter = QPainter(self.image)
painter.setPen(QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(self.startPoint, endPoint)
rad = self.myPenWidth / 2 + 2
self.update(QRect(self.startPoint, endPoint).normalized().adjusted(-rad, -rad, +rad, +rad))
def resizeImage(self, image, newSize):
if image.size() == newSize:
return
newImage = QImage(newSize, QImage.Format_RGB32)
newImage.fill(qRgb(255, 255, 255))
painter = QPainter(newImage)
painter.drawImage(QPoint(0, 0), image)
self.image = newImage
class MainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setCentralWidget(DrawingArea())
self.show()
def main():
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I can't figure out how to show the preview of the line I'm drawing and I haven't found a suitable answer yet. How can I go about doing this ?
You can draw the lines just within the paintEvent() method instead than directly on the image, then paint on the image when the mouse is actually released.
class DrawingArea(QWidget):
def __init__(self, parent=None):
super(DrawingArea, self).__init__(parent)
self.setAttribute(Qt.WA_StaticContents)
self.scribbling = False
self.myPenWidth = 1
self.myPenColor = QColor('#000000')
self.image = QImage()
self.startPoint = self.endPoint = None
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.startPoint = event.pos()
def mouseMoveEvent(self, event):
if self.startPoint:
self.endPoint = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if self.startPoint and self.endPoint:
self.updateImage()
def paintEvent(self, event):
painter = QPainter(self)
dirtyRect = event.rect()
painter.drawImage(dirtyRect, self.image, dirtyRect)
if self.startPoint and self.endPoint:
painter.drawLine(self.startPoint, self.endPoint)
def updateImage(self):
if self.startPoint and self.endPoint:
painter = QPainter(self.image)
painter.setPen(QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(self.startPoint, self.endPoint)
painter.end()
self.startPoint = self.endPoint = None
self.update()
Note that you don't need to call update() within the resize event, as it's automatically called.
I also removed the unnecessary update rect calls, as it's almost useless in this case: specifying a rectangle in which the update should happen is usually done when very complex widgets are drawn (especially when lots of computations are executed to correctly draw everything and only a small part of the widget actually needs updates). In your case, it's almost more time consuming to compute the actual update rectangle than painting all the contents of the widget.
I think this page shows some really nice solutions for the problem of yours. For example, it shows how to implement a custom class which actually gives you a "drawing board":
class Canvas(QLabel):
def __init__(self):
super().__init__()
pixmap = QtGui.QPixmap(600, 300)
self.setPixmap(pixmap)
self.last_x, self.last_y = None, None
self.pen_color = QtGui.QColor('#000000')
def set_pen_color(self, c):
self.pen_color = QtGui.QColor(c)
def mouseMoveEvent(self, e):
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return # Ignore the first time.
painter = QtGui.QPainter(self.pixmap())
p = painter.pen()
p.setWidth(1)
p.setColor(self.pen_color)
painter.setPen(p)
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
You can use this Canvas class (or whatever name you would give it) everywhere you need. For example in the MainWindow:
class MainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.canvas = Canvas()
self.canvas.set_pen_color('#fffee5') # set the colour you want
self.setCentralWidget(self.canvas)
self.show()
Hope this could help! Happy coding! :)

Widget used for infinitely resizable painting in librecad

Librecad uses a widget which can be infinitely resized, you can zoom in and out as much as you can. Which widget does it uses?
When I paint into a common widget, the painting is done at certain coordinates of the widget. However, I would like to draw at floating coordinates of the widget and use a line width which is fixed to certain pixels of the viewport.
Before resizing:
After resizing:
Which widget provides this functionality?
You have to use QGraphicsView and QGraphicsScene(see Graphics View Framework):
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.factor = 1.2
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.setRenderHints(
QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform
)
self.setMouseTracking(True)
self.setScene(
QtWidgets.QGraphicsScene(QtCore.QRectF(-400, -400, 800, 800), self)
)
QtWidgets.QShortcut(QtGui.QKeySequence.ZoomIn, self, activated=self.zoomIn) # Ctrl + +
QtWidgets.QShortcut(QtGui.QKeySequence.ZoomOut, self, activated=self.zoomOut) # Ctrl + -
#QtCore.pyqtSlot()
def zoomIn(self):
self.zoom(self.factor)
#QtCore.pyqtSlot()
def zoomOut(self):
self.zoom(1 / self.factor)
def zoom(self, f):
self.scale(f, f)
def drawForeground(self, painter, rect):
super(GraphicsView, self).drawForeground(painter, rect)
if not hasattr(self, "cursor_position"):
return
painter.save()
painter.setTransform(QtGui.QTransform())
pen = QtGui.QPen(QtGui.QColor("yellow"))
pen.setWidth(4)
painter.setPen(pen)
r = self.mapFromScene(rect).boundingRect()
linex = QtCore.QLine(
r.left(), self.cursor_position.y(), r.right(), self.cursor_position.y(),
)
liney = QtCore.QLine(
self.cursor_position.x(), r.top(), self.cursor_position.x(), r.bottom(),
)
for line in (linex, liney):
painter.drawLine(line)
painter.restore()
def mouseMoveEvent(self, event):
self.cursor_position = event.pos()
self.scene().update()
super(GraphicsView, self).mouseMoveEvent(event)
def wheelEvent(self, event):
angle = event.angleDelta().y()
if angle < 0:
self.zoomIn()
else:
self.zoomOut()
super(GraphicsView, self).wheelEvent(event)
if __name__ == "__main__":
import sys
import random
app = QtWidgets.QApplication(sys.argv)
w = GraphicsView()
for _ in range(4):
r = QtCore.QLineF(
*random.sample(range(-200, 200), 2), *random.sample(range(50, 150), 2)
)
it = w.scene().addLine(r)
pen = QtGui.QPen(QtGui.QColor(*random.sample(range(255), 3)))
pen.setWidthF(5.0)
pen.setCosmetic(True)
it.setPen(pen)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

How to draw both object on the same window?

Prologue: Both of the object represented below are on the same window.
I am having problem with the not updating
I am having a problem related to self.setGeometry(x,y,w,h) function. So what I want to achieve is multiple rect, rendered in parallel, which each has a line protruding out of their rectangle when clicked. (Creating a connector though that is not the point of this topic).
In this case, I have rect A and rect B rendering together. Rect A has a line protruding out to the position of the mouse.
(obj A) (obj B)
____ ____
| | | |
| \ | | |
---\ ----
\
(Mouse)
An example code of what am I trying to achieve.
# File: connectors.py
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget
class Connector(QWidget):
def __init__(self, rect: List[int]):
super().__init__()
self.setGeometry(0, 0, 1920, 1080)
self.rect = rect
self.clicked = False
self.begin = QPoint(rect[0], rect[1])
self.end = QPoint(0,0)
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
qp.setPen(Qt.red)
qp.drawRect(*self.rect)
if self.clicked:
qp = QPainter()
qp.begin(self)
qp.setPen(Qt.red)
qp.drawLine(self.begin, self.end)
self.update()
def mousePressEvent(self, event):
if event.button() == 1:
self.clicked = True
self.end = event.pos()
def mouseMoveEvent(self, event):
if self.clicked:
self.end = event.pos()
def mouseReleaseEvent(self, event):
if event.button() == 1:
self.clicked = False
self.end = event.pos()
# File: main.py
scene = QGraphicsScene()
scene.addWidget( Connector((400, 400, 100, 100)) )
scene.addWidget( Connector((400, 600, 100, 100)) )
But what I am ending up is PyQt showing the top-most object onto the screen thus only showing one object, BUT I also tried to minimize the geometry leading the protruding line, when clicked, cutting off on the border.
Explanation:
In your case it has 2 widgets where you draw the rectangles where one is on top of another so you will only see one of them: the one above.
Solution:
Qt GraphicsView Framework (QGraphicsView, QGraphicsScene, QGraphicsXItem, etc.) works differently and painting is not used directly since they offer basic items that implement all the functionalities so in this case you must use QGraphicsRectItem with QGraphicsLineItem and modify it with the information of the QGraphicsView.
Considering the above, the solution is:
import sys
from PyQt5.QtCore import QRectF, Qt
from PyQt5.QtWidgets import (
QApplication,
QGraphicsLineItem,
QGraphicsRectItem,
QGraphicsScene,
QGraphicsView,
)
class RectItem(QGraphicsRectItem):
def __init__(self, rect, parent=None):
super().__init__(parent)
self.setRect(QRectF(*rect))
self.setPen(Qt.red)
self._line_item = QGraphicsLineItem(self)
self.line_item.setPen(Qt.red)
l = self.line_item.line()
l.setP1(self.rect().topLeft())
l.setP2(self.rect().topLeft())
self.line_item.setLine(l)
self.line_item.hide()
#property
def line_item(self):
return self._line_item
def move_line_to(self, sp):
lp = self.mapFromScene(sp)
l = self.line_item.line()
l.setP2(lp)
self.line_item.setLine(l)
class GraphicsView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setScene(QGraphicsScene())
self.scene().addItem(RectItem((400, 400, 100, 100)))
self.scene().addItem(RectItem((400, 600, 100, 100)))
def mousePressEvent(self, event):
vp = event.pos()
sp = self.mapToScene(vp)
self.move_lines(sp)
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
vp = event.pos()
sp = self.mapToScene(vp)
self.move_lines(sp)
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
for item in self.items():
if isinstance(item, RectItem):
item.line_item.hide()
super().mouseReleaseEvent(event)
def move_lines(self, sp):
for item in self.items():
if isinstance(item, RectItem):
item.line_item.show()
item.move_line_to(sp)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = GraphicsView()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

How do I keep drawing on image after window resize in PyQt?

I have written a code to draw a rectangle on an image in a QGraphicsView but if I resize the image, for example full screen, the position of the rectangle becomes misplaced. Is there any way to fix this?
I think one possible solution is to align the image every time in top left corner but I cannot give 2 arguments in setAlignment().
Here is the code:(the buttons are just for show atm)
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.b1 = QPushButton('Next Image')
self.b2 = QPushButton('Crop')
self.b3 = QPushButton('Clear')
self.view = GraphicsView()
h_box = QHBoxLayout()
v_box = QVBoxLayout()
v_box.addWidget(self.b1)
v_box.addWidget(self.b2)
v_box.addWidget(self.b3)
h_box.addWidget(self.view)
h_box.addLayout(v_box)
self.setLayout(h_box)
#self.resize(800, 800)
self.setWindowTitle("Super Duper Cropper")
self.show()
class GraphicsView(QGraphicsView):
def __init__(self):
super().__init__()
self.setScene(QGraphicsScene())
self.item = QGraphicsPixmapItem(QPixmap('test.jpg'))
self.scene().addItem(self.item)
def mousePressEvent(self, event):
self.xi = event.x()
self.yi = event.y()
def mouseMoveEvent(self, event):
self.xf = event.x()
self.yf = event.y()
self.draw_rect()
def mouseReleaseEvent(self, event):
self.xf = event.x()
self.yf = event.y()
self.draw_rect()
def draw_rect(self):
self.scene().removeItem(self.item)
self.scene().addItem(self.item)
self.scene().addRect(self.xi, self.yi, self.xf-self.xi, self.yf-self.yi, pen=QPen(QColor(51, 153, 255), 2,
Qt.SolidLine), brush=QBrush(QColor(0, 255, 0, 40)))
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MyWidget()
window.show()
app.aboutToQuit.connect(app.deleteLater)
sys.exit(app.exec_())
You have the following errors:
The coordinates of the scene are different from the coordinates of the view, so you must use the mapToScene() method if you want to establish the right position of the rectangle.
Why do you add and remove the items? the best thing is to reuse
You want the position of the rectangle to be relative to the QGraphicsPixmapItem, so the rectangle has to be a child of the QGraphicsPixmapItem.
Using the above we obtain the following:
class GraphicsView(QGraphicsView):
def __init__(self):
super().__init__()
self.setScene(QGraphicsScene())
self.item = QGraphicsPixmapItem(QPixmap('test.jpg'))
self.scene().addItem(self.item)
self.rect_item = QGraphicsRectItem(QRectF(), self.item)
self.rect_item.setPen(QPen(QColor(51, 153, 255), 2, Qt.SolidLine))
self.rect_item.setBrush(QBrush(QColor(0, 255, 0, 40)))
def mousePressEvent(self, event):
self.pi = self.mapToScene(event.pos())
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
pf = self.mapToScene(event.pos())
if (self.pi - pf).manhattanLength() > QApplication.startDragDistance():
self.pf = pf
self.draw_rect()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
pf = self.mapToScene(event.pos())
if (self.pi - pf).manhattanLength() > QApplication.startDragDistance():
self.pf = pf
self.draw_rect()
super().mouseReleaseEvent(event)
def draw_rect(self):
r = QRectF(self.pi, self.pf).normalized()
r = self.rect_item.mapFromScene(r).boundingRect()
self.rect_item.setRect(r)

Categories

Resources