Self Dot
Self Dot

Reputation: 362

Using Tkinter Text indexing expressions to delete last line of text

I have a text widget which displays information as my program runs. I want to add functionality which will allow me to overwrite the last line of the text widget with a new line. My code looks like this:

class TextRedirector(object):
   def __init__(self, text_widget):
      self.text_widget = text_widget
   def write(self, the_string):
      self.text_widget.configure(state="normal")
      self.text_widget.insert("end", the_string)
      self.text_widget.see(END)
      self.text_widget.configure(state="disabled")
   def overwrite(self, the_string):
      self.text_widget.configure(state="normal")
      self.text_widget.delete("end-1l linestart+1c", "end")
      self.text_widget.insert("end", the_string)
      self.text_widget.see(END)
      self.text_widget.configure(state="disabled")

How do I get the line and column of the end position of text in Tkinter? -- I have seen this post, where Bryan Oakley appears to answer my question with textwidget.delete("end-1c linestart", "end"), but when I use this, the text alternates between being placed at the end of the last line, and actually overwriting the last line. That is to say, it works half of the time, and half of the time the new text is slapped on the end of the old text. My understanding of the index expressions for the Text widget (covered tersely at http://effbot.org/tkinterbook/text.htm) is that "end-1c linestart" means something like "end, back one character, beginning of line" so I don't understand why it would go to the end of a line of text, and only every other time. The result looks something like this at each step (each part is an updated version of the whole text widget, only the last lines are being modified):

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42iteration: 2 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.
iteration: 3 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.iteration: 4 / 42
-----

I tried using self.text_widget.delete("end-1l linestart+1c", "end"). This almost works, but I end up with

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42
iteration: 2 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42
iiteration: 3 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42
iiteration: 4 / 42

I have tried a few other things, using these indexing expressions to the best of my understanding, but I haven't solved the problem yet. I tried adding if statements in the overwrite function to handle different scenarios for text that might be at the end of the widget, for example if it ends in an empty line, or two empty lines, etc. I did not succeed with that either.

For full disclosure, I might mention I am using this Text widget as a sort of substitute for the printouts to the command line. Hence the name of the class, TextRedirector. I don't think this makes any difference to the issue at hand, but faulty assumptions are probably what got me here in the first place... The line after the class is this:

sys.stdout = TextRedirector(self.textbox)

And self.textbox is a Text widget created before the class is defined.

UPDATE: I tried saving the index from before the last insertion of text, and building a string expression based on that to delete the last line. The result still wasn't perfect.

class TextRedirector(object):
   def __init__(self, text_widget):
      self.text_widget = text_widget
      self.index_before_last_print = ""
   def write(self, the_string):
      self.index_before_last_print = self.text_widget.index("end")
      self.text_widget.configure(state="normal")
      self.text_widget.insert("end", the_string)
      self.text_widget.see(END)
      self.text_widget.configure(state="disabled")
   def overwrite(self, the_string):
      self.text_widget.configure(state="normal")
      self.text_widget.delete(self.index_before_last_print + "-1c linestart+1c", "end")
      self.index_before_last_print = self.text_widget.index("end")
      self.text_widget.insert("end", the_string)
      self.text_widget.see(END)
      self.text_widget.configure(state="disabled")

Here was the result

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iteration: 1 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iiteration: 2 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iiteration: 3 / 42
-----

!!!!! Settings Have Been Backed Up !!!!!

Computing coordinates...

Coordinates have been computed.

iiteration: 4 / 42

Upvotes: 2

Views: 5546

Answers (3)

user_Kito
user_Kito

Reputation: 31

You just use text.delete("end-2l","end-1l") to remove the last line written in the text box and then text.insert(INSERT,"your text here") to rewrite it.

Upvotes: 3

JKna
JKna

Reputation: 141

UPDATE: the behavior described below is documented in the Tcl/Tk documentation, here: https://www.tcl.tk/man/tcl8.6/TkCmd/text.htm. Specifically, the fact that insertions that refer to the position after the final newline in the widget actually occur before that newline is stated in the pathName insert documentation; and the pathName delete documentation says that deletions that do not leave the widget with a final newline will be silently revised to preserve the final newline.

Original answer:

I had the same problem and I've found a solution that works for me. Tl;dr: the Tk text widget has some puzzling (to me) irregularities when inserting and deleting at the end position, but if you always append new content with leading (rather than trailing) newlines, it should behave as you expect. Specifically, text.delete("end-1l","end") followed by text.insert("end",u"\nContent") will replace the last line.

By experimenting with the text widget in wish (the Tk shell) and Python 2.7, I discovered that:

1) A newly-created text widget contains a newline. You cannot easily delete it, and if you somehow manage to do so, the widget will behave very strangely thereafter.

