pyqt4 : Can I override a method without subclass? - overriding

I hope to change "keyPressEvent()" method of QWidget.
Undoubtedly, I create a new class inherits "QWidget", it can do that.
But, I have a returned QWidget from a method. So, I can't manage returned "QWidget" with subclass.
How can I make my own "keyPressEvent()" method of QWidget?

Install an event filter on the widget, and trap the KeyPress event:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.widget = QtGui.QWidget(self)
self.widget.setFocusPolicy(QtCore.Qt.StrongFocus)
self.widget.installEventFilter(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.widget)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.KeyPress and
source is self.widget):
print('key pressed: %s' % event.text())
return True
return QtGui.QWidget.eventFilter(self, source, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
window.resize(320, 240)
sys.exit(app.exec_())
Note that the example returns True for all key presses, which stops any further processing of those events. If you just want to "listen" for events without processing them, make sure the eventFilter returns False.

Related

Test that widget focus was set

When I set focus on a widget and then check whether it has focus, the result is False instead of True. When the application loads, however, the desired widget has focus.
from PySide2 import QtCore, QtWidgets, QtGui
app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
button = QtWidgets.QPushButton()
line_edit = QtWidgets.QLineEdit()
layout = QtWidgets.QVBoxLayout()
layout.addWidget(button)
layout.addWidget(line_edit)
widget.setLayout(layout)
widget.show()
print(f"{line_edit.hasFocus()}") # False
line_edit.setFocus(QtCore.Qt.OtherFocusReason) # without this, nothing has focus
print(f"{line_edit.hasFocus()}") # Expected True, actually False!
app.exec_()
How can I set focus in such a way that I could test it?
EDIT: I'm running these versions:
>>> import PySide2
>>> from PySide2 import QtCore, QtWidgets, QtGui
>>> print("Qt: ", PySide2.QtCore.__version__, " PySide2: ", PySide2.__version__)
Qt: 5.15.2 PySide2: 5.15.2
EDIT2: If I create a shortcut which prints the focus status, the result is True. I suspect that is because the app is executing. It still stands, how could I test this?
QtWidgets.QShortcut(QtGui.QKeySequence("F6"), widget, lambda: print(f"{line_edit.hasFocus()}"))
In order to test whether a widget has focus, the QCoreApplication event loop must be running and the widget must be visible. Focus is the result of an event and it only makes sense to talk about focus if the widget is showing (Qt seems to require the widget be visible for a focus event to occur). This presents a problem for running tests without user interaction: how do we automatically exit the main event loop? The answer is in the question; by using events.
QCoreApplication.exec_() starts the main event loop. Events are put on a queue and processed in turn (according to a priority). The loop goes on forever until it receives an event telling it to stop. Use postEvent to add an event to the queue. There are different handlers for different events. The QWidget.focusInEvent method is called when the widget takes focus. Other events pass through the general QObject.eventFilter method.
We can install a custom event filter on a QObject using its installEventFilter method. This method takes a QObject with a custom eventFilter:
class CustomEventFilterObject(QObject):
def eventFilter(self, obj, event):
if event.type() == QEvent.User:
print("Handle QEvent types")
return True
else:
# standard event processing
return QObject.eventFilter(self, obj, event)
# ...
custom_event_filter = CustomEventFilterObject()
widget.installEventFilter(custom_event_filter)
Armed with this knowledge, we can contrive a solution (and is it ever contrived!).
Here is the original question, modified so that the show method is no longer called from this module:
# focus_problem.py
from PySide2 import QtCore, QtWidgets, QtGui
widget = QtWidgets.QWidget()
button = QtWidgets.QPushButton()
line_edit = QtWidgets.QLineEdit()
layout = QtWidgets.QVBoxLayout()
layout.addWidget(button)
layout.addWidget(line_edit)
widget.setLayout(layout)
# line_edit.setFocus(QtCore.Qt.OtherFocusReason) # without this, nothing has focus
We know that the line_edit focus is only testable after app.exec_() has started the event loop. Create a custom focusInEvent function and attach it to the line_edit. This will get called if the line_edit takes focus. If that happens, exit the application with a return value indicating success. In case focus never lands on the line_edit, create a custom eventFilter on the widget. Post an event that closes the application with a return value indicating failure. Make sure this event is posted after the focus is set. Since focus is set on the line_edit when the module is imported, this won't be an issue for us.
Using unittest, the test looks like:
# test_focus_problem.py
#
# Run with:
#
# python3 -m unittest discover focus_problem/ --failfast --quiet
import unittest
from PySide2 import QtCore, QtWidgets, QtGui, QtTest
# need a QApplication in order to define the widgets in focus_problem
# module
if not QtWidgets.QApplication.instance():
QtWidgets.QApplication([])
import focus_problem
class TestFocusEvent(unittest.TestCase):
# called before the body of test_application_starts_with_line_edit_in_focus is executed
def setUp(self):
def focusInEvent(event):
QtWidgets.QApplication.instance().exit(0) # Success
# override the focus event handler
focus_problem.line_edit.focusInEvent = focusInEvent
class KillApplicationIfLineEditNeverTakesFocus(QtCore.QObject):
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.User:
QtWidgets.QApplication.instance().exit(1) # Fail
return True
else:
return QtCore.QObject.eventFilter(self, obj, event)
custom_filter = KillApplicationIfLineEditNeverTakesFocus()
focus_problem.widget.installEventFilter(custom_filter)
focus_problem.widget.show()
QtWidgets.QApplication.instance().postEvent(focus_problem.widget, QtCore.QEvent(QtCore.QEvent.User))
self.rv = QtWidgets.QApplication.instance().exec_()
def test_application_starts_with_line_edit_in_focus(self):
self.assertEqual(self.rv, 0)
The application is started using setUp on a dedicated test class. This allows test_application_starts_with_line_edit_in_focus to collect the return value and report it along with other tests. It checks to see if the application was killed by the focusInEvent (a success) or the eventFilter (a failure).
I'd be surprised if there wasn't an easier way to do all this. :)

