Is it possible to have a red squiggly line appear under words in a Tkinter text widget without using canvas? (Like for misspelled words)
This is just an example of using user-defined XBM as the bgstipple
of part of the text inside a Text
widget to simulate the squiggly line effect:
- create a XBM image, for example
squiggly.xbm
, like below:
A XBM with 10x20 pixels
- then you can config a tag in
Text
widget using the above XBM image file asbgstipple
in red color:
# config a tag with squiggly.xbm as bgstipple in red color
textbox.tag_config("squiggly", bgstipple="@squiggly.xbm", background='red')
- and apply the tag to the portion of text inside
Text
widget:
textbox.insert("end", "hello", "squiggly") # add squiggly line
Below is a sample code:
import tkinter as tk
root = tk.Tk()
textbox = tk.Text(root, width=30, height=10, font=('Courier New',12), spacing1=1)
textbox.pack()
# config a tag with squiggly.xbm as bgstipple in red color
textbox.tag_config("squiggly", bgstipple="@squiggly.xbm", background='red')
textbox.insert("end", "hello", "squiggly") # add squiggly line
textbox.insert("end", " world! ")
textbox.insert("end", "Python", "squiggly") # add squiggly line
textbox.insert("end", "\nthis is second line")
root.mainloop()
And the output:
Note that the height of the XBM image need to match the font size and spacing between lines.
I customized a frame,its layout(just an idea,it needs improvement):
When user input "Enter",it will generate a new entry and a new label(squiggly line widget).
If user type "Backspace" and this entry is null,it will delete both this entry and label(squiggly line widget).
When user type "Up" arrow,it will make previous entry widget focused.
....
Code:
import tkinter
class CustomText(tkinter.Frame):
def __init__(self,master):
super(CustomText,self).__init__()
self.last_line = 0
self.index_line = 0
self.master = master
self['background'] = 'white'
self.check_func = self.master.register(self.check)
first_line = tkinter.Entry(self,font=("",16),relief="flat",validate="key",validatecommand=(self.check_func,'%W','%P'))
first_line.pack(fill="x")
first_underline = tkinter.Label(self,background="white",fg="red",font=("",4))
first_underline.pack(anchor="nw")
self.widget_dict = {
first_line:first_underline # a dict which save the squiggly line widget(as a value) and entry widget(as a key)
}
# bind event:
first_line.bind("<Return>",self.create_new_line)
first_line.bind("<Up>",self.to_previous_line)
first_line.bind("<Down>",self.to_next_line)
first_line.bind("<FocusIn>",self.focused)
def focused(self,event): # when one entry widget is focused,change the index_line number
self.index_line = list(self.widget_dict.keys()).index(event.widget)
def create_new_line(self,event): # when user input enter,generate an entry and a label
self.index_line += 1
self.last_line += 1
new_line = tkinter.Entry(self,font=("",14),relief="flat",validate="key",validatecommand=(self.check_func,'%W','%P'))
new_line.pack(fill='x')
new_underline = tkinter.Label(self, background="white", fg="red", font=("", 4))
new_underline.pack(anchor="nw")
# also bind an event
new_line.bind("<Return>", self.create_new_line)
new_line.bind("<Up>",self.to_previous_line)
new_line.bind("<Down>",self.to_next_line)
new_line.bind("<FocusIn>",self.focused)
# the difference between the first line:when user delete all the words in this widget and he input "backspace" again, it will delete the entry and label widget,
new_line.bind("<BackSpace>",self.delete_this_line)
new_line.focus_set()
self.widget_dict[new_line] = new_underline
def to_next_line(self,event): # when user type "Down",go to the previous line
if self.index_line != self.last_line:
self.index_line += 1
to_widget = tuple(self.widget_dict.keys())[self.index_line]
to_widget.focus_set()
if event: # to the same index of next entry widget.
to_widget.icursor(event.widget.index("insert"))
def to_previous_line(self,event): # when user type "Up",go to the previous line
if self.index_line:
self.index_line -= 1 # the number of index minus 1
to_widget = tuple(self.widget_dict.keys())[self.index_line]
to_widget.focus_set()
if event:
to_widget.icursor(event.widget.index("insert"))
def delete_this_line(self,event):
if not event.widget.get():
self.last_line -= 1
self.widget_dict[event.widget].destroy() # delete it in visual
del self.widget_dict[event.widget] # delete reference in the self.widget_dict
event.widget.destroy()
del event.widget
self.to_previous_line(None)
def check(self,widget_str,input_str): # this is an error-check function
widget = self.nametowidget(widget_str) # convert the widgetname to a widget object
# an example
error_str = "abc"
if input_str == error_str: # now is to check the grammar
underline_widget = self.widget_dict[widget]
underline_widget['text'] = "〜"*len(error_str)*2 # add a squiggly line visually
return True
root = tkinter.Tk()
t = CustomText(root)
t.pack()
root.mainloop()
Example image(show the squiggly line when user input "abc"):
What needs to be improved:
- the line-height of label(squiggly line) should be smaller.(To make the squiggly line and entry widget closer)
- Actually,the label(squiggly line) could be image.(In my example,one character == two "~" characters)
- the function of checking.
- you could add two scrollbars.