我有一个相对简单的解决方案,但它很复杂并且可能难以理解,因为它需要一些关于 Tkinter 和底层 tcl/tk 文本小部件如何工作的知识。我将在此处将其作为一个完整的解决方案进行介绍,您可以按原样使用它,因为我认为它说明了一种非常有效的独特方法。
请注意,无论您使用什么字体,无论您是否在不同的行上使用不同的字体、是否嵌入了小部件等,此解决方案都有效。
导入 Tkinter
在我们开始之前,如果您使用的是 python 3.0 或更高版本,以下代码假定 tkinter 是这样导入的:
import tkinter as tk
...或者这个,对于python 2.x:
import Tkinter as tk
行号小部件
让我们处理行号的显示。我们想要做的是使用画布,以便我们可以精确定位数字。我们将创建一个自定义类,并给它一个名为的新方法redraw
,该方法将重绘关联文本小部件的行号。我们还给它一个方法attach
,用于将文本小部件与此小部件相关联。
该方法利用了文本小部件本身可以通过该dlineinfo
方法准确地告诉我们一行文本在哪里开始和结束的事实。这可以准确地告诉我们在画布上绘制行号的位置。它还利用了如果一行不可见则dlineinfo
返回的事实,我们可以使用它来知道何时停止显示行号。None
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)
如果将它与文本小部件相关联,然后调用该redraw
方法,它应该可以很好地显示行号。
自动更新行号
这可行,但有一个致命的缺陷:你必须知道什么时候调用redraw
. 您可以创建一个在每次按键时触发的绑定,但您还必须在鼠标按钮上触发,并且您必须处理用户按键并使用自动重复功能等的情况。行号还需要如果窗口扩大或缩小或用户滚动,则重新绘制,因此我们陷入了试图找出可能导致数字变化的所有可能事件的兔子洞。
还有另一种解决方案,即让文本小部件在发生变化时触发一个事件。不幸的是,文本小部件不直接支持通知程序更改。为了解决这个问题,我们可以使用代理来拦截对文本小部件的更改并为我们生成一个事件。
在对“https://stackoverflow.com/q/13835207/7432”问题的回答中,我提供了一个类似的解决方案,展示了如何让文本小部件在发生变化时调用回调。这一次,我们将生成一个事件,而不是回调,因为我们的需求有点不同。
自定义文本类
<<Change>>
这是一个创建自定义文本小部件的类,该小部件将在插入或删除文本或滚动视图时生成事件。
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, *args):
# let the actual widget perform the requested action
cmd = (self._orig,) + args
result = self.tk.call(cmd)
# generate an event if something was added or deleted,
# or the cursor position changed
if (args[0] in ("insert", "replace", "delete") or
args[0:3] == ("mark", "set", "insert") or
args[0:2] == ("xview", "moveto") or
args[0:2] == ("xview", "scroll") or
args[0:2] == ("yview", "moveto") or
args[0:2] == ("yview", "scroll")
):
self.event_generate("<<Change>>", when="tail")
# return what the actual widget returned
return result
把它们放在一起
最后,这是一个使用这两个类的示例程序:
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.text = CustomText(self)
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=self.vsb.set)
self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
self.linenumbers = TextLineNumbers(self, width=30)
self.linenumbers.attach(self.text)
self.vsb.pack(side="right", fill="y")
self.linenumbers.pack(side="left", fill="y")
self.text.pack(side="right", 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")
def _on_change(self, event):
self.linenumbers.redraw()
...当然,在文件末尾添加它以引导它:
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()