views:

153

answers:

1

I have a simple program I'm writing to try to get some skills with Tkinter. The problem I'm running into here is that when I click on the different file names in the Listbox, the Label changes value one click behind whatever I'm currently clicking on. To replicate:

  1. Start the program, label is blank.
  2. Click on any of the entries in the Listbox, Label says '/path/file1'.
  3. Click on any other entry in the Listbox, Label changes to whatever you clicked on step 2.

In the console output, you can see that the Listbox's current item seems to be one behind whatever you click on, like the current value is getting set to the Label first and then the click is being processed to highlight the new item in the Listbox. What am I missing here?

(I'm still very new to Python, and even newer to Tk, so sorry for the non-idiomatic style and Hungarian notation for the widgets; it's helping me keep track of what widgets do what.)

import Tkinter as tk

class TkTest: # Some tutorials show "class ClassName(Frame):" here.  Do I need to pass a Tkinter Frame object or not?
    def __init__(self, master):
        # Some tutorials show "Frame.__init__(self, master)" here, if Frame is passed as a parameter to the class.  Is this needed?

        # Set up the main frame.
        self.fraMain = tk.Frame(master)
        self.fraMain.pack()

        # Set up a list box containing all the paths to choose from.
        self.lstPaths = tk.Listbox(self.fraMain)
        paths = [
            '/path/file1',
            '/path/file2',
            '/path/file3',
        ]
        for path in paths:
            self.lstPaths.insert(tk.END, path)
        self.lstPaths.bind('<Button-1>', self.update_label)
        self.lstPaths.pack()

        # Set up a label which should show the currently selected item from the list box.
        self.currentpath = tk.StringVar()
        self.lblCurrentPath = tk.Label(self.fraMain, textvariable=self.currentpath)
        self.lblCurrentPath.pack()

    def update_label(self, event):
        print self.lstPaths.get(tk.ACTIVE),
        print self.lstPaths.curselection()
        self.currentpath.set(self.lstPaths.get(tk.ACTIVE))

root = tk.Tk()
app = TkTest(root)
root.mainloop()
+1  A: 

The problem has to do with the fundamental design of Tk. The short version is, bindings on specific widgets fire before the default class bindings for a widget. It is in the class bindings that the selection of a listbox is changed. This is exactly what you observe -- you are seeing the selection before the current click.

The best solution is to bind to the virtual event <<ListboxSelect>> which is fired after the selection has changed. Other solutions (unique to Tk and what gives it some of its incredible power and flexibility) is to modify the order that the bindings are applied. This involves either moving the widget bindtag after the class bindtag, or adding a new bindtag after the class bindtag and binding it to that.

Since binding to <<ListboxSelect>> is the better solution I won't go into details on how to modify the bindtags, though it's straight-forward and I think fairly well documented.

Bryan Oakley
That did the trick, thanks! I have not seen any references to <<ListboxSelect>> anywhere in the tutorials I have been going through, and even googling for that exact word does not seem to give much information. Is there a list anywhere of all the virtual events? This really seems like an annoying 'gotcha' in Tk...
Sam
@Sam: There is no single list of all events. The Tk man pages list the events for each widget on the man page for that widget. So, for example, <<ListboxSelect>> is listed on the listbox man page here: http://tcl.tk/man/tcl8.4/TkCmd/listbox.htm
Bryan Oakley