Reputation: 4554
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()}"))
Upvotes: 0
Views: 440
Reputation: 4554
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. :)
Upvotes: 1