Tkinter adding line number to text widget
Asked Answered
D

7

28

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?

Deepfreeze answered 4/5, 2013 at 0:43 Comment(1)
Are you talking about having numbers embedded on the side by each line, then, all at once? Or do you just want to update the current line number in something like a statusbar? In the latter case, you can just do 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
P
61

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()
Perennate answered 4/5, 2013 at 14:14 Comment(28)
@ Bryan Oakley thanks a ton for detailed reply. Will try this now and report back to you. About your comment: ` to have the text widget fire an event whenever "something changes". Unfortunately, the text widget doesn't have direct support for that. ` I read that text method provides a modification option to check if a file has been modified using the "edit modified" method. see -link Thanks again - off to see if i can handle it :)Deepfreeze
@ Bryan Oakley Your first solution worked perfectly for my needs, so i did not see the second solution. As to the question of when to call redraw, i called it on any-key press, and on cut, copy, paste, undo and redo events and that takes care of most of things - i guess :)Deepfreeze
@QuakiGabbar: yes, Tk throws an event when the widget is modified, but you would have to reset the "modified" state after every change, and then you wouldn't be able to use it for its real intended purpose of knowing when the contents are different from when it was originally loaded.Perennate
@ Bryan Oakley thnks, i am witnessing a strange issue with the first linenumber code that you gave above. I displays perfectly well when the window size is small. But when i maximise the window to full screen , line number 1 starts displaying, from almost half screen down. I guess the problem is with lines: 'self.create_text(2,y,anchor="nw", text=linenum) i = self.textwidget.index("%s+1line" % i)'. but i am still to figure out. Any suggestions would be appreciated :)Deepfreeze
@ Bryan Oakley - sorry for bombarding questions :) I have heard that idle is written in tkinter. Where, if at all, can i find the source code for idle window ? i searched my python directory but could not locate it anywhereDeepfreeze
@QuakiGabbar: I can't reproduce the problem you say you have. Are you saying that the code I posted has a problem when run full screen, or your program which is adapted from this code has the problem?Perennate
@ Bryan Oakley Thanks. can you please explain these 2 lines of code height = dline[3] & y = dline[1], i dont see you using height anywhere in the code after that, and if i insert a print y in my code, i see it jumps by 16 on every consecutive call to the function.(something like 2 2 | 2 18 | 2 18 34 | 2 18 34 50 | 2 18 34 50 66).why is this jump of 16. incidentally on a fullscreen, the line numbers stop displaying after line 16.. any ideas ? i had to change your code to procedural style, but that should not cause any problem i guess.. thanks againDeepfreeze
@QuakiGabbar: you are right; the line computing the height is useless; I was using it for something else in a different project.Perennate
@QuakiGabbar: I'll ask again: is the problem with the line numbers stopping at half the screen a problem with the code in my post, or a problem in your code? I don't see that problem with my code.Perennate
@ Bryan Oakley:the problem should not have been in my code as i figured out a slightly different way of doing the line numbers using label rather than canvas. something like: mylabel.config(text='') ln, col = mytext.index('end').split('.') txt='' for i in range(1, int(ln)): txt += str(i)+'\n' mylabel.config(text=txt) works great..automatically takes care of line breaks with \n. thnks for help :) tcDeepfreeze
@BryanOakley Is it possible to change the text of the line numbers as well as the tkinter text widget. I tried using config and it doesn't work.Uniflorous
@Arshia: yes, of course you can change the text of the line numbers, and you can change the text in the text widget. There's nothing special about those widgets. The text widget works exactly like a standard widget, just with the extra virtual event.Perennate
Very nice answer this really helped me. I especially liked that it python2/3 usable and you put python3 first.Tedder
While this is a great answer, looks and works great, it does have performance issues, especially when you add lots of text to the text widget. Take a look at my answer below for an alternative way of doing this. :)Schlenger
@YasserElsayed: have you actually observed performance issues, or only think there are performance issues? It shouldn't matter how many lines of text you have because it only draws the linenumbers for the visible lines. I've tested it with 10,000 lines of text and it seems to work fine. Of course, if you enter 10,000 lines of text one at a time it will be slightly slower, but even then the performance seems acceptable.Perennate
@BryanOakley I've played around with it for quite some time actually, and hit performance issues loading files. I do load lines one by one not the whole file at once, and I see an average of one to two seconds until the lines actually appear. It's worth mentioning I also do heavy tagging and coloring on the widget.Schlenger
@YasserElsayed: the problem is in loading the text one line at a time. You are correct that this method makes that method slower, but there are two simple fixes: add the ability to turn this feature off during the initial loading (simply remove the <<Change>> binding at startup), or read a whole file and insert it in a single statement.Perennate
@BryanOakley I'm trying to implement this code in one of my projects and I am having some issues. Just as an example, when I try to create a new file, I need to delete the text inside the custom text class but I get an attribute error when I use 'CustomText.delete("1.0", END)' that says 'str' object has no attribute 'tk'. Is there a way to add the change event to normal text widgets? That way I would still be able to use the functions that I created for new files and saving, etc.Argolis
@Shock9616: I don't know what you mean by "add the change event to normal text widgets" -- that's exactly what this code does: it adds a change event to a normal text widget. The delete method works fine with the code in this answer. In your comment it appears you're calling the delete method on the class rather than an instance of the class.Perennate
@BryanOakley Oh! Right! That almost fixed it all. The only thing that isn't quite working right now is when I try to cut text, I get a TclError saying that the text doesn't contain any characters tagged with "sel". This error is being caused by the line in _proxy that says 'result = self.tk.call(cmd)'Argolis
Please help me with this error: (#65228977)Euromarket
Would it be possible to use my own text buffer instead of the Text widget with your TextLineNumbers widget? I've been struggling to find any graceful way to use a hand-written buffer with tkinter, or any other Python GUI library that I can find.Catechize
@BryanOakley How could I make it so that the TextLineNumbers dynamically resized so that it can handle really large contents?Sketchy
@TRCK: I don't understand your question. The line numbers widget can be as tall as the physical screen, and can display any range of numbers up to the limit of what the text widget can hold.Perennate
If you have more than 10,000 lines it is too big horizontally and starts to go over the edgeSketchy
@TRCK: have you tried simply making the line number canvas wider?Perennate
@BryanOakley Well it would be better to dynamically resize the text size of the line numbers correct?Sketchy
How do I get the copy paste functions to work with this solution?Graecize
S
5

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 textarea:

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 line:

self.linenumbers.tag_configure('line', justify='right')

Disable the widget so that it cannot be edited by the user

self.linenumbers.config(state=DISABLED)

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 uniscrollbar:

    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:

enter image description here

Schlenger answered 7/5, 2016 at 10:23 Comment(2)
I'm not sure how you are inserting the line numbers, but there is a problem with this solution if you use any kind of wrapping. Although you can calculate how many lines are wrapped in the visible text widget, it gets more difficult to calculate invisible wrapped lines above the visible code. Perhaps you could still calculate that using the scrollbar data, or somehow measuring the characters with bbox. So if using wrapping, I'd recommend a solution with Canvas.Feer
Good observation, I missed that. You're right, although I'd still say this is better than using a canvas for performance. The way you can fix this is just calculate the wrapping character, since the size of the textbox is measured in number of characters anyway, rather than real pixel size.Schlenger
I
4

After thoroughly reading through each solution mentioned here, and trying some of them out myself, I decided to use Brian Oakley's solution with some modifications. This might not be a more efficient solution, but should be enough for someone who is looking for a quick and easy to implement method, which is also simple in principle.

It draws the line's in the same manner, but instead of generating <<Change>> events, it simply binds the key press, scroll, left click events to the text as well as left click event to the scrollbar. In order to not glitch when, e.g. a paste command is performed, it then waits 2ms before actually redrawing the line numbers.

EDIT: This is also similair to FoxDot's solution, but instead of constantly refreshing the line numbers, they are only refreshed on the bound events

Below is an example code with delays implemented, along with my implementation of the scroll

import tkinter as tk


# This is a scrollable text widget
class ScrollText(tk.Frame):
    def __init__(self, master, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        self.text = tk.Text(self, bg='#2b2b2b', foreground="#d1dce8", 
                            insertbackground='white',
                            selectbackground="blue", width=120, height=30)

        self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
        self.text.configure(yscrollcommand=self.scrollbar.set)

        self.numberLines = TextLineNumbers(self, width=40, bg='#313335')
        self.numberLines.attach(self.text)

        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.numberLines.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
        self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        self.text.bind("<Key>", self.onPressDelay)
        self.text.bind("<Button-1>", self.numberLines.redraw)
        self.scrollbar.bind("<Button-1>", self.onScrollPress)
        self.text.bind("<MouseWheel>", self.onPressDelay)

    def onScrollPress(self, *args):
        self.scrollbar.bind("<B1-Motion>", self.numberLines.redraw)

    def onScrollRelease(self, *args):
        self.scrollbar.unbind("<B1-Motion>", self.numberLines.redraw)

    def onPressDelay(self, *args):
        self.after(2, self.numberLines.redraw)

    def get(self, *args, **kwargs):
        return self.text.get(*args, **kwargs)

    def insert(self, *args, **kwargs):
        return self.text.insert(*args, **kwargs)

    def delete(self, *args, **kwargs):
        return self.text.delete(*args, **kwargs)

    def index(self, *args, **kwargs):
        return self.text.index(*args, **kwargs)

    def redraw(self):
        self.numberLines.redraw()


'''THIS CODE IS CREDIT OF Bryan Oakley (With minor visual modifications on my side): 
https://mcmap.net/q/487567/-tkinter-adding-line-number-to-text-widget'''


class TextLineNumbers(tk.Canvas):
    def __init__(self, *args, **kwargs):
        tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
        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, fill="#606366")
            i = self.textwidget.index("%s+1line" % i)


'''END OF Bryan Oakley's CODE'''

if __name__ == '__main__':
    root = tk.Tk()
    scroll = ScrollText(root)
    scroll.insert(tk.END, "HEY" + 20*'\n')
    scroll.pack()
    scroll.text.focus()
    root.after(200, scroll.redraw())
    root.mainloop()

Also, I noticed that with Brian Oakley's code, if you use the mouse wheel to scroll (and the scrollable text is full), the top number lines sometimes glitch out and get out of sync with the actual text, which is why I decided to add the delay in the first place. Though I only tested it on my own implementation of Scrolled Text widget, so this bug might be unique to my solution, although it is still peculiar

Infamous answered 9/3, 2020 at 18:24 Comment(0)
T
2

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

Compare:

# 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))
    ...
Tancred answered 4/5, 2013 at 11:48 Comment(0)
E
2

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[1]
            linenum = str(i).split(".")[0]
            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)
