Reputation: 1231
I'm writing my own code editor and I want it to have numbered lines on left side. Based on this answer I wrote this sample code:
#!/usr/bin/env python3
import tkinter
class CodeEditor(tkinter.Frame):
def __init__(self, root):
tkinter.Frame.__init__(self, root)
# Line numbers widget
self.__line_numbers_canvas = tkinter.Canvas(self, width=40, bg='#555555', highlightbackground='#555555', highlightthickness=0)
self.__line_numbers_canvas.pack(side=tkinter.LEFT, fill=tkinter.Y)
self.__text = tkinter.Text(self)
self.__text['insertbackground'] = '#ffffff'
self.__text.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=True)
def __update_line_numbers(self):
self.__line_numbers_canvas.delete("all")
i = self.__text.index('@0,0')
self.__text.update() #FIX: adding line
while True:
dline = self.__text.dlineinfo(i)
if dline:
y = dline[1]
linenum = i[0]
self.__line_numbers_canvas.create_text(1, y, anchor="nw", text=linenum, fill='#ffffff')
i = self.__text.index('{0}+1line'.format(i)) #FIX
else:
break
def load_from_file(self, path):
self.__text.delete('1.0', tkinter.END)
f = open(path, 'r')
self.__text.insert('0.0', f.read())
f.close()
self.__update_line_numbers()
class Application(tkinter.Tk):
def __init__(self):
tkinter.Tk.__init__(self)
code_editor = CodeEditor(self)
code_editor.pack(fill=tkinter.BOTH, expand=True)
code_editor.load_from_file(__file__)
def run(self):
self.mainloop()
if __name__ == '__main__':
app = Application()
app.run()
Unfortunately something is wrong inside __update_line_numbers
. This method should write line numbers from top to bottom on my Canvas
widget but it prints only the number for the first line (1) and then exits. Why?
Upvotes: 2
Views: 3671
Reputation: 365657
The root problem is that you're calling dlineinfo
before returning to the runloop, so the text hasn't been laid out yet.
As the docs explain:
This method only works if the text widget is updated. To make sure this is the case, you can call the update_idletasks method first.
As usual, to get more information, you have to turn to the Tcl docs for the underlying object, which basically tell you that the Text widget may not be correct about which characters are and are not visible until it's updated, in which case it may be returning None
not because of any problem, but just because, as far as it's concerned, you're asking for the bbox of something that's off-screen.
A good way to test whether this is the problem is to call self.__text.see(i)
before calling dlineinfo(i)
. If it changes the result of dlineinfo
, this was the problem. (Or, if not that, at least something related to that—for whatever reason, Tk thinks everything after line 1 is off-screen.)
But in this case, even calling update_idletasks
doesn't work, because it's not just updating the line info that needs to happen, but laying out the text in the first place. What you need to do is explicitly defer this call. For example, add this line to the bottom of load_from_file
and now it works:
self.__text.after(0, self.__update_line_numbers)
You could also call self.__text.update()
before calling self.__update_line_numbers()
inline, and I think that should work.
As a side note, it would really help you to either run this under the debugger, or add a print(i, dline)
at the top of the loop, so you can see what you're getting, instead of just guessing.
Also wouldn't it be easier to just increment a linenumber
and use '{}.0'.format(linenumber)
instead of creating complex indexes like @0,0+1line+1line+1line
that (at least for me) don't work. You can call Text.index()
to convert any index to canonical format, but why make it so difficult? You know that what you want is 1.0
, 2.0
, 3.0
, etc., right?
Upvotes: 1
Reputation: 385900
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. It won't work perfectly, because you have other bugs. Even better, call the function when the GUI goes idle, or on a Visibility event or something like that. A good rule of thumb is to never call update
unless you understand why you should never call update()
. In this case, however, it's relatively harmless.
Another problem is that you keep appending to i, but always use i[0]
when writing to the canvas. When you get to line 2, i
will be "1.0+1line". For line three it will be "1.0+1line+1line", and so on. The first character will always be "1".
What you should be doing is asking tkinter to convert your modified i to a canonical index, and using that for the line number. For example:
i = self.__text.index('{0}+1line'.format(i))
This will convert "1.0+1line" to "2.0", and "2.0+1line" to "3.0" and so on.
Upvotes: 1