How to prevent clip a QRubberband in a certain region? - python

I am planning to make a small photo cropping software and I encounter this problem where when I moved the QRubberband I made, it is kind of moving past to the borders of the QLabel.
Here is the sample code: (Left-click and drag to make a QRubberband and Right-click to move the QRubberband)
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ResizableRubberBand(QWidget):
def __init__(self, parent=None):
super(ResizableRubberBand, self).__init__(parent)
self.draggable = True
self.dragging_threshold = 5
self.mousePressPos = None
self.mouseMovePos = None
self.borderRadius = 5
self.setWindowFlags(Qt.SubWindow)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignLeft | Qt.AlignTop)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignRight | Qt.AlignBottom)
self._band = QRubberBand(
QRubberBand.Rectangle, self)
self._band.show()
self.show()
def resizeEvent(self, event):
self._band.resize(self.size())
def paintEvent(self, event):
# Get current window size
window_size = self.size()
qp = QPainter()
qp.begin(self)
qp.setRenderHint(QPainter.Antialiasing, True)
qp.drawRoundedRect(0, 0, window_size.width(), window_size.height(),
self.borderRadius, self.borderRadius)
qp.end()
def mousePressEvent(self, event):
if self.draggable and event.button() == Qt.RightButton:
self.mousePressPos = event.globalPos() # global
self.mouseMovePos = event.globalPos() - self.pos() # local
super(ResizableRubberBand, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.draggable and event.buttons() & Qt.RightButton:
globalPos = event.globalPos()
moved = globalPos - self.mousePressPos
if moved.manhattanLength() > self.dragging_threshold:
# Move when user drag window more than dragging_threshold
diff = globalPos - self.mouseMovePos
self.move(diff)
self.mouseMovePos = globalPos - self.pos()
super(ResizableRubberBand, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.mousePressPos is not None:
if event.button() == Qt.RightButton:
moved = event.globalPos() - self.mousePressPos
if moved.manhattanLength() > self.dragging_threshold:
# Do not call click event or so on
event.ignore()
self.mousePressPos = None
super(ResizableRubberBand, self).mouseReleaseEvent(event)
class mQLabel(QLabel):
def __init__(self, parent=None):
QLabel.__init__(self, parent)
self.setContentsMargins(0,0,0,0)
self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton: #and (hasattr(self, 'bla')):
self.first_mouse_location = (event.x(), event.y())
self.band = ResizableRubberBand(self)
self.band.setGeometry(event.x(), event.y(), 0, 0)
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
first_mouse_location_x = self.first_mouse_location[0]
first_mouse_location_y = self.first_mouse_location[1]
new_x, new_y = event.x(), event.y()
difference_x = new_x - first_mouse_location_x
difference_y = new_y - first_mouse_location_y
self.band.resize(difference_x, difference_y)
class App(QWidget):
def __init__(self):
super().__init__()
## Set main window attributes
self.setFixedSize(1000,600)
# Add Label
self.label = mQLabel()
self.label.setStyleSheet("border: 1px solid black;")
self.label_layout = QHBoxLayout(self)
self.label_layout.addWidget(self.label)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I can't really think of a simple solution where I can do it. My first instinct is to get the size of the parent, but I don't know what should I do next.

You need to compare the widget geometry based on the parent rectangle.
Note that you shouldn't use the global position as the tracking should always happen in local coordinates (relative to the parent). Also, as soon as the dragging has started, you should not need to check again the manhattanLength(), and the self.mousePressPos should be cleared in any case on release.
class ResizableRubberBand(QRubberBand):
is_dragging = False
def mousePressEvent(self, event):
if self.draggable and event.button() == Qt.RightButton:
self.mousePressPos = event.pos()
super(ResizableRubberBand, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.draggable and event.buttons() & Qt.RightButton:
diff = event.pos() - self.mousePressPos
if not self.is_dragging:
if diff.manhattanLength() > self.dragging_threshold:
self.is_dragging = True
if self.is_dragging:
geo = self.geometry()
parentRect = self.parent().rect()
geo.translate(diff)
if not parentRect.contains(geo):
if geo.right() > parentRect.right():
geo.moveRight(parentRect.right())
elif geo.x() < parentRect.x():
geo.moveLeft(parentRect.x())
if geo.bottom() > parentRect.bottom():
geo.moveBottom(parentRect.bottom())
elif geo.y() < parentRect.y():
geo.moveTop(parentRect.y())
self.move(geo.topLeft())
self.clearMask()
super(ResizableRubberBand, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.mousePressPos is not None:
if event.button() == Qt.RightButton and self.is_dragging:
event.ignore()
self.is_dragging = False
self.mousePressPos = None
super(ResizableRubberBand, self).mouseReleaseEvent(event)
Also note that you're not actually using QRubberBand in a very good way, also because you're drawing over its border. In any case, a better implementation would directly subclass QRubberBand:
class ResizableRubberBand(QRubberBand):
def __init__(self, parent=None):
super(ResizableRubberBand, self).__init__(QRubberBand.Rectangle, parent)
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.draggable = True
self.is_dragging = False
self.dragging_threshold = 5
self.mousePressPos = None
self.borderRadius = 5
self.setWindowFlags(Qt.SubWindow)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignLeft | Qt.AlignTop)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignRight | Qt.AlignBottom)
self.show()
def resizeEvent(self, event):
self.clearMask()
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.setRenderHint(QPainter.Antialiasing)
qp.translate(.5, .5)
qp.drawRoundedRect(self.rect().adjusted(0, 0, -1, -1),
self.borderRadius, self.borderRadius)
If you used QRubberBand only for aesthetic purposes, you don't need it at all, you can just subclass from QWidget and use the style functions to draw a "fake" rubber band:
def paintEvent(self, event):
qp = QPainter(self)
qp.setRenderHint(QPainter.Antialiasing)
qp.translate(.5, .5)
opt = QStyleOptionRubberBand()
opt.initFrom(self)
style = self.style()
style.drawControl(style.CE_RubberBand, opt, qp)
qp.drawRoundedRect(self.rect().adjusted(0, 0, -1, -1),
self.borderRadius, self.borderRadius)

Related

Pyqt: custom frame doesn't detect mouse click under mouseMoveEvent

I have created a custom Frame for a Qgraphicswidget. I have come across two problems, First, one being that clicks are not being detected under MouseMoveEvent, that is event.button() always returns 0 even if there is a mouse click. Second, is that my setCursor() doesn't change the cursor. Here is my code under the custom frame class.
from PyQt5 import QtGui, QtCore
from PyQt5.QtCore import Qt, QRectF, QEvent, QPoint
from PyQt5.QtGui import QPen, QColor, QPainter, QBrush, qRgb, QPolygon
from PyQt5.QtWidgets import *
import sys
class Frame(QFrame):
def __init__(self, parent=None, option=[], margin=0):
super(Frame, self).__init__()
self.parent = parent
self._triangle = QPolygon()
self.options = option
self._margin = margin
self.start_pos = None
# self.parent.setViewport(parent)
self.setStyleSheet('background-color: lightblue')
self.setMouseTracking(True)
self.installEventFilter(self)
self.show()
def update_option(self, option):
self.options = option
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.setPen(Qt.white)
qp.setBrush(Qt.gray)
qp.drawPolygon(self._triangle)
def _recalculate_triangle(self):
p = QPoint(self.width() - 20, self.height() - 10)
q = QPoint(self.width() - 10, self.height() - 20)
r = QPoint(self.width() - 10, self.height() - 10)
self._triangle = QPolygon([p, q, r])
self.update()
def resizeEvent(self, event):
self._recalculate_triangle()
super().resizeEvent(event)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
if event.button() == Qt.LeftButton and self._triangle.containsPoint(
event.pos(), Qt.OddEvenFill
):
self.parent.viewport().setCursor(Qt.SizeFDiagCursor)
self.start_pos = event.pos()
# print(self.start_pos)
else:
self.parent.viewport().unsetCursor()
self.start_pos = None
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.parent.viewport().setCursor(Qt.SizeFDiagCursor)
else:
self.parent.viewport().unsetCursor()
self.start_pos = None
if event.button() == Qt.LeftButton:
if event.button() == QtCore.Qt.LeftButton and self.start_pos is not None:
self.parent.viewport().setCursor(Qt.SizeFDiagCursor)
delta = event.pos() - self.start_pos
self.n_resize(self.width()+delta.x(), self.height()+delta.y())
self.start_pos = event.pos()
elif not self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.parent.viewport().unsetCursor()
self.start_pos = None
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.parent.viewport().unsetCursor()
self.start_pos = None
super().mouseReleaseEvent(event)
def n_resize(self, width, height):
self.resize(width, height)
if __name__ == '__main__':
q = QApplication(sys.argv)
a = Frame()
sys.exit(q.exec())
I have also tried using eventfilter but of no use.
EDIT:
from PyQt5 import QtGui, QtCore
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QPen, QColor, QPainter, QBrush, QPolygon
from PyQt5.QtWidgets import *
import sys
class graphLayout(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.scene = QGraphicsScene()
self.lines = []
self.draw_grid()
self.set_opacity(0.3)
widget = QGraphicsProxyWidget()
t = stack(self)
t.setFlag(QGraphicsItem.ItemIsMovable)
t.resize(340, 330)
self.scene.addItem(t)
self.setScene(self.scene)
self.show()
def create_texture(self):
image = QtGui.QImage(QtCore.QSize(30, 30), QtGui.QImage.Format_RGBA64)
pen = QPen()
pen.setColor(QColor(189, 190, 191))
pen.setWidth(2)
painter = QtGui.QPainter(image)
painter.setPen(pen)
painter.drawRect(image.rect())
painter.end()
return image
def draw_grid(self):
texture = self.create_texture()
brush = QBrush()
# brush.setColor(QColor('#999'))
brush.setTextureImage(texture) # Grid pattern.
self.scene.setBackgroundBrush(brush)
borderColor = Qt.black
fillColor = QColor('#DDD')
def set_visible(self, visible=True):
for line in self.lines:
line.setVisible(visible)
def delete_grid(self):
for line in self.lines:
self.scene.removeItem(line)
del self.lines[:]
def set_opacity(self, opacity):
for line in self.lines:
line.setOpacity(opacity)
def wheelEvent(self, event):
if event.modifiers() == Qt.ControlModifier:
delta = event.angleDelta().y()
if delta > 0:
self.on_zoom_in()
elif delta < 0:
self.on_zoom_out()
super(graphLayout, self).wheelEvent(event)
def mousePressEvent(self, event):
if event.button() == Qt.MidButton:
self.setCursor(Qt.OpenHandCursor)
self.mousepos = event.localPos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
# This helps to pan the area
if event.buttons() == Qt.MidButton:
delta = event.localPos() - self.mousepos
h = self.horizontalScrollBar().value()
v = self.verticalScrollBar().value()
self.horizontalScrollBar().setValue(int(h - delta.x()))
self.verticalScrollBar().setValue(int(v - delta.y()))
self.mousepos = event.localPos()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.unsetCursor()
self.mousepos = event.localPos()
super().mouseReleaseEvent(event)
def on_zoom_in(self):
if self.transform().m11() < 3.375:
self.setTransformationAnchor(self.AnchorUnderMouse)
self.scale(1.5, 1.5)
def on_zoom_out(self):
if self.transform().m11() > 0.7:
self.setTransformationAnchor(self.AnchorUnderMouse)
self.scale(1.0 / 1.5, 1.0 / 1.5)
class stack(QGraphicsWidget):
_margin = 0
def __init__(self, parent=None):
super().__init__()
self.options = []
self.gridlayout = parent
graphic_layout = QGraphicsLinearLayout(Qt.Vertical, self)
self.width, self.height = 10, 10
self.outer_container = Frame(parent, self.options, self._margin)
self.outer_container.setContentsMargins(0, 0, 0, 0)
self.setParent(self.outer_container)
layout = QVBoxLayout()
self.headerLayout = QGridLayout()
self.headerLayout.setContentsMargins(2, 0, 0, 0)
self.top_bar = QFrame()
self.top_bar.setFrameShape(QFrame.StyledPanel)
self.top_bar.setLayout(self.headerLayout)
self.top_bar.setContentsMargins(0, 0, 0, 0)
self.top_bar.setMaximumHeight(30)
# self.contentLayout = QVBoxLayout()
self.contentLayout = QFormLayout()
self.contentLayout.setContentsMargins(10, 10, 10, 10)
self.contentLayout.setSpacing(5)
layout.addWidget(self.top_bar)
layout.addLayout(self.contentLayout)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.outer_container.setContentsMargins(0, 0, 0, 0)
self.setContentsMargins(0, 0, 0, 0)
self.setMaximumSize(400, 800)
self.outer_container.setLayout(layout)
widget = QGraphicsProxyWidget()
widget.setWidget(self.outer_container)
# todo: figure out a way to add top_bar widget
graphic_layout.addItem(widget)
graphic_layout.setSpacing(0)
graphic_layout.setContentsMargins(0, 0, 0, 0)
# widget move and resize note: don't touch any of these
self.__mouseMovePos = None
self._triangle = QPolygon()
self.start_pos = None
def addHeaderWidget(self, widget=None, column=0, bg_color='green'):
self.top_bar.setStyleSheet(f'background-color:{bg_color};')
self.headerLayout.addWidget(widget, 0, column)
class Frame(QFrame):
def __init__(self, parent=None, option=[], margin=0):
super(Frame, self).__init__()
self.parent = parent
self._triangle = QPolygon()
self.options = option
self._margin = margin
self.start_pos = None
# self.parent.setViewport(parent)
self.setStyleSheet('background-color: lightblue')
self.setMouseTracking(True)
self.installEventFilter(self)
self.show()
def update_option(self, option):
self.options = option
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.setPen(Qt.white)
qp.setBrush(Qt.gray)
qp.drawPolygon(self._triangle)
def _recalculate_triangle(self):
p = QPoint(self.width() - 20, self.height() - 10)
q = QPoint(self.width() - 10, self.height() - 20)
r = QPoint(self.width() - 10, self.height() - 10)
self._triangle = QPolygon([p, q, r])
self.update()
def resizeEvent(self, event):
self._recalculate_triangle()
super().resizeEvent(event)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
if event.button() == Qt.LeftButton and self._triangle.containsPoint(
event.pos(), Qt.OddEvenFill
):
self.parent.viewport().setCursor(Qt.SizeFDiagCursor)
self.start_pos = event.pos()
# print(self.start_pos)
else:
self.parent.viewport().unsetCursor()
self.start_pos = None
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.parent.viewport().setCursor(Qt.SizeFDiagCursor)
else:
self.parent.viewport().unsetCursor()
self.start_pos = None
if event.button() == Qt.LeftButton:
if event.button() == QtCore.Qt.LeftButton and self.start_pos is not None:
self.parent.viewport().setCursor(Qt.SizeFDiagCursor)
delta = event.pos() - self.start_pos
self.n_resize(self.width()+delta.x(), self.height()+delta.y())
self.start_pos = event.pos()
elif not self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.parent.viewport().unsetCursor()
self.start_pos = None
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.parent.viewport().unsetCursor()
self.start_pos = None
super().mouseReleaseEvent(event)
def n_resize(self, width, height):
self.resize(width, height)
if __name__ == '__main__':
q = QApplication(sys.argv)
a = graphLayout()
sys.exit(q.exec())
There are various issues on your code, but the base problems are:
mouse button state cannot be retrieved by event.button() in a MouseMove event, and event.buttons() should be used instead: the difference is clear: button() shows the buttons that generate the event (and a mouse move event is obviously not generated by any button), buttons() shows the button state when the event is generated;
events that are not explicitly managed by an object are always propagated to its parent(s), which means that your mouse movements are also possibly processed by the parent widget, then the graphics proxy, the scene, the viewport, the view, etc, and that up to the top level window, until one of the previous objects actually returns True from event() or an event filter; in your case it results in moving the graphics item, since you enabled the ItemIsMovable flag.
I don't know why the cursor is not actually set, but frankly your code is so convoluted that I really cannot find the reason.
Since what you're actually looking for is a way to resize the widget, I suggest you another solution.
While implementing a resizing with custom painting is certainly feasible, in most cases it's well enough to use a QSizeGrip (as already suggested to you in another post), which is a widget that allows resizing top-level windows and is automatically able to understand which "corner" use for the resizing based on its position. Remember that the parent of the QSizeGrip is very important, because it uses it to understand which is its top level window, and, in this case, the "container frame", even if it's in a QGraphicsScene.
Note that QSizeGrip should not be added to a layout, and it should always be manually moved according to its corner position and the size of its parent (unless it's placed on the top left corner), and since you already need custom painting, it's better to subclass it.
from PyQt5 import QtCore, QtGui, QtWidgets
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 mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.startPos = event.pos()
def mouseMoveEvent(self, event):
if self.startPos:
self.move(self.pos() + (event.pos() - self.startPos))
def mouseReleaseEvent(self, event):
self.startPos = None
class GraphicsRoundedFrame(QtWidgets.QGraphicsProxyWidget):
def __init__(self):
super().__init__()
self.container = Container()
self.setWidget(self.container)
def addWidget(self, widget):
self.container.layout().addWidget(widget)
def paint(self, qp, opt, widget):
qp.save()
p = QtGui.QPainterPath()
p.addRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)
qp.setClipPath(p)
super().paint(qp, opt, widget)
qp.restore()
class View(QtWidgets.QGraphicsView):
def __init__(self):
super().__init__()
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
self.setRenderHints(QtGui.QPainter.Antialiasing)
scene.setSceneRect(0, 0, 1024, 768)
texture = QtGui.QImage(30, 30, QtGui.QImage.Format_ARGB32)
qp = QtGui.QPainter(texture)
qp.setBrush(QtCore.Qt.white)
qp.setPen(QtGui.QPen(QtGui.QColor(189, 190, 191), 2))
qp.drawRect(texture.rect())
qp.end()
scene.setBackgroundBrush(QtGui.QBrush(texture))
testFrame = GraphicsRoundedFrame()
scene.addItem(testFrame)
testFrame.addWidget(QtWidgets.QLabel('I am a label'))
testFrame.addWidget(QtWidgets.QPushButton('I am a button'))
import sys
app = QtWidgets.QApplication(sys.argv)
w = View()
w.show()
sys.exit(app.exec_())

Resizing widget by dragging

I have a custom widget. I would like to resize by dragging a corner of the widget. Currently, it automatically resizes when the cursor is near the corner of the widget
Here is the sample code. I would like to resize the widget by dragging the red triangle at the bottom right corner. Can I know how to do that?
from PyQt5.QtCore import Qt, QPoint, QPointF, QEvent
from PyQt5.QtGui import QPainter, QIcon, QColor, QPolygon
from PyQt5.QtWidgets import *
import sys
class Stack(QWidget):
def __init__(self, parent=None):
super(Stack, self).__init__(parent)
self._triangle = QPolygon()
self.setMouseTracking(True)
self.old_x_pos = 0
self.old_y_pos = 0
def _recalculate_triangle(self):
p = QPoint(self.width() - 20, self.height() - 10)
q = QPoint(self.width() - 10, self.height() - 20)
r = QPoint(self.width() - 10, self.height() - 10)
self._triangle = QPolygon([p, q, r])
self.update()
def resizeEvent(self, event):
self._recalculate_triangle()
def paintEvent(self, event):
super(Stack, self).paintEvent(event)
qp = QPainter(self)
qp.setPen(Qt.white)
qp.setBrush(Qt.white)
qp.drawRect(10, 10, 150, 150)
qp.setPen(Qt.white)
qp.setBrush(Qt.red)
qp.drawPolygon(self._triangle)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and self._triangle.containsPoint(
event.pos(), Qt.OddEvenFill
):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()
super(Stack, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.setCursor(Qt.SizeFDiagCursor)
self.setCursor(Qt.SizeFDiagCursor)
if event.x() > self.old_x_pos and event.y() > self.old_y_pos:
self.resize(self.width() + (self.width() - event.x()), self.height() + (self.height() - event.y()))
elif event.x() < self.old_x_pos and event.y() < self.old_y_pos:
self.resize(self.width() - (self.width() - event.x()), self.height() - (self.height() - event.y()))
else:
self.unsetCursor()
print(self.old_y_pos)
self.old_x_pos = event.x()
self.old_y_pos = event.y()
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Stack()
win.show()
sys.exit(app.exec_())
Ok, I figured it out. For anyone else here is the modified code from the question. Using this code, you can resize the widget as well as move your custom widget around in the main window or the frame. To apply to your code you can simply copy-paste the code under __init__() and code under the mouse events.
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QPainter, QPolygon
from PyQt5.QtWidgets import *
class Stack(QWidget):
def __init__(self, parent=None):
super(Stack, self).__init__(parent)
self.__mouseMovePos = None
self.setMouseTracking(True)
self._triangle = QPolygon()
self.start_pos = None
def _recalculate_triangle(self):
p = QPoint(self.width() - 20, self.height() - 10)
q = QPoint(self.width() - 10, self.height() - 20)
r = QPoint(self.width() - 10, self.height() - 10)
self._triangle = QPolygon([p, q, r])
self.update()
def resizeEvent(self, event):
self._recalculate_triangle()
def paintEvent(self, event):
super(Stack, self).paintEvent(event)
qp = QPainter(self)
qp.setPen(Qt.white)
qp.setBrush(Qt.white)
qp.drawRect(10, 10, 150, 150)
qp.setPen(Qt.white)
qp.setBrush(Qt.red)
qp.drawPolygon(self._triangle)
def mousePressEvent(self, event):
self.__mousePressPos = None
self.__mouseMovePos = None
if event.button() == Qt.LeftButton:
if event.button() == Qt.LeftButton and self._triangle.containsPoint(
event.pos(), Qt.OddEvenFill
):
self.setCursor(Qt.SizeFDiagCursor)
self.start_pos = event.pos()
else:
self.__mousePressPos = event.globalPos()
self.__mouseMovePos = event.globalPos()
self.start_pos = None
self.unsetCursor()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()
if event.buttons() == Qt.LeftButton:
if event.buttons() == Qt.LeftButton and self.start_pos is not None:
self.setCursor(Qt.SizeFDiagCursor)
delta = event.pos() - self.start_pos
self.resize(self.width() + delta.x(), self.height() + delta.y())
self.start_pos = event.pos()
elif not self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
currPos = self.mapToGlobal(self.pos())
globalPos = event.globalPos()
diff = globalPos - self.__mouseMovePos
newPos = self.mapFromGlobal(currPos + diff)
self.move(newPos)
self.__mouseMovePos = globalPos
self.start_pos = None
self.unsetCursor()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.__mousePressPos is not None:
moved = event.globalPos() - self.__mousePressPos
if moved.manhattanLength() > 3:
event.ignore()
return
self.start_pos = None
super().mouseReleaseEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Stack()
win.show()
sys.exit(app.exec_())

Change the cursor if hovered over a particular region of a widget

I have created a custom widget in which I have drawn a small triangle. I would like to know whether I can change my cursor if hover over the triangle I have drawn over the widget.
Note: I have tried to use mouseMoveEvent but it doesn't update unless clicked. I have also tried to eventFilter but it looks like it doesn't even enter that method.
Here is a similar code not the same. The triangle is at the bottom. I want the cursor to automatically update when it's over the red triangle
from PyQt5.QtCore import Qt, QPoint, QPointF, QEvent
from PyQt5.QtGui import QPainter, QIcon, QColor
from PyQt5.QtWidgets import *
from PyQt5 import QtCore
import sys
class Stack(QWidget):
def __init__(self, parent=None):
super(Stack, self).__init__(parent)
self.cursor_x = [x for x in range(self.width() - 20, self.width())]
self.cursor_y = [y for y in range(self.height() - 20, self.height())]
def paintEvent(self, event):
super(Stack, self).paintEvent(event)
qp = QPainter(self)
qp.setPen(Qt.white)
qp.setBrush(Qt.white)
qp.drawRect(10, 10, 150, 150)
p = QPointF(self.width() - 20, self.height() - 10)
q = QPointF(self.width() - 10, self.height() - 20)
r = QPointF(self.width() - 10, self.height() - 10)
qp.setPen(Qt.white)
qp.setBrush(Qt.red)
qp.drawPolygon(p, q, r)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
if event.x() in self.cursor_x and event.y() in self.cursor_y:
print('yes')
self.setCursor(QtCore.Qt.SizeFDiagCursor)
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.x() in self.cursor_x and event.y() in self.cursor_y:
self.setCursor(QtCore.Qt.SizeFDiagCursor)
super(Stack, self).mouseMoveEvent(event)
def eventFilter(self, obj, event):
print(event)
print('hello')
if obj is self and event.type() == QEvent.HoverEnter:
print("Mouse is over the label")
super().eventFilter(event)
def mouseReleaseEvent(self, event):
if event.x() not in self.cursor_x and event.y() not in self.cursor_y:
self.unsetCursor()
super(Stack, self).mouseReleaseEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Stack()
win.show()
sys.exit(app.exec_())
You have to deactivate the cursor also when it is outside the region. For the mouseMoveEvent you must enable the mouseTracking property. And finally I use a QPolygon to check if the cursor is inside or outside the rectangle:
class Stack(QWidget):
def __init__(self, parent=None):
super(Stack, self).__init__(parent)
self._triangle = QPolygon()
self.setMouseTracking(True)
def _recalculate_triangle(self):
p = QPoint(self.width() - 20, self.height() - 10)
q = QPoint(self.width() - 10, self.height() - 20)
r = QPoint(self.width() - 10, self.height() - 10)
self._triangle = QPolygon([p, q, r])
self.update()
def resizeEvent(self, event):
self._recalculate_triangle()
def paintEvent(self, event):
super(Stack, self).paintEvent(event)
qp = QPainter(self)
qp.setPen(Qt.white)
qp.setBrush(Qt.white)
qp.drawRect(10, 10, 150, 150)
qp.setPen(Qt.white)
qp.setBrush(Qt.red)
qp.drawPolygon(self._triangle)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and self._triangle.containsPoint(
event.pos(), Qt.OddEvenFill
):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()
def mouseMoveEvent(self, event):
if self._triangle.containsPoint(event.pos(), Qt.OddEvenFill):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and self._triangle.containsPoint(
event.pos(), Qt.OddEvenFill
):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()

How to get widgets under a QPaintEvent overlay to register mouse events

I am using an overlay with a QPaintEvent to draw over other widgets. I still need the widgets underneath to register mouse events. When I call raise() with the WA_TransparentForMouseEvents flag, I gain control of the widgets again, but of course, lose the paintevent as it's no longer registering any mouse events. What are my options here?
class Overlay(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Overlay, self).__init__(parent)
self.hotBox = parent
self.resize(self.hotBox.width, self.hotBox.height)
def paintEvent(self, event):
#args: [QEvent]
if any ([self.hotBox.name=="main", self.hotBox.name=="viewport"]):
self.raise_()
self.setWindowFlags(QtCore.Qt.WA_TransparentForMouseEvents)
#Initialize painter
painter = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(115, 115, 115), 3, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
painter.setPen(pen)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setBrush(QtGui.QColor(115, 115, 115))
painter.drawEllipse(self.hotBox.point, 5, 5)
#perform paint
if self.hotBox.mousePosition:
mouseX = self.hotBox.mousePosition.x()
mouseY = self.hotBox.mousePosition.y()
line = QtCore.QLine(mouseX, mouseY, self.hotBox.point.x(), self.hotBox.point.y())
painter.drawLine(line)
painter.drawEllipse(mouseX-5, mouseY-5, 10, 10)
If you want to create an overlay using the events mouseXXXEvent you must use an eventFilter, for this solution I based on this answer of #KubaOber adding more functionalities:
import sys
from PySide2 import QtCore, QtGui, QtWidgets
class Overlay(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Overlay, self).__init__(parent)
self.setAttribute(QtCore.Qt.WA_NoSystemBackground)
self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.start_line, self.end_line = QtCore.QPoint(), QtCore.QPoint()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtGui.QColor(80, 80, 255, 128))
if not self.start_line.isNull() and not self.end_line.isNull():
painter.drawLine(self.start_line, self.end_line)
def mousePressEvent(self, event):
self.start_line = event.pos()
self.end_line = event.pos()
self.update()
def mouseMoveEvent(self, event):
self.end_line = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.start_line = QtCore.QPoint()
self.end_line = QtCore.QPoint()
class OverlayFactoryFilter(QtCore.QObject):
def __init__(self, parent=None):
super(OverlayFactoryFilter, self).__init__(parent)
self.m_overlay = None
def setWidget(self, w):
w.installEventFilter(self)
if self.m_overlay is None:
self.m_overlay = Overlay()
self.m_overlay.setParent(w)
def eventFilter(self, obj, event):
if not obj.isWidgetType():
return False
if event.type() == QtCore.QEvent.MouseButtonPress:
self.m_overlay.mousePressEvent(event)
elif event.type() == QtCore.QEvent.MouseButtonRelease:
self.m_overlay.mouseReleaseEvent(event)
elif event.type() == QtCore.QEvent.MouseMove:
self.m_overlay.mouseMoveEvent(event)
elif event.type() == QtCore.QEvent.MouseButtonDblClick:
self.m_overlay.mouseDoubleClickEvent(event)
elif event.type() == QtCore.QEvent.Resize:
if self.m_overlay and self.m_overlay.parentWidget() == obj:
self.m_overlay.resize(obj.size())
elif event.type() == QtCore.QEvent.Show:
self.m_overlay.raise_()
return super(OverlayFactoryFilter, self).eventFilter(obj, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
factory = OverlayFactoryFilter()
w = QtWidgets.QWidget()
factory.setWidget(w)
button = QtWidgets.QPushButton("Press me", w)
w.show()
sys.exit(app.exec_())

Broken drawing shapes

In trying to Implement paint, I decided to begin with shapes
My init:
def __init__(self, parent=None):
self.modified = False
self.rectangle = False
self.ellipse = False
self.begin = QPoint()
self.end = QPoint()
self.myPenWidth = 1
self.myFigureColor = Qt.black
self.image = QImage()
I have an event in which there are functions for drawing shapes
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton) and self.rectangle:
self.draw_rectandle(event)
if (event.button() == Qt.LeftButton) and self.ellipse:
self.draw_ellipse(event)
Here we use the function in which is located a drawing figures:
I'll post them below
def mouseMoveEvent(self, event):
if (event.buttons() & Qt.LeftButton) and self.rectangle:
self.end = event.pos()
self.update()
if (event.buttons() & Qt.LeftButton) and self.ellipse:
self.end = event.pos()
self.update()
And
def mouseReleaseEvent(self, event):
if (event.buttons() & Qt.LeftButton) and self.rectangle:
self.begin = event.pos()
self.end = event.pos()
if (event.buttons() & Qt.LeftButton) and self.ellipse:
self.begin = event.pos()
self.end = event.pos()
And another function paintEvent:
def paintEvent(self, event):
painter = QPainter(self)
dirtyRect = event.rect()
painter.drawImage(dirtyRect, self.image, dirtyRect)
if self.rectangle == True:
painter.setPen(QPen(self.myFigureColor, self.myPenWidth,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawRect(QRect(self.begin, self.end))
if self.ellipse == True:
painter.setPen(QPen(self.myFigureColor, self.myPenWidth,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawEllipse(QRect(self.begin, self.end))
The functions of figures themselves:
def draw_rectandle(self, event):
painter = QPainter(self.image)
painter.setPen(QPen(self.myFigureColor, self.myPenWidth,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawRect(QRect(self.begin, self.end))
self.begin = event.pos()
self.end = event.pos()
self.modified = True
self.update()
def draw_ellipse(self, event):
painter = QPainter(self.image)
painter.setPen(QPen(self.myFigureColor, self.myPenWidth,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawEllipse(QRect(self.begin, self.end))
self.begin = event.pos()
self.end = event.pos()
self.modified = True
self.update()
Also "pseudo binds"
def rectangleOn(self):
self.ellipse = False
self.rectangle = True
def ellipseOn(self):
self.rectangle = False
self.ellipse = True
They are used to change the function flags when the shape button is pressed.
The buttons themselves look like if you need to:
Actions:
self.rectangle = QAction(QIcon('Image/rectangle.png'), 'Rectangle', self)
self.rectangle.triggered.connect(self.scribbleArea.rectangleOn)
self.ellipse = QAction(QIcon('Image/Ellipse.png'), 'Ellipse', self)
self.ellipse.triggered.connect(self.scribbleArea.ellipseOn)
Toolbar with button:
toolbar = self.addToolBar('Tools')
toolbar.addAction(self.rectangle)
toolbar.addAction(self.ellipse)
I think that there is no need for a function to change the color and size. Therefore, while I will not post.
Now I'll tell you what the problem is.
In paintEvent, I have a drawing showing a shape. That is, we pull the edge, and see how the shape changes. But then from there it is not saved anywhere.
And in mousePressEvent already drawing the rectangle itself without this drag animation
It seems that all is well, but here there is a bug
As you can see, the drawing of the rectangle goes to the mousePressEvent. And this means that the rectangle appears only when I start drawing the next one. This is already a mistake. BUT! If I draw a few rectangles and start drawing an ellipse, the last rectangle becomes an ellipse.
And also with other my shapes.
If I start drawing a line, and then switch to an ellipse and start drawing ellipse in another place, the line drawn by me will immediately become a very narrow ellipse.
Therefore, I need to somehow pull out this function from the mousePressEvent, so that the desired shape is drawn, with such switching between tools.
If you need more details, write с:
P.S. I apologize for my English, if something is wrong с:
I can not reproduce because you show your code to pieces, I recommend you always show a united code so we can easily execute those that we want to help.
As you have noticed, you must save the data in the image if you want them to remain and the temporary data to paint them directly in the widget. The logic of my solution is that from mousePressEvent until an instant before mouseReleaseEvent is painted directly in the widget, and then saved in the image, for this I created a class called AbstractScribbleArea that implements that general logic, and if you want to implement the logic for other shapes you should only overwrite the draw() method.
from PyQt5 import QtCore, QtGui, QtWidgets
class AbstractScribbleArea(QtWidgets.QWidget):
def __init__(self, parent=None):
super(AbstractScribbleArea, self).__init__(parent)
self._start = QtCore.QPoint()
self._end = QtCore.QPoint()
self._pixmap = QtGui.QPixmap()
self._shape = ""
self._color = QtGui.QColor("black")
self._pen_width = 1
self._pen = QtGui.QPen(self._color, self._pen_width,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self._start = event.pos()
self._end = event.pos()
self.update()
super(AbstractScribbleArea, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self._end = event.pos()
self.update()
super(AbstractScribbleArea, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
# draw on image
painter = QtGui.QPainter(self._pixmap)
painter.setPen(self._pen)
self.draw(painter, self._start, self._end, True)
self._start = QtCore.QPoint()
self._end = QtCore.QPoint()
self.update()
super(AbstractScribbleArea, self).mouseReleaseEvent(event)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(self.rect(), self._pixmap, self.rect())
painter.setPen(self._pen)
self.draw(painter, self._start, self._end, False)
def resizeEvent(self, event):
w = self.width() if self.width() > self._pixmap.width() else self._pixmap.width()
h = self.height() if self.height() > self._pixmap.height() else self._pixmap.height()
s = QtCore.QSize(w, h)
if s != self._pixmap.size():
pixmap = QtGui.QPixmap(self._pixmap)
self._pixmap = QtGui.QPixmap(s)
painter = QtGui.QPainter(self._pixmap)
painter.fillRect(QtCore.QRect(0, 0, w, h), QtCore.Qt.white)
painter.drawPixmap(pixmap.rect(), pixmap)
super(AbstractScribbleArea, self).resizeEvent(event)
def draw(self, painter, start, end, is_released):
raise NotImplementedError
def sizeHint(self):
return QtCore.QSize(640, 480)
def set_pen_width(self, width):
self._pen.setWidth(width)
def set_pen_color(self, color):
self._pen.setColor(QtGui.QColor(color))
def set_shape(self, shape):
self._shape =shape
def shape(self):
return self._shape
class ScribbleArea(AbstractScribbleArea):
def draw(self, painter, start, end, is_released):
if start.isNull() or end.isNull():
return
if self.shape() == "rectangle":
self.draw_rectangle(painter, start, end)
elif self.shape() == "ellipse":
self.draw_ellipse(painter, start, end)
def draw_rectangle(self, painter, start, end):
rect = QtCore.QRect(start, end)
painter.drawRect(rect)
def draw_ellipse(self, painter, start, end):
rect = QtCore.QRect(start, end)
painter.drawEllipse(rect)
#QtCore.pyqtSlot()
def rectangleOn(self):
self.set_shape("rectangle")
#QtCore.pyqtSlot()
def ellipseOn(self):
self.set_shape("ellipse")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scribbleArea = ScribbleArea()
self.scribbleArea.set_pen_width(10)
self.scribbleArea.set_pen_color("red")
self.setCentralWidget(self.scribbleArea)
self.rectangle = QtWidgets.QAction(QtGui.QIcon('Image/rectangle.png'), 'Rectangle', self)
self.rectangle.triggered.connect(self.scribbleArea.rectangleOn)
self.ellipse = QtWidgets.QAction(QtGui.QIcon('Image/Ellipse.png'), 'Ellipse', self)
self.ellipse.triggered.connect(self.scribbleArea.ellipseOn)
toolbar = self.addToolBar('Tools')
toolbar.addAction(self.rectangle)
toolbar.addAction(self.ellipse)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

Categories