How do I bind callbacks to a listbox using tkinter? - python

I'm playing around with tkinter and am struggling somewhat with the use of listboxes. I want to bind an arbitrary function to a selection event. As I understand it - it should go something like this.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
lb = tk.Listbox(self)
lb.insert("end", "one")
lb.insert("end", "two")
lb.insert("end", "three")
lb.bind("<Double-Button-1>", self.OnDouble)
lb.pack(side="top", fill="both", expand=True)
def OnDouble(self, event):
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
print "selection:", selection, ": '%s'" % value
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
My understanding is that when an element in the list box is double-clicked, a virtual event is created which can be bound to a callback function - in this case the OnDouble function defined.
I'm a little confused about the intricacies of this, would someone be able to explain exactly what is happening in this OnDouble function?
I'm trying to trigger an event when an element is selected from a listbox - i'm using pygubu to design the GUI but I understand that the syntax still follows tkinter conventions:
class TestApp:
def __init__(self):
self.builder = builder = pygubu.Builder()
builder.add_resource_path(PROJECT_PATH)
builder.add_from_file(PROJECT_UI)
self.mainwindow = builder.get_object('toplevel_1')
builder.connect_callbacks(self)
self.box = builder.get_object('listbox_1')
self.list = [1,2,3,4]
self.box.insert(tk.END, self.list[0])
self.box.insert(tk.END, self.list[1])
self.box.insert(tk.END, self.list[2])
self.box.insert(tk.END, self.list[3])
self.console = builder.get_object('text_1')
def print_console(self, msg):
self.console.configure(state="normal")
self.console.insert(tk.END, msg+"\n")
self.console.configure(state="disabled")
self.console.update_idletasks()
def run(self):
self.mainwindow.mainloop()
if __name__ == '__main__':
app = TestApp()
app.run()
Essentially I want the console to print (using the defined command) something like f'You have selected number {number}' when said number is double clicked on the list box. How can I do this? I would appreciate the workings behind it, so I can apply it to more complex exercises, and understand the solution rather than just copy/paste it.

Items in a listbox can be selected with a single click, but they can also be selected via the keyboard. For this reason, binding to a specific physical event such as <Double-1> is not the best way to be notified when the selection changes.
In the specific case of the listbox, binding to <<ListboxSelect>> is the best event to bind to, as that event will be generated after the selection has been set regardless of how it was set.
For example:
lb.bind("<<ListboxSelect>>", self.onSelect)
When a function is called via a binding it will automatically be passed an argument that contains details about the event that triggered the function call. One of the parameters is widget, which is a reference to the widget that received the call.
In your specific case you could ignore that and use self.listbox within the bound function, but using event.widget is a good habit to get into. That is why the code you copied starts with widget = event.widget
The next step is to get the selected item or items. In your case only a single selection is allowed at a time. However, the curselection method always returns a list. selection[0] will refer to the item selected, if there is a selection.
Note: the <<ListboxSelect>> event is triggered both when the selection is set or it is unset, so the code in your question could fail if the items are deselected by some means.
Finally, once you have the index of the selected item (eg: selection[0]), you can use that numerical index to get the text of the selected item. That is what the line value = widget.get(selection[0]) is doing.

Related

Python tkinter override default ctrl-h binding

