For example, the telephone format is +999 99 9999-9999. That is, the GtkEntry automatically add the characters (+,[space] and -) as the user types.
1. How to make an entry validator?
In order to do an entry validator in gtk, you need to connect the insert_text signal to a validation method. It goes like so:
class EntryWithValidation(Gtk.Entry):
"""A Gtk.Entry with validation code"""
def __init__(self):
Gtk.Entry.__init__(self)
self.connect("insert_text", self.entryInsert)
def entryInsert(self, entry, text, length, position):
# Called when the user inserts some text, by typing or pasting.
# The `position` argument is not working as expected in Python
pos = entry.get_position()
# Your validation code goes here, outputs are new_text and new_position (cursor)
if new_text:
# Set the new text (and block the handler to avoid recursion).
entry.handler_block_by_func(self.entryInsert)
entry.set_text(new_text)
entry.handler_unblock_by_func(self.entryInsert)
# Can't modify the cursor position from within this handler,
# so we add it to be done at the end of the main loop:
GObject.idle_add(entry.set_position, new_pos)
# We handled the signal so stop it from being processed further.
entry.stop_emission("insert_text")
This code will generate a warning: Warning: g_value_get_int: assertion 'G_VALUE_HOLDS_INT (value)' failed Gtk.main() because of the incapacity of handling return arguments in the Python bindings of Gtk signals. This question gives details about the bug. As suggested in the accepted answer, you can override the default signal handler like so:
class EntryWithValidation(Gtk.Entry, Gtk.Editable):
def __init__(self):
super(MyEntry, self).__init__()
def do_insert_text(self, new_text, length, position):
# Your validation code goes here, outputs are new_text and new_position (cursor)
if new_text:
self.set_text(new_text)
return new_position
else:
return position
2.The validation code
You now need to write the validation code. It is a bit fiddly since we need to place the cursor at the end of the inserted text but we may have added some extra characters while formatting.
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject
class TelNumberEntry(Gtk.Entry):
"""A Gtk.Entry field for phone numbers"""
def __init__(self):
Gtk.Entry.__init__(self)
self.connect("insert_text", self.entryInsert)
def entryInsert(self, entry, text, length, position):
pos = entry.get_position()
old_text = entry.get_text()
# Format entry text
# First we filter digits in insertion text
ins_dig = ''.join([c for c in text if c.isdigit()])
# Second we insert digits at pos, truncate extra-digits
new_text = ''.join([old_text[:pos], ins_dig, old_text[pos:]])[:17]
# Third we filter digits in `new_text`, fill the rest with underscores
new_dig = ''.join([c for c in new_text if c.isdigit()]).ljust(13, '_')
# We are ready to format
new_text = '+{0} {1} {2}-{3}'.format(new_dig[:3], new_dig[3:5],
new_dig[5:9], new_dig[9:13]).split('_')[0]
# Find the new cursor position
# We get the number of inserted digits
n_dig_ins = len(ins_dig)
# We get the number of digits before
n_dig_before = len([c for c in old_text[:pos] if c.isdigit()])
# We get the unadjusted cursor position
new_pos = pos + n_dig_ins
# If there was no text in the entry, we added a '+' sign, therefore move cursor
new_pos += 1 if not old_text else 0
# Spacers are before digits 4, 6 and 10
for i in [4, 6, 10]:
# Is there spacers in the inserted text?
if n_dig_before < i <= n_dig_before + n_dig_ins:
# If so move cursor
new_pos += 1
if new_text:
entry.handler_block_by_func(self.entryInsert)
entry.set_text(new_text)
entry.handler_unblock_by_func(self.entryInsert)
GObject.idle_add(entry.set_position, new_pos)
entry.stop_emission("insert_text")
if __name__ == "__main__":
window = Gtk.Window()
window.connect("delete-event", Gtk.main_quit)
entry = TelNumberEntry()
window.add(entry)
window.show_all()
Gtk.main()
Related
I'm very new to QT so please assume I know nothing. I want an auto-complete where it only matches against the text after the last comma.
e.g. if my word bank is ["alpha", "beta", "vector space"], and the user currently has typed "epsilon,dog,space" then it should match against "vector space" since "space" is a substring of "vector space".
I'm using PyQt6 and my current code looks something like this (adapted from a YouTube tutorial):
line_of_text = QLineEdit("")
word_bank = [name for name,value in names_dict.items()]
completer = QCompleter(word_bank)
completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains)
line_of_text.setCompleter(completer)
So currently, if word_bank is ["alpha", "beta", "vector space"], then if line_of_text had the string "epsilon,dog,space" then it wouldn't match against anything because "epsilon,dog,space" isn't a substring of "alpha" nor "beta" nor "vector space".
How can I alter my code to achieve what I would like to achieve? -- I'm experienced with programming, just not with Qt.
PS: I have tried doing
line_of_text.textChanged[str].connect(my_function)
where my_function takes only the substring of line_of_text after the last comma and feeds it to completer.setCompletionPrefix and then calls completer.complete().
This simply does not work. I assume the reason is that completer.complete() updates the completion prefix from line_of_text, causing the call to completer.setCompletionPrefix to be overwritten immediately after.
One solution is to use the textChanged signal to set the completion prefix, and bypass the built-in behaviour of the line-edit to allow greater control of when and how the completions happen.
Below is a basic implementation that shows how to do that. By default, it only shows completions when the text after the last comma has more than single character - but that can be easily adjusted:
from PyQt6 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.edit = QtWidgets.QLineEdit()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.edit)
word_bank = ['alpha', 'beta', 'vector space']
self.completer = QtWidgets.QCompleter(word_bank)
self.completer.setCaseSensitivity(
QtCore.Qt.CaseSensitivity.CaseInsensitive)
self.completer.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
self.completer.setWidget(self.edit)
self.completer.activated.connect(self.handleCompletion)
self.edit.textChanged.connect(self.handleTextChanged)
self._completing = False
def handleTextChanged(self, text):
if not self._completing:
found = False
prefix = text.rpartition(',')[-1]
if len(prefix) > 1:
self.completer.setCompletionPrefix(prefix)
if self.completer.currentRow() >= 0:
found = True
if found:
self.completer.complete()
else:
self.completer.popup().hide()
def handleCompletion(self, text):
if not self._completing:
self._completing = True
prefix = self.completer.completionPrefix()
self.edit.setText(self.edit.text()[:-len(prefix)] + text)
self._completing = False
if __name__ == '__main__':
app = QtWidgets.QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 300, 50)
window.show()
app.exec()
(Assuming str is the string from the text input) Have you tried separating str using the , character, then iterating over each of the words separated by a comma.
for word in str.split(","):
for check in word_bank:
etc...
the connected function will be called in your case whenever the string entered changes.
I'm trying to make a simple HTML Editor with Tkinter, with Python 3.10. Right now I'm trying to implement auto-completion using the Tkinter Text widget. The widget checks the word currently typed to see if it matches with any of the HTML tags in self._tags_list. And if it does, it auto-completes it.
Code:
import random
import tkinter as tk
class IDE(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
self._tags_list = ['<html></html>', '<head></head>', '<title></title>', '<body></body>', '<h1></h1>',
'<h2></h2>', '<h3></h3>', '<h4></h4>', '<h5></h5>', '<h6></h6>', '<p></p>', '<b></b>']
self.bind("<Any-KeyRelease>", self._autocomplete)
self.bind("<Tab>", self._completion, add=True)
self.bind("<Return>", self._completion, add=True)
def callback(self, word):
# Returns possible matches of `word`
matches = [x for x in self._tags_list if x.startswith(word)]
return matches
def _completion(self, _event):
tag_ranges = self.tag_ranges("autocomplete")
if tag_ranges:
self.mark_set("insert", tag_ranges[1])
self.tag_remove("sel", "1.0", "end")
self.tag_remove("autocomplete", "1.0", "end")
return "break"
def _autocomplete(self, event):
if event.keysym not in ["Return"] and not self.tag_ranges("sel"):
self.tag_remove("sel", "1.0", "end")
self.tag_remove("autocomplete", "1.0", "end")
word = self.get("insert-1c wordstart", "insert-1c wordend")
print(f"word: {word}")
matches = self.callback(word)
print(f"matches: {matches}")
if matches:
remainder = random.choice(matches)[len(word):]
print(remainder)
insert = self.index("insert")
self.insert(insert, remainder, ("sel", "autocomplete"))
self.mark_set("insert", insert)
return
if __name__ == "__main__":
window = tk.Tk()
window.title("Autocomplete")
window.geometry("500x500")
text = IDE(window)
text.pack(fill=tk.BOTH, expand=True)
window.mainloop()
Used from #TRCK.
When I type in the "<" to start an HTML tag, it detects the first possible auto-completion (a random tag from self._tags_list minus the "<" at the beginning). However, when I type in a second character, it detects that character as a separate word and does not auto-complete. I've tested it with words not starting with "<" and it worked fine.
For example, terminal output when I type "<h" into the widget:
When I hit "<":
word: <
matches: ['<html></html>', '<head></head>', '<title></title>', '<body></body>', '<h1></h1>', '<h2></h2>', '<h3></h3>', '<h4></h4>', '<h5></h5>', '<h6></h6>', '<p></p>', '<b></b>']
remainder: h3></h3>
When I add the "h":
word: h
matches: []
Image of Tkinter window:
Can someone please tell me why this is happening and how to fix it? I've checked all around and found nothing yet. Sorry if it is obvious, but I am now learning Tkinter. Also if there are any other problems in the code, please tell me.
A word in wordstart is defined as
"a word is either a string of consecutive letter, digit, or underbar (_) characters, or a single character that is none of these types."
when you type more characters after <, the wordstart starts from the character next to the <. You can make this work by adding -1c at the end of wordstart
word = self.get("insert -1c wordstart -1c", "insert -1c wordend")
By adding
if "<" in word:
word = word[word.index("<"):]
after word you can have a slight improvement.
Besides that you also need to exclude BackSpace, Ctrl+a etc from your keysym
I created n tk.Entry in a GUI over a for loop using a class. The method add_input_entries create the entries and I bonded the update to the Keypress event and the update to the keyrelease event.
def add_input_entries(self): # method of the class creating the entry on a loop
for item in self.entry:
item.grid(row=self.row, column=self.__col)
self.__col += self.__col
item.bind("<KeyRelease>", self._accept) # update the entry values as the key is released
item.bind("<KeyPress>", self._validate) # validate the inpur as the key is pressed
this is the method for the validation.
def _validate(self, event):
item = event.char
if item not in ('.', ',') and not item.isnumeric():
messagebox.showerror(title='Title', message='Invalid character - only numeric characters allowed!!')
return 'break'
else:
if self.nac == 0:
self.entry_values[1].set(self.entry_values[0].get())
print(item)
now I cannot type any other keys but the ones defined. How can I allow the user to delete the character from the entry and press enter?
A tk.Entry widget has a validate option which is probably easier to use.
import tkinter as tk
def str_is_float( chars ):
try:
v = float( chars )
return chars[-1] != " "
# This treats '123 ' as invalid even though it coverts to a float.
except ValueError:
return len(chars) == 0 or chars in [ "-", ".", "-." ]
# This allows the strings '-', '.' and '-.' which could start a float.
root = tk.Tk()
validate_cmd = ( root.register( str_is_float ), '%P' )
# '%P' is to pass the new string (after the last keypress ) to the validation.
widget = tk.Entry( root, validate = 'key', validatecommand = validate_cmd )
widget.grid( padx = 10, pady = 10 )
root.mainloop()
It's possible to validate the string with other approaches e.g. regex. I've found allowing an empty string is required to make the entry usable.
tk documentation for validate
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)
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('-')