如何在tkinter.Text小部件中显示行号?

2 投票
2 回答
3548 浏览
提问于 2025-04-18 14:20

我正在写自己的代码编辑器,想让左侧有行号。根据这个回答,我写了下面的示例代码:

#!/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 个回答

1

问题的根本原因是文本还没有在屏幕上显示出来,所以调用 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",依此类推。

1

根本问题在于你在返回到运行循环之前调用了 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.02.03.0 等等,对吧?

撰写回答