Read-only text widget

The Tk text widget is commonly used to display read-only data, often formatted as rich text. The user must not be able to edit the data, but should be able to scroll, select and copy, and possibly search as well. It takes a little work to do this properly, as the text widget does not support this pattern directly. There are three approaches:

  • Disabling the text widget.
  • Replacing the Text bindings.
  • Wrapping the text widget.
  • New: Wrapping the text widget (another way).

Of these, the third is usually the most fruitful. Ideally, tklib would provide a "rotext" widget as an ideal implementation of this third appoach, but at present it does not.

Disabling the Text Widget

The simplest approach is to disable the text widget on creation, and re-enable it when modifying its contents:

# On creation:
.text configure -state disabled

# On update
.text configure -state normal
.text delete 1.0 end
.text insert end $mytext
.text configure -state disabled

This is simple, and reasonably bulletproof, but it has disadvantages.

  • The widget will not take focus.
  • Setting the state to -disable disables not only inserts and deletes, but also other bindings as well. For example, the keystrokes that would ordinarily scroll the contents, or copy selected text, no longer work.
  • It's easy to forget to set the -state on update.
  • TBD: Others?

Thus, this approach is suitable for quick-and-dirty scripts but not for software aimed at end-users.

Replacing the Text Bindings

The second approach is to change the event bindings on the particular text widget so as to eliminate the bindings that modify the content. This approach has the advantage that you can customize the behavior exactly as you want it, but it requires a deep understanding of the Text bindings to get it right.

There are two general schemes. First, you can add bindings to the specific widget that block the default Text bindings:

bind .text <KeyPress> break
...

Alternatively, you can define a new bindtag, Rotext, say, that includes only the specific Text event bindings you want, and use bindtags to install it in place of Text.

In addition, there are usually some ancillary changes to be made beyond simply changing the bindings. For example, one usually wants to make the insertion cursor go away by setting -insertwidth to 0.

Wrapping the Text Widget

Replacing the text widget's bindings yields good results, but it's tricky; and consequently you usually want to do it once and package it up in a nice bit of code so that you don't need to do it ever again. The easiest way to package up a set of widget modifications is as some kind of megawidget; and so long as you're doing that, there's a simpler approach than editing all of the bindings.

Whenever a Text event binding wants to insert or delete text from the widget, it uses the widget's insert, delete or replace subcommand. The trick, then, is to replace insert and delete with no-ops. The event bindings need not be deleted, because they will be ignored. The basic method of doing this is as follows:

rename .text .text.internal

proc .text {args} {
    switch -exact -- [lindex $args 0] {
        insert {}
        delete {}
        replace {}
        default { 
            return [eval .text.internal $args] 
        }
    }
}

