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
Related
I basically got the unwanted characters removed from the Text widget, but when I hit a line break and try to scroll the text with the keyboard or mouse, I just can't (it always stays in the same place).
this is done in the "validate text" method
class NewNewsFrame(Frame):
def __init__(self, parent):
self.Parent = parent
self.initializecomponents()
pass
def validate_text(self, widger):
value = widger.get("1.0", "end-1c")
print value.fon
if not value.isalnum():
var = str()
for i in value:
print(f"({i})")
var += i if(i.isalpha() or i.isdigit() or i == "(" or i == ")" or i == " " or i == "," or i == "." or i == "\n")else ""
widger.delete("1.0", "end-1c")
widger.insert("end-1c", var)
pass
def initializecomponents(self):
Frame.__init__(self, self.Parent)
self.DescriptionLabel = Label(self)
self.DescriptionBox = Text(self)
# DescriptionLabel
self.DescriptionLabel.config(text="Description of the news:",bg=self["bg"], fg="#FFFFFF", font="none 15",anchor="nw")
self.DescriptionLabel.place(relwidth=1, relheight=0.1, relx=0, rely=0.11, anchor="nw")
# DescriptionBox
self.DescriptionBox.bind("<KeyPress>", lambda event: self.validate_text(self.DescriptionBox))
self.DescriptionBox.bind("<FocusOut>", lambda event: self.validate_text(self.DescriptionBox))
self.DescriptionBox.place(relheight=0.4, relwidth=1, relx=0, rely=0.16, anchor="nw")
pass
I tried to find how keyboard scrolling works, but I still don't know how to do it
The problem is that you're deleting and restoring all of the text with every keypress. This causes the cursor position to change in unexpected ways that breaks the default bindings.
If you're wanting to prevent certain characters from being entered, there's a better way. If your validation function returns the string "break", that prevents the character from being inserted. You don't have to re-scan the entire contents or delete and restore the text, because the bad characters never get entered in the first place.
Your validation function might look something like this:
def validate_text(self, event):
if event.char.isalpha() or event.char.isdigit() or event.char in "() ,.\n":
pass
else:
return "break"
Next, simplify the binding to look like the following. Tkinter will automatically pass the event parameter to the function.
self.DescriptionBox.bind("<KeyPress>", self.validate_text)
This will break your <FocusOut> binding, but I'm not sure it's needed.
For more information about how events are processed and why returning "break" does what it does, see this answer to the question Basic query regarding bindtags in tkinter. That question is about an Entry widget but the concept is the same for all widgets.
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 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
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()
I've been having a lot of difficulties with this code, and I just can't find any solutions, I will post my code below.
from tkinter import *
a = []
class test(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.parent.title('testing')
self.pack(fill=BOTH, expand=1)
self.d = DoubleVar()
self.d.set('None')
def grab():
b = ent.get()
a.append(b)
c = [s.strip('qwertyuiopasdfghjklzxcvbnm') for s in a]
self.d.set(c[-1])
if c[-1] == '':
self.d.set('None')
ent = Entry(self)
ent.grid(row=0, column=0)
but = Button(self, text='Get', command=grab)
but.grid(row=1, column=0)
Label(self, textvariable=self.d).grid(row=2, column=0)
root = Tk()
app = test(root)
root.mainloop
I guess my objective is to be able to ignore, or delete the letters that are placed inside of the entry box, as you can see, I've used the strip method, but it doesn't work the way I would like it to. If anyone could offer some advice, or a code, or link me to a question that I overlooked, that would be amazing, and I would be greatful.
EDIT: It already clears letters before and after, but nothing in between
A validator would be the correct pattern to solve this problem. Interactively validating Entry widget content in tkinter has an implementation in tkinter that you should be able to use.
A little bit of lambda should do the trick
a = "123a3456b"
filter(lambda '0' <= x <= '9', a)
print a
"1233456"
You're getting letters inside the numbers because you're putting the string into a list first.
strip() removes leading and trailing characters, so if you have a string: aaa000bbb111ccc, stripping letters from it will only remove the outer-most letters. If you split the string, however, and then strip letters from each element of the stripped string, you'll effectively remove all the letters. Then, you can join() the remaining parts of the list together to get back to your string. Consider this example:
>>> import string # string.ascii_letters returns a string of all letters (upper and lower), just easier than typing them
>>> def check(x):
return ''.join([char.strip(string.ascii_letters) for char in x])
>>> var = 'aaa000bbb111ccc'
>>> var_as_list = [var]
>>> check(var)
'000111'
>>> check(var_as_list)
'000bbb111'
So, c should be:
c = ''.join([s.strip('qwertyuiopasdfghjklzxcvbnm') for s in b.get()])
You should also consider some further validation, if you want the field to only contain floats. Here's one method to trace any changes to a StringVar() instance and restrict changes to it to only being numbers and periods:
from tkinter import *
import string
def check(*args):
# make a 'whitelist' of allowable characters
whitelist = string.digits + '.'
# set var to its current value - any characters not in whitelist
var.set(''.join([i for i in var.get() if i in whitelist]))
root = Tk()
var = StringVar()
var.set('0.0')
Entry(root, textvariable=var).grid(row=0, column=0)
Label(root, textvariable=var).grid(row=1, column=0)
var.trace('w', check) # if var changes, call check()
mainloop()
a clean way to do this is simply:
filter(lambda s: not str.isalpha(s), data)