【问题标题】:Tkinter adding line number to text widgetTkinter 将行号添加到文本小部件
【发布时间】:2013-05-04 00:43:31
【问题描述】:

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

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 像素。

我正在尝试这样的事情:

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

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)

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

【问题讨论】:

  • 你是说在每一行的旁边嵌入数字,然后,一次?或者您只是想在状态栏中更新当前行号?在后一种情况下,您只需执行myTextWidget.index(INSERT).split(".")[0],然后您就可以将号码放在任何您想要的地方。如果你想要行号(而不是一个数字),你总是可以排列一个没有边框的平行标签,打印出上面的每一行,并让它与你的主文本小部件同步移动,但我还没有测试过看看效果如何。

标签: python tkinter


【解决方案1】:

我有一个相对简单的解决方案,但它很复杂并且可能难以理解,因为它需要一些关于 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”问题时,我提供了一个类似的解决方案,展示了如何让文本小部件在发生变化时调用回调。这一次,我们将生成一个事件而不是回调,因为我们的需求有点不同。

自定义文本类

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

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

【讨论】:

  • @Bryan Oakley 非常感谢您的详细回复。现在将尝试此操作并向您报告。关于您的评论:`让文本小部件在“发生变化”时触发事件。不幸的是,文本小部件没有直接支持。 ` 我读到该文本方法提供了一个修改选项来检查文件是否已使用“编辑修改”方法进行了修改。见 -link 再次感谢 - 看看我能不能处理它:)
  • @Bryan Oakley 您的第一个解决方案非常适合我的需求,所以我没有看到第二个解决方案。至于何时调用重绘的问题,我在任意键按下时调用它,在剪切、复制、粘贴、撤消和重做事件时调用它,这可以处理大部分事情 - 我猜 :)
  • @QuakiGabbar:是的,Tk 会在小部件被修改时抛出一个事件,但是每次更改后您都必须重置“已修改”状态,然后您将无法使用它它的真正目的是了解内容何时与最初加载时不同。
  • @ Bryan Oakley thnks,我看到您在上面给出的第一个 linenumber 代码存在一个奇怪的问题。当窗口尺寸较小时,我的显示效果非常好。但是当我将窗口最大化到全屏时,第 1 行开始显示,从几乎半屏向下。我猜问题出在行上:'self.create_text(2,y,anchor="nw", text=linenum) i = self.textwidget.index("%s+1line" % i)'。但我仍然要弄清楚。任何建议将不胜感激:)
  • @Bryan Oakley - 抱歉轰炸问题:) 我听说 idle 是用 tkinter 写的。如果有的话,我在哪里可以找到空闲窗口的源代码?我搜索了我的 python 目录,但在任何地方都找不到它
【解决方案2】:

这是我做同样事情的尝试。我在上面尝试了 Bryan Oakley 的答案,它看起来和工作都很棒,但它是以性能为代价的。每次我将很多行加载到小部件中时,都需要很长时间才能做到这一点。为了解决这个问题,我使用了一个普通的Text 小部件来绘制行号,我是这样做的:

创建文本小部件并将其网格化到要为其添加行的主要文本小部件的左侧,我们称之为textarea。确保您也使用与textarea 相同的字体:

self.linenumbers = Text(self, width=3)
self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS)
self.linenumbers.config(font=self.__myfont)

添加一个标签来右对齐添加到行号小部件的所有行,我们称之为line

self.linenumbers.tag_configure('line', justify='right')

禁用小部件,使其无法被用户编辑

self.linenumbers.config(state=DISABLED)

现在棘手的部分是添加一个滚动条,我们称之为uniscrollbar 来控制主要文本小部件以及行号文本小部件。为此,我们首先需要两个方法,一个由滚动条调用,然后可以更新两个文本小部件以反映新位置,另一个在文本区域滚动时调用,它将更新滚动条:

def __scrollBoth(self, action, position, type=None):
    self.textarea.yview_moveto(position)
    self.linenumbers.yview_moveto(position)

def __updateScroll(self, first, last, type=None):
    self.textarea.yview_moveto(first)
    self.linenumbers.yview_moveto(first)
    self.uniscrollbar.set(first, last)

现在我们准备创建uniscrollbar

    self.uniscrollbar= Scrollbar(self)
    self.uniscrollbar.grid(row=self.__uniscrollbarRow, column=self.__uniscrollbarCol, sticky=NS)
    self.uniscrollbar.config(command=self.__scrollBoth)
    self.textarea.config(yscrollcommand=self.__updateScroll)
    self.linenumbers.config(yscrollcommand=self.__updateScroll)

瞧!您现在有了一个非常轻量级的带有行号的文本小部件:

【讨论】:

  • 我不确定您是如何插入行号的,但是如果您使用任何类型的包装,此解决方案就会出现问题。尽管您可以计算在可见文本小部件中包含多少行,但在可见代码上方计算不可见的换行会变得更加困难。也许您仍然可以使用滚动条数据来计算,或者以某种方式使用 bbox 测量字符。因此,如果使用包装,我会推荐使用 Canvas 的解决方案。
  • 很好的观察,我错过了。你是对的,尽管我仍然会说这比使用画布进行性能更好。解决这个问题的方法就是计算换行字符,因为文本框的大小是用字符数来衡量的,而不是真正的像素大小。
【解决方案3】:

我在一个名为 unpythonic 的网站上看到了一个示例,但它假设 txt 的行高 为 6 像素

