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

时间:2014-07-22 19:59:29

标签: python tkinter

我正在编写自己的代码编辑器,我希望它在左侧有编号的行。基于this answer我写了这个示例代码:

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

答案 0 :(得分:1)

根问题是你在返回runloop之前调用dlineinfo,所以文本还没有布局。

正如the docs解释:

  

此方法仅在更新文本窗口小部件时有效。要确保这种情况,您可以先调用update_idletasks方法。

像往常一样,要获得更多信息,您必须转向Tcl docs for the underlying object,这基本上告诉您文本窗口小部件可能不正确,哪些字符在更新之前是不可见的,在这种情况下它可能正在返回None而不是因为有任何问题,而只是因为,就你所关心的而言,你要求的是关于屏幕外的东西的bbox。

测试这是否是问题的一个好方法是在调用self.__text.see(i)之前调用dlineinfo(i)。如果它改变了dlineinfo的结果,那就是问题所在。 (或者,如果不是这样,至少与此有关的东西 - 无论出于何种原因,Tk认为第1行之后的所有内容都在屏幕外。)

但在这种情况下,即使调用update_idletasks也不起作用,因为它不仅仅是更新需要发生的行信息,而是首先布置文本。您需要做的是明确推迟此调用。例如,将此行添加到load_from_file的底部,现在可以正常工作:

self.__text.after(0, self.__update_line_numbers)

您也可以在调用self.__text.update()内联之前致电self.__update_line_numbers(),我认为这应该有用。


作为旁注,它可以帮助你在调试器下运行它,或者在循环的顶部添加print(i, dline),这样你就可以看到你得到的东西,而不仅仅是猜测。

仅增加linenumber并使用'{}.0'.format(linenumber)而不是创建像@0,0+1line+1line+1line那样(至少对我来说)不起作用的复杂索引也不会更容易。您可以调用Text.index()将任何索引转换为规范格式,但为什么要这么难?您知道您想要的是1.02.03.0等,对吗?

答案 1 :(得分:1)

问题的根本原因是文本尚未在屏幕上绘制,因此对dlineinfo的调用不会返回任何有用的内容。

如果您在绘制行号之前添加对self.update() 的调用,您的代码将会更好一些。它不会完美,因为你有其他错误。更好的是,当GUI空闲时,或者在Visibility事件或类似事件上调用该函数。一个好的经验法则是永远不要致电update,除非您了解为什么您永远不应该致电update()。然而,在这种情况下,它相对无害。

另一个问题是你继续追加i,但在写入画布时总是使用i[0]。当你到达第2行时,i将是“1.0 + 1行”。对于第三行,它将是“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”等等。