Qt DragLeaveEvent not being called when leaving with a drag operation - python

I have a row of buttons, each of which can accept drops. However, when I leave a button with my cursor with another button being dragged, the 'dragLeaveEvent' is not being called.
class Button(QtGui.QPushButton):
def __init__(self):
super(Button, self).__init__()
self.setAcceptDrops(True)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
drag = QtGui.QDrag(self)
mime = QtCore.QMimeData()
mime.setText("f")
drag.setMimeData(mime)
drag.exec_()
def dragEnterEvent(self, event):
print "enter"
def dragLeaveEvent(self, event):
print "leave"
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.mainLayout = QtGui.QVBoxLayout()
self.setLayout(self.mainLayout)
for i in range(10):
btn = Button()
self.mainLayout.addWidget(btn)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec_())

As the documentation about dragEnterEvent() reports (it's from Qt5, but the same was valid for Qt4 also):
If the event is ignored, the widget won't receive any drag move events.
Note: any drag move events.
This also means drop events.
By default, all drag events are ignored for most widgets if the drag enter event is not accepted, so if you want to receive all events (including the leave event) that first event must be accepted.
class Button(QtGui.QPushButton):
# ...
def dragEnterEvent(self, event):
event.accept()
print "enter"

Related

Signals or events for QMenu tear-off

Is there a signal/event I can make use of for the QMenu tear-off?
I have a QMenu subclass in which it has .setTearOffEnabled(True), but I would like to set this tear-off to always be on the top if the user clicks on the tear-off 'bar'.
I am unable to utilise QtCore.Qt.WindowStaysOnTopHint, as this will result in my menu already being in the tear-off state.
For example: if my main tool area is bigger than the tear-off, and I click on my main tool, the tear-off window will be behind it.
In the following code the clicked signal is emitted when the tear off (dotted lines) is pressed:
import sys
from PyQt5 import QtCore, QtWidgets
class Menu(QtWidgets.QMenu):
clicked = QtCore.pyqtSignal()
def mouseReleaseEvent(self, event):
if self.isTearOffEnabled():
tearRect = QtCore.QRect(
0,
0,
self.width(),
self.style().pixelMetric(
QtWidgets.QStyle.PM_MenuTearoffHeight, None, self
),
)
if tearRect.contains(event.pos()):
self.clicked.emit()
QtCore.QTimer.singleShot(0, self.after_clicked)
super(Menu, self).mouseReleaseEvent(event)
#QtCore.pyqtSlot()
def after_clicked(self):
tornPopup = None
for tl in QtWidgets.QApplication.topLevelWidgets():
if tl.metaObject().className() == "QTornOffMenu":
tornPopup = tl
break
if tornPopup is not None:
print("This is the tornPopup: ", tornPopup)
tornPopup.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow(parent=None)
menu = Menu("Menu", tearOffEnabled=True)
menu.clicked.connect(lambda: print("clicked"))
w.menuBar().addMenu(menu)
for i in range(5):
action = QtWidgets.QAction("action{}".format(i), w)
menu.addAction(action)
w.show()
sys.exit(app.exec_())
When a menu is torn off, it is hidden and Qt replaces it with a copy created from an internal subclass of QMenu. So to set the WindowStaysOnTopHint on a torn off menu, you would first need to find a way to get a reference to it. One way to do this would be to set an event-filter on the application object and watch for child events of the right type:
class MenuWatcher(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ChildAdded and
event.child().metaObject().className() == 'QTornOffMenu'):
event.child().setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
This class will operate on all torn-off menus created in the application.
However, if the event-filtering was done by the source menu class, its own torn-off menu could be identified by comparing menu-items:
class Menu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTearOffEnabled(True)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded:
child = event.child()
if (child.metaObject().className() == 'QTornOffMenu' and
all(a is b for a, b in zip(child.actions(), self.actions()))):
child.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)

How to avoid mouse events being eaten by QGraphicsView

