I'm having trouble getting an event binding to work with python/tkinter. I'm simply trying to click and have the location printed, but every time I do this, "-1" is the result.
Here's my code
from Tkinter import *
import Tkinter
class make_list(Tkinter.Listbox):
def __init__(self,master, **kw):
frame = Frame(master)
frame.pack()
self.build_main_window(frame)
kw['selectmode'] = Tkinter.SINGLE
Tkinter.Listbox.__init__(self, master, kw)
master.bind('<Button-1>', self.click_button)
master.curIndex = None
#display the clicked location
def click_button(self, event):
self.curIndex = self.nearest(event.x)
print self.curIndex
#display the window, calls the listbox
def build_main_window(self, frame):
self.build_listbox(frame)
#listbox
def build_listbox(self, frame):
listbox = Listbox(frame)
for item in ["one", "two", "three", "four"]:
listbox.insert(END, item)
listbox.insert(END, "a list entry")
listbox.pack()
return
if __name__ == '__main__':
tk = Tkinter.Tk()
make_list(tk)
tk.mainloop()
updated code - I got rid of frame but I can't seem to figure out why I'm getting -1 for the first print statement in the function click_button
from Tkinter import *
import Tkinter
class make_list(Tkinter.Listbox):
#display the clicked location
def click_button(self, event):
##this block works
w = event.widget
index = int(w.curselection()[0])
value = w.get(index)
print value
##this doesn't
self.curIndex = self.nearest(event.y)
print self.curIndex
self.curIndex = event.widget.nearest(event.y)
print self.curIndex
#display the window, calls the listbox
def build_main_window(self):
self.build_listbox()
#listbox
def build_listbox(self):
listbox = Listbox()
listbox.bind('<<ListboxSelect>>', self.click_button)
for item in ["one", "two", "three", "four"]:
listbox.insert(END, item)
listbox.insert(END, "a list entry")
listbox.pack()
return
if __name__ == '__main__':
tk = Tkinter.Tk()
start = make_list(tk)
start.build_main_window()
start.mainloop()
In the comments of an answer you ask for the best practice. The best practice is to bind to <<ListboxSelect>> which will fire immediately after the item is selected in the listbox.
This answer to a similar question has an example.
listbox nearest item is found by y, not x.
self.nearest(event.x) # wrong
self.nearest(event.y) # right
Update: I didn't notice the real problem first:
listbox = Listbox(frame)
It's not the same listbox which you subclassed, it's another unrelated listbox. Your listbox (which is make_list) is empty, that's why it always returns -1 for nearest.
Perhaps subclassing a frame is a good idea (anyway, better than subclassing listbox and adding a frame with another listbox into it). Then you'll have to bind event on that real listbox which is not empty.
Quick way to see how it will work when fixed is to call nearest of a real listbox with event.widget:
self.curIndex = event.widget.nearest(event.y)
Related
So I was given some example code of how to essentially control multiple Listboxes within one function, it seems to work during the example code but after implementing i am struggling to see what I've missed out.
Example code:
import tkinter as tk
class MultiListbox(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
for i in range(5):
lb = tk.Listbox(self, height=10, exportselection=False)
lb.pack(side="left", fill="y")
for j in range(10):
lb.insert("end", f"Listbox {i+1} value {j+1}")
lb.bind("<Double-1>", self.removeSeq)
def removeSeq(self, event):
lb = event.widget
curselection = lb.curselection()
index = curselection[0] if curselection else None
for listbox in self.winfo_children():
listbox.delete(index)
root = tk.Tk()
mlb = MultiListbox(root)
mlb.pack(side="top", fill="both", expand=True)
root.mainloop()
Where I am trying to implement logic:
Imports
import tkinter as tk,tkinter.ttk as ttk, pyautogui, numpy, easygui, cv2, os, time, _thread, re, math, subprocess
from tkinter import BOTH, END, LEFT
pyautogui.FAILSAFE = True
Class
class Acgm003App:
def __init__(self, master=None):
for i in range(5):
self.lb = tk.Listbox(self.modeSelect)
self.lb.configure(background='#2f2a2d', exportselection='false', font='{Arial} 12 {}', foreground='#feffff', height='23')
self.lb.configure(relief='flat', width='12')
self.lb.pack(side='left')
for j in range(10):
self.lb.insert("end", f"Listbox {i+1},{j+1}")
self.lb.bind("<Double-1>", self.getIndexLB)
Function
def getIndexLB(self, event):
print('hello')
self.lb = event.widget
curselection = self.lb.curselection()
index = curselection[0] if curselection else None
for listbox in self.lb.winfo_children():
print(index)
listbox.delete(index)
pass
I am just not getting anything back at all, I put print('hello') there just to make sure it was binded correctly, it prints just fine, but no result.
The code is intended to delete listbox items in other listboxes by taking the corresponding index of the curselection, sort of a work around to a tk.treeview.
Let me know if you can help!
I didn't test it but I think you use it with wrong object.
Original code use
lb = tk.Listbox(self, ...)
to add listbox to self and later it searchs children in self
for listbox in self.winfo_children():
You add listbox to self.modeSelect
tk.Listbox(self.modeSelect, ...)
so you should search children in self.modeSelect
for listbox in self.modeSelect.winfo_children():
But this method can make problem if you add other widgets in self.modeSelect because it will try to use .delete(index) also on other widgets. And then you should check if you get tk.Listbox
for child in self.modeSelect.winfo_children():
if isinstance(child, tk.Listbox):
child.delete(index)
EDIT:
import tkinter as tk
class Acgm003App:
def __init__(self, master=None):
self.modeSelect = tk.Frame(master)
self.modeSelect.pack()
# other child in `self.modeSelect`
self.label = tk.Label(self.modeSelect, text="Hello World")
self.label.pack(side='top')
for i in range(5):
self.lb = tk.Listbox(self.modeSelect)
self.lb.pack(side='left')
for j in range(10):
self.lb.insert("end", f"Listbox {i+1},{j+1}")
self.lb.bind("<Double-1>", self.getIndexLB)
def getIndexLB(self, event):
self.lb = event.widget
curselection = self.lb.curselection()
index = curselection[0] if curselection else None
for child in self.modeSelect.winfo_children():
# check if child is `tk.Listbox` or other widget
if isinstance(child, tk.Listbox):
child.delete(index)
root = tk.Tk()
app = Acgm003App(root)
root.mainloop()
Below is a popup menu command I would like to send event data through. I want the event for its x and y data so I know what cell of the ttk styled treeview (being used as a table) to operate on. Currently it calls the "self.toggle_sort_bool" method but I want it to call the function at the end "self.sort_children(event, cur_tree_children)," but do not because I need to figure out passing/receiving the event here. Note: I know that sending is automatic but receiving is not. Am I overlooking something?
self.heading_popup_menu = tk.Menu(self.treeview, tearoff=0)
self.heading_popup_menu.add_command(label="Reverse Sort", command=self.toggle_sort_bool)
Here is where the journey of the event begins with a right click on the ttk styled treeview.
self.treeview.bind('<Button-3>', self.pop_up_right_click_detail)
The event's x_root and y_root are sent to the tk_popup. Should/can I overload this to send the whole event? It seems the x and y of the event in the root are sent to tell the popup where to...pop up.
def pop_up_right_click(self, event):
try:
self.heading_popup_menu.tk_popup(event.x_root, event.y_root, 0)
finally:
self.heading_popup_menu.grab_release()
Here is the function I want to call from the menu command.
def sort_children(self, event, cur_tree_children):
region = self.treeview.identify("region", event.x, event.y)
if region == "heading":
#get column number
col = self.treeview.identify_column(event.x)
col = int(re.sub('\D', '', col))
col_names = cur_tree_children.pop(0)
cur_tree_children.sort(reverse=self.reverse_sort_bool.get(), key=lambda tup: self.sort_disparate_types(tup[col-1])) #toggle reverse somehow
cur_tree_children.insert(0, col_names)
self.depopulate_tree()
self.populate_tree()
Is it possible to send an event through a menu? I am confused because of how disjointed the--for lack of better terminology--events are in calling a function through a right click pop up menu. While this is all part of one big GUI class, I do not want to use class instance variables to communicate the target cell data because I believe that is messy and bad practice and thus should be avoided wherever possible.
P.S. If I had enough reputation I would make the tag BryanOakley and post this under it.
A common way to do what you want is to modify the menu command immediately before displaying the menu. You can either define postcommand which defines a function that is run before the menu is displayed, or you can do the modification in the code that causes the menu to pop up.
Since you want to pass the event to the function, the best solution is to modify the menu right before popping it up since that function already has the event object.
Another other option would be to have your function set some instance variables, rather than modifying the menu. You can then reference those instance variables in the function called from the menu.
Since you said you don't want to use instance variables, here's an example showing how to modify the menu:
def show_popup(self, event):
self.popup.entryconfig("Do Something", command=lambda: self.something(event))
self.popup.tk_popup(event.x_root, event.y_root)
Example
Here's a complete working example. The code displays a window with a treeview widget which has some dummy data. If you right-click over the treeview you will see a menu with one item. When you click that item it will display information in a label about where the click occurred.
import tkinter as tk
from tkinter import ttk
class Example(object):
def __init__(self):
self.root = tk.Tk()
self.treeview = ttk.Treeview(self.root, columns=("one", "two", "three"))
self.label = tk.Label(self.root, width=40)
self.label.pack(side="bottom", fill="x")
self.treeview.pack(fill="both", expand=True)
self.popup = tk.Menu(self.root, tearoff=False)
self.popup.add_command(label="Do something")
self.treeview.bind('<Button-3>', self.show_popup)
for column in self.treeview.cget("columns"):
self.treeview.column(column, width=50)
for i in range(10):
values = ("a%s" % i, "b%s" %i, "c%s" %i)
self.treeview.insert('', 'end', text="Item %s" % i, values=values)
def start(self):
self.root.mainloop()
def show_popup(self, event):
self.popup.entryconfig("Do something", command=lambda: self.do_something(event))
self.popup.tk_popup(event.x_root, event.y_root)
def do_something(self, event):
region = self.treeview.identify("region", event.x, event.y)
col = self.treeview.identify_column(event.x)
message = "you clicked %s,%s region=%s column=%s" % (event.x, event.y, region, col)
self.label.configure(text=message)
if __name__ == "__main__":
Example().start()
I'm trying to get tkinter to return the index of an item clicked in listbox. Here's my code.
def fileSelection(self):
selection = listbox.curselection
print(selection)
listbox.bind("<Button-1>", fileSelection)
Right now it prints
bound method Listbox.curselection of tkinter.Listbox object at 0x00320E30
no matter what item is clicked on. If I change the code to include a button like this:
button = Button(text=u"test", command=OnButtonClick)
def OnButtonClick():
selection = listbox.curselection()
print(selection)
and select the Listbox item, then click the button, it will print the index of the selected item, as expected, but that's an extra step I don't want.
def fileSelection(self):
selection = listbox.curselection
print(selection)
Looks like you forgot the parentheses.
def fileSelection(self):
selection = listbox.curselection()
print(selection)
According to effbot.org, polling the widget allows you to do on click updates.
self.current = None
self.listbox = Listbox(self)
self.listbox.pack()
self.poll()
def poll(self):
now = self.listbox.curselection()
if now != self.current:
self.list_has_changed(now)
self.current = now
self.after(250, self.poll)
def list_has_changed(self, selection):
print "selection is", selection
There are a number of ways of getting callbacks when Text or Entry widgets are changed in Tkinter, but I haven't found one for Listbox's (it doesn't help that much of the event documentation I can find is old or incomplete). Is there some way of generating an event for this?
def onselect(evt):
# Note here that Tkinter passes an event object to onselect()
w = evt.widget
index = int(w.curselection()[0])
value = w.get(index)
print('You selected item %d: "%s"' % (index, value))
lb = Listbox(frame, name='lb')
lb.bind('<<ListboxSelect>>', onselect)
You can bind to the <<ListboxSelect>> event. This event will be generated whenever the selection changes, whether it changes from a button click, via the keyboard, or any other method.
Here's a simple example which updates a label whenever you select something from the listbox:
import tkinter as tk
root = tk.Tk()
label = tk.Label(root)
listbox = tk.Listbox(root)
label.pack(side="bottom", fill="x")
listbox.pack(side="top", fill="both", expand=True)
listbox.insert("end", "one", "two", "three", "four", "five")
def callback(event):
selection = event.widget.curselection()
if selection:
index = selection[0]
data = event.widget.get(index)
label.configure(text=data)
else:
label.configure(text="")
listbox.bind("<<ListboxSelect>>", callback)
root.mainloop()
This event is mentioned in the canonical man page for listbox. All predefined virtual events can be found on the bind man page.
I had the problem that I needed to get the last selected item in a listbox with selectmode=MULTIPLE. In case someone else has the same problem, here is what I did:
lastselectionList = []
def onselect(evt):
# Note here that Tkinter passes an event object to onselect()
global lastselectionList
w = evt.widget
if lastselectionList: #if not empty
#compare last selectionlist with new list and extract the difference
changedSelection = set(lastselectionList).symmetric_difference(set(w.curselection()))
lastselectionList = w.curselection()
else:
#if empty, assign current selection
lastselectionList = w.curselection()
changedSelection = w.curselection()
#changedSelection should always be a set with only one entry, therefore we can convert it to a lst and extract first entry
index = int(list(changedSelection)[0])
value = w.get(index)
tkinter.messagebox.showinfo("You selected ", value)
listbox = tk.Listbox(frame,selectmode=tk.MULTIPLE)
listbox.bind('<<ListboxSelect>>', onselect)
listbox.pack()
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