how to make widget receive mouse release event when context menu appears

On Ubuntu20.04, I can't have the widget receive the mouse release event when context menu appears while Windows can receive. My pyqt version is 5.15.2.
I've considered sending a mouse release event manually, but I don't know which systems will receive the mouse release event when context menu appears, and doing so may cause repeated release events. Is there any better solution?
# coding:utf-8
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMenu, QLabel
class Menu(QMenu):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.addAction('action 1')
self.addAction('action 2')
def showEvent(self, e):
print('show context menu')
super().showEvent(e)
class Widget(QLabel):
def mousePressEvent(self, e):
print('mouse press, button:', e.button())
super().mousePressEvent(e)
def mouseReleaseEvent(self, e):
print('mouse release, button:', e.button())
super().mouseReleaseEvent(e)
class Demo(QWidget):
def __init__(self):
super().__init__()
self.widget = Widget('Click Me', self)
self.widget.move(175, 180)
self.resize(400, 400)
def contextMenuEvent(self, e):
menu = Menu(self)
menu.exec(e.globalPos())
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Demo()
w.show()
app.exec_()
The general rule is that the context menu event is fired when the right mouse buttons is released on Windows and pressed on Linux.
To avoid inconsistency and have a generic behavior that just ignores the OS and always does the same, you can set an internal flag and check it whenever is required: since the context menu event might take precedence, call a function from both mouse press and context menu.
Remember that:
if you don't want to handle the context menu event and want it to propagate to the parent (like in your case), the event must be ignored; if you override contextMenuEvent this must be done explicitly either with event.ignore() or by calling the base implementation, assuming that it does not accept the event;
QLabel might handle the event if the text interaction flag supports mouse/keyboard navigation (to allow the clipboard menu);
context menu event can also be triggered by the menu-key on the keyboard, so you should ensure that its reason() is Mouse; this is not actually required if the function that does the "unpress" part always check for the above flag (and it should), but it's still good practice to check for the reason for completeness and consistency;
class Widget(QLabel):
_isPressed = False
def mousePressEvent(self, e):
print('mouse press, button:', e.button())
super().mousePressEvent(e)
self._isPressed = True
def mouseReleaseEvent(self, e):
super().mouseReleaseEvent(e)
self.unpress()
def contextMenuEvent(self, e):
super().contextMenuEvent(e)
# or, alternatively:
e.ignore()
if e.reason() == e.Mouse:
self.unpress()
def unpress(self):
if not self._isPressed:
return
self._isPressed = True
# do whatever you need

Determine QWidget that had last focus before button press

