Validate QLineEdit before changing focus
-
Hello.
I'm new to PyQt and I'm trying to figure out how can I validate the content of a QLineEdit before focus goes to the next widget.
I've been working a little with QValidators and they are great to control the input, but that is not the kind of validation I need.
Let me explain it with an example:- A QLineEdit is expecting a numeric value with the Id of a client.
- The user inputs the Id and it is checked, for example, in a database:
- If it's correct, the client's name is displayed in a QLabel.
- If it isn't the user is warned and the focus stays in the QLineEdit until he inputs a correct Id.
How can I achieve that behaviour?
Thank's in advance. -
It's a workaround, but it's working for me just now.
The idea is using two EventFilters :
- A normal Filter.
- A temporary Filter.
The "normal" filter installs the temporary filter in widget that is going to be focused next when processing the FocusOut event. The only thing that the temporary filter does is to accept all the focus events and stop them.
The temporary filter removes itself from the widget in the FocusOut event.
This way, the "normal" filter can do internal and external validations before losing the focus.
I'm doing some tests and it's working for me. I want to implement it inside a custum widget.
class DYFieldEventFilter(QtCore.QObject): class TemporaryFilter(QtCore.QObject): def __init__(self): super(DYField.DYFieldEventFilter.TemporaryFilter, self).__init__() def eventFilter(self, widget: 'QObject', event: 'QEvent') -> bool: focus_events = { QtCore.QEvent.Type.FocusAboutToChange: "FocusAboutToChange", QtCore.QEvent.Type.FocusIn: "FocusIn", QtCore.QEvent.Type.FocusOut: "FocusOut", } if event.type() in focus_events: event.accept() if event.type() == QtCore.QEvent.Type.FocusOut: widget.removeEventFilter(self) return True return False def __init__(self, field): super(DYField.DYFieldEventFilter, self).__init__() self.field = field def eventFilter(self, widget: 'QObject', event: QtCore.QEvent) -> bool: if event.type() == QtCore.QEvent.Type.FocusOut: # Internal validation. self.field.validate_field() # External validation. self.field.validate.emit() if not self.field.valid(): # If the content of the widget is not valid, focus will be kept on it. # To achieve that, when focusing out, we install a TemporaryEventFilter in the next focused # widget to accept and stop all the Focus Events. # That filter will be removed in the FocusOut event of the TemporaryEventFilter. next_focused = QtWidgets.QApplication.focusWidget() temp_filter = DYField.DYFieldEventFilter.TemporaryFilter() if next_focused: next_focused.installEventFilter(temp_filter) event.accept() widget.setFocus() widget.selectAll() return True return False
-
-
Can't you write your own
QValidator::State QValidator::validate(QString &input, int &pos) const
override to do what you want? So far as I know, if that returns "invalid" the focus stays in the edit widget. You can probably start fromQValidator::State QIntValidator::validate(QString &input, int &pos) const
to get the integer validation, and add your own logic to check the value from the database? -
If, for whatever reason, you don't do it with a validator (e.g. you write your own code on editing finished), you can always put the focus back to the widget on failure in your own code?
-
-
Hi JonB. Thanks for your response.
As long as I know QValidator validates every single key stroke, and I want to validate only when the user has finished typing and the focus is going to the next Widget.
I've trying writing a slot for the editingFinished signal and it works, but only if the field is edited. I mean, an empty field doesnt fire the editingFinished signal, or if I try something wrong, I can check that it's wrong and I send the focus back to the QLineEdit, but if the user doesn't change the text again, editingFinished wont be fired again.
I thought that the correct way of handling this was with an EvenFilter capturing, for example, the FocusAboutToChange event, like this:class Filter(QtCore.QObject): def __init__(self): super(Filter, self).__init__() def eventFilter(self, object: 'QObject', a1: 'QEvent') -> bool: if a1.type() in [QtCore.QEvent.Type.FocusAboutToChange]: if validation_is_wrong(): return True return False
and installing the event filter in the LineEdit:
self._filter = Filter() self.led_nombre_empresa = QtWidgets.QLineEdit() self.led_nombre_empresa.installEventFilter(self._filter)
But it doesn't work as I expected.
-
@jdsantos1978 said in Validate QLineEdit before changing focus:
But it doesn't work as I expected.
You should explain what you mean by that. Do you receive the focus event?
-
I've written an small example.
I shouldn't be able to type in the second QLineEdit until "123" is typed in the first one.
Acording to doc, eventfilter should return True if i want the event to be stopped:
from PyQt6 import QtWidgets, QtCore, QtGui import sys class MyWindow(QtWidgets.QWidget): def __init__(self): super(MyWindow, self).__init__() self.__add_widgets() self.setGeometry(300,200,300,200) def __add_widgets(self): self.edit_id = QtWidgets.QLineEdit() self.lbl_name = QtWidgets.QLabel("Name goes here...") self.edit_other = QtWidgets.QLineEdit("Cannot enter here until 123 in first QLineEdit") vbox = QtWidgets.QVBoxLayout() vbox.addWidget(self.edit_id) vbox.addWidget(self.lbl_name) vbox.addWidget(self.edit_other) self.setLayout(vbox) self.edit_id.installEventFilter(self) def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: if object == self.edit_id: if event.type() == QtCore.QEvent.Type.FocusAboutToChange: if not self.__validate_id(): print ("Not good :(") self.edit_id.setFocus() return True else: print ("Good :)") return False return super().eventFilter(object, event) def __validate_id(self) -> bool: customer_id = self.edit_id.text() if customer_id == "123": self.lbl_name.setText("Pepito Pérez") return True else: self.lbl_name.setText("Unknown Customer") return False app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.show() sys.exit(app.exec())
-
Hi,
One design question: why allow your user to access anything that requires that id to be valid ?
It rather looks that there should be an explicit check like when you log into a database.
-
Hi.
I know that most of the validation of a form can be done in a "Ok" Button, but think in the following example (it's a java app):
In all the "Cta." fields:
- I want the user to write a valid Id. That can be validated at the OK. Button.
- I want the user to write an Id (not empty). That can be done at the Ok Button.
- I want to give the user the posibility of creating a new "Cta." if the id he has entered doesn't exist in the database.
This last behaviour could be achieved handling the FocusOut signal but:
- I would probably have interferences handling FocusOut of all those fields.
- I wouldn't want any validation if the Search Button or Cancel Button where pressed.
It would be great to have a "validate" signal in the widgets before the focus signals, and a causes_validation (True / False) flag in the widgets to handle this.
Anyway, I've figured out a way of achieving this handling the Focus Events. I have to work it out a little and I'll post it here.
-
Maybe a custom QValidator ?
-
Thanks SGaist.
As I said before, QValidator validates every single stroke, and I want a final validation before passing the focus.
Probably the best approach is to write and intall an EvenFilter that handles the QtCore.QEvent.Type.FocusAboutToChange event:
"When the filter object’s eventFilter() implementation is called, it can accept or reject the event, and allow or deny further processing of the event. If all the event filters allow further processing of an event (by each returning false), the event is sent to the target object itself. If one of them stops processing (by returning true), the target and any later event filters do not get to see the event at all."
The problem is that although I stop the FocusAboutToChange event, even the FocusOut event in the widget, it doesn't stop firing those same events in the next Widget, and that's precisely what I want to avoid.
-
Can you show your event filter ?
-
It's a workaround, but it's working for me just now.
The idea is using two EventFilters :
- A normal Filter.
- A temporary Filter.
The "normal" filter installs the temporary filter in widget that is going to be focused next when processing the FocusOut event. The only thing that the temporary filter does is to accept all the focus events and stop them.
The temporary filter removes itself from the widget in the FocusOut event.
This way, the "normal" filter can do internal and external validations before losing the focus.
I'm doing some tests and it's working for me. I want to implement it inside a custum widget.
class DYFieldEventFilter(QtCore.QObject): class TemporaryFilter(QtCore.QObject): def __init__(self): super(DYField.DYFieldEventFilter.TemporaryFilter, self).__init__() def eventFilter(self, widget: 'QObject', event: 'QEvent') -> bool: focus_events = { QtCore.QEvent.Type.FocusAboutToChange: "FocusAboutToChange", QtCore.QEvent.Type.FocusIn: "FocusIn", QtCore.QEvent.Type.FocusOut: "FocusOut", } if event.type() in focus_events: event.accept() if event.type() == QtCore.QEvent.Type.FocusOut: widget.removeEventFilter(self) return True return False def __init__(self, field): super(DYField.DYFieldEventFilter, self).__init__() self.field = field def eventFilter(self, widget: 'QObject', event: QtCore.QEvent) -> bool: if event.type() == QtCore.QEvent.Type.FocusOut: # Internal validation. self.field.validate_field() # External validation. self.field.validate.emit() if not self.field.valid(): # If the content of the widget is not valid, focus will be kept on it. # To achieve that, when focusing out, we install a TemporaryEventFilter in the next focused # widget to accept and stop all the Focus Events. # That filter will be removed in the FocusOut event of the TemporaryEventFilter. next_focused = QtWidgets.QApplication.focusWidget() temp_filter = DYField.DYFieldEventFilter.TemporaryFilter() if next_focused: next_focused.installEventFilter(temp_filter) event.accept() widget.setFocus() widget.selectAll() return True return False