Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to highlight the current line of a Text widget?

I'm working on a simple GUI code editor in Python, and I want to have the line of text on which the cursor sits to be highlighted at all times.

Right now, my TextEditor class looks like:

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)

How can I do this?

like image 557
JeremyKun Avatar asked Dec 28 '22 04:12

JeremyKun


2 Answers

There is nothing built in to tkinter that directly supports that. However, something that is good enough for most purposes would be to write a function that polls for the cursor position and updates the highlighting at regular intervals.

For example:

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

Obviously, the longer the interval the more "lag" that will be introduced, and the shorter the interval the more CPU is used, but there's a pretty large sweet spot between the extremes where there's almost no perceptible lag, and an imperceptible bump in CPU usage.

There's another way to do it that doesn't involve polling and is absolutely foolproof. You can move the highlight to precisely when the insertion cursor actually moves, but it involves writing some embedded Tcl code to create a proxy of the actual tk widget that is hidden in the implementation of the Tkinter Text object.

Finally, a third way is to set up custom bindings for all of the possible events that modify the cursor location. While possible, it's difficult to get 100% right since you have to account for all events that modify the cursor position, as well as handle places in your code that that might move the cursor without using an event. Still, using bindings is a perfectly good solution, it just requires a bit more work.

like image 150
Bryan Oakley Avatar answered Dec 31 '22 13:12

Bryan Oakley


There's absolutely no need to poll like Bryan Oakley says in his answer, nor do you need to embed Tcl code in your Python code. My solution is to just bind to the events which may end up moving the cursor, namely <Key> and <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()
like image 33
ArtOfWarfare Avatar answered Dec 31 '22 12:12

ArtOfWarfare