Issue
For some testings i have a simple QLineEdit that creates a QWidget with a Qt.Popup window flag after a key is pressed. I choose the Qt.Popup flag because it has no borders and gets destroyed after the user presses something else in the application.
The Problem is: My QLineEdit loses focus after a key is pressed and I want to keep the focus on the field so that a user can write fluid input.
I don't want to user QCompleter or something because later in time there will be some self written Widgets that take place in the Popup.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class main(QMainWindow):
def __init__(self):
super().__init__()
self.button1 = QLineEdit(self)
self.button1.keyReleaseEvent = lambda e: self.open_menu(self.button1)
self.setCentralWidget(self.button1)
self.show()
def open_menu(self, obj):
self.Frame = QWidget()
self.Frame.setStyleSheet("border:2px solid #000;")
self.Frame.setFixedWidth(150)
self.Frame.setFixedHeight(300)
self.Frame.setWindowFlags(Qt.Popup)
self.Frame.show()
app = QApplication([])
window = main()
app.exec()
Solution
One simple possibility would be to use Qt.ToolTip
instead.
Unfortunately, that wouldn't be a valid choice, since a ToolTip
window can be persistent and completely ignores events that should hide it: for instance, it will be still there if you click on another window, and it may even still appear in other virtual desktops when switching to them.
The problem with focus can be easily solved, though, since it's enough to automatically redirect events received by the popup to the line edit.
In order to do this, you must use an event filter, which is also a more appropriate way than overwriting the keyReleaseEvent
with another function (as you did for the QLineEdit), especially if it's anonymous (a lambda): that approach is almost always discouraged, as it can create issues that are difficult to track down, and makes also difficult to properly call the super() class implementation.
The solution is to add an event filter for both the line edit and the popup, and then:
- if the event is a key type, directly call the event handler of the line edit, no matter if the source is the popup;
- chose to whether show or hide the popup;
- when the popup is shown, set a private flag to let the event handler know that the line edit is going to lose focus because the popup is being shown;
- ignore the focus out event for the line edit in case the flag above is set;
- set back the flag;
The last 3 points are necessary in order to let the line edit properly handle focus out events if the focus is actually lost (eg. the user clicked on another widget/window, the window was minimized or hidden, etc).
Also be aware that a more correct approach should ensure that the popup has no focus policy (to avoid focus stealing by keyboard or mouse events), and that the line edit should become the focusProxy()
of the popup.
Another important aspect is to avoid recreating the popup every time, as that could cause flickering (the new one is shown after the previous one has been hidden/deleted), so you should always try to reuse the same popup widget, at least for the same line edit instance.
All the above aspects are exactly what Qt does for QCompleter when it's set on a QLineEdit, and similarly to what happens with editable QComboBoxes.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class MainWindow(QMainWindow):
eatFocusOut = False
def __init__(self):
super().__init__()
self.lineEdit = QLineEdit(self)
self.setCentralWidget(self.lineEdit)
self.popup = QWidget()
self.popup.setStyleSheet("border:2px solid #000;")
self.popup.setFixedSize(150, 300)
self.popup.setWindowFlags(Qt.Popup)
self.popup.setFocusPolicy(Qt.NoFocus)
self.popup.setFocusProxy(self.lineEdit)
self.lineEdit.installEventFilter(self)
self.popup.installEventFilter(self)
def open_menu(self):
self.eatFocusOut = True
rect = self.lineEdit.rect()
self.popup.move(self.lineEdit.mapToGlobal(rect.bottomLeft()))
self.popup.show()
self.eatFocusOut = False
def eventFilter(self, obj, event):
if (
obj == self.lineEdit
and event.type() == event.FocusOut
and self.eatFocusOut
):
if self.popup.isVisible():
return True
if event.type() == event.KeyPress:
self.lineEdit.event(event)
if event.key() == Qt.Key_Escape:
if self.popup.isVisible():
self.popup.hide()
elif not self.popup.isVisible():
self.open_menu()
return True
return super().eventFilter(obj, event)
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
Note that I took the liberty to change object names to more appropriate ones (if it's a line edit, it's not a "button"), including a capitalized name for the class. Also, you said that you want the popup to show when a key is pressed, so I used keyPress
instead of keyRelease
. Finally, show()
should never be called within the __init__
of a widget (see this related answer to know why).
Another important aspect to consider is that complex widgets (including container widgets, like QMainWindow is) may implement their own event filter, or you may need one for other aspects, which would make its implementation and related code rather cumbersome.
There are at least two possible solutions which are probably better:
- create a standalone QObject subclass that creates its own popup and implements its specifically targeted
eventFilter()
as done above; this is what QCompleter actually does, since it is a basic QObject, not a widget; - make a QLineEdit subclass that implements the popup mechanism on its own;
If you want to know more about a "QCompleter-like" behavior, consider studying the Qt sources (I would suggest to use this code browser).
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.