Tkinter adding line number to text widget
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?
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.
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
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 linenum = str(i).split(".") 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 "binding to cursor movement doesnt change INSERT mark" 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 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(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()
How can I add line numbers to a text widget? [Tkinter] : Python, Even if I insert a whole passage, there will only be a single line number. This is the only method that I use to modify the text widget. def setText(self, text): self. To Run These Script Codes alone, Just Change line 24 from " from Graphics import Tkinter " To "import Tkinter " Or To Run This Codes as MagicStick Script. Don't Do Any Changes in this codes. Now, let's join this script in MagicStick Text Editor Project. To Connect This Script With MagicStick project, we need to do only two steps.
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
# assume each line is at least 6 pixels high step = 6
step - how often (in pixels) program check text widget for new lines. If height of line in text widget is 30 pixels, this program performs 5 checks and draw only one number.
You can set it to value that <6 if font is very small.
There is one condition: all symbols in
text widget must use one font, and widget that draw numbers must use the same font.
# http://tkinter.unpythonic.net/wiki/A_Text_Widget_with_Line_Numbers class EditorClass(object): ... self.lnText = Text(self.frame, ... state='disabled', font=('times',12)) self.lnText.pack(side=LEFT, fill='y') # The Main Text Widget self.text = Text(self.frame, bd=0, padx = 4, font=('times',12)) ...
How To display line numbers in tkinter text widget, This Is our Fourth part of Python MagicStick Text Editor Project and In this post, i will show you how to add linenumber feature easily in MagicStick The root cause of the problem is that the text hasn't been drawn on the screen yet, so the call to dlineinfo will not return anything useful.. If you add a call to self.update() before drawing the line numbers, your code will work a little better.
Here's my attempt at doing the same thing. I tried Bryan Oakley's answer above, it looks and works great, but it comes at a price with performance. Everytime I'm loading lots of lines into the widget, it takes a long time to do that. In order to work around this, I used a normal
Text widget to draw the line numbers, here's how I did it:
Create the Text widget and grid it to the left of the main text widget that you're adding the lines for, let's call it
textarea. Make sure you also use the same font you use for
self.linenumbers = Text(self, width=3) self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS) self.linenumbers.config(font=self.__myfont)
Add a tag to right-justify all lines added to the line numbers widget, let's call it
Disable the widget so that it cannot be edited by the user
Now the tricky part is adding one scrollbar, let's call it
uniscrollbar to control both the main text widget as well as the line numbers text widget. In order to do that, we first need two methods, one to be called by the scrollbar, which can then update the two text widgets to reflect the new position, and the other to be called whenever a text area is scrolled, which will update the scrollbar:
def __scrollBoth(self, action, position, type=None): self.textarea.yview_moveto(position) self.linenumbers.yview_moveto(position) def __updateScroll(self, first, last, type=None): self.textarea.yview_moveto(first) self.linenumbers.yview_moveto(first) self.uniscrollbar.set(first, last)
Now we're ready to create the
self.uniscrollbar= Scrollbar(self) self.uniscrollbar.grid(row=self.__uniscrollbarRow, column=self.__uniscrollbarCol, sticky=NS) self.uniscrollbar.config(command=self.__scrollBoth) self.textarea.config(yscrollcommand=self.__updateScroll) self.linenumbers.config(yscrollcommand=self.__updateScroll)
Voila! You now have a very lightweight text widget with line numbers:
Is it possible to create a "display line numbers" feature in a tkinter text?, """A wrapper to show line numbers for Tkinter Text widgets. for e in range(noOfEditors): ed = EditorClass(root) pane.add(ed.frame) s = 'line . Instead of specifying a line and character number, you can just use the constant END: textArea.insert(tk.END, 'Some default text here') This will add your text from that start character right to the end character.
There is a very simple method that I've used based on Bryan Oakley's answer above. Instead of listening for any changes made, simply "refresh" the widget using the
self.after() method, which schedules a call after a number of milliseconds. Very simple way of doing it. In this instance I attach the text widget at instantation but you could do this later if you want.
class TextLineNumbers(tk.Canvas): def __init__(self, textwidget, *args, **kwargs): tk.Canvas.__init__(self, *args, **kwargs) self.textwidget = textwidget self.redraw() 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 linenum = str(i).split(".") self.create_text(2,y,anchor="nw", text=linenum) i = self.textwidget.index("%s+1line" % i) # Refreshes the canvas widget 30fps self.after(30, self.redraw)
line numbers in text widget, The following codelet allows to display line numbers in a text widget. $f] } pack [text .t] .t tag configure linenum -background yellow .t insert Indexes are used to point to positions within the text handled by the text widget. Like Python sequence indexes, text widget indexes correspond to positions between the actual characters. Tkinter provides a number of different index types: line/column (“line.column”) line end (“line.end”) INSERT. CURRENT. END. user-defined marks
Displaying the line number, Let's work towards showing the line numbers to the left of the Text widget. This will require us to tweak the code at various places. How to get Tkinter input from the Text widget?. EDIT. I asked this question to help others with the same problem - that is the reason why there is no example code. This issue had been troubling me for hours and I used this question to teach others.
Line Number Tkinter Text Are, my current solution is i am inserting line number at the beginning of the for each column (sorry don't know anything about the Text widget). We create a text widget by using the Text() method. We set the height to 2, i.e. two lines and the width to 30, i.e. 30 characters. We can apply the method insert() on the object T, which the Text() method had returned, to include text. We add two lines of text.
tk_lineno/main.py at master · bekar/tk_lineno · GitHub, #!/usr/bin/env python. ''' Line Number Text with font resize binding '''. # A wrapper to show line numbers for Tkinter Text widgets. This option specifies how much extra vertical space to add between displayed lines of text when a logical line wraps. Default is 0. 23: spacing3. This option specifies how much extra vertical space is added below each line of text. If a line wraps, this space is added only after the last line it occupies on the display. Default is 0. 24: state. Normally, text widgets respond to keyboard and mouse events; set state=NORMAL to get this behavior.