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.