(This is, to a first approximation, the way Snit's Not Incr Tcl builds megawidgets.) The basic solution can be polished considerably:

  • The widget should probably provide alternate subcommands for doing insertions and deletions, e.g., "ins" and "del".
  • The -insertwidth should be set to 0; and then the bindings for scrolling the widget should probably be changed (scrolling the widget by moving an invisible insertion point leads to frustration).

The body of this page describes many variants of this basic approach .. most of historical interest at best. More useful are the reasonably current examples:

New: Wrapping the text widget (another way)

Tested code (8.6) by emiliano and ccbbaa:

# read only text widget, idea and code by emiliano and ccbbaa, tested: tcl/tk8.6, linux - version 20191217-0
namespace eval ::roText {
  proc roText {w args} {
    if {[info exists ::roText::$w]} {
      puts stderr "::roText::$w and possibly $w already exist"
      return ;# discuss: better way to flag error to caller, return "" for now
    }
    text $w {*}$args
    bind $w <Control-KeyPress-z> break ;# delete all
    bind $w <Control-KeyPress-k> break ;# kill line
    bind $w <Control-KeyPress-h> break ;# Backspace alternate
    bind $w <Control-KeyPress-d> break ;# Del alternate
    bind $w <Control-KeyPress-o> break ;# Ins newline
    bind $w <Key-Delete> break
    bind $w <Key-BackSpace> break
    rename $w ::roText::$w
    proc ::$w {cmd args} [format {
      set w %s
      set inf [lindex [info level 1] 0] ;# caller proc name; find tk vs scr.
      #puts "* $cmd $args ([info level]) '$inf'" ;# debug
      if {($cmd ni "insert delete") || ( ($cmd in "insert delete") \
        && ([string range $inf 0 3] != "tk::") \
        && ($inf != "tk_textCut") && ($inf != "tk_textPaste") ) \
      } {
        ::roText::$w $cmd {*}$args
      } 
    } $w $w]
    bind ::$w <Destroy> [list rename $w {}]
    return $w ;# created
  }
}

# usage test
roText::roText .txt ;# create
pack .txt
.txt insert end {Test
test
test}

Discussion

This Page Needs Divine Help: Won't you think of the Newbies? Why so much bla bla bla for such a simple request? This page is the Anthem of What's Wrong With The Tclers Wiki. God Save The Queen

In response to the above, the answer is ".text configure -state disabled". There, done. It's read only and bullet proof.


The above suggestion to use "state disabled" is no good. What people want is a "No edit" text widged. ie. the text widged which does not allow the end user to edit the content, but allows to copy from it. And allows user to use page up/down keys, arrow keys, keeps the cursor visible, highlights selections, etc.. Again "No edit" but everything else!

Further more the "state disabled" works inconsistently in Windows and on MacIntosh (at least when I use it in Python/tkinter) In Windows it is allmost "no edit", allows copying text, highlights, scrolls with keys. Just the cursor is "broken" ie invisible. In MacIntosh, unfortunately "state disabled" disables so much as to make it an unusable garbage.

The suggestions below how to screw around the internals of TCL are not much of a help Esp no help for Python/Tkinter users. But even for TCL users it suggests modifying internal implementation, and so it can work today and get broken by a subsequent TCL release. It is a wrong, amateurish way to code.

DKF: It's more like subclassing (excepting that Tk itself doesn't really have a full OO system like that). All the text widget's insertions and deletions go through the insert and delete methods; Tk doesn't ever go by the back-door. The “tricks” below will continue to work.

