0

I am trying to generate a variable number of checkboxes and pass in a unique set of arguments for the command call function. At present all checkboxes when clicked only pass on the attributes of the last generated checkbox(see code below). Any help or suggestions you would be willing to offer would be wonderful. Thanks!

from Tkinter import *
import tkMessageBox
import ttk

root = Tk()

checkData = []
conditionID = []
def onCheck(conditionID,checkData):
    print checkData.get()
    print conditionID

for i in range(0,10): 
     checkData.append(BooleanVar())
     conditionID.append(i)
     l = ttk.Checkbutton(root, text ="",variable=checkData[i],command=lambda: onCheck(conditionID[i],checkData[i]), onvalue=True, offvalue=False)
     w=Message(root,background='ivory',text="test" + str(i),width=60)
     l.grid(column=1,row=i+1)
     w.grid(column=2,row=i+1)

root.mainloop()
Nas Banov
  • 28,347
  • 6
  • 48
  • 67
Mathieas
  • 5
  • 5

2 Answers2

3

You need to "bind" the value of the variables to the lambda at the time you define the lambda. You do it by passing the variables to the lambda function like so:

..., command=lambda id=conditionID[i], data=checkData[i]: onCheck(id, data), ...)

Note, however, that if you change conditionID or checkData later, this command won't notice that change. Arguably, a better solution is to do the lookup at runtime, by passing in i only:

..., command=lambda i=i: onCheck(conditionID[i], checkData[i]), ...
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • You have to be carreful with this solution: As the argument are resolve at lambda definition, it will never changed, whatever append. This works here cause the booleanVar is a mutable and there is still a "undirection". However, if you change the value of the conditionID list, onCheck() will never see it. – mgautierfr Jan 30 '14 at 15:04
  • @mgautier: true. The better solution is to pass in `i`, and do the lookup when the event fires. I've edited my answer. Thanks for pointing that out. – Bryan Oakley Jan 30 '14 at 17:26
2

This is a common problem in python and it is not directly related to tkinter:

Let's look this code :

i = 0

def foo():
    # here i will be lookup in the global namespace *when foo will be executed*
    print i

foo() # => 0
i = 1
foo() # => 1

# if we want to force the "evaluation" of i at function definition,
# we have to put it in the function definition
def bar(i=i):
    # here i is the function argument, and it default value is the value of the "global i" at the time the function was defined
    print i

bar() # => 1
i=2
bar() # => 1
foo() # => 2

In your case, this is the same problem but with lambda (and lambdas are functions) The evaluation of "i" in the lambda is made when at the execution of the lambda (when all checkbuttons are created and i==9)

So you have to define your command argument this way:

l = ttk.Checkbutton(root, text ="",variable=checkData[i],command=lambda i=i: onCheck(conditionID[i],checkData[i]), onvalue=True, offvalue=False)

or if you want to be more explicit:

l = ttk.Checkbutton(root, text ="",variable=checkData[i],command=lambda index=i: onCheck(conditionID[index],checkData[index]), onvalue=True, offvalue=False)

or more over:

for i in range(0,10): 
    checkData.append(BooleanVar())
    conditionID.append(i)
    # we define a new function (10 times)
    # i is resolve at function definition (now)
    def call_onCheck(index=i):
        # the arguments of onCheck are resolved at function execution.
        # it will take what is in the lists at the execution time.
        # it may change or not (in your case : not)
        return onCheck(conditionID[index], checkData[index])
    l = ttk.Checkbutton(root, text ="",variable=checkData[i],command=call_onCheck, onvalue=True, offvalue=False)
    w=Message(root,background='ivory',text="test" + str(i),width=60)
    l.grid(column=1,row=i+1)
    w.grid(column=2,row=i+1)

As the content of the lists don't change* (in the code you provide), you could also write:

from Tkinter import *
import tkMessageBox
import ttk

root = Tk()

# Those lists are not strictly necessary, but you may want to check this list from other function, So I keep it
checkData = []
# Here you store i the the i index of the list. I'm pretty sure this list is not necessary
conditionID = []

def onCheck(conditionID,checkData):
    print checkData.get()
    print conditionID

for i in range(0,10): 
    boolVar = BooleanVar()
    checkData.append(boolVar)
    conditionID.append(i)
    l = ttk.Checkbutton(root,
                        text ="",
                        variable=boolVar,
                        # we don't need to add a "resolution step" at execution as the values we will need are already known at lambda definition
                        command=lambda boolVar=boolVar, i=i :  onCheck(i, boolVal)),
                        onvalue=True, offvalue=False)
    w=Message(root,background='ivory',text="test" + str(i),width=60)
    l.grid(column=1,row=i+1)
    w.grid(column=2,row=i+1)

root.mainloop()

* The BooleanVar's in the list will change but it is the same booleanVar object, so from the list't point of view, values don't change.

mgautierfr
  • 729
  • 3
  • 8
  • Thank you all for the answers. I am quite new to Python and this type of interaction with a function is new to me. Just to clarify, when I assign values to each lambda I am actually setting up 10 functions each with hardset/locked values associated with 'i', conditionID[i] becomes conditionID[1] for i = 1 and whatever value is passed into lambda is the value of conditionID[1] and is no longer connected to the list conditionID[1]? – Mathieas Jan 30 '14 at 15:23
  • (I've update my answer.) Yes, you create 10 functions, you are not connected to i anymore but you still connected to the list conditionID. My last code proposition is totally unconnected from the lists. – mgautierfr Jan 30 '14 at 16:13