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()
Related
I want to create a bar with minimum control features when minimizing the main application window, similar to the bar in the image below but in PyQt6.
Currently, I am creating a new small screen on the minimize action but I could not remove the bar which has the close, minimize and maximize buttons from it.
I eventually got it and I was able to write the below code to produce it
class FloatingDialogBar(QWidget):
def __init__(self, parent=None, project: str = None, task: str = None):
super(FloatingDialogBar, self).__init__(parent)
self.project = project if project else 'N/A'
self.task = task if task else 'N/A'
self.__init_ui()
self.__init_layouts()
def greetings(self):
# Do Something
pass
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.offset = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.offset is not None and event.buttons() == Qt.MouseButton.LeftButton:
self.move(self.pos() + event.pos() - self.offset)
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.offset = None
super().mouseReleaseEvent(event)
def __init_ui(self):
self.setWindowFlags(
self.windowFlags() |
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
left = 100
top = 100
self.width = 320
height = 60
self.setGeometry(
left,
top,
self.width,
height,
)
self.setMaximumWidth(self.width)
self.setMinimumWidth(self.width)
self.setMinimumHeight(height)
self.setMaximumHeight(height)
pass
def __init_layouts(self):
self.frame = QFrame(self)
self.frame.setStyleSheet(consts.FLOATING_BAR_STYLESHEET)
# Create widgets
layout = QHBoxLayout()
self.lbl_project = QLabel(self.project)
self.lbl_project.setStyleSheet(consts.FLOATING_BAR_QLabel_STYLESHEET)
layout.addWidget(self.lbl_project)
self.lbl_task = QLabel(self.task)
self.lbl_task.setStyleSheet(consts.FLOATING_BAR_QLabel_STYLESHEET)
layout.addWidget(self.lbl_task)
layout.setContentsMargins(0, 0, 0, 0)
self.frame.setLayout(layout)
self.frame.setMaximumWidth(self.width)
self.frame.setMinimumWidth(self.width)
self.frame.setMinimumHeight(60)
self.frame.setMaximumHeight(60)
pass
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! :)
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_())
I would like explaining what is it I want to achieve, and what is it, that does not work probably. When a user draw a rectangular or rounded shape, a ring inside rounded shape, and a rectangular ring with rounded edge inside rectangular shape want to be drawn. So far I have achieved with my code... shown below
As shown above, inner rectangular should be subtracted from outer rectangular. And edges should be rounded.
What I want to achieve and similar for rounded shape
The code :
from PyQt5 import QtCore, QtGui, QtWidgets
class Foo(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Foo, self).__init__(parent)
self.setGeometry(QtCore.QRect(200, 100, 800, 800))
self.button = Button()
self.paint = Createpaintwidget()
self.button.valuesChanged.connect(self.paint.set_size_squares)
self.button.valueChanged.connect(self.paint.set_size_round)
self.lay = QtWidgets.QVBoxLayout(self)
self.lay.addWidget(self.paint)
self.lay.addWidget(self.button)
class Createpaintwidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.sizeHint()
self.setBackgroundRole(QtGui.QPalette.Base)
self.setAutoFillBackground(True)
self._size = QtCore.QSizeF()
self._path = QtGui.QPainterPath()
self._rect = QtCore.QRectF()
self._type = QtGui.QRegion.Rectangle
self._factor = 1.0
self._sizeouter = QtCore.QSizeF()
self._rectouter = QtCore.QRectF()
self._sizeinner = QtCore.QSizeF()
self._rectinner = QtCore.QRectF()
self._pos = QtCore.QPointF()
self._initial_flag = False
fnt = self.font()
fnt.setPointSize(20)
self.setFont(fnt)
print(self._size, self._rect, self._type, self._pos)
def showEvent(self, event):
if not self._initial_flag:
self._pos = self.rect().center()
self._initial_flag = True
#QtCore.pyqtSlot(int, int)
def set_size_squares(self, w, h):
cb, ct, yb = 25, 25, 8
self._path = QtGui.QPainterPath()
self._size = QtCore.QSizeF(w, h)
self._sizeouter = QtCore.QSizeF(w-cb, h-ct)
self._sizeinner = QtCore.QSizeF(w-cb-yb, h-ct-yb)
self._type = QtGui.QRegion.Rectangle
self.updatePath()
print(self._size, self._rect, self._type, self._pos)
#QtCore.pyqtSlot(int)
def set_size_round(self, v):
cb, yb = 25, 8
self._path = QtGui.QPainterPath()
self._size = QtCore.QSizeF(v, v)
self._sizeouter = QtCore.QSizeF(v-cb, v-cb)
self._sizeinner = QtCore.QSizeF(v-cb-yb, v-cb-yb)
self._type = QtGui.QRegion.Ellipse
self.updatePath()
def paintEvent(self, event):
pen = QtGui.QPen()
brush = QtGui.QBrush(QtCore.Qt.black)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(pen)
painter.setBrush(brush)
painter.translate(self.rect().center())
painter.scale(self._factor, self._factor)
painter.translate(-self.rect().center())
painter.translate(self._pos)
painter.drawPath(self._path)
if self._type == QtGui.QRegion.Rectangle:
painter.fillRect(self._rectouter, QtGui.QBrush(QtCore.Qt.cyan, QtCore.Qt.SolidPattern))
painter.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
painter.drawRect(self._rectouter)
painter.fillRect(self._rect, QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
painter.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
painter.drawRect(self._rect)
painter.fillRect(self._rectinner, QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
painter.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
painter.drawRect(self._rectinner)
elif self._type == QtGui.QRegion.Ellipse:
painter.setBrush(QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
painter.drawEllipse(self._rect)
painter.setBrush(QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.SolidPattern))
painter.drawEllipse(self._rectouter)
painter.setBrush(QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
painter.drawEllipse(self._rectinner)
def mousePressEvent(self, event):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
self._initial_pos = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
delta = event.pos() - self._initial_pos
self._path.translate(delta)
self._rect.translate(delta)
self._rectinner.translate(delta)
self._rectouter.translate(delta)
self.update()
self._initial_pos = event.pos()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
QtWidgets.QApplication.restoreOverrideCursor()
super().mouseReleaseEvent(event)
def updatePath(self):
fm = QtGui.QFontMetrics(self.font())
r = QtCore.QRectF(QtCore.QPointF(), self._size)
ro = QtCore.QRectF(QtCore.QPointF(), self._sizeouter)
ri = QtCore.QRectF(QtCore.QPointF(), self._sizeinner)
r.moveCenter(QtCore.QPointF())
ro.moveCenter(QtCore.QPointF())
ri.moveCenter(QtCore.QPointF())
r.moveCenter(QtCore.QPointF())
self._rectouter = QtCore.QRectF(ro)
self._rectinner = QtCore.QRectF(ri)
self._rect = QtCore.QRectF(r)
self._path.moveTo(QtCore.QPointF())
self.update()
def wheelEvent(self, event):
self._factor *= 1.01**(event.angleDelta().y()/15.0)
self.update()
super().wheelEvent(event)
class Button(QtWidgets.QWidget):
valueChanged = QtCore.pyqtSignal(int)
valuesChanged = QtCore.pyqtSignal(int,int)
def __init__(self, parent=None):
super(Button, self).__init__(parent)
roundbutton = QtWidgets.QPushButton('Round')
squarebutton = QtWidgets.QPushButton('Square')
Alay = QtWidgets.QVBoxLayout(self)
Alay.addWidget(roundbutton)
Alay.addWidget(squarebutton)
self.value = QtWidgets.QLabel()
roundbutton.clicked.connect(self.getbuttonfunc)
squarebutton.clicked.connect(self.sqaurebuttonfunc)
#QtCore.pyqtSlot()
def getbuttonfunc(self):
number, ok = QtWidgets.QInputDialog.getInt(self, self.tr("Set Number"),
self.tr("Input:"), 1, 1)
if ok:
self.valueChanged.emit(number)
#QtCore.pyqtSlot()
def sqaurebuttonfunc(self):
number, ok = QtWidgets.QInputDialog.getInt(self, self.tr("Set Number"),
self.tr("Input:"), 1, 1)
if ok:
self.valuesChanged.emit(number, number)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Foo()
w.show()
sys.exit(app.exec_())
For these cases it is better to use a QPainterPathStroker and pass it a QPainterPath that I have the round rectangle with addRoundedRect():
def paintEvent(self, event):
pen = QtGui.QPen()
brush = QtGui.QBrush(QtCore.Qt.black)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(pen)
painter.setBrush(brush)
painter.translate(self.rect().center())
painter.scale(self._factor, self._factor)
painter.translate(-self.rect().center())
painter.translate(self._pos)
painter.drawPath(self._path)
S = (self._rectouter.size() + self._rectinner.size())/2
s = (self._rectouter.size() - self._rectinner.size())/2
r = QtCore.QRectF(QtCore.QPointF(), S)
r.moveCenter(self._rectouter.center())
path = QtGui.QPainterPath()
painter.setBrush(QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
if self._type == QtGui.QRegion.Rectangle:
painter.drawRect(self._rect)
path.addRoundedRect(r, 20, 20)
elif self._type == QtGui.QRegion.Ellipse:
painter.drawEllipse(self._rect)
path.addEllipse(r)
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(s.width())
stroke_path = stroker.createStroke(path)
# painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtGui.QBrush(QtCore.Qt.cyan, QtCore.Qt.SolidPattern))
painter.drawPath(stroke_path)
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)