QLineEdit unexpected behavior using QRegExpValidator - python

I'm using a QLineEdit widget to enter email addresses and I set up a QRegExpValidor to validate the entry.
The validator is working as far as preventing the input of characters not allowed in the QRegExp, but funny enough, it allows to enter intermediate input, either by pushing the enter key or by firing the "editingfinished" signal.
I verified the return of the validator which is correct (Intermediate or Acceptable).
Checking the PyQt5 documentation it seams that an intermediate state of the validator does not prevent the focus to change to another widget. Furthermore, it stops the firing of the editingFinished and returnedPressed signals so the user may enter wrong address as far as it marches partially the RegExp.
Ref: https://doc.qt.io/qt-5/qlineedit.html#acceptableInput-prop
I was able to solve my needs by removing the validator from the QLineEdit widget, and placing a method "checkValidator" linked to the cursorPositionChanged of the QLineEdit widget discarting the last character entered when nonacceptable, and setting the focus back to the QLineEdit when it validats intermmediate. It works just fine but when the the focus was reset, the others signals on other widgets were fired one at a time. Momentarily, I handle this by checking if the sender has focus at the start of the methods (see lookForFile)
Even though I could handle the issue, I appreciate very much anybody explaining me the proper way to use the RegExpValidator and or why resetting the focus fires the other signals out of the blue.
def setUI(self):
self.setWindowTitle("EMail Settings")
self.setModal(True)
rx = QRegExp(r"[a-z0-9_%]+#[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
lblAddress = QLabel("EMail Address")
self.lineAddress = QLineEdit(self)
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
#self.lineAddress.setValidator(self.mailValidator)
self.lineAddress.cursorPositionChanged.connect(self.checkValidator)
self.lineAddress.returnPressed.connect(self.checkValidator)
lblPassword = QLabel("Password")
self.linePwd = QLineEdit()
self.linePwd.setEchoMode(QLineEdit.PasswordEchoOnEdit)
lblOauth2 = QLabel("Oauth2 Token")
self.lineOauth = QLineEdit()
pushOauth = QPushButton("...")
pushOauth.setObjectName("token")
pushOauth.clicked.connect(self.lookForFile)
pushOauth.setFixedWidth(30)
#pyqtSlot()
def checkValidator(self):
self.lineAddress.blockSignals(True)
v = self.mailValidator.validate(self.lineAddress.text(), len(self.lineAddress.text()))
if v[0] == 0:
self.lineAddress.setText(self.lineAddress.text()[:-1])
elif v[0] == 1:
self.lineAddress.setFocus()
elif v[0] == 2:
pass
print("validates", v)
self.lineAddress.blockSignals(False)
#pyqtSlot()
def lookForFile(self):
try:
if not self.sender().hasFocus():
return
baseDir = "C"
obj = self.sender()
if obj.objectName() == "Draft":
capt = "Email Draft"
baseDir = os.getcwd() + "\\draft"
fileType = "Polo Management Email (*.pad)"
dialog = QFileDialog(self, directory=os.getcwd())
dialog.setFileMode(QFileDialog.Directory)
res = dialog.getExistingDirectory()
elif obj.objectName() == "token":
capt = "Gmail Outh2 token File"
fileType = "Gmail token Files (*.json)"
baseDir = self.lineOauth.text()
res = QFileDialog.getOpenFileName(self, caption=capt, directory=baseDir, filter=fileType)[0]
fileName = res
if obj.objectName() == "Draft":
self.lineDraft.setText(fileName)
elif obj.objectName() == "tokenFile":
self.lineOauth.setText(fileName)
except Exception as err:
print("settings: lookForFile", err.args)
Hope to answer #eyllanesc and Qmusicmante request with this minimal reproducible example. I change the regex for a simple one allowing for a string of lower case a-z follow by a dot and three more lowercase characters.
What I intend is the validator not allowing the user to enter a wrong input. The example allows for "xxxzb.ods" but also allows "xxxzb" or "xxxzb.o" for instance.
In short, not allowing the user to enter a wrong input.
This is my minimal reproducible example:
class CheckValidator(QDialog):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setUI()
def setUI(self):
self.setWindowTitle("EMail Settings")
self.setModal(True)
rx = QRegExp(r"[a-z]+\.[a-z]{3}")
lblAddress = QLabel("Check Line")
self.lineAddress = QLineEdit()
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
self.lineAddress.setValidator(self.mailValidator)
self.lineAddress.cursorPositionChanged[int, int].connect(lambda
oldPos, newPos: self.printValidator(newPos))
lblCheck = QLabel("Check")
lineCheck = QLineEdit()
formLayout = QFormLayout()
formLayout.addRow(lblAddress, self.lineAddress)
formLayout.addRow(lblCheck, lineCheck)
self.setLayout(formLayout)
#pyqtSlot(int)
def printValidator(self, pos):
print(self.mailValidator.validate(self.lineAddress.text(), pos))
if __name__ == '__main__':
app = QApplication(sys.argv)
tst = CheckValidator()
tst.show()
app.exec()

I found a solution and I post it here for such a case it may help somebody else.
First I remove the QRegExpValidator from the QLineEdit widget. The reason being is that QLineEdit only fires editingFinished (and we'll need it ) only when QRegExpValidator returns QValidator.Acceptable while the validator is present.
Then we set a method fired by the 'cursorPositionchanged' signal of QlineEdit widget. On this method, using the QRegExpValidator we determine if the last character entered is a valid one. If not we remove it.
Finally, I set a method fired by the 'editingFinished' signal using the RegEx exactMatch function to determine if the entry is valid. If it is not, we give the user the option to clear the entry or to return to the widget to continue entering data. The regex used is for testing purposes only, for further information about email validation check #Musicamante comments.
This is the code involved:
def setUI(self):
................
................
rx = QRegExp(r"[a-z0-9_%]+#[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
lblAddress = QLabel("EMail Address")
self.lineAddress = QLineEdit(self)
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
self.lineAddress.cursorPositionChanged[int, int].connect(lambda oldPos,
newPos: self.checkValidator(newPos))
self.lineAddress.editingFinished.connect(lambda : self.checkRegExp(rx))
#pyqtSlot(int)
def checkValidator(self, pos):
v = self.mailValidator.validate(self.lineAddress.text(), pos ))
if v[0] == 0:
self.lineAddress.setText(self.lineAddress.text()[:-1])
#pyqtSlot(QRegExp)
def checkRegExp(self, rx):
if not rx.exactMatch(self.lineAddress.text()) and self.lineAddress.text():
if QMessageBox.question(self, "Leave the Address field",
"The string entered is not a valid email address! \n"
"Do you want to clear the field?", QMessageBox.Yes|QMessageBox.No) ==
QMessageBox.Yes:
self.lineAddress.clear()
else:
self.lineAddress.setFocus()

Related

Using QDoubleValidator to limit QLineEdit input and reset the value to nearest acceptable

I would like to use QLineEdit to get user input for values. I want to limit the input between a range so I should use QDoubleValidator. I would like it to work such that if they go over the allowed value, it sets the text to the top() value and if they go under then it sets it to the bottom() value.
I looked at using the textChanged, returnPressed, and inputRejected signals. The reason I am having trouble is that once I set the validator with the range, returnPressed will not enter the check_validator function which they mention here. Then, I thought maybe I could catch it with the input rejected signal but for some reason that doesn't seem to work either. Here is some code:
class LineEdit(QLineEdit):
def __init__(self, text, parent=None):
super(LineEdit, self).__init__(parent)
self.validator = QDouble Validator()
self.setValidator(self.validator)
self.text = text
self.textChanged.connect(self.new_text)
self.returnPressed(self.check_validator)
def new_text(self, text):
self.ntext = text
def check validator:
try:
if float(self.ntext) > self.validator.top():
self.text = str(self.validator.top()
if float(self.ntext) < self.validator.bottom():
self.text = str(self.validator.bottom()
else:self.text = self.ntext
self.setText(self.text)
except:
mssg = QMessageBox.about(self, "Error", "Input can only be a number")
mssg.exec()
self.setText(self.text)
def valRange(self, x1, x2):
self.validator.setRange(x1, x2)
I also get an attribute error saying 'noneType' object has no attribute 'exec' when the window does pop up. I think I am missing a step on how to close that window properly.
You could reimplement keyPressEvent to catch return presses when hasAcceptableInput is false. There is also a drawback to overriding the text property, now any programatic calls to setText will not update the text of the QLineEdit. There's no reason to do it.
class LineEdit(QLineEdit):
def __init__(self, *args, **kwargs):
super(LineEdit, self).__init__(*args, **kwargs)
self.validator = QDoubleValidator(0, 10, 4, notation=QDoubleValidator.StandardNotation)
self.setValidator(self.validator)
self.textChanged.connect(self.new_text)
self.returnPressed.connect(self.check_validator)
self.ntext = None
def keyPressEvent(self, event):
super().keyPressEvent(event)
if event.key() == Qt.Key_Return and not self.hasAcceptableInput():
self.check_validator()
def new_text(self, text):
if self.hasAcceptableInput():
self.ntext = text
def check_validator(self):
try:
if float(self.text()) > self.validator.top():
self.setText(str(self.validator.top()))
elif float(self.text()) < self.validator.bottom():
self.setText(str(self.validator.bottom()))
except:
mssg = QMessageBox.about(self, "Error", "Input can only be a number")
self.setText(self.ntext)

Editing behavior of “backspace” key of QDoubleSpinBox

I’ve got a problem with QDoubleSpinBox. Editing behavior of “backspace” key somehow depends on the size of the suffix.
If I set “m” as suffix, then set the cursor at the end of the spinbox and press “backspace”, the cursor jumps over the “m” suffix to the value which then can be edited with further “backspaces”.
If I set the suffix to “mm” or any double-lettered word, the cursor remains at the end of the spinbox no matter how many “backspaces” I press.
I tried to debug what comes into “validate” method, and got a peculiar result:
When the “backspace” is pressed while cursor is at the end of "0,00m", validate receives "0,00m".
When the “backspace” is pressed while cursor is at the end of "0,00_m" validate receives "0,00__m"
When the “backspace” is pressed while cursor is at the end of "0,00_mm" validate receives "0,00_m_mm"
What is the cause of such behavior and how can I overcome it?
# coding=utf-8
from PyQt5 import QtWidgets
class SpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self):
super().__init__()
def validate(self, text, index):
res = super().validate(text, index)
print(text, res, self.text())
return res
if __name__ == "__main__":
q_app = QtWidgets.QApplication([])
sb = SpinBox()
sb.setSuffix(" m")
sb.show()
q_app.exec_()
The source code for QDoubleSpinBox/QAbstractSpinBox is extremely convoluted when it comes to key-event handling - I couldn't work out what default behaviour is supposed to be, or even where it might be implemented. There might be a bug somewhere, but I wouldn't want to bet on it.
It looks like the only option is to reimplement keyPressEvent:
class SpinBox(QtWidgets.QDoubleSpinBox):
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Backspace:
suffix = self.suffix()
if suffix:
edit = self.lineEdit()
text = edit.text()
if (text.endswith(suffix) and
text != self.specialValueText()):
pos = edit.cursorPosition()
end = len(text) - len(suffix)
if pos > end:
edit.setCursorPosition(end)
return
super().keyPressEvent(event)

QValidator fixup issue

Following is my code to validate an account code, and add dashes at certain intervals. An example account code is 140-100-1000-6610-543. The code takes the regex: \d{3}-\d{3}-\d{4}-\d{4}-\d{3] and allows the user to type in numbers, and where there is a dash in the regex, it places the dash for the user. Or rather, it should. On our production server (('Qt version:', '4.8.1'), ('SIP version:', '4.13.2'), ('PyQt version:', '4.9.1')) it does work. Our dev server is upgraded to ('Qt version:', '4.8.6'), ('SIP version:', '4.15.5'), ('PyQt version:', '4.10.4') and it doesn't work there.
On each server, I type in 140. On the production (older version), the line edit value changes to 140-. On the newer version development server, it does not add the dash.
Please let me know if you see my issue, and let me know if this is a PyQt issue, or a Qt issue.
import sys
from PyQt4 import QtGui, QtCore
DEBUG = True
class acValidator(QtGui.QRegExpValidator):
def __init__(self, regexp, widget):
QtGui.QRegExpValidator.__init__(self, regexp, widget)
self.widget = widget
def validate(self, text, pos):
'''function to decide if this account code is valid'''
valid, _npos = QtGui.QRegExpValidator.validate(self, text, pos)
if valid != QtGui.QValidator.Acceptable:
self.fixup(text)
print 'acWidget.validate result of fixup', text
# move position to the end, if fixup just added a dash
# to the end
newstr = self.widget.text()
newpos = len(str(newstr))
if pos + 1 == newpos and newstr[pos:newpos] == '-':
pos = newpos
# return the valid variable, and the current position
return (valid, pos)
def fixup(self, text):
'''place dashes, if we can'''
# pylint: disable=no-member
if DEBUG:
print 'acWidget.py fixup'
reParts = self.regExp().pattern().split('-')
if DEBUG:
print list(reParts)
newreg = ''
for i in range(len(reParts)):
newreg += reParts[i]
if DEBUG:
print i, reParts[i]
nr = QtCore.QRegExp(newreg)
# pylint: disable=no-member
if nr.exactMatch(text) and not self.regExp().exactMatch(text):
if DEBUG:
print 'adding dash'
text += '-'
return
newreg += '-'
def isValid(self):
'''return a true or false for the validity based on whether the
widget is a lineEdit or a comboBox (acCB). true only if the
validator returns QtGui.QValidator.Acceptable
'''
valid, _npos = QtGui.QRegExpValidator.validate(self,
self.widget.text(),
self.widget.cursorPosition())
if valid == QtGui.QValidator.Acceptable:
return True
return False
class acWidget(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
self.regex = r'\d{3}-\d{3}-\d{4}-\d{4}-\d{3}'
self.setMinimumWidth(200)
self.setValidator(acValidator(QtCore.QRegExp(self.regex),
self))
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = QtGui.QDialog()
layout = QtGui.QVBoxLayout(form)
a = acWidget(form)
layout.addWidget(a)
form.setLayout(layout)
form.setMinimumWidth(400)
form.setMinimumHeight(200)
form.show()
app.exec_()
The problem is caused by the C++ signatures of fixup and validate requiring that the text argument is modifiable. If you're using Python 2, this distinctly unpythonic way of doing things is honoured by PyQt; whereas with Python 3, the signatures have been changed so that the methods simply return the modified values.
The specific issue in your code can be found here:
def fixup(self, text):
...
text += '-'
It seems that, in earlier versions of PyQt, augmented assignment on a QString did an implicit in-place mutation. But in more recent versions, it works more like normal python augmented assignment and simply re-binds the local variable like this:
text = text + '-'
To work around this, you can do an explict in-place mutation:
text.append('-')

Get input from custom dialog with entry, ok, and cancel button and return - Python/GTK3

In Python with gobject, I am having immense issues getting input from the user.
Here is my code:
def get_network_pw(self, e):
def okClicked(self):
print(pwd.get_text())
return pwd.get_text()
pwDialog.destroy()
def cancelClicked(self):
print("nope!")
pwDialog.hide()
return None
#Getting the about dialog from UI.glade
pwDialog = self.builder.get_object("passwordDialog")
okBtn = self.builder.get_object("pwdOkBtn")
cancelBtn = self.builder.get_object("pwdCancelBtn")
pwd = self.builder.get_object("userEntry")
# Opening the about dialog.
#okBtn.connect("clicked", okClicked)
#cancelBtn.connect("clicked", cancelClicked)
pwDialog.run()
I am not sure where I am going wrong? It refuses to return the userEntry text. I also tried the code from Python/Gtk3 : How to add a Gtk.Entry to a Gtk.MessageDialog? and Simple, versatile and re-usable entry dialog (sometimes referred to as input dialog) in PyGTK to no avail.
EDIT
I have a dialog I made in glade. It contains a GtkTextBox (userEntry), an Ok button (pwdOkBtn) and a cancel button (pwdCancelBtn). When the user clicks OK, it theoretically should return what they entered in the text box (say, 1234). When they click cancel, it should return None. However, when they click Ok, it returns "", and when they click cancel, it returns "". I'm not sure where I am going wrong here.
Extra code tries:
I tried the following code as well:
def get_network_pw(self, e):
d = GetInputDialog(None, "Enter Password")
dialog = d.run()
if dialog is 1:
print("OK")
else:
print("Nope!")
d.hide()
GetInputDialog:
class GetInputDialog(Gtk.Dialog):
def __init__(self, parent, title):
Gtk.Dialog._init(self, title, parent)
self.response = "Cancel"
self.setupHeader()
self.setupUI()
def setupUI(self):
wdg = self.get_content_area() #explained bellow
self.txtSource = Gtk.Entry() #create a text entry
wdg.add(self.txtSource)
self.show_all() #show the dialog and all children
def setupHeader(self, title="Get User Input"):
hb = Gtk.HeaderBar()
hb.props.show_close_button = True
hb.props.title = title
self.set_titlebar(hb)
btnOk = Gtk.Button("OK")
btnOk.connect("clicked", self.btnOkClicked)
hb.pack_start(btnOk)
btnCancel = Gtk.Button("Cancel")
btnCancel.connect("clicked", self.btnCancelClicked)
hb.pack_start(btnCancel)
def btnOkClicked(self, e):
self.response = "Ok" #set the response var
dst = self.txtSource #get the entry with the url
txt = dst.get_text()
return 1
def btnCancelClicked(self, e):
self.response = "Cancel"
return -1
I think you're overcomplicating it. The run method returns the id of the button pressed in a dialog:
Don't use the .hide() and .destroy() methods in that way, those are for different situations. .destroy() destroys the widget, so you should not call it unless you know what you're doing.
Place the .hide() after the .run().
Capture the return value of the run(), and setup the buttons in the dialog to a different Response ID in Glade.
The relevant part of the code is:
def _btn_cb(self, widget, data=None):
"""
Button callback
"""
ret = self.dialog.run()
self.dialog.hide()
if ret == 0:
self.label.set_text(
self.entry.get_text()
)
The full code for this example is here:
https://gist.github.com/carlos-jenkins/c27bf6d5d76723a4b415
Extra: If you want to check a condition to accept the Ok button (don't know, for example that the entry is valid) execute the run() in a while loop, check if button is Cancel then break, else check the validity of the input, if valid do something and break, else continue:
def _btn_cb(self, widget, data=None):
"""
Button callback
"""
while True:
ret = self.dialog.run()
if ret == -1: # Cancel
break
try:
entry = int(self.entry.get_text())
self.label.set_text(str(entry))
break
except ValueError:
# Show in an error dialog or whatever
print('Input is not an integer!')
self.dialog.hide()

Finding checked QRadioButton among many into a QVBoxLayout

I used the code below to dynamically create a group of radio buttons:
self.wPaymantType.qgbSomeSelectionGroup = QtGui.QGroupBox()
vbox = QtGui.QVBoxLayout()
for row in listOfChoices:
radio = QtGui.QRadioButton(row)
if bIsFirst:
radio.setChecked(True)
bIsFirst = False
if len(row.name) > nMaxLen:
nMaxLen = len(row.name)
vbox.addWidget(radio)
self.wPaymantType.qgbSomeSelectionGroup.setLayout(vbox)
How can I iterate through all radio buttons to find out which one is checked?
I tried something like this, but I didn't get anything good from it:
qvbl = self.qgbSomeSelectionGroup.children()[0]
for i in range(0, qvbl.count()):
child = qvbl.itemAt(i)
radio = QtGui.QRadioButton(child.widget())
if radio != None:
if radio.isChecked():
print "radio button num " + str(i) + " is checked"
Your code is not minimal and self-contained, so it's really hard to help you -- but I've anyway gone through the effort of building a near-minimal self-contained approximation of what you're trying to do and which does seem to work correctly -- here comes...:
from PyQt4 import QtGui
import sys
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.dowid()
self.setCentralWidget(self.thewid)
def dowid(self):
self.thewid = QtGui.QGroupBox()
vbox = QtGui.QVBoxLayout()
self.radiobuttons = []
listOfChoices = 'one two three'.split()
for i, row in enumerate(listOfChoices):
radio = QtGui.QRadioButton(row)
if i == 0:
radio.setChecked(True)
self.radiobuttons.append(radio)
vbox.addWidget(radio)
self.thewid.setLayout(vbox)
def examine(self):
for i, radio in enumerate(self.radiobuttons):
if radio.isChecked():
print "radio button num " + str(i) + " is checked"
else:
print "radio button num " + str(i) + " is NOT checked"
if __name__ == '__main__':
app = QtGui.QApplication([])
mainWin = MainWindow()
mainWin.show()
rc = app.exec_()
mainWin.examine()
This seems to do what you want. Key change is to keep the actual Python widget objects around rather than trying to restore them from the layout vbox -- that attempt seems to not be working as intended, at least as regards correct access to the crucial detail about whether a given radio button is checked or not, which is of course the heart of your Q.
I believe the reason why it's not working is your
radio = QtGui.QRadioButton(child.widget())
call at the code where you're checking if your checkbox is checked. I think what you're trying to do is typecast the child object to QtGui.QRadioButton and it doesn't work in this case. Instead you should be creating a new widget. Try changing it to smth. like this:
qvbl = self.qgbSomeSelectionGroup.layout()
for i in range(0, qvbl.count()):
widget = qvbl.itemAt(i).widget()
if (widget!=0) and (type(widget) is QtGui.QRadioButton):
if widget.isChecked():
print "radio button num " + str(i) + " is checked"
the code above should be iterating through child objects of the layout object, check their type and print "radio button..." in case it's radio buttong and it's checked
hope this helps, regards
I guess a better way to identify which button is checked is to use a QButtonGroup, as it provides a container to organize groups of button widgets. It is not a visual object, so it doesn't substitute the layout for visually arranging your radio buttons, but it does allow you to make them mutually exclusive and associate an integer "id" with them, letting you know which one is checked without the need to iterate through all the widgets present in the layout.
If you decide to use it, your code should turn into something like this:
self.wPaymantType.qgbSomeSelectionGroup = QtGui.QGroupBox()
vbox = QtGui.QVBoxLayout()
radioGroup = QtGui.QButtonGroup()
radioGroup.setExclusive(True)
for i,row in enumerate(listOfChoices):
radio = QtGui.QRadioButton(row)
radioGroup.addButton(radio, i)
if bIsFirst:
radio.setChecked(True)
bIsFirst = False
if len(row.name) > nMaxLen:
nMaxLen = len(row.name)
vbox.addWidget(radio)
self.wPaymantType.qgbSomeSelectionGroup.setLayout(vbox)
To identify the checked button, you can use QButtonGroup's checkedId method:
buttonId = radioGroup.checkedId()
or if you want to retrive the button object itself you can use the checkedButton method:
button = radioGroup.checkedButton()

Categories

Resources