Issue
I have a script that is loading an image and displaying it in a QGraphicsView, it also sets the size of the GraphicsView to the dimensions of the image.
I need to be able to zoom in / out while only being able to draw on the image. Currently I can zoom in / out but can draw anywhere on the GraphicsView, meaning that when the image is zoomed out I can draw outside of it as seen below.
What can I do to make it to where the RubberBand can only be drawn on the image?
Here is my GraphicsView:
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QPen
from SelectionBand import *
from time import sleep
class CustomGraphicsView(QGraphicsView):
def __init__(self, parent=None):
super(CustomGraphicsView, self).__init__()
self.setScene(QGraphicsScene(self))
self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.scene().setBackgroundBrush(Qt.gray)
self.brush = QBrush(Qt.green)
self.pen = QPen(Qt.blue)
def setImage(self, image):
self.img = self.scene().addPixmap(image)
self.resize(self.img.boundingRect().width(),self.img.boundingRect().height())
self.scene().setSceneRect(self.img.boundingRect())
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.scale(1.05, 1.05)
elif x < 0:
self.scale(.95, .95)
else:
super().wheelEvent(event)
def mousePressEvent(self, event):
pos = self.mapToScene(event.pos())
self.xPos = pos.x()
self.yPos = pos.y()
self.band = SelectionBand(self)
self.band.setGeometry(pos.x(), pos.y(), 0, 0)
item = self.scene().addWidget(self.band)
item.setFlag(QGraphicsItem.ItemIsMovable)
item.setZValue(1)
def mouseMoveEvent(self, event):
pos = self.mapToScene(event.pos())
self.width = pos.x() - self.xPos
self.height = pos.y() - self.yPos
if self.width < 0 and self.height < 0:
self.band.setGeometry(pos.x(), pos.y(), abs(self.width), abs(self.height))
elif self.width < 0:
self.band.setGeometry(pos.x(), self.yPos, abs(self.width), abs(self.height))
elif self.height < 0:
self.band.setGeometry(self.xPos, pos.y(), abs(self.width), abs(self.height))
else: self.band.setGeometry(self.xPos, self.yPos, abs(self.width), abs(self.height))
Here is the RubberBand code:
from typing import ItemsView
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QHBoxLayout, QRubberBand, QWidget,QSizeGrip
from PyQt5.QtCore import QEasingCurve, Qt, QEvent
from PyQt5.QtGui import QCursor, QHoverEvent, QPixmap
import sys
# Functionality:
# - On initial mouse click
# Create new RubberBand, select area until mouse is released
# - Allow for resizing when selected
class SelectionBand(QWidget):
def __init__(self, parent=None):
super(SelectionBand, self).__init__()
# self.setMouseTracking=True
self.draggable = True
self.dragging_threshold = 5
self.borderRadius = 5
self.setWindowFlags(Qt.SubWindow)
layout = QHBoxLayout(self)
layout.setContentsMargins(0,0,0,0)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignRight | Qt.AlignBottom)
self._band = QtWidgets.QRubberBand(
QtWidgets.QRubberBand.Rectangle, self)
self._band.show()
self.show()
def resizeEvent(self, event):
self._band.resize(self.size())
Solution
Since you're using the scene rect as a reference, you must check that the geometry of the selection is within the scene margins, and you should also consider the minimum size of the rubber band widget.
Also, instead of making complex computations to check if the width or height are "negative", you can create a QRectF using the two points (the clicked position and that received from the mouse movement), then use the normalized()
function to get the same rectangle but with positive width and height.
class CustomGraphicsView(QGraphicsView):
def mousePressEvent(self, event):
self.startPos = self.mapToScene(event.pos())
if not self.band:
self.band = SelectionBand(self)
self.bandItem = self.scene().addWidget(self.band)
self.bandItem.setFlag(QGraphicsItem.ItemIsMovable)
self.bandItem.setZValue(1)
sceneRect = self.sceneRect()
if self.startPos.x() < sceneRect.x():
self.startPos.setX(sceneRect.x())
elif self.startPos.x() > sceneRect.right() - self.band.minimumWidth():
self.startPos.setX(sceneRect.right() - self.band.minimumWidth())
if self.startPos.y() < sceneRect.y():
self.startPos.setY(sceneRect.y())
elif self.startPos.y() > sceneRect.bottom() - self.band.minimumHeight():
self.startPos.setY(sceneRect.bottom() - self.band.minimumHeight())
self.bandItem.setPos(self.startPos)
def mouseMoveEvent(self, event):
mousePos = self.mapToScene(event.pos())
# create a normalized rectangle based on the two points
rect = QRectF(self.startPos, mousePos).normalized()
if rect.width() < self.band.minimumWidth():
rect.setWidth(self.band.minimumWidth())
if rect.x() < self.startPos.x():
rect.moveLeft(self.startPos.x())
if rect.height() < self.band.minimumHeight():
rect.setHeight(self.band.minimumHeight())
if rect.y() < self.startPos.y():
rect.moveBottom(self.startPos.y())
sceneRect = self.sceneRect()
if rect.x() < sceneRect.x():
rect.setX(sceneRect.x())
elif rect.x() > sceneRect.right() - self.band.minimumWidth():
rect.setX(sceneRect.right() - self.band.minimumWidth())
if rect.right() > sceneRect.right():
rect.setRight(sceneRect.right())
if rect.y() < sceneRect.y():
rect.setY(sceneRect.y())
elif rect.y() > sceneRect.bottom() - self.band.minimumHeight():
rect.setY(sceneRect.bottom() - self.band.minimumHeight())
if rect.bottom() > sceneRect.bottom():
rect.setBottom(sceneRect.bottom())
self.bandItem.setGeometry(rect)
Consider that using a QWidget for the selection is not a good idea (and your implementation is a bit flawed anyway): you're not using most of the features of a QWidget (except for the size grip, which is not used anyway), so using it is a bit pointless.
You could instead use a subclass of QGraphicsRectItem. This has many advantages: not only it is more coherent for its usage and for the graphics view framework, but, for instance, you can use a cosmetic pen for the border, which will always be clearly shown even when zooming out.
class SelectionBandItem(QGraphicsRectItem):
_minimumWidth = _minimumHeight = 1
def __init__(self):
super().__init__()
color = QApplication.palette().highlight().color()
pen = QPen(color, 1)
pen.setCosmetic(True)
self.setPen(pen)
color.setAlpha(128)
self.setBrush(color)
self.setFlag(QGraphicsItem.ItemIsMovable)
def minimumWidth(self):
return self._minimumWidth
def minimumHeight(self):
return self._minimumHeight
def setGeometry(self, rect):
# we cannot use setRect, as it will only set the drawn rectangle but
# not the position of the item, which makes it less intuitive; it's
# better to use the given rectangle to set the position and then
# resize the item's rectangle
self.setPos(rect.topLeft())
current = self.rect()
current.setSize(rect.size())
self.setRect(current)
def geometry(self):
return self.rect().translated(self.pos())
class CustomGraphicsView(QGraphicsView):
# ...
def mousePressEvent(self, event):
self.startPos = self.mapToScene(event.pos())
if not self.band:
self.band = SelectionBandItem()
self.scene().addItem(self.band)
sceneRect = self.sceneRect()
if self.startPos.x() < sceneRect.x():
self.startPos.setX(sceneRect.x())
elif self.startPos.x() > sceneRect.right():
self.startPos.setX(sceneRect.right())
if self.startPos.y() < sceneRect.y():
self.startPos.setY(sceneRect.y())
elif self.startPos.y() > sceneRect.bottom():
self.startPos.setY(sceneRect.bottom())
self.band.setPos(self.startPos)
def mouseMoveEvent(self, event):
# all of the above, except for this:
self.band.setGeometry(rect)
Then, if you want to add support for the size grip, implement that in the mouseMoveEvent
of the SelectionBandItem
class.
Unrelated notes:
self.width
andself.height
are properties of all Qt widgets, you should not overwrite them with other values, especially if those values are not related toself
; also, since you're continuously changing those values in themouseMoveEvent
, there's really no point in making them instance attributes: if you need access to the size of the band from other functions, just useself.band.width()
orself.band.height()
;- there's no point in dividing the angle delta by 120 if you're just checking if it's greater or less than 0;
- new items are always added on top of the item stack at 0 z-level, so there's little use in setting it to 1;
setMouseTracking
is a function and not a variable;- when adding new widgets to a scene, you should not call
show
in their__init__
, as it causes them to show as a normal window for an instant; - the
stretch
argument ofQBoxLayout.addWidget
already defaults to zero, if you want to specify the alignment it's better to explicitly use the keyword; also, if you add a widget to a layout there's no need for the parent argument:layout.addWidget(QSizeGrip(), alignment=Qt.AlignRight|Qt.AlignBottom)
; - if you're adding the graphics view to a layout, calling its
resize()
is completely useless; the scope of a layout manager is to manage the layout by setting the position and size of all its items, so manually requesting any geometry change is conceptually wrong; while it can resize the widget, as soon as any of the parent or sibling is resized, the layout will override that request;
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.