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?
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.
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With