Tkinter adding line number to text widget
Question: Trying to learn tkinter and python. I want to display line number for the Text
widget in an adjacent frame

    
    
    from Tkinter import *
    root = Tk()
    txt = Text(root)
    txt.pack(expand=YES, fill=BOTH)
    frame= Frame(root, width=25)
    #
    
    frame.pack(expand=NO, fill=Y, side=LEFT)
    root.mainloop()
    

I have seen an example on a site called unpythonic but its assumes that line
height of txt is 6 pixels.

I am trying something like this:

1) Binding Any-KeyPress event to a function that returns the line on which the
keypress occurs:

    
    
    textPad.bind("<Any-KeyPress>", linenumber)
    
    
    def linenumber(event=None):
        line, column = textPad.index('end').split('.')
        #creating line number toolbar
        try:
           linelabel.pack_forget()
           linelabel.destroy()
           lnbar.pack_forget()
           lnbar.destroy()
        except:
          pass
       lnbar = Frame(root,  width=25)
       for i in range(0, len(line)):
          linelabel= Label(lnbar, text=i)
          linelabel.pack(side=LEFT)
          lnbar.pack(expand=NO, fill=X, side=LEFT)
    

Unfortunately this is giving some weird numbers on the frame. Is there a
simpler solution ? How to approach this.

thnks


Answer: I have a relatively foolproof solution, but it's complex and will likely be
hard to understand because it requires some knowledge of how Tkinter and the
underlying tcl/tk text widget works. I'll present it here as a complete
solution that you can use as-is because I think it illustrates a unique
approach that works quite well.

Note that this solution works no matter what font you use, and whether or not
you use different fonts on different lines, have embedded widgets, and so on.

## Importing Tkinter

Before we get started, the following code assumes tkinter is imported like
this if you're using python 3.0 or greater:

    
    
    import tkinter as tk
    

... or this, for python 2.x:

    
    
    import Tkinter as tk
    

## The line number widget

Let's tackle the display of the line numbers. What we want to do is use a
canvas, so that we can position the numbers precisely. We'll create a custom
class, and give it a new method named `redraw` that will redraw the line
numbers for an associated text widget. We also give it a method `attach`, for
associating a text widget with this widget.

This method takes advantage of the fact that the text widget itself can tell
us exactly where a line of text starts and ends via the `dlineinfo` method.
This can tell us precisely where to draw the line numbers on our canvas. It
also takes advantage of the fact that `dlineinfo` returns `None` if a line is
not visible, which we can use to know when to stop displaying line numbers.

    
    
    class TextLineNumbers(tk.Canvas):
        def __init__(self, *args, **kwargs):
            tk.Canvas.__init__(self, *args, **kwargs)
            self.textwidget = None
    
        def attach(self, text_widget):
            self.textwidget = text_widget
    
        def redraw(self, *args):
            '''redraw line numbers'''
            self.delete("all")
    
            i = self.textwidget.index("@0,0")
            while True :
                dline= self.textwidget.dlineinfo(i)
                if dline is None: break
                y = dline[1]
                linenum = str(i).split(".")[0]
                self.create_text(2,y,anchor="nw", text=linenum)
                i = self.textwidget.index("%s+1line" % i)
    

If you associate this with a text widget and then call the `redraw` method, it
should display the line numbers just fine.

This works, but has a fatal flaw: you have to know when to call `redraw`. You
could create a binding that fires on every key press, but you also have to
fire on mouse buttons, and you have to handle the case where a user presses a
key and uses the auto-repeat function, etc. The line numbers also need to be
redrawn if the window is grown or shrunk or the user scrolls, so we fall into
a rabbit hole of trying to figure out every possible event that could cause
the numbers to change.

There is another solution, which is to have the text widget fire an event
whenever "something changes". Unfortunately, the text widget doesn't have
direct support for that. However, we can write a little Tcl code to intercept
changes to the text widget and generate an event for us. In an answer to the
question "[binding to cursor movement doesnt change INSERT
mark](http://stackoverflow.com/q/13835207/7432)" I offered a similar solution
that shows how to have a text widget call a callback whenever something
changes. This time, instead of a callback we'll generate an event since our
needs are a little different.

## A custom text class

Here is a class that creates a custom text widget that will generate a
`<<Change>>` event whenever text is inserted or deleted, or when the view is
scrolled.

    
    
    class CustomText(tk.Text):
        def __init__(self, *args, **kwargs):
            tk.Text.__init__(self, *args, **kwargs)
    
            self.tk.eval('''
                proc widget_proxy {widget widget_command args} {
    
                    # call the real tk widget command with the real args
                    set result [uplevel [linsert $args 0 $widget_command]]
    
                    # generate the event for certain types of commands
                    if {([lindex $args 0] in {insert replace delete}) ||
                        ([lrange $args 0 2] == {mark set insert}) || 
                        ([lrange $args 0 1] == {xview moveto}) ||
                        ([lrange $args 0 1] == {xview scroll}) ||
                        ([lrange $args 0 1] == {yview moveto}) ||
                        ([lrange $args 0 1] == {yview scroll})} {
    
                        event generate  $widget <<Change>> -when tail
                    }
    
                    # return the result from the real widget command
                    return $result
                }
                ''')
            self.tk.eval('''
                rename {widget} _{widget}
                interp alias {{}} ::{widget} {{}} widget_proxy {widget} _{widget}
            '''.format(widget=str(self)))
    

## Putting it all together

Finally, here is an example program which uses these two classes:

    
    
    class Example(tk.Frame):
        def __init__(self, *args, **kwargs):
            tk.Frame.__init__(self, *args, **kwargs)
            self.text = CustomText(self)
            self.vsb = tk.Scrollbar(orient="vertical", command=self.text.yview)
            self.text.configure(yscrollcommand=self.vsb.set)
            self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
            self.linenumbers = TextLineNumbers(self, width=30)
            self.linenumbers.attach(self.text)
    
            self.vsb.pack(side="right", fill="y")
            self.linenumbers.pack(side="left", fill="y")
            self.text.pack(side="right", fill="both", expand=True)
    
            self.text.bind("<<Change>>", self._on_change)
            self.text.bind("<Configure>", self._on_change)
    
            self.text.insert("end", "one\ntwo\nthree\n")
            self.text.insert("end", "four\n",("bigfont",))
            self.text.insert("end", "five\n")
    
        def _on_change(self, event):
            self.linenumbers.redraw()
    

... and, of course, add this at the end of the file to bootstrap it:

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