I want to implement the following functionality into my Qt application:
User opens one or more 'input' widgets (instances of an InputWidget class), each containing a QLineEdit widget
User opens a 'helper' dialog
User selects a value in the 'helper' dialog
User presses the 'Insert' QPushButton in the 'helper' dialog
The selected value from the 'helper' dialog' is inserted into the QLineEdit of that 'input' dialog that had the last focus before the 'Insert' button was pressed
So, basically, what I want is that if the user clicks on 'Insert' in the following screenshot, the string 'Apple' should appear in the focused input dialog. The code example below does kind of work, only that the string is (usually, see below) inserted into the second one.
Here's is the code example that creates this setup:
from PyQt5.QtWidgets import (QApplication, QWidget, QHBoxLayout,
QLineEdit, QLabel, QPushButton, QComboBox)
import sys
# this is the missing bit
def determineWhichWidgetHadLastFocus():
for widget in QApplication.instance().topLevelWidgets():
if isinstance(widget, InputWidget):
# do something wonderful to determine whether this widget
# is the one that had last focus
wonderful = True
if wonderful:
return widget
return None
class BaseWidget(QWidget):
""" Base widget type """
def __init__(self, name):
super(BaseWidget, self).__init__()
self.setWindowTitle(name)
self.setupUi()
self.show()
def setupUi(self):
pass
class InputWidget(BaseWidget):
""" InputWidget contains a QLabel and a QLineEdit widget """
def setupUi(self):
self.label = QLabel("Input string:")
self.edit = QLineEdit()
layout = QHBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.edit)
class HelperWidget(BaseWidget):
""" HelperWidget contains a QLineEdit and a QPushButton widget. Pressing
the button inserts the content of the edit widget into the edit widget of
the last activated InputWidget """
def setupUi(self):
self.combo = QComboBox()
self.combo.addItems(["Apple", "Pear", "Banana"])
self.button = QPushButton("Insert")
self.button.clicked.connect(self.insertString)
layout = QHBoxLayout(self)
layout.addWidget(self.combo)
layout.addWidget(self.button)
def insertString(self):
widget = determineWhichWidgetHadLastFocus()
if widget:
widget.edit.insert(self.combo.currentText())
def main():
app = QApplication(sys.argv)
diag1 = InputWidget("Input dialog")
diag2 = InputWidget("Another input")
helper = HelperWidget("Helper")
app.exec_()
if __name__ == "__main__":
main()
The missing part is the determineWhichWidgetHadLastFocus() function.
This function is supposed to do something wonderful that allows it to determine which 'input' was the last to hold focus. Currently, it traverses the list of top level widgets from QApplication, but the order of top level widgets doesn't reflect the activation order (it usually, but not always appears to be the order of creation).
One idea that came to my mind was to install an event filter that keeps track of the FocusIn events. That would be easy for the InputWidget class in my example, but might not work so well for my real application that has many QLineEdits, QTextEdits and descended classes all over the place. I'd rather not go that way.
Any other ideas?
It turns out that the event filter idea is the way to go after all. What I did was first create an event filter class that emits a signal if the sending object is a QLineEdit object:
from PyQt5.QtCore import QObject, QEvent, pyqtSignal
class inputFocusFilter(QObject):
focusIn = pyqtSignal(object)
def eventFilter(self, widget, event):
if event.type() == QEvent.FocusIn and isinstance(widget, QLineEdit):
# emit a `focusIn` signal, with the widget as its argument:
self.focusIn.emit(widget)
return super(inputFocusFilter, self).eventFilter(widget, event)
This filter is installed for a custom QApplication class so that any event that is created passes the filter. The signal focusIn is connected to a setter function that remembers the last input widget that received focus:
class MyApplication(QApplication):
def __init__(self, *arg, **kwarg):
super(MyApplication, self).__init__(*arg, **kwarg)
self._input_focus_widget = None
self.event_filter = inputFocusFilter()
self.event_filter.focusIn.connect(self.setInputFocusWidget)
self.installEventFilter(self.event_filter)
def setInputFocusWidget(self, widget):
self._input_focus_widget = widget
def inputFocusWidget(self):
return self._input_focus_widget
MyApplication is used instead of QApplication in the first line of main(). Now, the call to determineWhichWidgetHadLastFocus() in HelperWidget.insertString() can be replaced by QApplication.instance().inputFocusWidget(), and everything works as intended.

Slot gets called twice despite pyqtSlot decorator

This is a class which form i made in qt5 designer. The slot is called twice when I click the button.
class CustomerList(QWidget, Ui_CustomerList):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setupUi(self)
self.buttX.clicked.connect(self.on_buttX_clicked)
#pyqtSlot()
def on_buttX_clicked(self):
print("on_buttX_clicked")
if __name__ == '__main__':
app = QApplication(sys.argv)
w = CustomerList()
w.show()
sys.exit(app.exec_())
What am I missig here?
Your button is called buttX in designer, so the "Auto connect by name" feature in setupUi() finds a matching slot.
You can either
remove the explicit connect
rename the button
rename the slot
I would personally go for the latter, i.e. use a slot name that does not have the pattern the "auto name connect" is looking for.
E.g. onButtXClicked

Display a popup window before the mainwindow runs

How can I make a popup window that appears before the mainwindow begins? I want the popup to have several QLineEdit widgets to receive input that I will need for the mainwindow. I searched for solutions, but I could not understand most of the examples I found. Can some one help me?
Just make a subclass of QDialog, execute it modally before running your normal startup logic.
Thats how I did it for an app that required a login, worked just fine. This would be the general idea in Python (it takes me less time to think about it in PyQt):
import sys
from PyQt4 import QtGui, QtCore
from mymodule import MyDialog, MyWindow
def main(argv):
app = QtGui.QApplication(argv)
# make a dialog that runs in its own event loop
dlg = MyDialog()
if ( not dlg.exec_() ): # in C++, this would be dlg->exec()
sys.exit(0)
var1, var2, var3 = dlg.values()
window = MyWindow()
window.setPropertyOne(var1)
window.setPropertyTwo(var2)
window.setPropertyThree(var3)
window.show()
sys.exit(app.exec_())
if ( __name__ == '__main__' ):
main(sys.argv)

Resources