Elenoraelenore answered 4/1, 2017 at 9:48 Comment(0)
C
0

I used some object oriented approach and created custom class that handles text numeeration automatically, but it does not support wrapping of text content. It mainly uses events to handle new line changes in text content. So it has some restrictions but I think that its quite simple, so I decided to add it here.

Here is an example:

from tkinter import Frame, Text
import tkinter as tk

class CustomTextField(Frame):
    def __init__(self, parent, content, **kwargs):

        super(CustomTextField, self).__init__(parent, **kwargs)
        # lines numeration
        self.lines_no = len(content.split('\n')) # get initial lines number

        # create text widget for lines numeration
        self.numeration = Text(self, width=5)
        self.numeration.pack(side='left', fill='y')
        self.numeration.tag_configure("center", justify="center")
        self.numeration.insert('end', '\n'.join(str(x) for x in range(1, self.lines_no+1)), 'center')
        self.numeration.configure(state="disabled")

        # text content
        self.content = Text(self, wrap='none')
        self.content.pack(side='left', fill='y', expand=True)
        self.content.insert('end', content)

        # event handling
        self.content.bind("<KeyPress>", self.handle_new_line)

    ########<METHODS>#################

    def handle_new_line(self, event):
        # enable numeration editing
        self.numeration.configure(state="normal")
        if event.keysym == 'BackSpace' and event.state == 0 and self.numeration.get('insert') == '\n': # when new line is deleted
            self.lines_no = self.lines_no-1
            self.numeration.delete("end-2c linestart", "end")
        elif event.keysym == 'Return'and (event.state == 0 or event.state == 1): # when new line is entered
            self.lines_no = self.lines_no+1
            self.numeration.insert('end', '\n' + str(self.lines_no), 'center')
        self.numeration.configure(state="disabled")

