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:

enter image description here

A XBM with 10x20 pixels

  • then you can config a tag in Text widget using the above XBM image file as bgstipple 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:

enter image description hereenter image description here

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"):

enter image description here


What needs to be improved:

  1. the line-height of label(squiggly line) should be smaller.(To make the squiggly line and entry widget closer)
  2. Actually,the label(squiggly line) could be image.(In my example,one character == two "~" characters)
  3. the function of checking.
  4. you could add two scrollbars.