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.
Automatically updating the line numbers
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 notifying the program of changes. To get around that, we can use a proxy to intercept changes to the text widget and generate an event for us.
In an answer to the question "https://mcmap.net/q/503202/-binding-to-cursor-movement-doesnt-change-insert-mark/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)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, *args):
# let the actual widget perform the requested action
cmd = (self._orig,) + args
result = self.tk.call(cmd)
# generate an event if something was added or deleted,
# or the cursor position changed
if (args[0] in ("insert", "replace", "delete") or
args[0:3] == ("mark", "set", "insert") or
args[0:2] == ("xview", "moveto") or
args[0:2] == ("xview", "scroll") or
args[0:2] == ("yview", "moveto") or
args[0:2] == ("yview", "scroll")
):
self.event_generate("<<Change>>", when="tail")
# return what the actual widget returned
return result
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(self, 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()
myTextWidget.index(INSERT).split(".")[0]
and then you have the number to put wherever you want. If you want line numbers (rather than one number), you could always line up a parallel label without a border, print out every line on it, and have it move in sync with your main text widget, but I haven't tested that to see how well it works. – Dumpish