How to receive hover events for child widgets? - python

I have a QWidget containing another (child) widget for which I'd like to process hoverEnterEvent and hoverLeaveEvent. The documentation mentions that
Mouse events occur when a mouse cursor is moved into, out of, or within a widget, and if the widget has the Qt::WA_Hover attribute.
So I tried to receive the hover events by setting this attribute and implementing the corresponding event handlers:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
class TestWidget(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
layout.addWidget(TestLabel('Test 1'))
layout.addWidget(TestLabel('Test 2'))
self.setLayout(layout)
self.setAttribute(Qt.WA_Hover)
class TestLabel(QLabel):
def __init__(self, text):
super().__init__(text)
self.setAttribute(Qt.WA_Hover)
def hoverEnterEvent(self, event): # this is never invoked
print(f'{self.text()} hover enter')
def hoverLeaveEvent(self, event): # this is never invoked
print(f'{self.text()} hover leave')
def mousePressEvent(self, event):
print(f'{self.text()} mouse press')
app = QApplication([])
window = TestWidget()
window.show()
sys.exit(app.exec_())
However it doesn't seem to work, no hover events are received. The mousePressEvent on the other hand does work.
In addition I tried also the following things:
Set self.setMouseTracking(True) for all widgets,
Wrap the TestWidget in a QMainWindow (though that's not what I want to do for the real application),
Implement event handlers on parent widgets and event.accept() (though as I understand it, events propagate from inside out, so this shouldn't be required).
How can I receive hover events on my custom QWidgets?

The QWidget like the QLabel do not have the hoverEnterEvent and hoverLeaveEvent methods, those methods are from the QGraphicsItem so your code doesn't work.
If you want to listen to the hover events of the type you must override the event() method:
import sys
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
class TestWidget(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
layout.addWidget(TestLabel("Test 1"))
layout.addWidget(TestLabel("Test 2"))
class TestLabel(QLabel):
def __init__(self, text):
super().__init__(text)
self.setAttribute(Qt.WA_Hover)
def event(self, event):
if event.type() == QEvent.HoverEnter:
print("enter")
elif event.type() == QEvent.HoverLeave:
print("leave")
return super().event(event)
def main():
app = QApplication(sys.argv)
window = TestWidget()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

Did you know that you can do this with QWidget's enterEvent and leaveEvent? All you need to do is change the method names. You won't even need to set the Hover attribute on the label.
from PyQt5.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
class Window(QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
layout = QGridLayout()
self.label = MyLabel(self)
layout.addWidget(self.label)
self.setLayout(layout)
text = "hover label"
self.label.setText(text)
class MyLabel(QLabel):
def __init__(self, parent=None):
super(MyLabel, self).__init__(parent)
self.setParent(parent)
def enterEvent(self, event):
self.prev_text = self.text()
self.setText('hovering')
def leaveEvent(self, event):
self.setText(self.prev_text)
if __name__ == "__main__":
app = QApplication([])
w = Window()
w.show()
app.exit(app.exec_())

Related

How to use eventfilter and installeventfilter methods?

I am using python 3.8 and qt5 for the desktop app.
I want to delete selected QTreeWidgetItem when the Delete key is pressed.
I tried the below code, but it did not work:
class EventFilter(QObject):
def __init__(self):
super().__init__()
pass
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if isinstance(a1, QKeyEvent) and a1.matches(QKeySequence_StandardKey=QKeySequence.Delete):
print("event filter")
pass
return super().eventFilter(a0, a1)
treeWidget.installEventFilter(EventFilter())
# treeWidget.viewport().installEventFilter(EventFilter())
Some posts said to install on viewport, but that did not work either.
This is my app's structure:
QMainWindow
-- QTabWidget
---- QTreeWidget
Can you give me some hints on how to use eventfilter and installeventfilter, or any other recommended way?
Probably the cause of the problem is the scope of the EventFilter object since not being assigned to a variable it will be eliminated instantly. A possible solution is to assign a variable that has sufficient scope, another option is to pass it a parent since it is a QObject, in this case I will use the second.
import sys
from PyQt5.QtCore import QEvent, QObject, pyqtSignal
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QMainWindow, QTabWidget, QTreeWidget
class EventFilter(QObject):
delete_pressed = pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if obj is self.widget and event.type() == QEvent.KeyPress:
if event.matches(QKeySequence.Delete):
self.delete_pressed.emit()
return super().eventFilter(obj, event)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
tabWidget = QTabWidget()
self.setCentralWidget(tabWidget)
treeWidget = QTreeWidget()
tabWidget.addTab(treeWidget, "Tree")
eventFilter = EventFilter(treeWidget)
eventFilter.delete_pressed.connect(self.handle_delete_pressed)
def handle_delete_pressed(self):
print("delete pressed")
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
On the other hand, it is not neccesary to reinvent the wheel since there are already QShortcuts that serve to detect when the user presses key combinations
import sys
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QShortcut,
QTabWidget,
QTreeWidget,
)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
tabWidget = QTabWidget()
self.setCentralWidget(tabWidget)
treeWidget = QTreeWidget()
tabWidget.addTab(treeWidget, "Tree")
shortcut = QShortcut(QKeySequence.Delete, treeWidget)
shortcut.activated.connect(self.handle_delete_pressed)
def handle_delete_pressed(self):
print("delete pressed")
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

Handle mouse events on both parent and child widgets (button don't consume event?)

Here's my setup:
I have a eventFilter on my QTableWidget to handle both mousePress and mouseRelease events.
I also have a custom class for the QPushButton to handle mousePress and mouseRelease events.
However when I trigger the mousePress event by clicking and holding on the button, the QTableWidget doesn't see the event.
Just in case here's the code for both the custom QPushButton class and QTableWidget eventFilter.
class Button(QPushButton):
key = None
def __init__(self, title, parent):
super().__init__(title, parent)
def mousePressEvent(self, e):
super().mousePressEvent(e)
if e.button() == Qt.LeftButton:
print('press')
class TableWidget(QTableWidget):
def __init__(self, rows, columns, parent=None):
QTableWidget.__init__(self, rows, columns, parent)
self._last_index = QPersistentModelIndex()
self.viewport().installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QEvent.MouseButtonRelease:
print("mouse release")
return QTableWidget.eventFilter(self, source, event)
What I'm hoping to see is when I press the custom button and then release mouse, that it will print "mouse release" in the console.
This doesn't happen.
But it does print it successfully when I click anywhere in the table except the button.
Let me know in the comments if you want me to add any extra information.
Explanation:
As I already pointed out in this post: The handling of mouse events between the widgets goes from children to parents, that is, if the child does not accept the event (it does not use it) then it will pass the event to the parent.. That can be verified using the following example:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QPushButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
class TableWidget(QTableWidget):
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
print("released")
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.tablewidget = TableWidget(4, 4)
container = QWidget()
lay = QVBoxLayout(container)
lay.addWidget(QLabel("QLabel", alignment=Qt.AlignCenter))
lay.addWidget(QPushButton("QPushButton"))
container.setFixedSize(container.sizeHint())
item = QTableWidgetItem()
self.tablewidget.setItem(0, 0, item)
item.setSizeHint(container.sizeHint())
self.tablewidget.setCellWidget(0, 0, container)
self.tablewidget.resizeRowsToContents()
self.tablewidget.resizeColumnsToContents()
lay = QVBoxLayout(self)
lay.addWidget(self.tablewidget)
def main():
import sys
app = QApplication(sys.argv)
widget = Widget()
widget.resize(640, 480)
widget.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
If the QPushButton is pressed then the mouseReleaseEvent method of the QTableWidget will not be invoked because the QPushButton consumes it, unlike when the QLabel is pressed since it does not consume it and the QTableWidget does.
Solution:
As I pointed out in the other post, a possible solution is to use an eventfilter to the QWindow and to be able to filter by verifying that the click is given on a cell.
from PyQt5.QtCore import (
pyqtSignal,
QEvent,
QObject,
QPoint,
Qt,
)
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QPushButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
class MouseObserver(QObject):
pressed = pyqtSignal(QPoint, QPoint)
released = pyqtSignal(QPoint, QPoint)
moved = pyqtSignal(QPoint, QPoint)
def __init__(self, window):
super().__init__(window)
self._window = window
self.window.installEventFilter(self)
#property
def window(self):
return self._window
def eventFilter(self, obj, event):
if self.window is obj:
if event.type() == QEvent.MouseButtonPress:
self.pressed.emit(event.pos(), event.globalPos())
elif event.type() == QEvent.MouseMove:
self.moved.emit(event.pos(), event.globalPos())
elif event.type() == QEvent.MouseButtonRelease:
self.released.emit(event.pos(), event.globalPos())
return super().eventFilter(obj, event)
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.tablewidget = QTableWidget(4, 4)
container = QWidget()
lay = QVBoxLayout(container)
lay.addWidget(QLabel("QLabel", alignment=Qt.AlignCenter))
lay.addWidget(QPushButton("QPushButton"))
container.setFixedSize(container.sizeHint())
item = QTableWidgetItem()
self.tablewidget.setItem(0, 0, item)
item.setSizeHint(container.sizeHint())
self.tablewidget.setCellWidget(0, 0, container)
self.tablewidget.resizeRowsToContents()
self.tablewidget.resizeColumnsToContents()
lay = QVBoxLayout(self)
lay.addWidget(self.tablewidget)
def handle_window_released(self, window_pos, global_pos):
lp = self.tablewidget.viewport().mapFromGlobal(global_pos)
ix = self.tablewidget.indexAt(lp)
if ix.isValid():
print(ix.row(), ix.column())
def main():
import sys
app = QApplication(sys.argv)
widget = Widget()
widget.resize(640, 480)
widget.show()
mouse_observer = MouseObserver(widget.windowHandle())
mouse_observer.released.connect(widget.handle_window_released)
sys.exit(app.exec_())
if __name__ == "__main__":
main()

QComboBox click triggers a leaveEvent on the main QDialog

I am trying to build a hover Dialog but i am stuck at the interaction between QComboBox selection and QDialog's leaveEvent. it looks like when i try to click on combobox to select something, it triggers a leaveEvent which then hides my QDialog. Why is this happening? What can i try to ensure that the Dialog is only hidden when I move my mouse outside of the Dialog?
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class hoverDialog(QDialog):
def __init__(self, parent=None):
super().__init__()
self.setAttribute(Qt.WA_DeleteOnClose)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.v = QVBoxLayout()
self.combobox = QComboBox()
self.combobox.addItems(['Work-around','Permanent'])
self.textedit = QPlainTextEdit()
self.v.addWidget(self.combobox)
self.v.addWidget(self.textedit)
self.setLayout(self.v)
#self.setMouseTracking(True)
def leaveEvent(self, event):
self.hide()
return super().leaveEvent(event)
class Table(QWidget):
def __init__(self, parent=None):
super().__init__()
self.label4 = QLabel()
self.label4.setText("hover popup")
self.label4.installEventFilter(self)
self.checkbox1 = QCheckBox()
self.pushButton3 = QPushButton()
self.pushButton3.setText('Generate')
self.pushButton3.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.pushButton3.clicked.connect(self.buttonPressed)
self.hbox5 = QHBoxLayout()
self.hbox5.addWidget(self.checkbox1)
self.hbox5.addWidget(self.label4)
self.hbox5.addWidget(self.pushButton3)
self.vbox1 = QVBoxLayout()
self.vbox1.addLayout(self.hbox5)
self.setLayout(self.vbox1)
self.autoResolve = hoverDialog(self)
def eventFilter(self, obj, event):
if obj == self.label4 and event.type() == QtCore.QEvent.Enter and self.autoResolve.isHidden():
self.onHovered()
return super().eventFilter(obj, event)
def onHovered(self):
pos = QtGui.QCursor.pos()
self.autoResolve.move(pos)
self.autoResolve.show()
def buttonPressed(self):
if self.checkbox.isChecked():
print('do something..')
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Table()
form.show()
app.exec_()
Looking at the sources, I believe that the origin of the problem is in Qt's behavior whenever a new popup widget is shown.
I cannot guarantee this at 100%, but in any case the simplest solution is to hide the widget only if the combo popup is not shown:
def leaveEvent(self, event):
if not self.combobox.view().isVisible():
self.hide()
Note that your approach is not really perfect: since the dialog could be shown with a geometry that is outside the current mouse position, it will not be able to hide itself until the mouse actually enters it. You should probably filter FocusOut and WindowDeactivate events also.

How to show new QMainWindow in every loop in PyQT5?

I'm trying to write a Python program using PyQt5 that will display a window in each iteration of the for loop. I would like to close after incrementing and displaying the next window. However, I do not know how to stop the loop every iteration and at the moment I am getting 6 windows at once.
main.py
import sys
from PyQt5.QtWidgets import (QLineEdit, QVBoxLayout, QMainWindow,
QWidget, QDesktopWidget, QApplication, QPushButton, QLabel,
QComboBox, QFileDialog, QRadioButton)
from PyQt5.QtCore import pyqtSlot, QByteArray
from alert import Window2
from test import test
class SG(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(300, 150)
self.setWindowTitle('TEST')
self.resultsGen = QPushButton('TEST', self)
self.resultsGen.clicked.connect(lambda: self.on_click())
self.show()
#pyqtSlot()
def on_click(self):
test(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
sg = SG()
sys.exit(app.exec_())
alert.py
from PyQt5.QtWidgets import (QLineEdit, QVBoxLayout, QMainWindow,
QWidget, QDesktopWidget, QApplication, QPushButton, QLabel,
QComboBox, QFileDialog, QRadioButton)
from PyQt5.QtCore import pyqtSlot, QByteArray
from PyQt5.QtGui import QPixmap
from PyQt5 import QtGui, QtCore
class Window2(QMainWindow):
def __init__(self):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
self.setWindowTitle("Window22222")
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
label = QLabel(self)
pixmap = QPixmap('cropped/8.png')
label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
lay.addWidget(label)
self.textbox = QLineEdit(self)
self.textbox.move(20, 20)
self.textbox.resize(280, 40)
# Create a button in the window
self.button = QPushButton('Show text', self)
self.button.move(20, 80)
# connect button to function on_click
self.button.clicked.connect(lambda: self.on_clickX())
self.show()
#pyqtSlot()
def on_clickX(self):
textboxValue = self.textbox.text()
print(textboxValue)
self.textbox.setText("")
self.hide()
test.py
from alert import Window2
def test(self):
for x in range(6):
w = Window2()
As soon as you run the for cycle, all the code of the initialization will be executed, which includes the show() call you used at the end of initPopup().
A simple solution is to create a new signal that is emitted whenever you hide a window, and connect that signal to a function that creates a new one until it reaches the maximum number.
main.py:
import sys
from PyQt5.QtWidgets import QWidget, QApplication, QPushButton
from alert import Window2
class SG(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.alerts = []
def initUI(self):
self.resize(300, 150)
self.setWindowTitle('TEST')
self.resultsGen = QPushButton('TEST', self)
self.resultsGen.clicked.connect(self.nextAlert)
self.show()
def nextAlert(self):
if len(self.alerts) >= 6:
return
alert = Window2()
self.alerts.append(alert)
alert.setWindowTitle('Window {}'.format(len(self.alerts)))
alert.closed.connect(self.nextAlert)
alert.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
sg = SG()
sys.exit(app.exec_())
alert.py:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Window2(QMainWindow):
closed = pyqtSignal()
def __init__(self):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
label = QLabel(self)
pixmap = QPixmap('cropped/8.png')
label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
lay.addWidget(label)
self.textbox = QLineEdit(self)
lay.addWidget(self.textbox)
# Create a button in the window
self.button = QPushButton('Show text', self)
lay.addWidget(self.button)
# connect button to function on_click
self.button.clicked.connect(lambda: self.on_clickX())
self.show()
#pyqtSlot()
def on_clickX(self):
textboxValue = self.textbox.text()
print(textboxValue)
self.textbox.setText("")
self.hide()
self.closed.emit()
Just note that with this very simplified example the user might click on the button of the "SG" widget even if an "alert" window is visibile. You might prefer to use a QDialog instead of a QMainWindow and make the main widget a parent of that dialog.
main.py:
class SG(QWidget):
# ...
def nextAlert(self):
if len(self.alerts) >= 6:
return
alert = Window2(self)
# ...
alert.py:
class Window2(QDialog):
closed = pyqtSignal()
def __init__(self, parent):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
# a QDialog doesn't need a central widget
lay = QVBoxLayout(self)
# ...
Also, if an alert window is closed using the "X" button the new one will not be shown automatically. To avoid that, you can implement the "closeEvent" and ignore the event, so that the user will not be able to close the window until the button is clicked. As QDialogs can close themself when pressing the escape key, I'm also ignoring that situation.
alert.py:
class Window2(QMainWindow):
# ...
def closeEvent(self, event):
event.ignore()
def keyPressEvent(self, event):
if event.key() != Qt.Key_Escape:
super().keyPressEvent(event)

PyQt5 window not opening on show() when parent window is specified

I have defined a simple main window and second pop-up window. When I call the MainWindow.create_new_window() method, the SecondWindow does not show up as a new window but its QLabel is created within the MainWindow instance. Here's the code:
import sys
from PyQt5.QtWidgets import QApplication, QPushButton, QLabel, QWidget, QVBoxLayout
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.second_window = None
self.main_layout = QVBoxLayout(self)
self.new_window_button = QPushButton('New Window', self)
self.new_window_button.clicked.connect(self.create_new_window)
self.main_layout.addWidget(self.new_window_button)
def create_new_window(self):
if self.second_window is None:
self.second_window = SecondWindow(self)
self.second_window.show()
class SecondWindow(QWidget):
def __init__(self, *args, **kwargs):
super(SecondWindow, self).__init__(*args, **kwargs)
self.main_layout = QVBoxLayout(self)
self.hello_label = QLabel('Hello I am the second window.', self)
self.main_layout.addWidget(self.hello_label)
if __name__ == '__main__':
app = QApplication(sys.argv)
mainwin = MainWindow()
mainwin.show()
sys.exit(app.exec_())
When I create the second window without specifying the MainWindow instance as the parent (self.second_window = SecondWindow()), it opens as expected. Can anyone tell me what's going on here?
By default, a QWidget that has a parent implies that the widget will be placed inside the parent, so you observe that behavior.
If you want it to be a window then you must activate the flag Qt::Window
# ...
from PyQt5.QtCore import Qt
# ...
class SecondWindow(QWidget):
def __init__(self, *args, **kwargs):
super(SecondWindow, self).__init__(*args, **kwargs)
self.setWindowFlags(self.windowFlags() | Qt.Window) # <---
# ...
Other options is to use a QDialog that is a type of widget that by default already has that flag activated and whose objective is to ask the user for information.
From documentation:
If parent is 0, the new widget becomes a window. If parent is another widget, this widget becomes a child window inside parent. The new widget is deleted when its parent is deleted.
When I run your code, I get that new widget inside of the main one - as is described in documentation.
So basically, you should set parent to a QWidget only if you indend to use it as a window's widget (insert it in a layout, or use it as central widget etc.); if you want to use it as-is you don't need a parent.
If you need your widget to have a parent and be a separate window, better idea may be to use QDialog instead of QWidget. Just make your SecondWindow class subclass QDialog instead and you're good to go.
Example code (I've changed both your Windows to QDialog):
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QApplication, QDialog
class MainWindow(QDialog):
def __init__(self):
super(MainWindow, self).__init__()
self.second_window = None
self.main_layout = QVBoxLayout(self)
self.new_window_button = QPushButton('New Window', self)
self.new_window_button.clicked.connect(self.create_new_window)
self.main_layout.addWidget(self.new_window_button)
def create_new_window(self):
if self.second_window is None:
self.second_window = SecondWindow(self)
# set second window as modal, because MainWindow is QDialog/QWidget.
self.setModal(True)
self.second_window.show()
class SecondWindow(QDialog):
def __init__(self, *args, **kwargs):
super(SecondWindow, self).__init__(*args, **kwargs)
self.main_layout = QVBoxLayout(self)
self.hello_label = QLabel('Hello I am the second window.', self)
self.main_layout.addWidget(self.hello_label)
if __name__ == '__main__':
app = QApplication(sys.argv)
mainwin = MainWindow()
mainwin.show()
sys.exit(app.exec_())
In your imported packages import that staff:
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel

Categories