Constraining user input to a QLineEdit
-
@Kent-Dorfman I simply want to force the user to enter a string representation of a positive, floating-point value < 99.99 like this: "XX.YY" without forcing them to type the decimal point. Sounds silly but that is what the customer demands (or strongly desires anyway). Why? Because the previous version of the product (designed 15 years ago) does it that way.
I got pretty close yesterday by using QLineEdit::textEdited() and selectively inserting the decimal based on current and previous cursor positions. However, for some reason that isn't clear to me, QLineEdit::textEdited() is signaled in response to QLineEdit::insert(). That causes all manner of havoc in the case that the user backspaces over the decimal and then inserts a new numeral.
From the documentation I don't think that should happen right??? I would think QLineEdit::insert() would qualify as a programmatic change to the text and should only result in QLineEdit::textChanged() being signalled. QLineEdit::textEdited() should not be generated in that case correct??
The framework is 5.15LTS (which I realize went EOL back in the spring) ... could it just be a bug in QLineEdit of that vintage?
@Dallas-Posey
I would not have expectedtextEdited()
to be emitted against a programmaticinsert()
.If that is really, really happening you might be able to work around it with one of:
-
Where you do an
insert()
in code, instead get the whole text as a string, insert your character/.
into that, and usesetText()
to put the whole string back. -
Set a flag in code prior to your
insert()
, check the flag in your slot ontextEdited()
and ignore, clear the flag afterwards.
-
-
@JonB it is definitely happening. I fixed it with a simple in-flight mechanism in the signal handler:
QLineEdit* editor = this->findChild<QLineEdit*>("filterEdit"); // Private Member of fltsetter connect(editor, &QLineEdit::textEdited, this, &fltsetter::hdlTextEdited); ... void fltsetter::hdlTextEdited(const QString &text) { int currentCursorPos; static int inFlight = 0; qInfo() << "fltsetter::hdlTextEdited(" << text << ")"; qInfo() << "Current cursor pos: " << (currentCursorPos = editor->cursorPosition()); qInfo() << "Last cursor pos: " << lastCursorPos; qInfo() << "size: " << editor->text().size(); qInfo() << "InFlight: " << inFlight; if (inFlight--) { qInfo() << "Inflight!"; return; } // 1. Limit the size to 5 if (editor->text().size() > 5) { inFlight++; editor->backspace(); qInfo() << "Size limited to 5 chars"; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); return; } // 2. If adding char at position 2, insert decimal if ((currentCursorPos == 2) && (lastCursorPos == 1)) { inFlight++; editor->insert("."); qInfo() << "Inserting decimal"; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); return; } // 3. If adding third char after decimal erased, insert a decimal in pos 3 if ((currentCursorPos == 3) && (lastCursorPos == 2) && (text.at(currentCursorPos) != ".")) { inFlight += 3; QChar lastChar = text.at(currentCursorPos); editor->backspace(); editor->insert("."); editor->insert(lastChar); qInfo() << "Substituting decimal"; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); return; } inFlight = 0; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); }
The logs (which I can now pare-down some) verified that the signal handler was re-entering as a result of the insert() and backspace() methods being called and the inFlight mechanism fixed it right up. Behaves exactly as I would have expected (without the inflight) if the insert() and backspace() methods didn't generate the signal.
Anyway it works now. It's a clumsy, non-generic solution, but pays the bills in this case looks like.
-
@Kent-Dorfman I simply want to force the user to enter a string representation of a positive, floating-point value < 99.99 like this: "XX.YY" without forcing them to type the decimal point. Sounds silly but that is what the customer demands (or strongly desires anyway). Why? Because the previous version of the product (designed 15 years ago) does it that way.
I got pretty close yesterday by using QLineEdit::textEdited() and selectively inserting the decimal based on current and previous cursor positions. However, for some reason that isn't clear to me, QLineEdit::textEdited() is signaled in response to QLineEdit::insert(). That causes all manner of havoc in the case that the user backspaces over the decimal and then inserts a new numeral.
From the documentation I don't think that should happen right??? I would think QLineEdit::insert() would qualify as a programmatic change to the text and should only result in QLineEdit::textChanged() being signalled. QLineEdit::textEdited() should not be generated in that case correct??
The framework is 5.15LTS (which I realize went EOL back in the spring) ... could it just be a bug in QLineEdit of that vintage?
@Dallas-Posey said in Constraining user input to a QLineEdit:
I simply want to force the user to enter a string representation of a positive, floating-point value < 99.99 like this: "XX.YY" without forcing them to type the decimal point.
That's not a super unusual thing to do. Reminds me of when installers had screen to enter serial numbers, and you could type them without the hyphens.
Two alternative approaches to possibly consider:
-
Use QLineEdit's input mask functionality to make the decimal point a fixture the user can't delete.
-
Actually use two line edits, each with a validator allowing two digits, the first line edit automatically moving focus to the second one once full. The decimal point can then be just a label between the line edits.
-
-
@JonB it is definitely happening. I fixed it with a simple in-flight mechanism in the signal handler:
QLineEdit* editor = this->findChild<QLineEdit*>("filterEdit"); // Private Member of fltsetter connect(editor, &QLineEdit::textEdited, this, &fltsetter::hdlTextEdited); ... void fltsetter::hdlTextEdited(const QString &text) { int currentCursorPos; static int inFlight = 0; qInfo() << "fltsetter::hdlTextEdited(" << text << ")"; qInfo() << "Current cursor pos: " << (currentCursorPos = editor->cursorPosition()); qInfo() << "Last cursor pos: " << lastCursorPos; qInfo() << "size: " << editor->text().size(); qInfo() << "InFlight: " << inFlight; if (inFlight--) { qInfo() << "Inflight!"; return; } // 1. Limit the size to 5 if (editor->text().size() > 5) { inFlight++; editor->backspace(); qInfo() << "Size limited to 5 chars"; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); return; } // 2. If adding char at position 2, insert decimal if ((currentCursorPos == 2) && (lastCursorPos == 1)) { inFlight++; editor->insert("."); qInfo() << "Inserting decimal"; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); return; } // 3. If adding third char after decimal erased, insert a decimal in pos 3 if ((currentCursorPos == 3) && (lastCursorPos == 2) && (text.at(currentCursorPos) != ".")) { inFlight += 3; QChar lastChar = text.at(currentCursorPos); editor->backspace(); editor->insert("."); editor->insert(lastChar); qInfo() << "Substituting decimal"; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); return; } inFlight = 0; qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); }
The logs (which I can now pare-down some) verified that the signal handler was re-entering as a result of the insert() and backspace() methods being called and the inFlight mechanism fixed it right up. Behaves exactly as I would have expected (without the inflight) if the insert() and backspace() methods didn't generate the signal.
Anyway it works now. It's a clumsy, non-generic solution, but pays the bills in this case looks like.
@Dallas-Posey
If it works, fine! If I remember I will have a look tomorrow to see howtextEdited()
behaves under Qt6. The docs are pretty vague about what "whenever the text is edited" means. Oh, look at this at the top of the doc page https://doc.qt.io/qt-6/qlineedit.html#editing-textWhen the text changes, the textChanged() signal is emitted. When the text changes in some other way than by calling setText(), the textEdited() signal is emitted.
So actually there is your answer, though I didn't know it.
As I said earlier, ISTM you could have written your code to use
setText()
where you useinsert()
/backspace()
. (And aren't your calls inhdlTextEdited()
causing it to be called again from itself?)On a separate matter, it's up to you but this kind thing
QLineEdit* editor = this->findChild<QLineEdit*>("filterEdit"); // Private Member of fltsetter
is ugly. Did you consider creating a subclass of
QLineEdit
for your behaviour and using that for the line edit where appropriate? -
@Dallas-Posey
If it works, fine! If I remember I will have a look tomorrow to see howtextEdited()
behaves under Qt6. The docs are pretty vague about what "whenever the text is edited" means. Oh, look at this at the top of the doc page https://doc.qt.io/qt-6/qlineedit.html#editing-textWhen the text changes, the textChanged() signal is emitted. When the text changes in some other way than by calling setText(), the textEdited() signal is emitted.
So actually there is your answer, though I didn't know it.
As I said earlier, ISTM you could have written your code to use
setText()
where you useinsert()
/backspace()
. (And aren't your calls inhdlTextEdited()
causing it to be called again from itself?)On a separate matter, it's up to you but this kind thing
QLineEdit* editor = this->findChild<QLineEdit*>("filterEdit"); // Private Member of fltsetter
is ugly. Did you consider creating a subclass of
QLineEdit
for your behaviour and using that for the line edit where appropriate?Good catch on the documentation thing. From the 5.15 documentation:
Unlike textChanged(), this signal is not emitted when the text is changed programmatically, for example, by calling setText().
Yeah I can refactor and use setText(). Seems a better solution.
The definition of 'editor' I included here was in fact for clarity only. Actually the class is declared with a private member 'QLineEdit* editor' and then when I need the editor, I use findChild. The actual instantiation of the object is done in ui_fltsetter.h which I intuit to be created by some tool in the chain from the xml code generated by QtDesigner?? (from fltsetter.ui???). Honestly, I have only the vaguest understanding of how that process works, but the only way I know to get a handle to the instance is with findChild. Unless of course I created and configured the object problematically I guess, but doesn't that defeat the purpose of using the Designer? Could you suggest a better solution? I'm very interested in learning because I really like Qt. It was recommended to me years ago, but I ignored the advice as in my 40 year career up until now, I never needed a graphical UI. It's all been little machines gathering data and talking to each other. Qt is more than a UI though, I am happy to be discovering and even as my career winds down, I still love discovering new things.
Anyway, I do take your point though about creating a scion of QLineEdit. My initial foray into subclassing QtObjects was unsuccessful and due to schedule at that moment (running with scissors, dowsed in gasoline, pursued by stackholders carrying torches and pitchforks) I had to move on with best effort. In the original proto, forcing the user to engage their brain to enter a mixed number with the editing capabilities of the stock QLineEdit seemed acceptable.
-
@Dallas-Posey said in Constraining user input to a QLineEdit:
I simply want to force the user to enter a string representation of a positive, floating-point value < 99.99 like this: "XX.YY" without forcing them to type the decimal point.
That's not a super unusual thing to do. Reminds me of when installers had screen to enter serial numbers, and you could type them without the hyphens.
Two alternative approaches to possibly consider:
-
Use QLineEdit's input mask functionality to make the decimal point a fixture the user can't delete.
-
Actually use two line edits, each with a validator allowing two digits, the first line edit automatically moving focus to the second one once full. The decimal point can then be just a label between the line edits.
Yeah I hear ya, it is an old pattern and I did try the input mask but it I couldn't get it to work. I set the mask in QtDesigner like this:
But the inputMask is really just another form of validation right? It doesn't modify the behavior of the editor itself. That is, it doesn't force the user to type 4 digits separated by a period, it just invalidates the entry (hasAcceptableInput() returns false).
@IgKh said in Constraining user input to a QLineEdit:
Use QLineEdit's input mask functionality to make the decimal point a fixture the user can't delete.
Best I can tell, it won't prevent the decimal from being backspaced over ... though you can't set the text attribute to anything but a period in Designer. In the running program however, with the editor focused, the user can backspace right over it and type anything they want. Could it be because I also have a validator set?:
editor->setValidator(new QDoubleValidator(20, 99.99, 2, this ));
@IgKh said in Constraining user input to a QLineEdit:
Actually use two line edits, each with a validator allowing two digits, the first line edit automatically moving focus to the second one once full. The decimal point can then be just a label between the line edits.
Yeah this idea certainly occurred to me and seems perfectly valid. Part of this project though, is a learning exercise for me and I wanted to explore QLineEdit as thoroughly as possible. I do appreciate the help and information @IgKh
-
-
Good catch on the documentation thing. From the 5.15 documentation:
Unlike textChanged(), this signal is not emitted when the text is changed programmatically, for example, by calling setText().
Yeah I can refactor and use setText(). Seems a better solution.
The definition of 'editor' I included here was in fact for clarity only. Actually the class is declared with a private member 'QLineEdit* editor' and then when I need the editor, I use findChild. The actual instantiation of the object is done in ui_fltsetter.h which I intuit to be created by some tool in the chain from the xml code generated by QtDesigner?? (from fltsetter.ui???). Honestly, I have only the vaguest understanding of how that process works, but the only way I know to get a handle to the instance is with findChild. Unless of course I created and configured the object problematically I guess, but doesn't that defeat the purpose of using the Designer? Could you suggest a better solution? I'm very interested in learning because I really like Qt. It was recommended to me years ago, but I ignored the advice as in my 40 year career up until now, I never needed a graphical UI. It's all been little machines gathering data and talking to each other. Qt is more than a UI though, I am happy to be discovering and even as my career winds down, I still love discovering new things.
Anyway, I do take your point though about creating a scion of QLineEdit. My initial foray into subclassing QtObjects was unsuccessful and due to schedule at that moment (running with scissors, dowsed in gasoline, pursued by stackholders carrying torches and pitchforks) I had to move on with best effort. In the original proto, forcing the user to engage their brain to enter a mixed number with the editing capabilities of the stock QLineEdit seemed acceptable.
@Dallas-Posey said in Constraining user input to a QLineEdit:
Good catch on the documentation thing. From the 5.15 documentation:
Unlike textChanged(), this signal is not emitted when the text is changed programmatically, for example, by calling setText().
As I wrote, that is against the
textEdited()
signal entry. Even in 5.15 go to the Description paragraphs and look forWhen the text changes the textChanged() signal is emitted; when the text changes other than by calling setText() the textEdited() signal is emitted;
It's worth reading the Description section in each page.
If you want to work from Designer, it will save a
.ui
file and your build process will runuic
on that to generate aui_....h
file containing C++ code implementing what you have designed. And the will be#include
d into aclass_name.cpp
file you compile.Usually it's better not to
findChild<>()
from the outside world. The.cpp
can either (a) implement the behaviour or (b) export theQLineEdit
or similar for access from elsewhere as a dedicated C++ method. If you intend to do any designing yourself you will want to look at this.You can subclass widgets you use from Designer if you wish. It does require an extra "step", This is called Promotion.
-
Thanks @JonB . I will read the documentation more carefully.
@JonB said in Constraining user input to a QLineEdit:
If you want to work from Designer, it will save a .ui file and your build process will run uic on that to generate a ui_....h file containing C++ code implementing what you have designed. And the will be #included into a class_name.cpp file you compile.
Ok so there is an additional, pre-compiler compiler other than the moc right? So the uic translates the xml from the .ui files into the ui_.h headers, then the moc runs though all the code in the rest of the source and does it's thing, then that all gets fed to the compiler proper. Or some variant thereof? Cool.
Usually it's better not to findChild<>() from the outside world.
So not kosher to call this from a descendant of QDialog for example? That is what you mean by outside world?
Thx again for the help @JonB et. al. The Qt community is very helpful and friendly!
BTW: This worked much better after I thought about what you said earlier:
void fltsetter::hdlTextEdited(const QString &text) { int currentCursorPos; qInfo() << "fltsetter::hdlTextEdited(" << text << ")"; qInfo() << "Current cursor pos: " << (currentCursorPos = editor->cursorPosition()); qInfo() << "Last cursor pos: " << lastCursorPos; qInfo() << "size: " << editor->text().size(); disconnect(editor, &QLineEdit::textEdited, this, &fltsetter::hdlTextEdited); // 1. If adding char at position 2, insert decimal if ((currentCursorPos == 2) && (lastCursorPos == 1)) { qInfo() << "Inserting decimal"; editor->insert("."); qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); } // 2. If adding third char after decimal erased, insert a decimal in pos 3 else if ((currentCursorPos == 3) && (lastCursorPos == 2) && (text.back() != ".")) { qInfo() << "Substituting decimal"; auto lastChar = text.back(); editor->backspace(); editor->insert("."); qInfo() << "Last char: " << lastChar; editor->insert(lastChar); qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); } connect(editor, &QLineEdit::textEdited, this, &fltsetter::hdlTextEdited); qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); }
-
Thanks @JonB . I will read the documentation more carefully.
@JonB said in Constraining user input to a QLineEdit:
If you want to work from Designer, it will save a .ui file and your build process will run uic on that to generate a ui_....h file containing C++ code implementing what you have designed. And the will be #included into a class_name.cpp file you compile.
Ok so there is an additional, pre-compiler compiler other than the moc right? So the uic translates the xml from the .ui files into the ui_.h headers, then the moc runs though all the code in the rest of the source and does it's thing, then that all gets fed to the compiler proper. Or some variant thereof? Cool.
Usually it's better not to findChild<>() from the outside world.
So not kosher to call this from a descendant of QDialog for example? That is what you mean by outside world?
Thx again for the help @JonB et. al. The Qt community is very helpful and friendly!
BTW: This worked much better after I thought about what you said earlier:
void fltsetter::hdlTextEdited(const QString &text) { int currentCursorPos; qInfo() << "fltsetter::hdlTextEdited(" << text << ")"; qInfo() << "Current cursor pos: " << (currentCursorPos = editor->cursorPosition()); qInfo() << "Last cursor pos: " << lastCursorPos; qInfo() << "size: " << editor->text().size(); disconnect(editor, &QLineEdit::textEdited, this, &fltsetter::hdlTextEdited); // 1. If adding char at position 2, insert decimal if ((currentCursorPos == 2) && (lastCursorPos == 1)) { qInfo() << "Inserting decimal"; editor->insert("."); qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); } // 2. If adding third char after decimal erased, insert a decimal in pos 3 else if ((currentCursorPos == 3) && (lastCursorPos == 2) && (text.back() != ".")) { qInfo() << "Substituting decimal"; auto lastChar = text.back(); editor->backspace(); editor->insert("."); qInfo() << "Last char: " << lastChar; editor->insert(lastChar); qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); } connect(editor, &QLineEdit::textEdited, this, &fltsetter::hdlTextEdited); qInfo() << "New cursor pos: " << (lastCursorPos = editor->cursorPosition()); }
@Dallas-Posey
By "outside world" (forfindChild<>()
) I mean anywhere other than the.cpp
where it is defined. It is preferable if that exports it if intended, otherwise you are delving into somewhere else's internal details. Not a 100% rule, just preferable not to.You choose to
disconnect()
and then re-connect()
. For my own part I would prefer not to make a change like that, similarly for QObject::blockSignals(bool block) which you will see others would use here (actually it's not too bad in this case, the scope is pretty clear and limited). You can if you wish, but (say those didn't exist) why not use a simple flag (static
in the method or as a class member variable) which you set on entry and clear on exit, simplyreturn
if you re-enter while it is set?Or use
setText()
only in the first place :)Anyway you are good, there are many ways to skin a cat....
-
More good points @JonB . I did not know about know about QObject::blockSignals(bool block). A flag would certainly work too which is what my original 'inFlight' idea started out as. I'm gonna play with it some more ... needs some additional refactoring anyway.
Thx again!
Edit: I shied away from setText() because it doesn't re-validate the text. I don't look at acceptableInput until after the user presses ENTER ... not sure if that event re-runs the validation and maybe it's not an issue anyway.