比较:

# 假设每行至少 6 个像素
步长 = 6

step - 程序检查文本小部件是否有新行的频率(以像素为单位)。如果文本小部件中的行高为 30 像素,则此程序执行 5 次检查并仅绘制一个数字。
如果字体非常小,您可以将其设置为 有一个条件:text 小部件中的所有符号必须使用一种字体,并且绘制数字的小部件必须使用相同的字体。

# http://tkinter.unpythonic.net/wiki/A_Text_Widget_with_Line_Numbers
class EditorClass(object):
    ...


    self.lnText = Text(self.frame,
                    ...
                    state='disabled', font=('times',12))
    self.lnText.pack(side=LEFT, fill='y')
    # The Main Text Widget
    self.text = Text(self.frame,
                        bd=0,
                        padx = 4, font=('times',12))
    ...

【讨论】:

    【解决方案4】:

    根据上面 Bryan Oakley 的回答,我使用了一种非常简单的方法。无需监听所做的任何更改,只需使用 self.after() 方法“刷新”小部件,该方法会在几毫秒后安排调用。非常简单的方法。在这种情况下,我在实例化时附加了文本小部件,但如果需要,您可以稍后再执行此操作。

    class TextLineNumbers(tk.Canvas):
            def __init__(self, textwidget, *args, **kwargs):
            tk.Canvas.__init__(self, *args, **kwargs)
            self.textwidget = textwidget
            self.redraw()
    
        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)
    
            # Refreshes the canvas widget 30fps
            self.after(30, self.redraw)
    

    【讨论】:

      【解决方案5】:

      在彻底阅读了这里提到的每个解决方案并亲自尝试了其中一些之后,我决定使用 Brian Oakley 的解决方案并进行一些修改。这可能不是一个更有效的解决方案,但对于正在寻找一种快速且易于实施的方法的人来说应该足够了,这在原则上也很简单。

      它以相同的方式绘制线条,但不是生成&lt;&lt;Change&gt;&gt; 事件,它只是将按键、滚动、左键单击事件绑定到文本,并将左键单击事件绑定到滚动条。为了不出现故障,例如执行粘贴命令,然后等待2ms,然后再实际重绘行号。

      编辑:这也类似于 FoxDot 的解决方案,但不是不断刷新行号,而是仅在绑定事件上刷新

      下面是一个实现了延迟的示例代码,以及我对滚动的实现

      import tkinter as tk
      
      
      # This is a scrollable text widget
      class ScrollText(tk.Frame):
          def __init__(self, master, *args, **kwargs):
              tk.Frame.__init__(self, *args, **kwargs)
              self.text = tk.Text(self, bg='#2b2b2b', foreground="#d1dce8", 
                                  insertbackground='white',
                                  selectbackground="blue", width=120, height=30)
      
              self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
              self.text.configure(yscrollcommand=self.scrollbar.set)
      
              self.numberLines = TextLineNumbers(self, width=40, bg='#313335')
              self.numberLines.attach(self.text)
      
              self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
              self.numberLines.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
              self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
      
              self.text.bind("<Key>", self.onPressDelay)
              self.text.bind("<Button-1>", self.numberLines.redraw)
              self.scrollbar.bind("<Button-1>", self.onScrollPress)
              self.text.bind("<MouseWheel>", self.onPressDelay)
      
          def onScrollPress(self, *args):
              self.scrollbar.bind("<B1-Motion>", self.numberLines.redraw)
      
          def onScrollRelease(self, *args):
              self.scrollbar.unbind("<B1-Motion>", self.numberLines.redraw)
      
          def onPressDelay(self, *args):
              self.after(2, self.numberLines.redraw)
      
          def get(self, *args, **kwargs):
              return self.text.get(*args, **kwargs)
      
          def insert(self, *args, **kwargs):
              return self.text.insert(*args, **kwargs)
      
          def delete(self, *args, **kwargs):
              return self.text.delete(*args, **kwargs)
      
          def index(self, *args, **kwargs):
              return self.text.index(*args, **kwargs)
      
          def redraw(self):
              self.numberLines.redraw()
      
      
      '''THIS CODE IS CREDIT OF Bryan Oakley (With minor visual modifications on my side): 
      https://stackoverflow.com/questions/16369470/tkinter-adding-line-number-to-text-widget'''
      
      
      class TextLineNumbers(tk.Canvas):
          def __init__(self, *args, **kwargs):
              tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
              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, fill="#606366")
                  i = self.textwidget.index("%s+1line" % i)
      
      
      '''END OF Bryan Oakley's CODE'''
      
      if __name__ == '__main__':
          root = tk.Tk()
          scroll = ScrollText(root)
          scroll.insert(tk.END, "HEY" + 20*'\n')
          scroll.pack()
          scroll.text.focus()
          root.after(200, scroll.redraw())
          root.mainloop()
      

      另外,我注意到在 Brian Oakley 的代码中,如果您使用鼠标滚轮滚动(并且可滚动文本已满),顶部的数字行有时会出现故障并与实际文本不同步,这就是为什么我决定首先添加延迟。 虽然我只在自己实现的 Scrolled Text 小部件上对其进行了测试,但这个错误可能是我的解决方案所独有的,尽管它仍然很奇特

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-01
        • 2013-01-19
        • 1970-01-01
        • 1970-01-01
        • 2013-05-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多