1

I am using Bryan Oakley's code at Tkinter adding line number to text widget to create a Text widget with line numbers. But I want create a custom widget that I can use like Text widget but then with optional line numbers. Like in:

t = LinedText(top)
t.insert("insert", "Hello")
t.show()

But as of now, when I show line numbers, it covers text widget. The window is resized automatically. Why is that happening? My code:

import tkinter as tk


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)


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)))


class LinedText(CustomText):

    def __init__(self, *args, **kwargs):
        CustomText.__init__(self, *args, **kwargs)

        self.settings = self.Settings()
        self.linenumbers = None

        self.text = super()      

        self.vsb = tk.Scrollbar(orient="vertical", command=self.yview)
        self.vsb.pack(side="right", fill="y")

        self.text.configure(yscrollcommand=self.vsb.set)
        self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
        self.text.pack(side="left", 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")
        self.text.pack(side="left")

    def hide(self,event=None):
        if not self.settings.hide_linenumbers:
            self.settings.hide_linenumbers = True
            self.linenumbers.pack_forget()

    def show(self,event=None):
        if self.linenumbers == None:
            self.linenumbers = TextLineNumbers(self, width=30)
            self.linenumbers.attach(self.text)
            self.linenumbers.pack(side="left", fill="y")
        elif self.settings.hide_linenumbers:
            self.settings.hide_linenumbers = False
            self.linenumbers.pack(side="left", fill="y")

    def _on_change(self, event):
        if self.linenumbers:
            self.linenumbers.redraw()

    class Settings():
        def __init__(self):
            self.hide_linenumbers = True


if __name__ == "__main__":
    root = tk.Tk()
    text = LinedText(root)
    #text.pack(side="right", fill="both", expand=True)
    button = tk.Button(root, text="Hide", command=text.hide)
    button.pack()
    button = tk.Button(root, text="Show", command=text.show)
    button.pack()
    root.mainloop()

Also, I was assuming with .pack() as opposed to .pack(side="left"), widget was to be drawn below previous widgets. My buttons are being drawn to right. How do I get them to draw below text and line widgets? Do I absolutely need to use .grid() or Frame?

Community
  • 1
  • 1
Suresh Subedi
  • 660
  • 2
  • 10
  • 25

2 Answers2

2

The reason they overlap is that you make the line number widget a child of the text widget. You need to use a frame that is the parent of both the text widget and the line number widget, and then pack or grid them side-by-side. This is exactly how the original code you copied from works.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • I am trying to create something like `ScrolledText`. It also uses `Frame` to place `Scrollbar` and `Text` in it but it inherits from `Text` and has `Frame` as attribute. When I did that linenumbers didn't get updated. Must have something to do with two hacks involved in ScrolledText and CustomText breaking eachother. But maybe I'll have to make a new question for that. – Suresh Subedi Jul 19 '15 at 03:44
1

First of all, I am not sure I understood the logic in the functions show and hide, but in the code below, as you will see, I have changed a little bit those functions.

I am also not sure why you are using super() to initialise self.text in your LinedText class, but I think the problem starts exactly there.

What I first did was basically to make LinedText inherit from a Frame instead of from a CustomText, and create an instance variable called self.text of type CustomText in your LinedText class. I did this because I think of this LinedText class as a container for 2 other objects of type CustomText and TextLineNumbers.

I also decided to split the main window (the root) in 2 frames, one for the LinedText object, and one to contain the buttons Show and Hide. In this way, I can pack the frames, instead of single widgets, and I can for example pack the frame for the buttons at the bottom, and the frame for the LinedText object at the top.

Using Frames to organise the data is often the right and easier way to go when creating layouts.

One thing I also changed to make the TextLineNumber look pretty is where the numbers are drawn. Check the comments on the code.

Another thing I want to say is that you should compare the value of object with None using is or is not, instead of == and !=.

Also, I am not seeing the point of having a class (Settings) just with an instance variable, it does not make sense, and I guess you are doing this because this class will become larger in the future.

If you have problems regarding the fact that the window shrinks when you introduce or remove a widget, check out this question

How to stop Tkinter Frame from shrinking to fit its contents?

I hope you understand the logic of the methods show and hide, if not, just ask.

Here's the full code:

import tkinter as tk


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]

            # changed where text is draw: it starts from 4
            self.create_text(4, y, anchor="nw", text=linenum)  
            i = self.textwidget.index("%s+1line" % i)


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)))


class LinedText(tk.Frame):

    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)

        self.settings = self.Settings()
        self.linenumbers = None

        self.text = CustomText(self)
        self.vsb = tk.Scrollbar(orient="vertical", command=self.text.yview)
        self.vsb.pack(side="right", fill="y")

        self.text.configure(yscrollcommand=self.vsb.set)
        self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
        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")
        self.text.focus()        
        self.text.pack(side="right", fill="both", expand=True)

    def hide(self,event=None):
        if not self.settings.hide_linenumbers:
            self.settings.hide_linenumbers = True
            self.linenumbers.pack_forget()
            self.linenumbers = None

    def show(self,event=None):
        if self.linenumbers is None:
            self.linenumbers = TextLineNumbers(self, width=30)
            self.linenumbers.attach(self.text)
            self.linenumbers.pack(side="left", fill="y")
            self.settings.hide_linenumbers = False

    def _on_change(self, event):
        if self.linenumbers:
            self.linenumbers.redraw()

    class Settings():
        def __init__(self):
            self.hide_linenumbers = True


if __name__ == "__main__":
    root = tk.Tk()

    top_frame = tk.Frame(root)
    text = LinedText(top_frame)
    text.pack(expand=1, fill="both")
    top_frame.pack(side="top", expand=1, fill="both")

    bottom_frame = tk.Frame(root)
    button = tk.Button(bottom_frame, text="Hide", command=text.hide)
    button.pack(side="right")
    button = tk.Button(bottom_frame, text="Show", command=text.show)
    button.pack(side="right")
    bottom_frame.pack(side="bottom", fill="x")

    root.mainloop()
Community
  • 1
  • 1
nbro
  • 15,395
  • 32
  • 113
  • 196
  • I inherit from `Text` because I want to call `Text` methods directly from my `LinedText`. Now I have to do `self.text.insert()`. If I inherit from `Text` I can do `self.insert()`. Maybe I can place the frame as property/attribute/`self.frame` instead. I am using `super` to get instance of `CustomText` so that I can pack it. – Suresh Subedi Jul 18 '15 at 04:11
  • @SureshSubedi To be honest, I had never seen the use of `super` like in your example. Usually, you just use `self.` to refer to the current instance... Anyway I understand typing less can be easier, but in this case I think the way I structure the class is better, but you decide. – nbro Jul 18 '15 at 09:43