1

I'm working on a (toplevel in a) GUI that consists of an array of 8 OptionMenus, each of them containing the same option list. Currently, Im building these widgets using a for-loop, and I save references in a dictionary. All OptionMenus link to the same (lambda) callback function.

To stay practical: the items in the option list represent a sequence of processing steps, and the user can alter the order of processes.

A change in one of the lists will result in one process being executed twice, and one process not at all. However, I want each item to occur only once. Hence, each user input should be accompanied by a second OptionMenu alteration.

For example: initial order 1-2-3 --> user changes the second process: 1-3-3, which autocorrects to: 1-3-2, where each process is again executed only once.

To my understanding, I can only get this to work if I have a reference to the OptionMenu that was just altered (from within the callback function). I was looking into passing the widget into the callback. The sample code is an attempt to implement the second suggested method, but the result is not what I would have expected.

The thing is that the OptionMenu widget seems to behave somewhat differently from other widgets. The OptionMenu does not allow for a re-defintion of the command function. No matter what input I pass along with the command function, the callback only seems to retrieve the OptionMenu selection, which is insufficient information for me to determine my process order.

Suggestions would be much apreciated!

import tkinter as tk

class Application(tk.Frame):

    def __init__(self, master=None):
        super().__init__(master)
        self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
        self.create_widgets()


    def create_widgets(self):
        self.active_procs = ['proc 1','proc 2','proc 3','proc 4',
                             'proc 5','proc 6','proc 7','proc 8']
        itemnr, widgets = dict(), dict()
        for index in range(8):
            name_construct = 'nr' + str(index)
            itemnr[name_construct] = tk.StringVar(root)
            itemnr[name_construct].set(self.active_procs[index])
            widgets[name_construct] = tk.OptionMenu(self, itemnr[name_construct], *self.active_procs,
                                                    command=lambda widget=name_construct:
                                                    self.order_change(widget))
            widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
                                         sticky="nwse", padx=10, pady=10)


    def order_change(self,widget):
        print(widget)


root = tk.Tk()
root.title("OptionMenu test")

app = Application(master=root)

root.mainloop()
NvdB
  • 13
  • 6
  • 1
    I read through the question a couple of times and still am not quite sure what you're asking. Can you be a bit more specific about what's not working? Also, can you make a [mcve] that doesn't require a package like numpy, or is this problem specifically related to using numpy and the OptionMenu together? – Bryan Oakley Aug 04 '17 at 20:39
  • Also, please fix the indentation of your code. – Bryan Oakley Aug 04 '17 at 20:39
  • I copy-pasted a working code, not sure why the indentation changed.. but that's solved now! Also took out the numpy array, you're right, I don't think it's related. What's currently not working: I want [from within the order_change callback] access to the string name_construct, or alternatively a reference to the exact OptionMenu that was changed. – NvdB Aug 04 '17 at 20:50

2 Answers2

1

The OptionMenu will pass the new value to the callback, so you don't have to do anything to get the new value. That's why your widget value isn't the value of name_construct -- the value that is passed in is overwriting the default value that you're supplying in the lambda.

To remedy this you simply need to add another argument so that you can pass the value of name_construct to the callback to go along with the value which is automatically sent.

It would look something like this:

widgets[name_construct] = tk.OptionMenu(..., command=lambda value, widget=name_construct: self.order_change(value, widget))
...
def order_change(self, value, widget):
    print(value, widget)

Note: the OptionMenu isn't actually a tkinter widget. It's just a convenience function that creates a standard Menubutton with an associated Menu. It then creates one item on the menu for each option, and ties it all together with a StringVar.

You can get the exact same behavior yourself fairly easily. Doing so would make it possible to change what each item in the menu does when selected.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thank you Bryan! Sometimes I still get stuck at silly things.. Adding a second argument helped! I tried to +1, but I'm below 15 rep.. :-) – NvdB Aug 05 '17 at 10:01
0

For those interested, below you can find an example code of how I got the widget behaviour I wanted. I took Bryan's advice to replace the OptionMenu for a Menubutton/Menu combination. I also made use of this post to find duplicate entries in my process order list.

Any thoughts or suggestions on how to implement this in a cleaner or shorter way, or how to get the same functionality with a different interface (e.g. drag and drop), are ofcourse welcome!

import tkinter as tk

class Application(tk.Frame):


    def __init__(self, master=None):
        super().__init__(master)
        self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
        self.create_widgets()


    def create_widgets(self):
        # Assisting text
        l1 = tk.Label(self, text = "Data in", font=(None, 15))
        l1.grid(row=0, column=2)
        l2 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
        l2.grid(row=1, column=2)
        l3 = tk.Label(self, text = "Data out", font=(None, 15))
        l3.grid(row=11, column=2)
        l4 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
        l4.grid(row=10, column=2)

        # Process list
        self.active_procs = ['proc a','proc b','proc c','proc d',
                             'proc e','proc f','proc g','proc h']

        self.the_value, self.widgets, self.topmenu = dict(), dict(), dict()
        for index in range(8):
            name_construct = 'nr' + str(index)
            self.the_value[name_construct] = tk.StringVar(root)
            self.the_value[name_construct].set(self.active_procs[index])
            self.widgets[name_construct] = tk.Menubutton(self, textvariable=
                                            self.the_value[name_construct],
                                            indicatoron=True)
            self.topmenu[name_construct] = tk.Menu(self.widgets[name_construct],
                                            tearoff=False)
            self.widgets[name_construct].configure(menu=self.topmenu[name_construct])

            for proc in self.active_procs:
                self.topmenu[name_construct].add_radiobutton(label=proc, variable=
                                              self.the_value[name_construct],
                                              command=lambda proc=proc,
                                              widget=name_construct:
                                              self.order_change(proc,widget))
            self.widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
                                              sticky="nwse", padx=10, pady=10)


    def order_change(self,proc,widget):

        # Get the index of the last changed Menubutton
        index_user_change = list(self.widgets.keys()).index(widget) 

        procs_order = [] # Current order from widgets
        for index in range(8):
            name_construct = 'nr' + str(index)
            procs_order.append(self.widgets[name_construct].cget("text"))

        # 1 change may lead to 1 double and 1 missing process
        doubles = self.list_duplicates_of(procs_order,proc) 

        if len(doubles) == 2: # If double processes are present... 
            doubles.remove(index_user_change) # ...remove user input, change the other
            missing_proc = str(set(self.active_procs)^set(procs_order)).strip('{"\'}')
            index_change_along = int(doubles[0])

            # Update references
            self.active_procs[index_user_change] = proc 
            self.active_procs[index_change_along] = missing_proc 

            # Update widgets
            name_c2 = 'nr'+str(index_change_along)
            self.the_value[name_c2].set(self.active_procs[index_change_along])
            self.widgets[name_c2].configure(text=missing_proc)


    def list_duplicates_of(self,seq,item):
        start_at = -1
        locs = []
        while True:
            try:
                loc = seq.index(item,start_at+1)
            except ValueError:
                break
            else:
                locs.append(loc)
                start_at = loc
        return locs    


root = tk.Tk()
root.title("OptionMenu test")

app = Application(master=root)

root.mainloop()
NvdB
  • 13
  • 6