91

It doesn't look like it has that attribute, but it'd be really useful to me.

rectangletangle
  • 50,393
  • 94
  • 205
  • 275
  • 14
    A Tkinter `Entry` widget allows `entry.config(state='readonly')`. Unfortunately this doesn't seem to work for the `Text` widget. – Craig McQueen May 16 '13 at 04:51

13 Answers13

114

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)
ars
  • 120,335
  • 23
  • 147
  • 134
  • 15
    Then you can't select text, and copy it. – Craig McQueen May 16 '13 at 04:40
  • 3
    Selecting and copying (through CTRL-C in Windows and automatically in Linux) seem to work just fine for me. – Dologan Mar 20 '14 at 14:22
  • 15
    @CraigMcQueen You can actually do it by binding the `<1>` with a function that sets the focus on the text widget: `text.bind("<1>", lambda event: text.focus_set())`. – nbro Sep 14 '15 at 11:33
46
text = Text(app, state='disabled', width=44, height=5)

Before and after inserting, change the state, otherwise it won't update

text.configure(state='normal')
text.insert('end', 'Some Text')
text.configure(state='disabled')
renzowesterbeek
  • 489
  • 4
  • 6
36

Very easy solution is just to bind any key press to a function that returns "break" like so:

import Tkinter

root = Tkinter.Tk() 

readonly = Tkinter.Text(root)
readonly.bind("<Key>", lambda e: "break")
FoxDot
  • 371
  • 3
  • 3
25

The tcl wiki describes this problem in detail, and lists three possible solutions:

  1. The Disable/Enable trick described in other answers
  2. Replace the bindings for the insert/delete events
  3. Same as (2), but wrap it up in a separate widget.

(2) or (3) would be preferable, however, the solution isn't obvious. However, a worked solution is available on the unpythonic wiki:

 from Tkinter import Text
 from idlelib.WidgetRedirector import WidgetRedirector

 class ReadOnlyText(Text):
     def __init__(self, *args, **kwargs):
         Text.__init__(self, *args, **kwargs)
         self.redirector = WidgetRedirector(self)
         self.insert = self.redirector.register("insert", lambda *args, **kw: "break")
         self.delete = self.redirector.register("delete", lambda *args, **kw: "break")
freakboy3742
  • 1,026
  • 10
  • 15
11

If your use case is really simple, nbro's text.bind('<1>', lambda event: text.focus_set()) code solves the interactivity problem that Craig McQueen sees on OS X but that others don't see on Windows and Linux.

On the other hand, if your readonly data has any contextual structure, at some point you'll probably end up using Tkinter.Text.insert(position, text, taglist) to add it to your readonly Text box window under a tag. You'll do this because you want parts of the data to stand out based on context. Text that's been marked up with tags can be emphasized by calling .Text.tag_config() to change the font or colors, etc. Similarly, text that's been marked up with tags can have interactive bindings attached using .Text.tag_bind(). There's a good example of using these functions here. If a mark_for_paste() function is nice, a mark_for_paste() function that understands the context of your data is probably nicer.

wovano
  • 4,543
  • 5
  • 22
  • 49
cshilton
  • 171
  • 2
  • 5
8

This is how I did it. Making the state disabled at the end disallows the user to edit the text box but making the state normal before the text box is edited is necessary for text to be inserted.

from tkinter import *
text=Text(root)
text.pack()
text.config(state="normal")
text.insert(END, "Text goes here")
text.config(state="disabled")
Xx Stef xX
  • 99
  • 1
  • 5
7
from Tkinter import *
root = Tk()
text = Text(root)
text.insert(END,"Some Text")
text.configure(state='disabled')
sarbjit
  • 3,786
  • 9
  • 38
  • 60
5

Use this code in windows if you want to disable user edit and allow Ctrl+C for copy on screen text:

def txtEvent(event):
    if(event.state==12 and event.keysym=='c' ):
        return
    else:
        return "break"

txt.bind("<Key>", lambda e: txtEvent(e))
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Pal
  • 51
  • 1
  • 1
4

If selecting text is not something you need disabling the state is the simplest way to go. In order to support copying you can use an external entity - a Button - to do the job. Whenever the user presses the button the contents of Text will be copied to clipboard. Tk has an in-build support of handling the clipboard (see here) so emulating the behaviour of Ctrl-C is an easy task. If you are building let's say a console where log messages are written you can go further and add an Entry where the user can specify the number of log messages he wants to copy.

rbaleksandar
  • 8,713
  • 7
  • 76
  • 161
3

Many mentioned you can't copy from the text widget when the state is disabled. For me on Ubuntu Python 3.8.5 the copying issue turned out to be caused by the widget not having focus on Ubuntu (works on Windows).

I have been using the solution with setting the state to disabled and then switching the state, when I need to edit it programmatically using 1) text.config(state=tkinter.NORMAL) 2) editing the text and 3) text.config(state=tkinter.DISABLED). On windows I was able to copy text from the widget normally, but on Ubuntu it would look like I had selected the text, but I wasn't able to copy it.

After some testing it turned out, that I could copy it as long as the text widget had focus. On Windows the text widget seems to get focus, when you click it regardless of the state, but on Ubuntu clicking the text widget doesn't focus it.

So I fixed this problem by binding the text.focus_set() to the mouse click event "<Button>":

import tkinter
root = tkinter.Tk()
text0 = tkinter.Text(root, state=tkinter.DISABLED)
text0.config(state=tkinter.NORMAL)
text0.insert(1.0, 'You can not copy or edit this text.')
text0.config(state=tkinter.DISABLED)
text0.pack()

text1 = tkinter.Text(root, state=tkinter.DISABLED)
text1.config(state=tkinter.NORMAL)
text1.insert(1.0, 'You can copy, but not edit this text.')
text1.config(state=tkinter.DISABLED)
text1.bind("<Button>", lambda event: text1.focus_set())
text1.pack()

For me at least, that turned out to be a simple but effective solution, hope someone else finds it useful.

3

Disabling the Text widget is not ideal, since you would then need to re-enable it in order to update it. An easier way is to catch the mouse button and any keystrokes. So:

    textWidget.bind("<Button-1>", lambda e: "break")
    textWidget.bind("<Key>", lambda e: "break")

seems to do the trick. This is how I disabled my "line numbers" Text widget in a text editor. The first line is the more powerful one. I'm not sure the second is needed, but it makes me feel better having it there. :)

GaryMBloom
  • 5,350
  • 1
  • 24
  • 32
  • As a side note, disabling the left mouse button precludes one from clicking on and selecting the Text widget, which does most of the job. But disabling keys helps, too, in case the Text widget can be tabbed into or is given keyboard focus. – GaryMBloom Dec 23 '20 at 15:25
1

This can also be done in Frames

from tkinter import *
root = Tk()
area = Frame(root)
T = (area, height=5, width=502)
T.pack()
T.insert(1.0, "lorem ipsum")
T.config(state=DISABLED)
area.pack()
root.mainloop()
Luke
  • 11
  • 1
0

You could use a Label instead. A Label can be edited programmatically and cannot be edited by the user.

Mikeologist
  • 438
  • 4
  • 11
  • 2
    You lose a *lot* of functionality when you do that. – Bryan Oakley Nov 20 '19 at 00:09
  • @BryanOakley What functionality would you still need if it's intended to be used as read-only? – Mikeologist Jul 01 '21 at 17:22
  • 1
    the ability to scroll and the ability to apply formatting to individual characters are the two biggest things you lose. Plus, you lose the ability to select text, and word wrapping in the text widget is much better than in a label. – Bryan Oakley Jul 02 '21 at 03:11