But the real problem is elsewhere, in the code that decides whether to include a widget in the focus ring. This code (tk::FocusOK in the focus.tcl file in Tk's own library scripts) always skips disabled widgets, even where that's not a good idea (the text widget is a particular issue). This is arguably simply a bug of long-standing that's been worked around for ages which only wasn't obvious in the beginning because the old-style X11 cut-n-paste was done with a mouse. (That really changed in the mid-'90s, but it looks like this is a design decision from before then.)


Quick answer (synthesizes this page):

Make text widget $tx read-only:

foreach event {<KeyPress> <<PasteSelection>>} {
    bind $tx $event break
}
bind $tx <Control-c> {event generate %W <<Copy>>}

Back to normal:

bind $tx <KeyPress> {}

Ro: To whomever edited the above code (adding <<PasteSelection>>) I understand your reasoning, re: middle click on Unix. But how to set it back to normal? Not sure if it makes it too complex now. I was aiming for an 80% solution with minimal code, people interested in bulletproof implementations can read further down the page for more complex and more complete solutions.

slebetman: Sorry.. I did it. But if you've read this page fully then you'd realise that the "Quick answer" is really:

rename .t .t.internal
proc .t {args} {
    switch [lindex $args 0] {
        "insert" {}
        "delete" {}
        "default" { return [eval .t.internal $args] }
    }
}

To make programmatic edits:

.t.internal insert end $sometext

Back to normal:

rename .t.internal .t

Mat: this will raise an "command already exists" error, if you don't delete .t (rename .t "") before

This is a 100% solution with minimal code. Quick answers shouldn't contain incomplete solutions which may fail in certain conditions when a simple complete solution exists. In fact it's easy enough to wrap this as a proc:

proc makeReadOnly {textwidget} {
    rename $textwidget $textwidget.internal
    proc $textwidget {args} [string map [list WIDGET $textwidget] {
        switch [lindex $args 0] {
            "insert" {}
            "delete" {}
            "default" { return [eval WIDGET.internal $args] }
        }
    }]
}

GJS: I modified the makeReadOnly proc to add "-state readonly"

proc makeReadOnly {textwidget} {
    global readOnlyText
    set readOnlyText($textwidget) readonly
    rename $textwidget $textwidget.internal
    bind $textwidget <Destroy> [list rename $textwidget {}]
    proc $textwidget {args} [string map [list WIDGET $textwidget] {
        global readOnlyText
        switch [lindex $args 0] {
            configure {
                set pass [list]
                for {set i 0} {$i < [llength $args]} {incr i} {
                    switch -- [lindex $args $i] {
                        -state {
                            set readOnlyText(WIDGET) [lindex $args [incr i]]
                            if {$readOnlyText(WIDGET) eq ""} {
                                lappend pass -state
                            } elseif {$readOnlyText(WIDGET) ne "readonly"} {
                                lappend pass -state $readOnlyText(WIDGET)
                            } else {
                                lappend pass -state normal
                            }
                        }
                        default {
                            lappend pass [lindex $args $i]
                        }
                    }
                }
                return [eval WIDGET.internal $pass]
            }
            insert - delete {
                if {$readOnlyText(WIDGET) ne "readonly"} {
                    return [eval WIDGET.internal $args]
                }
            }
            default {
                return [eval WIDGET.internal $args]
            }
        }
    }]
}

Usenet posters frequently ask for a read-only text widget. This perhaps means that it would be a good idea to build one in; no one has done this yet for Tk's core or Tkinter, to the best of my knowledge, though Perl/Tk has enjoyed the benefits of Tk::ROText since the late '90s.

Ro If you mean 'build one in' as into the core, I disagree. It's so easy to do in just a few lines of pure Tcl. See the next entry for how to do it with the Widget Callback Package (Wcb, a pure Tcl package).

LV IMO, Providing an easy way to simply get a read-only text widget would be a useful thing to have - whether it is in Tk or Tklib. In this way, the novice programmer doesn't have to struggle with the half dozen methods presented on this page, trying to figure out what to use and how to get it to work in their application.

Ro End of 2009, I agree now. I've suffered under these broken solutions for too long.

jnc Sep 15, 2010, Multi-Line Entry Widget in Snit provides a -readonly option


I believe easily the most comprehensive and recommendable solution is the Widget Callback Package (Wcb, by Csaba Nemethi [L1 ]) which lets you do lots of other things besides readonly. It's pure Tcl and uses the command renaming approach instead of mass re-binding. (Roy Terry 25Dec01)

Ro Here is how to do it - solution created by Csaba Nemethi.

Features:

  • The package is a general solution to a recurring problem.
  • The package is in pure Tcl/Tk code.
  • This example still allows the user to copy the contents of the read-only widget! This is very handy if you're building some kind of log window.

Code:

#
# The hardest thing about this is just setting 
# your ::auto_path properly so Tcl can find the wcb package ;)
#

#
# Load the required package.
#
package require wcb

#
# Create and pack the read-only text widget.
#
set tx .tx
text $tx -wrap word
pack $tx -fill both -expand 1

#
# Reject user modifications to the text widget.
#
proc rejectModification {w args} {
    wcb::cancel
    return
}

#
# Protect the text widget from insertions and deletions by the user.
#
# Note:  Wcb renames $tx to _$tx and creates a wrapper command.
#
wcb::callback $tx before insert rejectModification
wcb::callback $tx before delete rejectModification 

#
# Create and pack a button that programmatically inserts text into
# the read-only text widget.
#
# Note: The real text widget is "_$tx" not "$tx".  This is because
#       the Wcb package renames the text object in the goal of
#       intercepting command messages.
#
set b .b
button $b -text "Insert a Greeting" -command [list _$tx insert end "Howdy\n"]
pack $b -fill x

Ro If you're making a read-only text widget, chances are that you want to enable keyboard focus switching when the tab key and the shift-tab key-combo is pressed. Check out text for how to do this with 6 lines of code.


There's an abundance of pure-Tcl procedural solutions. My favorite is to set the state of the text widget to disabled. Updates can still be effected programmatically either through use of a textvariable, or by toggling the state, updating the text, and then toggling the state again.

I believe the only disadvantage of this approach is that disabling the widget disables its ability to serve as a Copy (as in Copy-Paste) source. (This is not true! You can still copy from the text - Bruce)

(How? - I can't get it to happen - impossible to select! - EMJ)

(I think it's a bug in the Windows version; could it be that Bruce runs unix and EMJ uses Windows? -- Bryan Oakley ; true - EMJ)

I can do it windows too, the trick is to get focus to the window, the default bindings for the text widget don't assign focus on a button1 click unless the state is normal, so either explicitly call "focus .t" or else add a binding to do it.

bind .t <1> {focus %W}

- Bruce

LV Would it be worthwhile to report this different and request the binding in question be added for Windows?


Many solutions tinker with bindings. Eventually I expect we'll summarize in this space the information found in http://groups.google.com/groups?th=1c8bca5c914f27ac , http://groups.google.com/groups?th=b3b2de32ea3ba00c , and so on.

Aaaargh - Deja references are now useless! (Not really...)


(Kevin Kenny - 6 August 2001) Doing read-only text widgets with bindings is really doing it the hard way. The easy way is to [rename] the widget command. I don't have a full worked example of this for a read-only text widget, but I have one for a text entry that supports a -textvariable option: Text variable for text widget. The ideas there should be clear enough for someone to put together a read-only widget.

(bbh - 10 Jan 2002) The only problem with using rename on the widget command is that affects both user edits AND program edits. May times I have a read only text I want to be able to modify the text from my script, but i don't want the user to able to muck with it. That's why I like the bindings solution - it save me from wrapping all my text updates inside config calls to update the state of the widget back and forth.

KBK 8 April 2002 - I missed this message for a while. But what's the problem? If you rename .my.text.widget to internal.my.text.widget so as to intercept the commands that change the text, you can always call the renamed widget from your script when you want the text to change.

(bbh - 10 Nov 2002) Been a while since I've been to this page, all I can say is "DOH!" You are completely right on this one.

slebetman 21 December 2004 - a really short example of this:

pack [text .t] -fill both -expand 1
# make text read-only:
rename .t .t.internal
proc .t {args} {
    if {[lindex $args 0] != "insert"} {
        return [eval .t.internal $args]
    }
}

All standard text behaviors work including Control-c, arrow keys and mouse scrollwheel. To insert text simply do:

.t.internal insert end "new test"

jhh 22 November 2005 - I like this solution, but it still allows deleting text from the widget. I would replace the proc in the example above by:

proc .t {args} {
    switch [lindex $args 0] {
        "insert" {}
        "delete" {}
        "default" { return [eval .internal.t $args] }
    }
}

Here is a solution using bindings:

  • go to the tk lib directory
  • copy the text.tcl to ROtext.tcl
  • edit the ROtext.tcl file changing all "bind Text" refs to "bind ROText"
  • delete all procedures defined
  • delete all bindings that modify the text
  • either source this file in your code or edit text.tcl and add the following source [file join [file dirname [info script]]] ROtext.tcl]
  • Now in you code after createing a text widget just call bindtags $t [list $t ROText . all]

The user can then still interact with the widget, keyboard traversal, slections, etc. but no changing is allowed by user. This way the prgram can still add/delete text without having to wrap it with config -state calls - Bruce


BBH even better - use the methods at Inheriting Widget Binding Classes as it will do it on the fly with no need for extra files and such


MGH 13 September 2010 - Thanks, Bruce. This really saved my bacon on our platform, stuck using a pretty customized 8.4.5.1, where the rename method did not work because of things like "$w compare anchor > 0.0" trying to get passed to the internal widget, and our smart interpreter picks off the "> 0.0" as "redirect output to file 0.0". Nice in general. Bad in this case. Mattias.


ulis: the easiest way I found to make a text widget read-only:

text .a
.a insert 1.0 "some text"
bind .a <KeyPress> { break }

GPS: The problem with that solution as I see it is that the user might paste text via the mouse. In my read-only solution I allow the user to select text, but not remove it.

ulis: How to paste text via the mouse?

GPS: In Unix/X you press the middle mouse button after selecting text.

ulis: Okay, I found it. So, why not:

text .a
.a insert 1.0 "some text"
bind .a <KeyPress> { break }
bind .a <<PasteSelection>> { break }

GPS: That looks like a concise way of doing it. I can't think of any problems with that approach, unless the bindtags order somehow got out of whack (highly unlikely). I however modify a copy of the text widget bindings for my project, so it's easier for me to use the bind:* commands I wrote.

RS: I like ulis's version, which one could simplify to

foreach event {<KeyPress> <<PasteSelection>>} {bind .a $event break}

ulis, 2002-07-06: Idea came from 'rand mair fheal' ([email protected]) in a c.l.t thread: http://groups.google.com/groups?threadm=028ca7a9807678409e2a96daabcdf3a0.25900%40mygate.mailgate.org


ulis, 2002-07-27

The previous binding solution does not permit to copy selection with Control-c. This can be easily corrected by:

bind .a <Control-c> { event generate .a <<Copy>> }

But even that does not permit key navigation inside the text widget.

Here is a proc that set a text widget read-only by forbidding the insert and delete options usage.

Inserting and deleting chars are only forbidden during key press.

# ==========================
# set a text widget to a read-only or normal 'state'
# (the -state option is not modified)
# w is the path of the text widget
# if flag == 1, set $w to read-only
# if flag == 0, set $w to normal
# ==========================
proc ROText {w {flag 1}} {
    if {$flag} {
        # check if already donne
        if {[info procs ROTextKey$w] != ""} { return $w }
        # ------------------
        # set $w a read-only text widget
        # ------------------
        # key press global flag proc
        proc ROTextKey$w {} { return 0 }
        bind $w <KeyPress> [list proc ROTextKey$w {} { return 1 } ]
        bind $w <KeyRelease> [list proc ROTextKey$w {} { return 0 } ]
        # read-only $w proc
        rename $w ROText$w
        proc $w {cmd args} {
            # get widget path
            set w [lindex [info level 0] 0]
            if {[ROTextKey$w]} {
                # a key was pressed and not released:
                # forbid inserting & deleting chars
                switch -glob -- $cmd {
                    del*    { # forbidden }
                    ins*    { # forbidden }
                    default { uplevel [linsert $args 0 ROText$w $cmd] }
                }
            } else {
                # a key was not pressed
                # permit all
                uplevel [linsert $args 0 ROText$w $cmd] 
            }
        }
    } else {
        # ------------------
        # set $w a normal text widget
        # ------------------
        rename $w ""
        rename ROText$w $w
        rename ROTextKey$w ""
    }
    # return path
    return $w
}
# ==========================

Here is a little demo:

pack [ROText [text .t]]
.t insert end "this is a little demo\n"

The Snit's Not Incr Tcl page has a similar solution to the above; but it's shorter.


FW: The text widget's -disabled option was created for this very purpose, wasn't it? Other than the flashing cursor that's missing in a disabled text widget, there's no difference.

On some Windows Tcl versions you need to bind the widget to gain focus correctly so you can highlight text, etc., are there any other disadvantages to this approach?

text .t -state disabled
bind .t <1> {focus %W} ;# Not even needed in most cases

The only behavioral difference here is that appending text to the end of the widget means you have to set the state normal, add it, and set it disabled again.


LES on May 08, 2004: What I often wish for is a read-only tag. I mean, assigning to specific text segments the ability not to be changed or deleted although everything else around it can be edited normally. FW: For a hack, I might suggest an embedded entry. LES: I beg your pardon? FW: Embed an entry widget (without a border, and with the same background color) into the text widget. It's not so great, but it more or less accomplishes the task.

For one solution to having read-only text tags see Read-Only Text Megawidget with TclOO