Related
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()
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) 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?
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.
Importing 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
... or this, for python 2.x:
import Tkinter as tk
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.
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)
If you associate this with a text widget and then call the redraw method, it should display the line numbers just fine.
Automatically updating the line numbers
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.
In an answer to the question "https://stackoverflow.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.
A custom text class
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()
Here's my attempt at doing the same thing. I tried Bryan Oakley's answer above, it looks and works great, but it comes at a price with performance. Everytime I'm loading lots of lines into the widget, it takes a long time to do that. In order to work around this, I used a normal Text widget to draw the line numbers, here's how I did it:
Create the Text widget and grid it to the left of the main text widget that you're adding the lines for, let's call it textarea. Make sure you also use the same font you use for textarea:
self.linenumbers = Text(self, width=3)
self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS)
self.linenumbers.config(font=self.__myfont)
Add a tag to right-justify all lines added to the line numbers widget, let's call it line:
self.linenumbers.tag_configure('line', justify='right')
Disable the widget so that it cannot be edited by the user
self.linenumbers.config(state=DISABLED)
Now the tricky part is adding one scrollbar, let's call it uniscrollbar to control both the main text widget as well as the line numbers text widget. In order to do that, we first need two methods, one to be called by the scrollbar, which can then update the two text widgets to reflect the new position, and the other to be called whenever a text area is scrolled, which will update the scrollbar:
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)
Now we're ready to create the 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)
Voila! You now have a very lightweight text widget with line numbers:
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
Compare:
# assume each line is at least 6 pixels high
step = 6
step - how often (in pixels) program check text widget for new lines. If height of line in text widget is 30 pixels, this program performs 5 checks and draw only one number.
You can set it to value that <6 if font is very small.
There is one condition: all symbols in text widget must use one font, and widget that draw numbers must use the same font.
# 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))
...
After thoroughly reading through each solution mentioned here, and trying some of them out myself, I decided to use Brian Oakley's solution with some modifications. This might not be a more efficient solution, but should be enough for someone who is looking for a quick and easy to implement method, which is also simple in principle.
It draws the line's in the same manner, but instead of generating <<Change>> events, it simply binds the key press, scroll, left click events to the text as well as left click event to the scrollbar. In order to not glitch when, e.g. a paste command is performed, it then waits 2ms before actually redrawing the line numbers.
EDIT: This is also similair to FoxDot's solution, but instead of constantly refreshing the line numbers, they are only refreshed on the bound events
Below is an example code with delays implemented, along with my implementation of the scroll
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()
Also, I noticed that with Brian Oakley's code, if you use the mouse wheel to scroll (and the scrollable text is full), the top number lines sometimes glitch out and get out of sync with the actual text, which is why I decided to add the delay in the first place. Though I only tested it on my own implementation of Scrolled Text widget, so this bug might be unique to my solution, although it is still peculiar
There is a very simple method that I've used based on Bryan Oakley's answer above. Instead of listening for any changes made, simply "refresh" the widget using the self.after() method, which schedules a call after a number of milliseconds. Very simple way of doing it. In this instance I attach the text widget at instantation but you could do this later if you want.
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)
I am creating a card game in tkinter and need help with referencing to the frame names. My problem is that when I want to "refresh" the frame, I need to destroy and recreate it and this changes the progressive numbering of the frames.
Please take a look at the code below. The example shows that the third frame every time gets a new name as it gets recreated.
import tkinter as tk
class RootFrame(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.main_window = tk.Frame(self)
self.main_window.pack(side="top", fill="both", expand=True)
self.main_label = tk.Label(self.main_window, text="Main Window")
self.main_label.pack()
self.second_frame = SecondFrame(self.main_window, self)
self.second_frame.pack()
class SecondFrame(tk.Frame):
def __init__(self, parent, controller, *args, **kwargs):
super().__init__(*args, **kwargs)
self.controller = controller
label = tk.Label(self, text="Second Frame")
label.pack()
self.create_third_frame()
def create_third_frame(self):
self.third_frame = ThirdFrame(self, self.controller)
self.third_frame.pack()
def update_frame(self):
self.third_frame.destroy()
self.create_third_frame()
class ThirdFrame(tk.Frame):
def __init__(self, parent, controller, *args, **kwargs):
super().__init__(*args, **kwargs)
self.controller = controller
self.parent = parent
label = tk.Label(self, text="Third Frame")
label.pack()
refresh_button = tk.Button(
self, text="Resfresh", command=self.parent.update_frame)
refresh_button.pack()
print(self.winfo_name())
if __name__ == "__main__":
app = RootFrame()
app.mainloop()
The code above is used to illustrate the problem. Please run the code and you'll see the changing widget name in the terminal.
I use winfo_parent and winfo_name in my actual code to create different conditions for button bindings. For example, if the user clicks a widget1 in frame6 happens X and when I click a widget8 in frame2 happens Y. This works until I destroy() and recreate something and everything breaks.
I suppose that using winfo_name and winfo_parent for this kind of referencing is not the correct way to get around, but I really can't think of anything else.
I'm not sure exactly what you are asking, but you can assign a specific name to the widget:
def create_third_frame(self):
self.third_frame = ThirdFrame(self, self.controller, name='testframe')
self.third_frame.pack()
Then each time the name of the frame created will be consistent.
You can also reference the widget by name with Tk().nametowidget(), see this relevant answer here: Is it possible to search widget by name in Tkinter?
>>> from Tkinter import *
>>> win = Tk()
>>> button = Button( Frame( win, name = "myframe" ), name = "mybutton" )
>>> win.nametowidget("myframe.mybutton")
<Tkinter.Button instance at 0x2550c68>
I would recommend sticking with a OOP approach however, and just reference it with from your code like self.thirdframes where you might have a list or dict of ThirdFrame objects. This way your python code can easily reference the objects without going back to the tcl interpreter and parse the widget name. If you ever will only have one ThirdFrame, then just reference back to self.thirdframe whenever you need it.
i have a taskbar-like Frame, which contains custom Buttons with images. But everytime i click on this button, Tkinter displaced the button 1px to the right/buttom.
Is it possible to override this behaviour? Or do i have to derived from Tkinter.Label instead of Tkinter.Button ?
edit:
Adding some code:
import Tkinter
import logging
logger = logging.getLogger(__name__)
class DesktopBtn(Tkinter.Button):
'''
Represents a Button which can switch to other Desktops
'''
_FONTCOLOR="#FFFFFF"
def getRelativePath(self,folder,name):
import os
dir_path = os.path.dirname(os.path.abspath(__file__))
return os.path.abspath(os.path.join(dir_path, '..', folder, name))
def __init__(self, parent,desktopManager,buttonName, **options):
'''
:param buttonName: Name of the button
'''
Tkinter.Button.__init__(self, parent, **options)
logger.info("init desktop button")
self._imagePath=self.getRelativePath('res','button.gif')
self._BtnPresspath = self.getRelativePath('res','buttonP.gif')
self._BtnPressImage = Tkinter.PhotoImage(file=self._BtnPresspath)
self._image = Tkinter.PhotoImage(file=self._imagePath)
self.bind('<ButtonPress-1>',self._on_pressed)
self.bind('<ButtonRelease-1>',self._on_release)
self._parent = parent
self._btnName = buttonName
self._desktopManager = desktopManager
self.config(width=70, height=65,borderwidth=0,compound=Tkinter.CENTER,font=("Arial", 9,"bold"),foreground=self._FONTCOLOR, text=buttonName,wraplength=64,image=self._image, command=self._onClickSwitch)
def _on_pressed(self,event):
self.config(relief="flat")
self.config(image=self._BtnPressImage)
def _on_release(self,event):
self.config(image=self._image)
def _onClickSwitch(self):
self.config(relief="flat")
logger.info("Buttonclickmethod onClickSwitch")
self._desktopManager.switchDesktop(self._btnName)
def getButtonName(self):
return self._btnName
You can disable the animation of a button by returning "break" in the widget's bind, which stops the propagation of bound functions.
So you can either alter the function you normally have bound to the button to return "break".
Or you can add another bind, this does however prevent any binds that are made after this one:
tkButton.bind("<Button-1>", lambda _: "break", add=True)
Not sure whether this works with your specialized button, but how the button moves when it's clicked seems to depend on it's relief style. With relief=SUNKEN, the button seems not to move at all when clicked, and with borderwidth=0 it appears to be indistinguishable from a FLAT button.
Minimal example:
root = Tk()
image = PhotoImage(file="icon.gif")
for _ in range(5):
Button(root, image=image, borderwidth=0, relief=SUNKEN).pack()
root.mainloop()
Note that you set and re-set the relief to FLAT multiple times in your code, so you might have to change them all for this to take effect.
I think I found some kind of a solution using relief and border:
closebut = Button(title, text="X", relief=SUNKEN, bd=0, command=close)
closebut.pack(side=RIGHT)
You can observe that I used relief = SUNKEN and then bd = 0 to get a nice FLAT effect on my button!
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()
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) 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?
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.
Importing 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
... or this, for python 2.x:
import Tkinter as tk
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.
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)
If you associate this with a text widget and then call the redraw method, it should display the line numbers just fine.
Automatically updating the line numbers
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.
In an answer to the question "https://stackoverflow.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.
A custom text class
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()
Here's my attempt at doing the same thing. I tried Bryan Oakley's answer above, it looks and works great, but it comes at a price with performance. Everytime I'm loading lots of lines into the widget, it takes a long time to do that. In order to work around this, I used a normal Text widget to draw the line numbers, here's how I did it:
Create the Text widget and grid it to the left of the main text widget that you're adding the lines for, let's call it textarea. Make sure you also use the same font you use for textarea:
self.linenumbers = Text(self, width=3)
self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS)
self.linenumbers.config(font=self.__myfont)
Add a tag to right-justify all lines added to the line numbers widget, let's call it line:
self.linenumbers.tag_configure('line', justify='right')
Disable the widget so that it cannot be edited by the user
self.linenumbers.config(state=DISABLED)
Now the tricky part is adding one scrollbar, let's call it uniscrollbar to control both the main text widget as well as the line numbers text widget. In order to do that, we first need two methods, one to be called by the scrollbar, which can then update the two text widgets to reflect the new position, and the other to be called whenever a text area is scrolled, which will update the scrollbar:
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)
Now we're ready to create the 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)
Voila! You now have a very lightweight text widget with line numbers:
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
Compare:
# assume each line is at least 6 pixels high
step = 6
step - how often (in pixels) program check text widget for new lines. If height of line in text widget is 30 pixels, this program performs 5 checks and draw only one number.
You can set it to value that <6 if font is very small.
There is one condition: all symbols in text widget must use one font, and widget that draw numbers must use the same font.
# 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))
...
After thoroughly reading through each solution mentioned here, and trying some of them out myself, I decided to use Brian Oakley's solution with some modifications. This might not be a more efficient solution, but should be enough for someone who is looking for a quick and easy to implement method, which is also simple in principle.
It draws the line's in the same manner, but instead of generating <<Change>> events, it simply binds the key press, scroll, left click events to the text as well as left click event to the scrollbar. In order to not glitch when, e.g. a paste command is performed, it then waits 2ms before actually redrawing the line numbers.
EDIT: This is also similair to FoxDot's solution, but instead of constantly refreshing the line numbers, they are only refreshed on the bound events
Below is an example code with delays implemented, along with my implementation of the scroll
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()
Also, I noticed that with Brian Oakley's code, if you use the mouse wheel to scroll (and the scrollable text is full), the top number lines sometimes glitch out and get out of sync with the actual text, which is why I decided to add the delay in the first place. Though I only tested it on my own implementation of Scrolled Text widget, so this bug might be unique to my solution, although it is still peculiar
There is a very simple method that I've used based on Bryan Oakley's answer above. Instead of listening for any changes made, simply "refresh" the widget using the self.after() method, which schedules a call after a number of milliseconds. Very simple way of doing it. In this instance I attach the text widget at instantation but you could do this later if you want.
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)
I'm using Python and Tkinter, and I want the equivalent of onchange event from other toolkits/languages. I want to run code whenever the user updates the state of some widgets.
In my case, I have many Entry, Checkbutton, Spinbox and Radiobutton widgets. Whenever any one of these changes, I want to run my code (in this case, update a text box on the other panel).
(just remember that user may interact with those widgets using either mouse or keyboard, and even using Ctrl+V to paste text)
I think the correct method is to use trace on a tkinter variable that has been assigned to a widget.
For example...
import tkinter
root = tkinter.Tk()
myvar = tkinter.StringVar()
myvar.set('')
mywidget = tkinter.Entry(root,textvariable=myvar,width=10)
mywidget.pack()
def oddblue(a,b,c):
if len(myvar.get())%2 == 0:
mywidget.config(bg='red')
else:
mywidget.config(bg='blue')
mywidget.update_idletasks()
myvar.trace('w',oddblue)
root.mainloop()
The w in trace tells tkinter whenever somebody writes (updates) the variable, which would happen every time someone wrote something in the Entry widget, do oddblue. The trace always passes three values to whatever function you've listed, so you'll need to expect them in your function, hence a,b,c. I usually do nothing with them as everything I need is defined locally anyway. From what I can tell a is the variable object, b is blank (not sure why), and c is the trace mode (i.e.w).
For more info on tkinter variables check this out.
How I would solve this in Tcl would be to make sure that the checkbutton, spinbox and radiobutton widgets are all associated with an array variable. I would then put a trace on the array which would cause a function to be called each time that variable is written. Tcl makes this trivial.
Unfortunately Tkinter doesn't support working with Tcl arrays. Fortunately, it's fairly easy to hack in. If you're adventurous, try the following code.
From the full disclosure department: I threw this together this morning in about half an hour. I haven't actually used this technique in any real code. I couldn't resist the challenge, though, to figure out how to use arrays with Tkinter.
import Tkinter as tk
class MyApp(tk.Tk):
'''Example app that uses Tcl arrays'''
def __init__(self):
tk.Tk.__init__(self)
self.arrayvar = ArrayVar()
self.labelvar = tk.StringVar()
rb1 = tk.Radiobutton(text="one", variable=self.arrayvar("radiobutton"), value=1)
rb2 = tk.Radiobutton(text="two", variable=self.arrayvar("radiobutton"), value=2)
cb = tk.Checkbutton(text="checked?", variable=self.arrayvar("checkbutton"),
onvalue="on", offvalue="off")
entry = tk.Entry(textvariable=self.arrayvar("entry"))
label = tk.Label(textvariable=self.labelvar)
spinbox = tk.Spinbox(from_=1, to=11, textvariable=self.arrayvar("spinbox"))
button = tk.Button(text="click to print contents of array", command=self.OnDump)
for widget in (cb, rb1, rb2, spinbox, entry, button, label):
widget.pack(anchor="w", padx=10)
self.labelvar.set("Click on a widget to see this message change")
self.arrayvar["entry"] = "something witty"
self.arrayvar["radiobutton"] = 2
self.arrayvar["checkbutton"] = "on"
self.arrayvar["spinbox"] = 11
self.arrayvar.trace(mode="w", callback=self.OnTrace)
def OnDump(self):
'''Print the contents of the array'''
print self.arrayvar.get()
def OnTrace(self, varname, elementname, mode):
'''Show the new value in a label'''
self.labelvar.set("%s changed; new value='%s'" % (elementname, self.arrayvar[elementname]))
class ArrayVar(tk.Variable):
'''A variable that works as a Tcl array variable'''
_default = {}
_elementvars = {}
def __del__(self):
self._tk.globalunsetvar(self._name)
for elementvar in self._elementvars:
del elementvar
def __setitem__(self, elementname, value):
if elementname not in self._elementvars:
v = ArrayElementVar(varname=self._name, elementname=elementname, master=self._master)
self._elementvars[elementname] = v
self._elementvars[elementname].set(value)
def __getitem__(self, name):
if name in self._elementvars:
return self._elementvars[name].get()
return None
def __call__(self, elementname):
'''Create a new StringVar as an element in the array'''
if elementname not in self._elementvars:
v = ArrayElementVar(varname=self._name, elementname=elementname, master=self._master)
self._elementvars[elementname] = v
return self._elementvars[elementname]
def set(self, dictvalue):
# this establishes the variable as an array
# as far as the Tcl interpreter is concerned
self._master.eval("array set {%s} {}" % self._name)
for (k, v) in dictvalue.iteritems():
self._tk.call("array","set",self._name, k, v)
def get(self):
'''Return a dictionary that represents the Tcl array'''
value = {}
for (elementname, elementvar) in self._elementvars.iteritems():
value[elementname] = elementvar.get()
return value
class ArrayElementVar(tk.StringVar):
'''A StringVar that represents an element of an array'''
_default = ""
def __init__(self, varname, elementname, master):
self._master = master
self._tk = master.tk
self._name = "%s(%s)" % (varname, elementname)
self.set(self._default)
def __del__(self):
"""Unset the variable in Tcl."""
self._tk.globalunsetvar(self._name)
if __name__ == "__main__":
app=MyApp()
app.wm_geometry("400x200")
app.mainloop()
You have three different ways of doing the same:
1) Use the built-in "command" configuration, like the one you use on buttons
import tkinter as tk
from tkinter import messagebox as tk_messagebox
def spinbox1_callback():
tk_messagebox.showinfo("Spinbox callback", "You changed the spinbox.")
if __name__ == "__main__":
master = tk.Tk()
spinbox1 = tk.Spinbox(master, from_=0, to=10, command=spinbox1_callback)
spinbox1.pack()
tk.mainloop()
2) Use the event bindings to capture specific events:
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
import tkinter as tk
from tkinter import messagebox as tk_messagebox
root = tk.Tk()
def callback(event):
tk_messagebox.showinfo("clicked at", event.x, event.y)
frame = tk.Frame(root, width=100, height=100)
frame.bind("<Button-1>", callback)
frame.pack()
root.mainloop()
3) "trace" changes on a tkinter variable classes, so if your widget uses a StringVar, BooleanVar, IntVar, or DoubleVar in the textvariable parameter, you will get a callback once it gets updated. https://effbot.org/tkinterbook/variable.htm
import tkinter as tk
from tkinter import messagebox as tk_messagebox
if __name__ == "__main__":
master = tk.Tk()
widget_contents = tk.StringVar()
widget_contents.set('')
some_entry = tk.Entry(master,textvariable=widget_contents,width=10)
some_entry.pack()
def entry1_callback(*args):
tk_messagebox.showinfo("entry callback", "You changed the entry %s" % str(args))
some_entry.update_idletasks()
widget_contents.trace('w',entry1_callback)
tk.mainloop()
It's quite late, but yet, somebody found something that might be useful.
The whole idea comes from #bryan Oakley's post
If I understand well, the main problem is to detech Entry widget's . To detect it in spinbox, Checkbutton and Radiobutton you can use command options when creating widget.
To catch the <onChange> in Entry widget you can use Bryan`s approach using Tcl, which generates this event. As I said, this is not my solution, I've only changed it slightly for this case.
For example:
import tkinter as tk
from tkinter import ttk
def generateOnChange(obj):
obj.tk.eval('''
proc widget_proxy {widget widget_command args} {
# call the real tk widget command with the real args
set result [uplevel [linsert $args 0 $widget_command]]
# generate the event for certain types of commands
if {([lindex $args 0] in {insert replace delete}) ||
([lrange $args 0 2] == {mark set insert}) ||
([lrange $args 0 1] == {xview moveto}) ||
([lrange $args 0 1] == {xview scroll}) ||
([lrange $args 0 1] == {yview moveto}) ||
([lrange $args 0 1] == {yview scroll})} {
event generate $widget <<Change>> -when tail
}
# return the result from the real widget command
return $result
}
''')
obj.tk.eval('''
rename {widget} _{widget}
interp alias {{}} ::{widget} {{}} widget_proxy {widget} _{widget}
'''.format(widget=str(obj)))
def onEntryChanged(event = None):
print("Entry changed")
def onCheckChanged(event = None):
print("Check button changed")
def onSpinboxChanged(event = None):
print("Spinbox changed")
def onRadioChanged(event = None):
print("Radio changed")
if __name__ == '__main__':
root = tk.Tk()
frame = tk.Frame(root, width=400, height=400)
entry = tk.Entry(frame, width=30)
entry.grid(row=0, column=0)
generateOnChange(entry)
entry.bind('<<Change>>', onEntryChanged)
checkbutton = tk.Checkbutton(frame, command=onCheckChanged)
checkbutton.grid(row=1, column=0)
spinbox = tk.Spinbox(frame, width=100, from_=1.0, to=100.0, command=onSpinboxChanged)
spinbox.grid(row=2, column=0)
phone = tk.StringVar()
home = ttk.Radiobutton(frame, text='Home', variable=phone, value='home', command=onRadioChanged)
home.grid(row=3, column=0, sticky=tk.W)
office = ttk.Radiobutton(frame, text='Office', variable=phone, value='office', command=onRadioChanged)
office.grid(row=3, column=0, sticky=tk.E)
frame.pack()
root.mainloop()
Of course modify it to create different callback for plenty of instances (as you mentioned in the question) is easy now.
I hope somebody will find it useful.
So far, I have not encountered any thing equivalent of onChange in Tkinter.
Widgets can be bound to the various events and I have done that explicitly.
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm