4

The program works as intended when I simply use tkinter's widgets. When I use ttk's widgets the program repeats itself twice. I tried almost everything in my knowledge to fix this, I believe that *args have something to do with it. Is there anyway to prevent my function _up_options from running twice?

from tkinter import *
from tkinter import ttk
root = Tk()

first = StringVar(root)
second = StringVar(root)
Ore = {'Options': [''], 'Yes': ['One'], 'No': ['Two']}
entry1 = ttk.OptionMenu(root, first, *Ore.keys())
entry2 = ttk.OptionMenu(root, second, '')
entry1.pack()
entry2.pack()


def _up_options(*args):
    print('update_options')
    ores = Ore[first.get()]
    second.set(ores[0])
    menu = entry2['menu']
    menu.delete(0, 'end')

    for line in ores:
        print('for')
        menu.add_command(label=line, command=lambda choice=line: second.set(choice))


first.trace('w', _up_options)

root.mainloop()

PS, I used *args in my function to work. If anyone can explain this, I would be very grateful

BardiaB
  • 9
  • 3
Magotte
  • 143
  • 10

4 Answers4

5

I think I figured this out. The problem is that the variable actually is set twice by the ttk OptionMenu.

Take a look at this piece of code from the tkinter OptionMenu:

for v in values:
    menu.add_command(label=v, command=_setit(variable, v, callback))

This adds a button to the menu for each value, with a _setit command. When the _setit is called it sets the variable and another callback if provided:

def __call__(self, *args):
    self.__var.set(self.__value)
    if self.__callback:
        self.__callback(self.__value, *args)

Now look at this piece of code from the ttk OptionMenu:

for val in values:
    menu.add_radiobutton(label=val,
        command=tkinter._setit(self._variable, val, self._callback),
        variable=self._variable)

Instead of a command this adds a radiobutton to the menu. All radiobuttons are "grouped" by linking them to the same variable. Because the radiobuttons have a variable, when one of them is clicked, the variable is set to the value of the button. Next to this, the same command is added as in the tkinter OptionMenu. As said, this sets the variable and then fires another command of provided. As you can see, now the variable is updated twice, once because it is linked to the radiobutton and once more because it is set in the _setit function. Because you trace the changing of the variable and the variable is set twice, your code also runs twice.

Because the variable is set twice from within the ttk code, I guess there's not much you can do about that. If you don't change the variable from any other part of your code than from the OptionMenu though, you could choose to not trace the variable, but instead add your function as command to the OptionMenu:

entry1 = ttk.OptionMenu(root, first, *Ore.keys(), command=_up_options)

P.S. this was introduced with this commit after this bugreport.
I guess when adding the variable=self._variable the command should have been changed to just command=self._callback.

fhdrsdg
  • 10,297
  • 2
  • 41
  • 62
  • Now the question is why this does not happen in my PC I'm using Windows 10 and Python 3.6 in PycharmCE2018.2 any thoughts? – Moshe Slavin Nov 07 '18 at 10:26
  • Could you check your `ttk.py` file in Python36\Lib\tkinter to contain the `variable=self._variable` part? It's on line 1643 for me. `sys.version` is `3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 16:07:46) [MSC v.1900 32 bit (Intel)]`. – fhdrsdg Nov 07 '18 at 10:33
  • You are right! my file does not contain `variable=self._variable` I added it and I can see it been called twice! I removed it and saved it now it's back to normal! thanks for your time! – Moshe Slavin Nov 07 '18 at 10:56
2

You can understand the problem in the error message:

Exception in Tkinter callback Traceback (most recent call last): File "C:\Users\user\AppData\Local\Programs\Python\Python36\lib\tkinter__init__.py", line 1699, in call return self.func(*args) TypeError: _up_options() takes 0 positional arguments but 3 were given

Initially, you don't use _up_options When you change the Options you call _up_options to trace the first StringVar and change it to the value of the next object in the dictionary.

Now when you do that you are running on all the objects in the dictionary, therefore, you need the *args so the lambda function will run on all args given!

As for your problem:

When I use ttk's widgets the program repeats itself twice.

EDIT

See @fhdrsdg's answer!

The solution is just to change command=tkinter._setit(self._variable, val, self._callback) to command=self._callback.

Hope you find this helpful!

Moshe Slavin
  • 5,127
  • 5
  • 23
  • 38
  • 1
    I would actually recommend to not remove the `variable` since that has some other uses, like putting the checkmark at the right position. Instead I would change `command=tkinter._setit(self._variable, val, self._callback)` to `command=self._callback` – fhdrsdg Nov 07 '18 at 14:40
2

Instead of tracing the StringVar, add a callback as command argument for OptionMenu constructor.

Eva Lond
  • 175
  • 1
  • 8
0

I created a subclass of ttk.OptionMenu to solve this (as well as to provide slightly simpler usage of the widget and a more useful callback). I think this is a more stable approach than modifying the original class directly or just overriding the original method because it guarantees compatibility with potential changes to the built-in/original widget in future Tkinter versions.

class Dropdown( ttk.OptionMenu ):

    def __init__( self, parent, options, default='', variable=None, command=None, **kwargs ):

        self.command = command
        if not default:
            default = options[0]
        if not variable:
            variable = Tk.StringVar()

        if command:
            assert callable( command ), 'The given command is not callable! {}'.format( command )
            ttk.OptionMenu.__init__( self, parent, variable, default, *options, command=self.callBack, **kwargs )
        else:
            ttk.OptionMenu.__init__( self, parent, variable, default, *options, **kwargs )

    def callBack( self, newValue ):
        self.command( self, newValue )

You can then use it like this:

def callback( widget, newValue ):
    print 'callback called with', newValue
    print 'called by', widget

options = [ 'One', 'Two', 'Three' ]

dropdown = Dropdown( parent, options, command=callback )
dropdown.pack()

Besides avoiding the double-trace issue, other notable differences from the original ttk.OptionMenu includes not needing to supply a Tkinter variable or default value if you don't need them (the default item will be the first item in the options list if not provided), and being able to get the widget that called the callback function when it fires. The latter is very helpful if you have many dropdown widgets sharing the same callback and you need to know which one is being used within the call.

Soon after writing this, I also found another solution using lambda: Passing OptionMenu into a callback (or retrieving a reference to the used widget) I thought I might still share this Dropdown widget anyway since it can make the higher-level code simpler, and it provides a good base if you have some other custom methods to add in.

Durgan
  • 71
  • 5