# TESTS - I used jupyter notebook so it is in this form
# some random content
content = '''Tiberius III (died c. 706) was Byzantine emperor from 698 to 705. He was a mid-level 
commander who served in the Cibyrrhaeot Theme. In 696, he was part of an army sent by 
Emperor Leontius to retake Carthage from the Umayyads. After seizing the city, the army
was'''

# Create the main window
root = tk.Tk()

# Create the ScrolledText widget
text_widget = CustomTextField(root, content)
text_widget.pack(fill=tk.BOTH, expand=True) 



# Start the Tkinter event loop
root.mainloop()
Congdon answered 9/7, 2023 at 10:50 Comment(0)
C
0

@Bryan Oakley's code is bit complex for a normal programmer since it contains many functions which requires some knowledge of how Tkinter and the underlying tcl/tk text widget works.

This answer is also an extension of @yelsayed's method.

Therefore, here I have developed a relatively simple and easy to understand program to achieve the purpose, which is not full proof as of @Bryan's but is well suited for any small use case.

In most use case, size of both Line Numbering and Text would be same, hence this mechanism is being used.

class TextNumber results in a tk.Listbox widget which contains the line numbering. The Line Numbering is obtained by interacting with the tk.Text widget and by obtaining a range of numbers from 1 to last line in the tk.Text widget. This range of numbers is then updated into the tk.Listbox object.

