Tkinter 将行号添加到文本小部件

问题描述:

尝试学习 tkinter 和 python.我想在相邻框架中显示文本小部件的行号

Trying to learn tkinter and python. I want to display line number for the Text widget in an adjacent frame

from Tkinter import *
root = Tk()
txt = Text(root)
txt.pack(expand=YES, fill=BOTH)
frame= Frame(root, width=25)
#

frame.pack(expand=NO, fill=Y, side=LEFT)
root.mainloop()

我在一个叫做 unpythonic 的网站上看到过一个例子,但它假设 txt 的行高是 6 像素.

I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.

我正在尝试这样的事情:

I am trying something like this:

1) 将 Any-KeyPress 事件绑定到返回按键发生行的函数:

1) Binding Any-KeyPress event to a function that returns the line on which the keypress occurs:

textPad.bind("<Any-KeyPress>", linenumber)


def linenumber(event=None):
    line, column = textPad.index('end').split('.')
    #creating line number toolbar
    try:
       linelabel.pack_forget()
       linelabel.destroy()
       lnbar.pack_forget()
       lnbar.destroy()
    except:
      pass
   lnbar = Frame(root,  width=25)
   for i in range(0, len(line)):
      linelabel= Label(lnbar, text=i)
      linelabel.pack(side=LEFT)
      lnbar.pack(expand=NO, fill=X, side=LEFT)

不幸的是,这在框架上给出了一些奇怪的数字.有没有更简单的解决方案?如何解决这个问题?

Unfortunately this is giving some weird numbers on the frame. Is there a simpler solution? How to approach this?

我有一个相对简单的解决方案,但它很复杂并且可能难以理解,因为它需要一些关于 Tkinter 和底层 tcl/tk 文本小部件的知识作品.我将在此处将其作为一个完整的解决方案呈现给您,您可以按原样使用它,因为我认为它说明了一种非常有效的独特方法.

I have a relatively foolproof solution, but it's complex and will likely be hard to understand because it requires some knowledge of how Tkinter and the underlying tcl/tk text widget works. I'll present it here as a complete solution that you can use as-is because I think it illustrates a unique approach that works quite well.

请注意,无论您使用什么字体,无论您是否在不同的行上使用不同的字体、是否嵌入了小部件等,此解决方案都有效.

Note that this solution works no matter what font you use, and whether or not you use different fonts on different lines, have embedded widgets, and so on.

在我们开始之前,如果您使用的是 python 3.0 或更高版本,以下代码假设 tkinter 是这样导入的:

Before we get started, the following code assumes tkinter is imported like this if you're using python 3.0 or greater:

import tkinter as tk

... 或者这个,对于 python 2.x:

... or this, for python 2.x:

import Tkinter as tk

行号小部件

让我们处理行号的显示.我们想要做的是使用画布,以便我们可以精确定位数字.我们将创建一个自定义类,并为其提供一个名为 redraw 的新方法,该方法将为关联的文本小部件重绘行号.我们还为它提供了一个方法 attach,用于将文本小部件与该小部件相关联.

The line number widget

Let's tackle the display of the line numbers. What we want to do is use a canvas so that we can position the numbers precisely. We'll create a custom class, and give it a new method named redraw that will redraw the line numbers for an associated text widget. We also give it a method attach, for associating a text widget with this widget.

此方法利用了这样一个事实,即文本小部件本身可以通过 dlineinfo 方法告诉我们一行文本的确切开始和结束位置.这可以准确地告诉我们在画布上绘制行号的位置.它还利用了 dlineinfo 在一行不可见时返回 None 的事实,我们可以使用它来知道何时停止显示行号.

This method takes advantage of the fact that the text widget itself can tell us exactly where a line of text starts and ends via the dlineinfo method. This can tell us precisely where to draw the line numbers on our canvas. It also takes advantage of the fact that dlineinfo returns None if a line is not visible, which we can use to know when to stop displaying line numbers.

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 方法,它应该可以很好地显示行号.

If you associate this with a text widget and then call the redraw method, it should display the line numbers just fine.

这行得通,但有一个致命的缺陷:你必须知道什么时候调用redraw.您可以创建一个在每次按键时触发的绑定,但您也必须在鼠标按钮上触发,并且您必须处理用户按下一个键并使用自动重复功能等的情况.行号也需要如果窗口增大或缩小或用户滚动,将重新绘制,因此我们陷入了试图找出可能导致数字更改的每个可能事件的兔子洞.

This works, but has a fatal flaw: you have to know when to call redraw. You could create a binding that fires on every key press, but you also have to fire on mouse buttons, and you have to handle the case where a user presses a key and uses the auto-repeat function, etc. The line numbers also need to be redrawn if the window is grown or shrunk or the user scrolls, so we fall into a rabbit hole of trying to figure out every possible event that could cause the numbers to change.

还有另一种解决方案,即让文本小部件在发生变化时触发事件.不幸的是,文本小部件不直接支持通知程序更改.为了解决这个问题,我们可以使用代理来拦截对文本小部件的更改并为我们生成一个事件.

There is another solution, which is to have the text widget fire an event whenever something changes. Unfortunately, the text widget doesn't have direct support for notifying the program of changes. To get around that, we can use a proxy to intercept changes to the text widget and generate an event for us.

在对问题https://*.com/q/13835207/7432"的回答中我提供了一个类似的解决方案,展示了如何让文本小部件在发生变化时调用回调.这一次,我们将生成一个事件而不是回调,因为我们的需求有点不同.

In an answer to the question "https://*.com/q/13835207/7432" I offered a similar solution that shows how to have a text widget call a callback whenever something changes. This time, instead of a callback we'll generate an event since our needs are a little different.

这是一个创建自定义文本小部件的类,该小部件将在插入或删除文本或滚动视图时生成 <<Change>> 事件.

Here is a class that creates a custom text widget that will generate a <<Change>> event whenever text is inserted or deleted, or when the view is scrolled.

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        

综合起来

最后,这是一个使用这两个类的示例程序:

Putting it all together

Finally, here is an example program which uses these two classes:

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()

...当然,在文件末尾添加它以引导它:

... and, of course, add this at the end of the file to bootstrap it:

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(side="top", fill="both", expand=True)
    root.mainloop()