【问题标题】:How to highlight the current line of a Text widget?如何突出显示文本小部件的当前行?
【发布时间】:2012-03-30 06:39:13
【问题描述】:

我正在使用 Python 开发一个简单的 GUI 代码编辑器,我希望光标所在的文本行始终处于突出显示状态。

现在,我的TextEditor 类看起来像:

class TextEditor:

   def __init__(self, container):
      self.scrollbar = Scrollbar(container)
      self.scrollbar.pack(side=RIGHT, fill=Y)

      self.textbox = Text(container, height=40, undo=True, width=80,
                          font=tkFont.Font(family="Consolas", size=12))
      self.textbox.pack(side=LEFT)

      self.textbox.config(yscrollcommand=self.scrollbar.set)
      self.scrollbar.config(command=self.textbox.yview)

我该怎么做?

【问题讨论】:

    标签: python user-interface python-3.x tkinter


    【解决方案1】:

    tkinter 没有内置任何东西直接支持这一点。但是,对于大多数用途来说已经足够好的方法是编写一个函数来轮询光标位置并定期更新突出显示。

    例如:

    import Tkinter as tk
    
    class MyApp(tk.Tk):
        def __init__(self, *args, **kwargs):
            tk.Tk.__init__(self, *args, **kwargs)
            self.text = tk.Text(self)
            self.text.pack(side="top", fill="both", expand=True)
            self.text.tag_configure("current_line", background="#e9e9e9")
            self._highlight_current_line()
    
        def _highlight_current_line(self, interval=100):
            '''Updates the 'current line' highlighting every "interval" milliseconds'''
            self.text.tag_remove("current_line", 1.0, "end")
            self.text.tag_add("current_line", "insert linestart", "insert lineend+1c")
            self.after(interval, self._highlight_current_line)
    
    app = MyApp()
    app.mainloop()
    

    显然,间隔越长,引入的“滞后”就越多,间隔越短,使用的 CPU 越多,但在几乎没有可察觉的滞后和难以察觉的两个极端之间存在相当大的最佳平衡点增加 CPU 使用率。

    还有另一种不涉及轮询且绝对万无一失的方法。您可以将突出显示移动到插入光标实际移动的精确位置,但这涉及编写一些嵌入式 Tcl 代码来创建隐藏在 Tkinter Text 对象的实现中的实际 tk 小部件的代理。

    最后,第三种方法是为所有可能修改光标位置的事件设置自定义绑定。虽然可能,但很难做到 100% 正确,因为您必须考虑修改光标位置的所有 事件,以及处理代码中可能在不使用事件的情况下移动光标的位置。尽管如此,使用绑定是一个非常好的解决方案,它只是需要更多的工作。

    【讨论】:

    • 你为什么要投票?我刚刚检查过,我可以绑定到Text 中的按键和鼠标单击事件。除非我遗漏了什么,否则在我看来,这意味着您可以绑定到任何有可能移动光标的用户事件。至于您的光标可能移动的程序原因 - 您的代码负责执行此操作,因此无论何时发生这种情况,只需在之后立即调用 _highlight_current_line。
    • @ArtOfWarfare:为什么要投票?主要是因为它是一个简单的解决方案。是的,您可以绑定到每一个移动光标的事件,但是您知道所有这些事件是什么吗?您可能可以弄清楚,但是您还需要记住在以编程方式插入文本时调用您的代码。轮询简单、有效且几乎万无一失。
    【解决方案2】:

    绝对不需要轮询like Bryan Oakley says in his answer,也不需要在 Python 代码中嵌入 Tcl 代码。我的解决方案是绑定到可能最终移动光标的事件,即<Key><Button-1>

    import tkinter as tk
    
    class CurrentHighlightedLineText(tk.Text):
    
        """Text widget with current line highlighted"""
    
        def __init__(self, root, *args, **kwargs):
            tk.Text.__init__(self, root, *args, **kwargs)
    
            self.tag_configure('currentLine', background='#e9e9e9')
            self.bind('<Key>', lambda _: self.highlightCurrentLine())
            self.bind('<Button-1>', lambda _: self.highlightCurrentLine())
            self.highlightCurrentLine(delay=0)
    
        def highlightCurrentLine(self, delay=10):
    
            def delayedHighlightCurrentLine():
                self.tag_remove('currentLine', 1.0, "end")
                self.tag_add('currentLine', 'insert linestart', 'insert lineend+1c')
            # This bound function is called before the cursor actually moves.
            # So delay checking the cursor position and moving the highlight 10 ms.
    
            self.after(delay, delayedHighlightCurrentLine)
    
    
    if __name__ == "__main__":
        root = tk.Tk()
    
        text = CurrentHighlightedLineText(root)
        text.grid(row=0, column=0, sticky='nesw')
    
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
    
        root.mainloop()
    

    【讨论】:

    • 这很好地说明了为什么轮询即使不是更好的解决方案也一样好。有些事情会导致光标移动而您的代码无法处理(例如,通过右键单击或从菜单粘贴代码)。这并不意味着您的代码无法处理它们,只是很难考虑所有边缘情况。此外,您可以通过在类绑定之后添加一个特殊的绑定标签来更优雅地解决这个“延迟”问题,这样您的绑定就发生在光标移动之后而不是之前。
    • @BryanOakley:我想以编程方式修改文本可能可以通过覆盖tk.Text 定义的任何方法的组合来处理,并且可能处理一些魔术方法,例如__setitem__。我绝对不会建议用投票代替这个。如果您对使用延迟有无可争辩的改进,请编辑我的答案以处理该问题。我从来没有设法让特殊的绑定标签在 Tkinter 中工作,但也许我只是用错了。
    • 是的,您对如何处理小部件的编程修改是正确的。关键是,您的解决方案开始变得非常复杂。我并不是说这是一个糟糕的解决方案,只是说它是一个非常复杂的解决方案。
    • 根据您的要求,我已将您的答案更新为使用 bindtags,这样就不需要延迟了。
    • @BryanOakley:嗨,布莱恩,我尝试了您在我的答案中编辑的代码,但它对我不起作用。插入该代码似乎完全消除了输入的能力。通过将print 语句插入highlightCurrentLine 函数,我可以看到该函数被调用,但实际上从未插入文本。我回滚到编辑之前。如果这有什么不同,我正在使用 Python 2.7.6 和 Tcl 8.5。
    【解决方案3】:

    只是为了扩展 Bryan Oakley 的回答:当您想要选择一些包含当前行的部分但不是全部的文本时,简单地突出显示当前行会变得很痛苦,因为当前行的选定部分是使用当前行颜色而不是选定文本颜色着色,因此很难一眼看出当前行的哪一部分被选中。

    所以我修改了 Bryan 的示例代码,如果选择了任何文本,则不会突出显示当前行:

    import Tkinter as tk
    
    class MyApp(tk.Tk):
        def __init__(self, *args, **kwargs):
            tk.Tk.__init__(self, *args, **kwargs)
            self.text = tk.Text(self)
            self.text.pack(side="top", fill="both", expand=True)
            self.text.tag_configure("current_line", background="#e9e9e9")
            self._highlight_current_line()
    
        def _highlight_current_line(self, interval=100):
            '''Rehighlights the 'current line' every "interval" milliseconds'''
            self.text.tag_remove("current_line", 1.0, "end")
    
            # If text is selected, DON'T highlight the current line.
            try:
                text = self.text.get(tk.SEL_FIRST, tk.SEL_LAST)
            except tk.TclError:
                self.text.tag_add("current_line",
                                    "insert linestart", "insert lineend+1c")
    
            self.after(interval, self._highlight_current_line)
    
    app = MyApp()
    app.mainloop()
    

    我希望这对某人有所帮助。

    【讨论】:

      猜你喜欢
      • 2021-11-15
      • 1970-01-01
      • 2011-04-16
      • 2011-02-08
      • 1970-01-01
      • 1970-01-01
      • 2012-07-23
      相关资源
      最近更新 更多