class TextNumbers(tk.Listbox):
    def __init__(self, master, textwidget, **options):
        super().__init__(master, **options)

        self.textwidget = textwidget
        self.textwidget.bind("<Return>", self.update_num_list)
        self.textwidget.bind("<BackSpace>", self.update_num_list)
    
        self.number_var = tk.Variable(self, value=["1"])

        self.configure(listvariable=self.number_var, selectmode=tk.SINGLE)
        self.set_width(1)
        self.set_font()

    def set_font(self):
        font = self.textwidget.cget("font")
        self.configure(font = font)

    def set_width(self, num_len):
        self.configure(width=num_len+1)

    def update_num_list(self, event):
        linenums = self.get_num_lines()
        current_column = self.get_current_colomn()
        
        if current_column != 0 and event.keycode != 13: return
        number_list = list(range(1, linenums)) if event.keycode == 13 else list(range(1, linenums-1))

        self.set_width(len(str(linenums)))
        self.number_var.set(number_list)
        self.yview("end")
        
    def get_num_lines(self):
        num_lines = int(self.textwidget.index("end").split(".")[0])
        return (num_lines)

    def get_current_colomn(self):
        curr_column = int(self.textwidget.index("insert").split(".")[1])
        return (curr_column)

Later class NumberedText wraps all TextNumbers, tk.Text and a tk.Scrollbar into a tk.Frame which can now be used to get a tk.Text along with line numbers.


class NumberedText(tk.Frame):

    def __init__(self, master, **options):
        super().__init__(master, **options)

        style = Style(self)
        self.configure(bg="white")
        style.configure("TSeparator", relief="flat")
        
        self.uniscrollbar = tk.Scrollbar(self, relief="flat")
        self.uniscrollbar.pack(side="right", fill="y", expand=1)
        
        self.scroll_text()

        separator = Separator(self, orient='vertical')
        separator.pack(side="right", fill="y",expand=1, padx=2)

        self.number_widget()
        
        self.textarea.config(spacing1=0, spacing2=1, spacing3=1)
        
    def scroll_text(self):
        self.textarea = tk.Text(self, relief="flat")

        self.uniscrollbar["command"] = self.scroll_both
        self.textarea["yscrollcommand"] = self.update_scroll_both

        self.textarea.pack(side="right", fill="y", expand=1)
    
    def number_widget(self):
        self.linenumber = TextNumbers(self, self.textarea, relief="flat", state="disabled", justify="right",)

        self.uniscrollbar["command"] = self.scroll_both
        self.linenumber["yscrollcommand"] = self.update_scroll_both

        self.linenumber.pack(side="right", fill="y", expand=1)
        
    def mouse_wheel(self, event):
        self.scrolltext.yview_scroll(int(-1*(event.delta/120)), "units")
        self.number_widget.yview_scroll(int(-1*(event.delta/120)), "units")
    
    def scroll_both(self, action, position):
        self.textarea.yview_moveto(position)
        self.linenumber.yview_moveto(position)
    
    def update_scroll_both(self, first, last, type=None):
        self.textarea.yview_moveto(first)
        self.linenumber.yview_moveto(first)
        self.uniscrollbar.set(first, last)

Upon intialization, results in a beautiful Customized Tkinter Widget: NumberedText Widget Image

Pros:

  1. Easy and simple to understand.

Cons:

  1. Doesn't numbers the line if multiline text is added via pasting.
  2. Takes 30% CPU Upon repetitive line numbering.
Canorous answered 6/5 at 12:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.