How to Mark a Portion of a Text Widget as Readonly

How can you mark a portion of a text widget as readonly?

The most bullet-proof solution is to intercept the low-level insert and delete commands, and put logic in there to prevent insertions and deletions based on some sort of criteria. For example, you could disallow edits within any range of text that has the tag "readonly".

Here's an example of this technique. It takes advantage of the fact that all insertions and deletions ultimately call the insert or delete subcommand of the underlying tk widget command, and the fact that the widget command can be replaced with a Tcl proc.

try:
# python 2.x
import Tkinter as tk
except ImportError:
# python 3.x
import tkinter as tk

class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)

text = ReadonlyText(self)
sb = tk.Scrollbar(self, orient="vertical", command=text.yview)
text.configure(yscrollcommand=sb.set)
sb.pack(side="left", fill="y")
text.pack(side="right", fill="both", expand=True)

text.insert("end", "You can edit this line\n")
text.insert("end", "You cannot edit or delete this line\n", "readonly")
text.insert("end", "You can edit this, too.")

text.tag_configure("readonly", foreground="darkgray")

class ReadonlyText(tk.Text):
'''A text widget that doesn't permit inserts and deletes in regions tagged with "readonly"'''
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)

# this code creates a proxy that will intercept
# each actual insert and delete.
self.tk.eval(WIDGET_PROXY)

# this code replaces the low level tk widget
# with the proxy
widget = str(self)
self.tk.eval('''
rename {widget} _{widget}
interp alias {{}} ::{widget} {{}} widget_proxy _{widget}
'''.format(widget=widget))

WIDGET_PROXY = '''
if {[llength [info commands widget_proxy]] == 0} {
# Tcl code to implement a text widget proxy that disallows
# insertions and deletions in regions marked with "readonly"
proc widget_proxy {actual_widget args} {
set command [lindex $args 0]
set args [lrange $args 1 end]
if {$command == "insert"} {
set index [lindex $args 0]
if [_is_readonly $actual_widget $index "$index+1c"] {
bell
return ""
}
}
if {$command == "delete"} {
foreach {index1 index2} $args {
if {[_is_readonly $actual_widget $index1 $index2]} {
bell
return ""
}
}
}
# if we passed the previous checks, allow the command to
# run normally
$actual_widget $command {*}$args
}

proc _is_readonly {widget index1 index2} {
# return true if any text in the range between
# index1 and index2 has the tag "readonly"
set result false
if {$index2 eq ""} {set index2 "$index1+1c"}
# see if "readonly" is applied to any character in the
# range. There's probably a more efficient way to do this, but
# this is Good Enough
for {set index $index1} \
{[$widget compare $index < $index2]} \
{set index [$widget index "$index+1c"]} {
if {"readonly" in [$widget tag names $index]} {
set result true
break
}
}
return $result
}
}
'''

def main():
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

if __name__ == "__main__":
main()

Readonly tkinter text widget

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()

How to make Tkinter Text widget read only on certain characters and sentences

You can use a mark to keep track of the beginning of the editable part, I called it "input" in the code below. In addition I have adapted Bryan Oakley's answer https://stackoverflow.com/a/16375233/6415268: instead of updating line numbers in the proxy, it checks whether the characters are inserted/deleted in the editable part. To do so I use: Text.compare('insert' < 'input').

Here is a full example:

import tkinter as tk

class ConsoleText(tk.Text):

def __init__(self, master=None, **kw):
tk.Text.__init__(self, master, **kw)
self.insert('1.0', '>>> ') # first prompt
# create input mark
self.mark_set('input', 'insert')
self.mark_gravity('input', 'left')
# create proxy
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
# binding to Enter key
self.bind("<Return>", self.enter)

def _proxy(self, *args):
largs = list(args)

if args[0] == 'insert':
if self.compare('insert', '<', 'input'):
# move insertion cursor to the editable part
self.mark_set('insert', 'end') # you can change 'end' with 'input'
elif args[0] == "delete":
if self.compare(largs[1], '<', 'input'):
if len(largs) == 2:
return # don't delete anything
largs[1] = 'input' # move deletion start at 'input'
result = self.tk.call((self._orig,) + tuple(largs))
return result

def enter(self, event):
command = self.get('input', 'end')
# execute code
print(command)
# display result and next promp
self.insert('end', '\nCommand result\n\n>>> ')
# move input mark
self.mark_set('input', 'insert')
return "break" # don't execute class method that inserts a newline

root = tk.Tk()
tfield = ConsoleText(root, bg='black', fg='white', insertbackground='white')
tfield.pack()
root.mainloop()

Is there a way to make the Tkinter text widget read only?

You have to change the state of the Text widget from NORMAL to DISABLED after entering text.insert() or text.bind() :

text.config(state=DISABLED)

How do I make a Text widget readonly

There's no such a possible value "readonly" for the state of a Text widget. You can disable it, setting the state to "disabled" (and you can do it directly in the constructor):

e = Text(root, height=10, width=50, state='disabled') # no need to call config

From the Tk documentation:

If the text is disabled then characters may not be inserted or deleted
and no insertion cursor will be displayed, even if the input focus is
in the widget.

I think you should use a Label, if you want just to show some text, that's why labels exist.

How can I insert a string in a Entry widget that is in the readonly state?

This seems to work for me:

import Tkinter as tk

r = tk.Tk()

e = tk.Entry(r,width=60)
e.insert(0,'...')
e.configure(state='readonly')
e.grid(row=0,column=0)

r.mainloop()

How to disable input to a Text widget but allow programmatic input?

Have you tried simply disabling the text widget?

text_widget.configure(state="disabled")

On some platforms, you also need to add a binding on <1> to give the focus to the widget, otherwise the highlighting for copy doesn't appear:

text_widget.bind("<1>", lambda event: text_widget.focus_set())

If you disable the widget, to insert programatically you simply need to

  1. Change the state of the widget to NORMAL
  2. Insert the text, and then
  3. Change the state back to DISABLED

As long as you don't call update in the middle of that then there's no way for the user to be able to enter anything interactively.

How to make specific text non-removable in tkinter?

Another method would be to use validate.

def do_validate( text ):
# if this returns False the entered key(s) aren't accepted.
return text.startswith( "Enter message: " )

import tkinter as tk

root = tk.Tk()
root.geometry("500x500")
entry = tk.Entry(root, bg="black", fg="white")
entry.pack(side="top", fill="x")
entry.insert( tk.END, "Enter message: ")

validate_cmd = ( root.register( do_validate ), '%P' )
entry.config( validate = 'key', validatecommand = validate_cmd )

root.mainloop()

If the string created by the keystrokes doesn't start with "Enter Message: " the keystrokes are rejected and the Entry text is unchanged.



Related Topics



Leave a reply



Submit