Readonly tkinter text widget

11,116

Solution 1

The reason that the last character is inserted is because the default bindings (which causes the insert) happens after custom bindings you put on the widget. So your bindings fire first and then the default binding inserts the characters. There are other questions and answers here that discuss this in more depth. For example, see https://stackoverflow.com/a/11542200/

However, there is a better way to accomplish what you are trying to do. If you want to create a readonly text widget, you can set the state attribute to "disabled". This will prevent all inserts and deletes (and means you need to revert the state whenever you want to programmatically enter data).

On some platforms it will seem like you can't highlight and copy text, but that is only because the widget won't by default get focus on a mouse click. By adding a binding to set the focus, the user can highlight and copy text but they won't be able to cut or insert.

Here's an example using python 2.x; for 3.x you just have to change the imports:

import Tkinter as tk
from ScrolledText import ScrolledText

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        t = ScrolledText(self, wrap="word")
        t.insert("end", "Hello\nworld")
        t.configure(state="disabled")
        t.pack(side="top", fill="both", expand=True)

        # make sure the widget gets focus when clicked
        # on, to enable highlighting and copying to the
        # clipboard.
        t.bind("<1>", lambda event: t.focus_set())

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()

Solution 2

Please do not delete and reinsert your text :

  • It is huge performance issue.
  • It will remove any tags and marks set on the text
  • This will be visible to the user, and users don't like flickering interfaces
  • This is not necessary, Tkinter is customizable enough to just not allow the user change the content.

The best way I found to create a read only Text is to disable all the bindings leading to a text change.

My solution is to create a new Widget binding map containing only "read only commands". Then, just reconfigure your widget to use the new RO binding map instead of the default one :

from Tkinter import *

# This is the list of all default command in the "Text" tag that modify the text
commandsToRemove = (
"<Control-Key-h>",
"<Meta-Key-Delete>",
"<Meta-Key-BackSpace>",
"<Meta-Key-d>",
"<Meta-Key-b>",
"<<Redo>>",
"<<Undo>>",
"<Control-Key-t>",
"<Control-Key-o>",
"<Control-Key-k>",
"<Control-Key-d>",
"<Key>",
"<Key-Insert>",
"<<PasteSelection>>",
"<<Clear>>",
"<<Paste>>",
"<<Cut>>",
"<Key-BackSpace>",
"<Key-Delete>",
"<Key-Return>",
"<Control-Key-i>",
"<Key-Tab>",
"<Shift-Key-Tab>"
)


class ROText(Text):
    tagInit = False

    def init_tag(self):
        """
        Just go through all binding for the Text widget.
        If the command is allowed, recopy it in the ROText binding table.
        """
        for key in self.bind_class("Text"):
            if key not in commandsToRemove:
                command = self.bind_class("Text", key)
                self.bind_class("ROText", key, command)
        ROText.tagInit = True


    def __init__(self, *args, **kwords):
        Text.__init__(self, *args, **kwords)
        if not ROText.tagInit:
            self.init_tag()

        # Create a new binding table list, replace the default Text binding table by the ROText one
        bindTags = tuple(tag if tag!="Text" else "ROText" for tag in self.bindtags())
        self.bindtags(bindTags)

text = ROText()

text.insert("1.0", """A long text with several
lines
in it""")


text.pack()

text.mainloop()

Note that just the bindings are changed. All the Text command (as insert, delete, ...) are still usable.

Share:
11,116
yassin
Author by

yassin

Updated on June 19, 2022

Comments

  • yassin
    yassin almost 2 years

    I want to use tkinter text widget as a readonly widget. It should act as a transcript area. My idea is to keep this transcript in a file and whenever the user writes anything, just remove all the contents of the widget, and rewrite it again.

    The code will look like:

    transcript_entry = SimpleEditor()  # SimpleEditor is inherited from ScrolledText
    transcript_entry.text.delete("1.0", END)
    
    # this is just a test string, it should be the contents of the transcript file
    transcript_entry.text.insert("1.0", "This is test transcript")  
    transcript_entry.text.bind("<KeyPress>", transcript_entry.readonly)
    

    And readonly function will look like:

    def readonly(self, event):
        self.text.delete("1.0", END)
        # this is just a test string, it should be the contents of the transcript file
        self.text.insert("1.0", "This is test transcript")
    

    The bug here is that the last character entered by the user is added to the transcript. I suspect the reason is that the readonly function is called, then the user input is wrote to the widget. How to reverse this order & let the readonly function be called after the user input is wrote to the widget?

    Any hints?

  • Russell Smith
    Russell Smith about 10 years
    Why not just set the widget's state attribute to "disabled"?
  • mgautierfr
    mgautierfr about 10 years
    The "disable " state disable a lot of stuff. Every operations that change the content of the text are disable. insert/delete commands will not work anymore. In the same way, the text will not answer to "<<Copy>>" binding
  • Russell Smith
    Russell Smith almost 9 years
    This won't work if you past more than one character into the widget. And, of course, it doesn't prevent you from deleting characters. Also, it will throw an error if anything other than a text widget has focus.
  • glep
    glep almost 9 years
    True, but still a useful solution if all you require is the ability to capture key events without writing the captured characters to the widgets.