I want mouse events to reach the appropriate QGraphicsItem but they get no further than the QGraphicsView.
I have reduced my code to 41 lines, commented out event handlers but to no avail. It is either handled by QGraphicsView if that has a handler or, if not, is not caught at all.
I'm sure I'm missing something obvious but I can't see it.
from PyQt5.QtWidgets import *
class MyFrame(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setScene(QGraphicsScene())
self.scene().addItem(Group())
def mouseReleaseEvent(self, event):
print('in QGraphicsView')
return QGraphicsView.mouseReleaseEvent(self, event)
class Group(QGraphicsItemGroup):
def __init__(self, parent=None):
super().__init__()
item = MyEllipse(0, 0, 20, 20)
self.addToGroup(item)
def mouseReleaseEvent(self, event):
print('in QGraphicsItemGroup')
return QGraphicsItemGroup.mouseReleaseEvent(self, event)
class MyEllipse(QGraphicsEllipseItem):
def mouseReleaseEvent(self, event):
print('in QGraphicsEllipseItem')
return QGraphicsEllipseItem.mouseReleaseEvent(self, event)
if __name__ == '__main__':
app = QApplication([])
f = MyFrame()
f.show()
app.exec_()
mouseReleaseEvent is called if and only if the event that the mousePressEvent handles is accepted, so with the following code the event will arrive at QGraphicsItemGroup:
class Group(QGraphicsItemGroup):
def __init__(self, parent=None):
super().__init__()
item = MyEllipse(0, 0, 20, 20)
self.addToGroup(item)
def mousePressEvent(self, event):
QGraphicsItemGroup.mousePressEvent(self, event)
event.accept()
def mouseReleaseEvent(self, event):
print('in QGraphicsItemGroup')
QGraphicsItemGroup.mouseReleaseEvent(self, event)
But as #ekhumoro points out, the QGraphicsItemGroup acts as a single element so events are not transported to the items they handle.
If you want to detect when the item is pressed you can use the following method:
class Group(QGraphicsItemGroup):
def __init__(self, parent=None):
super().__init__()
self._item = MyEllipse(0, 0, 20, 20)
self.addToGroup(self._item)
def mousePressEvent(self, event):
QGraphicsItemGroup.mousePressEvent(self, event)
event.accept()
def mouseReleaseEvent(self, event):
print('in QGraphicsItemGroup')
if self._item.mapToParent(self._item.boundingRect()).containsPoint(event.pos(), Qt.OddEvenFill):
print("_item")
QGraphicsItemGroup.mouseReleaseEvent(self, event)

PyQt5: Check if mouse is held down in enter-event

My actual application is much more complicated than this, but the example below sums up the majority of my problem. I have multiple QLabels that I've subclassed to make them clickable. The labels display 16x16 images which requires a process of loading the images via Pillow, converting them to ImageQt objects, and then setting the pixmap of the label. In the example, I have 3 clickable QLabels that run the print_something function each time I click on them. My goal is to be able to hold the mouse down, and for each label I hover over, the function gets called. Any pointers would be great.
from PyQt5 import QtCore, QtWidgets, QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
import sys
class ClickableLabel(QtWidgets.QLabel):
def __init__(self):
super().__init__()
clicked = QtCore.pyqtSignal()
def mousePressEvent(self, ev):
if app.mouseButtons() & QtCore.Qt.LeftButton:
self.clicked.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central_widget = QtWidgets.QWidget()
self.setFixedSize(300, 300)
image = Image.open("16x16image.png")
image_imageqt = ImageQt(image)
hbox = QtWidgets.QHBoxLayout()
hbox.setSpacing(0)
hbox.addStretch()
label01 = ClickableLabel()
label01.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label01.clicked.connect(self.print_something)
hbox.addWidget(label01)
label02 = ClickableLabel()
label02.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label02.clicked.connect(self.print_something)
hbox.addWidget(label02)
label03 = ClickableLabel()
label03.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label03.clicked.connect(self.print_something)
hbox.addWidget(label03)
hbox.addStretch()
central_widget.setLayout(hbox)
self.setCentralWidget(central_widget)
def print_something(self):
print("Printing something..")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
The cause of the problem is stated in the docs for QMouseEvent:
Qt automatically grabs the mouse when a mouse button is pressed inside
a widget; the widget will continue to receive mouse events until the
last mouse button is released.
It does not look like there is a simple way around this, so something hackish will be required. One idea is to initiate a fake drag and then use dragEnterEvent instead of enterEvent. Something like this should probably work:
class ClickableLabel(QtWidgets.QLabel):
clicked = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.dragstart = None
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.dragstart = event.pos()
self.clicked.emit()
def mouseReleaseEvent(self, event):
self.dragstart = None
def mouseMoveEvent(self, event):
if (self.dragstart is not None and
event.buttons() & QtCore.Qt.LeftButton and
(event.pos() - self.dragstart).manhattanLength() >
QtWidgets.qApp.startDragDistance()):
self.dragstart = None
drag = QtGui.QDrag(self)
drag.setMimeData(QtCore.QMimeData())
drag.exec_(QtCore.Qt.LinkAction)
def dragEnterEvent(self, event):
event.acceptProposedAction()
if event.source() is not self:
self.clicked.emit()

Trigger Close Event for Tabs in PySide QTabWidget

I've written 3 separate widgets and each one contains a'closeEvent'. However the close events are NOT triggered when the widgets are placed as tabs inside the MainWindow. How can I emit a signal to properly fire the 'closeEvent' for each tab when the MainWindow closes?
I've done my best to simplify the code to focus on the main goal. I'd like to emit the close event for each tab so in the future when I add more tabs it will be more flexible and each tool will still be self contained.
import sys
from PySide import QtGui, QtCore
class TabA(QtGui.QWidget):
def __init__(self, parent=None):
super(TabA, self).__init__(parent)
self.resize(300, 300)
self.setWindowTitle('App A')
self.btn = QtGui.QPushButton("Button A")
grid = QtGui.QGridLayout()
grid.addWidget(self.btn)
self.setLayout(grid)
def closeEvent(self, event):
print "closing Tab A"
event.accept()
class TabB(QtGui.QWidget):
def __init__(self, parent=None):
super(TabB, self).__init__(parent)
self.resize(300, 300)
self.setWindowTitle('App A')
self.btn = QtGui.QPushButton("Button A")
grid = QtGui.QGridLayout()
grid.addWidget(self.btn)
self.setLayout(grid)
def closeEvent(self, event):
print "closing Tab A"
event.accept()
class TabC(QtGui.QWidget):
def __init__(self, parent=None):
super(TabC, self).__init__(parent)
self.resize(300, 300)
self.setWindowTitle('App A')
self.btn = QtGui.QPushButton("Button A")
grid = QtGui.QGridLayout()
grid.addWidget(self.btn)
self.setLayout(grid)
def closeEvent(self, event):
print "closing Tab A"
event.accept()
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.initUI()
def initUI(self):
self.resize(300, 300)
self.setWindowTitle('Tabs')
# Tabs
main_tabWidget = QtGui.QTabWidget()
main_tabWidget.addTab(TabA(), "TabA")
main_tabWidget.addTab(TabB(), "TabB")
main_tabWidget.addTab(TabC(), "TabC")
vbox = QtGui.QVBoxLayout()
vbox.addWidget(main_tabWidget)
vbox.setContentsMargins(0, 0, 0, 0)
main_widget = QtGui.QWidget()
main_widget.setLayout(vbox)
self.setCentralWidget(main_widget)
def main():
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The closeEvent method is only called for a top-level window when a close request is received, so it is not suitable for your use-case. However, there are several signals that are emitted while the application is shutting down that you can connect to.
So for instance you could do this:
class TabA(QtGui.QWidget):
def __init__(self, parent=None):
super(TabA, self).__init__(parent)
...
QtGui.qApp.aboutToQuit.connect(self.handleClose)
def handleClose(self):
print "closing Tab A"
This signal is emitted just before the event-loop exits, and there's also a QtGui.qApp.lastWindowClosed signal which is emitted just after the main window closes.
(PS: don't be tempted to use __del__ for this purpose, because the exact behaviour during shutdown is strictly undefined in PyQt, and can change between versions).

Moving a frameless QDialog with a MouseMoveEvent

So I have a frameless QDialog that I want to be able to move around simply by clicking and dragging it. Given the code below, dragging the dialog always snaps the very top-left (0,0) of the dialog to the mouse. How might I circumvent this, or rather, what might the math be for it?
Standard QDialog with the following basic subclass:
class Main(QtGui.QDialog):
def __init__(self, args):
QtGui.QDialog.__init__(self)
def mouseMoveEvent(self, event):
super(Main, self).mouseMoveEvent(event)
if self.leftClick == True: self.moveWindow(event.globalPos())
def mousePressEvent(self, event):
super(Main, self).mousePressEvent(event)
if event.button() == QtCore.Qt.LeftButton:
self.leftClick = True
def mouseReleaseEvent(self, event):
super(Main, self).mouseReleaseEvent(event)
self.leftClick = False
Instead of event.pos(), try calling event.globalPos(). From the QMouseEvent reference, "If you move the widget as a result of the mouse event, use the global position returned by globalPos() to avoid a shaking motion."
Proposed solution moves Window, but mouse cursor jumps to 0,0 of Window. I wanted mouse cursor to stay on x,y of the Window all the time.
Here is upgraded version of the code [in QT5]:
X=0
X2=8 #!!!!
Y=0
Y2=30 #!!!!
class Main(QtWidgets.QMainWindow):
leftClick = False #! IMPORTANT
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
def mouseMoveEvent(self, event):
super(Main, self).mouseMoveEvent(event)
if self.leftClick == True:
self.move(event.globalPos().x()-X-X2,event.globalPos().y()-Y-Y2)
def mousePressEvent(self, event):
super(Main, self).mousePressEvent(event)
if event.button() == QtCore.Qt.LeftButton:
self.leftClick = True
global X,Y
X=event.pos().x()
Y=event.pos().y()
def mouseReleaseEvent(self, event):
super(Main, self).mouseReleaseEvent(event)
self.leftClick = False
For frameless window (made with window.setMask()) I need some constants like X2 and Y2, because "masked" frameless window is a bit smaller than a real framed window. Don't know how to calculate this difference yet.
UP. After long time I've found one critical bug. If you press Left Mouse Button on any pushbutton (just after the application start) and drag the mouse cursor away from that pushbutton, your application will crash, because we refer to nonexistent variable LeftClick. That's why in class Main we need to create LeftClick.

Categories

Resources