如何在tkinter.Text小部件中显示行号?
我正在写自己的代码编辑器,想让左侧有行号。根据这个回答,我写了下面的示例代码:
#!/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()
可惜在__update_line_numbers
这个方法里出了点问题。这个方法应该在我的Canvas
小部件上从上到下写出行号,但它只打印了第一行的数字(1),然后就结束了。为什么会这样呢?
2 个回答
问题的根本原因是文本还没有在屏幕上显示出来,所以调用 dlineinfo
时不会返回任何有用的信息。
如果你在绘制行号之前加一个 self.update()
的调用,你的代码会稍微好一点。虽然这样做并不能完美解决问题,因为你还有其他的错误。更好的做法是,在图形界面空闲的时候,或者在可见性事件发生时调用这个函数。一个好的经验法则是,除非你明白为什么不应该调用 update()
,否则不要随便调用它。不过在这个情况下,调用它相对来说是无害的。
另一个问题是,你一直在给 i
添加内容,但在写入画布时总是使用 i[0]
。当你到第二行时,i
的值会变成 "1.0+1line"。到第三行时,它会变成 "1.0+1line+1line",以此类推。第一个字符总是 "1"。
你应该做的是让 tkinter 将你修改后的 i
转换成一个标准的索引,然后用这个索引来表示行号。例如:
i = self.__text.index('{0}+1line'.format(i))
这样可以将 "1.0+1line" 转换成 "2.0",将 "2.0+1line" 转换成 "3.0",依此类推。
根本问题在于你在返回到运行循环之前调用了 dlineinfo
,所以文本还没有被排版好。
正如文档所解释的:
这个方法只有在文本组件被更新后才能正常工作。为了确保这一点,你可以先调用
update_idletasks
方法。
像往常一样,要获取更多信息,你需要查看Tcl文档,这些文档基本上告诉你,文本组件在更新之前可能无法正确判断哪些字符是可见的,因此它可能返回 None
,这并不是因为有什么问题,而是因为在它看来,你请求的是一个在屏幕外的东西的边界框。
一个好的测试方法是,在调用 dlineinfo(i)
之前先调用 self.__text.see(i)
。如果这改变了 dlineinfo
的结果,那么这就是问题所在。(或者,如果不是这个问题,至少与这个问题有关——不管出于什么原因,Tk认为第1行之后的所有内容都在屏幕外。)
但是在这种情况下,即使调用 update_idletasks
也不起作用,因为不仅需要更新行信息,还需要先排版文本。你需要做的是明确地推迟这个调用。例如,在 load_from_file
的底部添加这一行,现在就可以正常工作:
self.__text.after(0, self.__update_line_numbers)
你也可以在调用 self.__update_line_numbers()
之前直接调用 self.__text.update()
,我认为这样也可以。
顺便提一下,使用调试器运行这个程序,或者在循环的顶部添加 print(i, dline)
,这样你可以看到你得到的结果,而不是仅仅猜测,这会对你很有帮助。
另外,难道不应该更简单地只增加一个 linenumber
,然后使用 '{}.0'.format(linenumber)
,而不是创建像 @0,0+1line+1line+1line
这样复杂的索引(至少对我来说)?你可以调用 Text.index()
将任何索引转换为标准格式,但为什么要让事情变得这么复杂呢?你知道你想要的是 1.0
、2.0
、3.0
等等,对吧?