>>> import Tkinter as tk
>>> root = tk.Frame()
>>> t = tk.Text(root)
>>> t.grid()
>>> root.grid()
>>> t.get('1.0','end')
u'\n'
>>> t.delete('1.0','end')
>>> t.get('1.0','end')
u'\n'

Well that is a surprise! What part of "delete everything from the start to the end" did you not understand, Tk?

2) Insertions at the "end" actually insert before the final newline. This appears to be in conflict with the Tk documentation, which indicates that the "end" position refers to the character position immediately after the final newline in the widget.

>>> t.insert('end',u"\nA line")
>>> t.get('1.0',"end')
u'\nA line\n'

Although we wanted to insert at the end, the insertion actually took place before the final newline.

>>> t.insert('end',u"\nLine 2") 
>>> t.get('1.0','end')
u'\nA line\nLine 2\n'

And this behavior seems consistent. What if we try to delete a line? We'll do that in the "intuitive" way: back up one line from "end" and delete from there to "end":

>>> t.delete('end-1l','end')
>>> t.get('1.0','end')
u'\nA line\n'

We are back to the previous state, which is good news! Another insertion puts the inserted line at the expected place:

>>> t.insert('end',u"\nA new line")
>>> t.get('1.0','end')
u'\nA line\nA new line\n'

But this only works as expected when we add lines using leading newlines. If we add them using trailing newlines, the text gets appended to the previous line, and an additional trailing newline gets added to the widget:

>>> t.insert('end',u"Trailing newline\n")
>>> t.get('1.0','end')
u'\nA line\nA new lineTrailing newline\n\n'

This is not what you would expect if you believe the Tk documentation - you would expect that inserting at the "end" would insert your test after the final newline. But alas, you would be wrong.

The following complete test program displays a text widget, along with an entry field and two buttons, one that adds a line from the entry field to the widget, and another that overwrites the final line of the widget from the entry field text. The addLine() and replaceLastLine() functions implement those behaviors in a straightforward way. The blank line at the beginning of the widget is a minor annoyance, which you could remedy by doing t.delete("1.0","1.0+1c"), but only after inserting some text into the widget.

import Tkinter as tk
root = tk.Frame()
t = tk.Text(root)
t.grid(row=0,column=0,columnspan=3)
root.grid()

def addLine():
  msg = lineField.get()
  t.insert("end",u"\n{}".format(msg))

def replaceLastLine():
  msg = lineField.get()
  t.delete("end-1l","end")
  t.insert("end",u"\n{}".format(msg))

lineField = tk.Entry(root)
lineField.grid(row=1,column=0)

addBtn = tk.Button(root,text="Add line",command=addLine)
addBtn.grid(row=1,column=1)

replBtn = tk.Button(root,text="Replace line",command=replaceLastLine)
replBtn.grid(row=1,column=2)

tk.mainloop()

Upvotes: 3

Bryan Oakley
Bryan Oakley

Reputation: 385830

I have seen this post [which] appears to answer my question with textwidget.delete("end-1c linestart", "end"), but when I use this, the text alternates between being placed at the end of the last line, and actually overwriting the last line.

The reason text gets appended is fairly simple to explain. If you insert a line that ends with a newline (eg: "foo\n") , "end-1c linestart" refers to the position immediately before that last newline. Thus, your code is overwriting just the newline which causes the new text to be appended to the old text.

If your code is inconsistent in that sometimes you insert a line of text with a newline and sometimes without -- or sometimes with more than one newline -- that would explain why the code seems to behave differently at different times.

If all writing to the text widget goes through your redirector class, one really simple solution is to add a tag to the last data added. Your overwrite method can then simply delete all text with this tag.

Here's a working example that provides two buttons, one to append text and one to overwrite text:

import tkinter as tk
import datetime

class TextRedirector(object):
    def __init__(self, text_widget):
        self.text_widget = text_widget
        self.text_widget.tag_configure("last_insert", background="bisque")

    def write(self, the_string):
        self.text_widget.configure(state="normal")
        self.text_widget.tag_remove("last_insert", "1.0", "end")
        self.text_widget.insert("end", the_string, "last_insert")
        self.text_widget.see("end")
        self.text_widget.configure(state="disabled")

    def overwrite(self, the_string):
        self.text_widget.configure(state="normal")
        last_insert = self.text_widget.tag_ranges("last_insert")
        self.text_widget.delete(last_insert[0], last_insert[1])
        self.write(the_string)

def overwrite():
    stdout.overwrite(str(datetime.datetime.now()) + "\n")

def append():
    stdout.write(str(datetime.datetime.now()) + "\n")

root = tk.Tk()
text = tk.Text(root)
stdout = TextRedirector(text)

append = tk.Button(root, text="append", command=append)
overwrite = tk.Button(root, text="overwrite", command=overwrite)

append.pack()
overwrite.pack()
text.pack()

root.mainloop()

Upvotes: 3

Related Questions