I have no problem with binding ctrl-h. However, when I do the ctrl-h, I notice that the last character is also removed from the entry. I think this might be the default ctrl-h binding of python. How can I disable that?
---Update---
I have added the return 'break' thing. But it still doesn't work. The last character is immediately removed before the askstring dialog shows up. Here is the code that is bound.
def replace(self):
target = simpledialog.askstring(title = 'Replace', prompt = 'Replace Target')
if not target:
return 'break'
withValue = simpledialog.askstring(title = 'Replace', prompt = 'Replace With')
if not withValue:
return 'break'
for entry in self.entries.values():
setEntry(entry, entry.get().replace(target, withValue))
return 'break'
By the way I bind it with the master not the entry because I have a lot of entries. Binding with the master is way easier.
This is how I bind it.
self.master.bind('<Control-h>', lambda event: self.replace())
self.master is defined here:
class Generator(Frame):
def __init__(self, master):
Frame.__init__(self, master)
## init variables
self.master = master
This is what I pass in.
root = Tk()
gui = Generator(root)
gui.pack()
root.title('Generator')
root.mainloop()
Because you are binding to the root window rather than an individual widget, there's nothing you can do in your function. Bindings are processed in this order:
bindings on a specific widget
bindings on a widget class
bindings on the toplevel window in which the widget exists
bindings on the special tag "all"
If at any time in the processing of those bindings a function returns the string "break", no further processing will happen. Thus, if you have a binding on a specific widget and return "break", the default class binding won't be processed. However, if you return "break" from a binding to the root window, that binding isn't processed until after the class binding. Therefore, it's impossible for this sort of binding to prevent the default behavior.
However, tkinter bindings are remarkably customizable, so there are solutions. Given that you potentially want to inhibit the default behavior, the most straight-forward solution is to either bind to the class so that you completely replace the default behavior, or bind to each widget individually so that you can prevent the class binding from happening.
If you really want the binding to be universal by binding to the root window, then the easiest solution might be to change the order of processing for all widgets that have default bindings for control-h.
For example, to move the handling of root-level bindings before class-level bindings, you can do something like this:
entry = tk.Entry(root)
bindtags = entry.bindtags()
entry.bindtags((bindtags[2], bindtags[0], bindtags[1], bindtags[3]))
For more information on exactly how bindings are processed, you might want to look at the following questions:
Basic query regarding bindtags in tkinter
How to bind self events in Tkinter Text widget after it will binded by Text widget?
Here is a simple example that binds to the root window, but changes the bind tags so that the default binding can be defeated by returning "break":
import tkinter as tk
def custom_backspace(event):
entry.insert("insert", "<backspace>")
return "break"
root = tk.Tk()
entry = tk.Entry(root)
entry.pack(fill="x")
bindtags = entry.bindtags()
entry.bindtags((bindtags[2], bindtags[0], bindtags[1], bindtags[3]))
root.bind("<Control-h>", custom_backspace)
root.mainloop()

Python Tkinter how to hide a widget without removing it

I know similar things have been asked a lot, but I've tried to figure this out for two hours now and I'm not getting anywhere. I want to have a button in a Tkinter window that is only visible on mouseover. So far I failed at making the button invisible in the first place (I'm familiar with events and stuff, that's not what this question is about) pack_forget() won't work, because I want the widget to stay in place. I'd like some way to do it like I indicated in the code below:
import tkinter as tki
class MyApp(object):
def __init__(self, root_win):
self.root_win = root_win
self.create_widgets()
def create_widgets(self):
self.frame1 = tki.Frame(self.root_win)
self.frame1.pack()
self.btn1 = tki.Button(self.frame1, text='I\'m a button')
self.btn1.pack()
self.btn1.visible=False #This doesnt't work
def main():
root_win = tki.Tk()
my_app = MyApp(root_win)
root_win.mainloop()
if __name__ == '__main__':
main()
Is there any way to set the visibility of widgets directly? If not, what other options are there?
Use grid as geometry manager and use:
self.btn1.grid_remove()
which will remember its place.
You can try using event to call function.
If "Enter" occurs for button then call a function that calls pack()
and if "Leave" occurs for button then call a function that calls pack_forget().
Check this link for event description:List of All Tkinter Events
If you wish your button to stay at a defined place then you can use place(x,y) instead of pack()

TixComboBox subwidget not selecting/being set to specified value on first use

