2

I am working on an application that is supposed to support both running from a console and from a GUI. The application has several options to choose from, and since in both running modes the program is going to have the same options obviously, I made a generalisation:

class Option:
    def __init__(self, par_name, par_desc):
        self.name = par_name
        self.desc = par_desc

class Mode():
    def __init__(self):
        self.options = []
        self.options.append(Option('Option1', 'Desc1'))
        self.options.append(Option('Option2', 'Desc2'))
        self.options.append(Option('Option3', 'Desc3'))
        self.options.append(Option('Option4', 'Desc4'))
        self.options.append(Option('Option5', 'Desc5'))
        #And so on

The problem is that in GUI, those options are going to be buttons, so I have to add a new field to an Option class and I'm doing it like this:

def onMouseEnter(par_event, par_option):
    helpLabel.configure(text = par_option.desc)
    return

def onMouseLeave(par_event):
    helpLabel.configure(text = '')
    return

class GUIMode(Mode):
    #...
    for iOption in self.options:
        iOption.button = Button(wrapper, text = iOption.name, bg = '#004A7F', fg = 'white')
        iOption.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event, iOption))
        iOption.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
    #...

There is also a "help label" showing the description of the option every time a mouse hovers over it, so there I am binding those functions.

What is happening is that while I am indeed successfully adding a new field with a button, the bind function seems to mess up and the result is this:

enter image description here

Help label is always showing the description of the last option added, no matter over which button I hover. The problem seems to go away if I directly modify the Option class instead, like this:

class Option:
    def __init__(self, par_name, par_desc):
        self.name = par_name
        self.desc = par_desc
        self.button = Button(wrapper, text = self.name, bg = '#004A7F', fg = 'white')
        self.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event, self))
        self.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))

But I obviously can't keep it that way because the console mode will get those fields too which I don't really want. Isn't this the same thing, however? Why does it matter if I do it in a constructor with self or in a loop later? I therefore assume that the problem might be in a way I dynamically add the field to the class?

Here is the full minimal and runnable test code or whatever it is called, if you want to mess with it: http://pastebin.com/0PWnF2P0

Thank you for your time

0

2 Answers 2

1

The problem is that the value of iOption is evaluated after the

for iOption in self.option:

loops are complete. Since you reset iOption on each iteration, when the loop is completed iOption has the same value, namely the last element in self.options. You can demonstrate this at-event-time binding with the snippet:

    def debug_late_bind(event):
        print(iOption)
        onMouseEnter(event, iOption)

    for iOption in self.options:
        iOption.button = Button(wrapper, text = iOption.name,
            bg = '#004A7F', fg = 'white')
        iOption.button.bind('<Enter>', debug_late_bind)

which will show that all events that iOption has the same value.

I split out the use of iOption to debug_late_bind to show that iOption comes in from the class scope and is not evaluated when the bind() call is executed. A more simple example would be

def print_i():
     print(i)

for i in range(5):
    pass

print_i()

which prints "4" because that is the last value that was assigned to i. This is why every call in your code to onMouseEnter(par_event, iOption) has the same value for iOption; it is evaluated at the time of the event, not the time of the bind. I suggest that you read up on model view controller and understand how you've tangled the view and the controller. The primary reason this has happened is that you've got two views (console and tk) which should be less coupled with the model.

Extracting the .widget property of the event is a decent workaround, but better still would be to not overwrite the scalar iOption, but instead use list of individual buttons. The code

for n, iOption in enumerate(self.options):

would help in creating a list. In your proposed workaround, you are encoding too much of the iOption model in the tkinter view. That's bound to bite you again at some point.

Sign up to request clarification or add additional context in comments.

4 Comments

I see, so instead of adding a new field to Option, I create a list of buttons, which I also add in the dictionary and keep the workaround from my answer? Or did I misunderstand
I also can't seem to grasp the idea of "iOption is evaluated after the loops are complete". Shouldn't iOption be iterating over each value in self.options and thus being evaluated at the begginning?
I added the section beginning "I split out the…" in hopes of addressing your questions.
Thanks, I think I understand now. Console has had no problems because it is pretty much completely decoupled, and I see now that I definitely should not add the button field to Option, because it would further couple the GUI view. So I should be keeping the buttons in the view, and I'm pretty sure that I can use the dictionary part and current solution, it doesn't couple the model really, right?
0

I don't know what the actual problem was with my original code, but I kind of just bypassed it. I added a dictionary with button as a key and option as a value and I just used the par_event.widget to get the option and it's description, which is working fine:

buttonOption = {}

def onMouseEnter(par_event):
    helpLabel.configure(text = buttonOption[par_event.widget].desc)
    return

def onMouseLeave(par_event):
    helpLabel.configure(text = '')
    return

class GUIMode(Mode):
    def run(self):
        #...
        for iOption in self.options:
            iOption.button = Button(wrapper, text = iOption.name, bg = '#004A7F', fg = 'white')
            iOption.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event))
            iOption.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
            buttonOption[iOption.button] = iOption
        #...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.