Tkinter – How to have a different text display in a Text widget to what is being edited

Question:

I am creating an application using Python and Tkinter.
The user inputs data into a Text widget. How can I make it so that the text being displayed is different to what is actually being edited?

I have two different variables: question and question_raw. question contains the data that should be displayed and question_data contains the data that should be edited.

For example, if the user types /times, it should automatically display as x. However, if they then backspace the x, it should go to /time, as the s would have been backspaced. I cannot just check for when an x is removed, as the user may type x normally.

Is this possible user default Tkinter widgets, or would I need to create my own? If I need to create my own, how would I go about this?

Thanks!

Asked By: KingsMMA

||

Answers:

Here is the plan: Every time the Text widget is edited, we check whether "/time" is inside the Text widget content. We will then overwrite the question_raw variable with the original text, and we will delete and re-write the contents of the Text widget.

import tkinter as tk   

question = ''
question_raw = ''

def text_edited(e):
    if '/time' in text.get(1.0, tk.END):
        question = text.get(1.0, tk.END).replace('/time', 'x')
        question_raw = text.get(1.0, tk.END)

root = tk.Tk()                           
text = tk.Text(root)                        
text.pack()                         
text.bind('<Key>',text_edited)
root.mainloop()
Answered By: Neurotricity

For example, if the user types /times, it should automatically display as x. However, if they then backspace the x, it should go to /time, as the s would have been backspaced. I cannot just check for when an x is removed, as the user may type x normally.

There is no feature in the text widget specifically to do this. However, the text widget has all of the necessary building blocks. You’ll have to add some custom bindings to make it work.

For example, after the user enters a space you can check to see if the preceding word is "/times". If so, you can replace it with "x" and add the tag "/times" to that x.

You will then need to create a custom binding for the backspace and delete characters. If you delete a character with a tag that begins with "/", remove the original character and replace it with the name of the tag.

Here’s a hacked together example that seems to work. It’s not production-ready, but works enough to serve as an example. It creates a custom class that behaves like a text widget, but you can pass a dictionary of replacement strings. When you type a space, it checks to see if the preceding word is in that dictionary. If it is, it replaces it with the replacement string (and colors it red just to make it easy to see).

If the cursor is immediately following one of these replacement strings, it gets the tag from the string, deletes the character, and replaces it with the tag name. (eg: backspace over "x" to replace it with "/times")

import tkinter as tk

class CustomText(tk.Text):
    def __init__(self, *args, **kwargs):
        self.macros = kwargs.pop("macros", {})
        super().__init__(*args, **kwargs)

        self.bind("<BackSpace>", self._backspace)
        self.bind("<space>", self._check_macro)

        for macro_name in self.macros.keys():
            self.tag_configure(macro_name, foreground="red")

    def _backspace(self, event):
        tags = self.tag_names("insert-1c")
        for tag_name in tags:
            if tag_name in self.macros:
                self.replace("insert-1c", "insert", tag_name)
                return "break"

    def _check_macro(self, event):
        word = self.get("insert-1c wordstart-1c", "insert")
        if word in self.macros:
            replacement = self.macros[word]
            self.replace("insert-1c wordstart-1c", "insert", self.macros[word], word, " ")
            return "break"

root = tk.Tk()
macros = {"/times": "x", "/minus": "-", "/plus": "+"}
text = CustomText(root, macros=macros)
text.pack(fill="both", expand=True)
root.mainloop()

Answered By: Bryan Oakley
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.