I have a TixExFileSelectDialog that is associated with several Entry objects in my program; the dialog is dynamically configured so that the selected file matches the text in the Entry the dialog is being used for. However, the first time the dialog is opened, regardless of which Entry is used, it only displays the pattern string, even when the Entry already has a default value. However, if I cancel out of the dialog and then reopen it, it displays the proper string. This happens when I set any combination of the combo box's selection and value options (one, the other, and both), as well as when I set the combo box's variable option to a StringVar. Is there something I'm missing in how TixComboBoxs function?
The code I'm currently using (with some reformatting/etc. for posting):
from tkinter.tix import *
opts = {'path': 'C:\\'}
class ImportGUI(Frame):
def _setfsd(self, directory='', pattern='*.xls', variable='', selection=None):
"Reconfigures the ExFileSelectDialog to enable reusability."
self.fsd.fsbox.config(directory=directory or opts['path'], # can't use opts['path'] as a default argument, because it could change.
pattern=pattern)
self.fsd.fsbox.file['variable'] = variable
if not variable:
self.fsd.fsbox.file['value'] = selection or pattern # Defaults to the pattern, which is the behavior of a fresh ExFileSelectionBox.
elif selection is not None: # variable exists, but setting selection manually as well
self.fsd.fsbox.file['value'] = selection
def _selectdatafile(self):
self._setfsd(selection='apple1.xls')
self.fsd.popup()
def _selectcodefile(self):
self._setfsd(selection='apple2.xls')
self.fsd.popup()
def createWidgets(self):
self.fsd = ExFileSelectDialog(self.master) # a top-level widget, so giving it the default root as master
self.dfrow = Frame(self)
self.cfrow = Frame(self)
self.dfentry = Entry(self.dfrow)
self.dfentry.pack(side='left')
self.cfentry = Entry(self.cfrow)
self.cfentry.pack(side='left')
self.dfbtn = Button(self.dfrow, text='...', command=self._selectdatafile)
self.dfbtn.pack(side='left')
self.cfbtn = Button(self.cfrow, text='...', command=self._selectcodefile)
self.cfbtn.pack(side='left')
self.dfrow.pack()
self.cfrow.pack()
self.pack()
def __init__(self, master=None):
Frame.__init__(self, master)
self.master.tk.eval('package require Tix')
self.createWidgets()
if __name__ == '__main__': # for testing
gui = ImportGUI()
gui.mainloop()
As it turns out, I had no need to do any of this in the first place because I can just use askopenfilename() from tkinter.filedialog to get exactly the functionality I desire, using the look-and-feel of the current OS. So much for Tix.
(Well, it wasn't exactly what I wanted, as the look was still a bit dated on Windows, but it was close enough. [IDLE seems to use it too, for that matter.])

accessing the return value of function that was bound to an event (tkinter)

Basically, what I've done is bound a click event to a function. For example:
self.button1.bind("<Button-1>",self.chooseDice)
What I want now is to access the result of chooseDice() in another function. What is the best way to go about doing that?
class GraphicsInterface:
#we want to initialize the game board here, set up the dice and buttons
def __init__(self):
self.window = Tk()
self.window.geometry("720x500")
#buttons under each die
self.clicked=[] #empty list to collect all the buttons that were clicked (see chooseDice function)
self.button1 = Button(self.window, text="Dice 1", width=13) #create the button object
self.button1.place(x=60, y=160)
#bind button click event to a function (chooseDice())
self.button1.bind("<Button-1>",self.chooseDice)
self.button2 = Button(self.window, text="Dice 2", width=13)
self.button2.place(x=185, y=160)
self.button2.bind("<Button-1>",self.chooseDice)
#using the event as an argument, append the text to the clicked list
def chooseDice(self, event):
self.clicked.append(event.widget.cget('text'))
self.diceList=[] #create a new empty list
for i in range(len(self.clicked)):
self.diceList.append(int(self.clicked[i][5])) #get just the int value of the last character (i.e. the dice number)
self.deactivate(event.widget) #deactivate the button
return self.diceList
You are already doing what you need to do. Your example code sets self.diceList to some value. Anywhere else in your code you can directly use self.diceList.
By the way -- you're writing code that is going to be hard to maintain over time. For example, what if you change the dice label to "Dice One" or simply "One" rather than "Dice 1"? Or, as your app progresses you might want graphical images instead of text on the buttons. You'll have to modify the code that parses the button name. You are essentially encoding information in a button label which is not a good idea.
A simple solution, that also makes your chooseDice method simpler and easier to understand, is to pass in the dice number in the callback. For example:
self.button1.configure(command=lambda btn=self.button1: self.chooseDice(btn, 1))
The above passes two parameters to the chooseDice method: the button instance (so you can disable it) and the button number (so you don't have to parse the button name to get it)
This also allows you to create your dice in a loop rather than hard-coding multiple copies of the same block of code. Here's a complete working example:
from Tkinter import *
class GraphicsInterface:
def __init__(self):
self.window = Tk()
self.window.geometry("720x500")
self.clicked=[]
self.buttons = []
for n in range(1, 3):
btn = Button(text="Button " + str(n))
btn.configure(command=lambda btn=btn, n=n: self.chooseDice(btn, n))
btn.pack()
self.buttons.append(btn)
btn = Button(text="Go!", command=self.go)
btn.pack()
self.window.mainloop()
def go(self):
print "buttons:", self.clicked
self.reset()
def reset(self):
'''Reset all the buttons'''
self.clicked = []
for button in self.buttons:
button.configure(state="normal")
def chooseDice(self, widget, number):
self.clicked.append(number)
widget.configure(state="disabled")
app = GraphicsInterface()
Finally, some last bits of advice:
Don't use place, it makes your GUIs harder to create, and they won't react well to changes in window size, changes in font, changes in platform, etc. Use pack and grid instead. Also, don't create fixed-width buttons. Again, this is to better handle changes in fonts. There are times when you want fixed width buttons, but it doesn't look like your code has any reason to use them.
Finally, I don't know what you're actually trying to accomplish, but usually if you're using buttons to track state (is this thing pressed or not?) you want to use checkboxes (pick N of N) or radiobuttons (pick 1 of N). You might want to consider switching to those instead of to buttons.
Refactor. Split this into two functions.
One returns the proper result, usable by other objects.
The other is bound to a GUI control, and uses the proper result to activate and deactivate GUI objects.
Indeed, you should always do this. You should always have functions that do normal Python stuff, work correctly without the GUI and can be unit tested without the GUI. Then you connect this working "model" to the GUI.
just add a self.result attribute to your class and set it at chooseDice()

Getting the widget that triggered an Event?

In Tkinter, I have a multiple widgets bound to the left-mouse button. They all trigger the same event when clicked.
How do I recover which widget was clicked?
I want it so when say Label2 was pressed, I'd be able to recover that Label2 was the widget that was pressed within the event it triggered.
def f(event):
caller = event.widget
You have a couple options. One way is to access the widget attribute of the event object. Another way is to pass an object reference in to your function. Here's an example that uses one of each.
import Tkinter as tk
def onClickA(event):
print "you clicked on", event.widget
event.widget.config(text="Thank you!")
def onClickB(event, obj):
print "you clicked on", obj
obj.config(text="Thank you!")
root = tk.Tk()
l1 = tk.Label(root, text="Click me")
l2 = tk.Label(root, text="No, click me!")
l1.pack()
l2.pack()
l1.bind("<1>", onClickA)
l2.bind("<1>", lambda event, obj=l2: onClickB(event, obj))
root.mainloop()
Sounds like all your widgets are sharing an event handler. This excerpt from the Tkinter 8.5 Reference by John W. Shipman - NM Tech, may help.
54.7. The extra arguments trick
Sometimes you would like to pass other
arguments to a handler besides the
event.
Here is an example. Suppose your
application has an array of ten
checkbuttons whose widgets are stored
in a list self.cbList, indexed by
the checkbutton number in range(10).
Suppose further that you want to write
one handler named .__cbHandler for
<Button-1> events in all ten of
these checkbuttons. The handler can
get the actual Checkbutton widget
that triggered it by referring to the
.widget attribute of the Event
object that gets passed in, but how
does it find out that checkbutton's
index in self.cbList?
It would be nice to write our handler
with an extra argument for the
checkbutton number, something like
this:
def __cbHandler(self, event, cbNumber):
But event handlers are passed only one
argument, the event. So we can't use
the function above because of a
mismatch in the number of arguments.
Fortunately, Python's ability to
provide default values for function
arguments gives us a way out. Have a
look at this code:
def __createWidgets(self):
...
self.cbList = [] # Create the checkbutton list
for i in range(10):
cb = Checkbutton(self, ...)
self.cbList.append(cb)
cb.grid(row=1, column=i)
def handler(event, self=self, i=i): # [1]
return self.__cbHandler(event, i)
cb.bind("<Button-1>", handler)
...
def __cbHandler(self, event, cbNumber):
...
[1] These lines define a new function
handler that expects three
arguments. The first argument is the
Event object passed to all event
handlers, and the second and third
arguments will be set to their default
values?the extra arguments we need to
pass it.
This technique can be extended to
supply any number of additional
arguments to handlers.
A slightly more concise way to do this, as #Bryan Oakley does for the second button in his answer, is to define each handler function in-line with a lambda expression, i.e.:
def __createWidgets(self):
...
self.cbList = [] # Create the checkbutton list
for i in range(10):
cb = Checkbutton(self, ...)
self.cbList.append(cb)
cb.grid(row=1, column=i)
cb.bind("<Button-1>", lambda event, self=self, i=i:
self.__cbHandler(event, i))
...
def __cbHandler(self, event, cbNumber):
...

Categories