0
from tkinter import *
import pandas as pd

df = pd.DataFrame({'item': list('abcde'), 'default_vals': [2,6,4,5,1]})

def input_data(df):
    box = Tk()
    height = str(int(25*(df.shape[0]+2)))
    box.geometry("320x" + height)
    box.title("my box")

    #initialise
    params, checkButtons, intVars = [], [], []
    default_vals = list(df.default_vals)
    itemList = list(df.item)

    for i,label in enumerate(itemList):
        Label(box, text = label).grid(row = i, sticky = W)
        params.append(Entry(box))
        params[-1].grid(row = i, column = 1)
        params[-1].insert(i, default_vals[i])
        intVars.append(IntVar())
        checkButtons.append(Checkbutton(variable = intVars[-1]))
        checkButtons[-1].grid(row = i, column = 3)

    def sumbit(event=None):  
        global fields, checked
        fields = [params[i].get() for i in range(len(params))]
        checked = [intVars[i].get() for i in range(len(intVars))]
        box.destroy()

    #add submit button
    box.bind('<Return>', sumbit) 
    Button(box, text = "submit",
           command = sumbit).grid(row = df.shape[0]+3, sticky = W)                                          
    box.focus_force() 
    mainloop()        

    return fields, checked

I am new to tkinter and not sure what I a trying to do is possible.

At present, my script (simplified here to a function rather than a class) builds a box with all the default values entered in the fields:

enter image description here

Instead, I want to start with empty fields which, once the corresponding checkButton is clicked will get the default value (should still be able to manually change it through the field as happens now), and also, once any value is entered in a given field, the corresponding checkButton is selected.

Are these possible?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Tony
  • 781
  • 6
  • 22

1 Answers1

1

It is possible, but let me preface my solution with a few cautions on your current code:

  1. It's rarely advisable to do a star import (from tkinter import *) as you don't have any control over what gets imported into your namespace. It's more advisable to explicitly import what you need as a reference:

    import tkinter as tk
    
    tk.Label() # same as if you wrote Label()
    tk.IntVar() # same as if you called IntVar()
    
  2. The behaviour you wanted, while possible, might not be necessarily user friendly. What happens when a user has already entered something, and unchecks the checkbox? Or what happens if the checkbox was selected and then the user deleted the information? These might be things you want to think about.

Having said that, the solution is to use add a trace callback function over your variable(s). You'll also need to add a StringVar() for the Entry boxes as you wanted a two way connection:

# add strVars as a list of StringVar() for your Entry box
params, checkButtons, intVars, strVars = [], [], [], []

During your iteration of enumerate(itemList), add these:

# Create new StringVar()
strVars.append(StringVar())

# add a trace callback for tracking changes over the StringVar()
strVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_strVar(idx))

# update your Entry to set textvariable to the new strVar
params.append(Entry(box, textvariable=strVars[-1]))


# similarly, add a trace for your IntVar
intVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_intVar(idx))

You'll need to define the two trace callback functions before you iterate through the widget creations:

def trace_intVar(idx):

    # if Checkbox is checked and Entry is empty...     
    if intVars[idx].get() and not params[idx].get():

        # prefill Entry with default value
        params[idx].insert(0, df.default_vals[idx])

def trace_strVar(idx):

    # if Entry has something...
    if strVars[idx].get():

        # and Checkbox is not checked...
        if not intVars[idx].get():

            # Set the checkbox to checked.
            intVars[idx].set(True)

    # but if Entry is empty...
    else:

        # Set the Checkbox to uncheck.
        intVars[idx].set(False)

Remember I mentioned the behaviour - I took a little liberty to clear the Checkbox if Entry is empty. If you however don't wish to do that, you'll need to modify the handling a little.

Note on the way the trace_add is written. The callback function is always passed with three default arguments, namely the Variable Name, The Variable Index (if any) and Operation (see this great answer from Bryan Oakley). Since we don't need any in this case (we can't reverse reference the variable name to the linked index between the variable lists), we'll have to manually wrap the callback with another lambda and ignore the three arguments:

lambda var,        # reserve first pos for variable name
       var_idx,    # reserve second pos for variable index
       oper,       # reserve third pos for operation
       idx=i:      # pass in i by reference for indexing point
trace_intVar(idx)  # only pass in the idx

You cannot just pass lambda...: trace_intVar(i) as i will be passed by value instead of reference in that case. Trust me, I've made this error before. Therefore we pass another argument idx with its default set to i, which will now be passed by reference.

If trace_add doesn't work, use trace('w', ...) instead.


For prosperity, here's the complete implemented solution to your question:

from tkinter import *
import pandas as pd

df = pd.DataFrame({'item': list('abcde'), 'default_vals': [2,6,4,5,1]})

def input_data(df):
    box = Tk()
    height = str(int(25*(df.shape[0]+2)))
    box.geometry("320x" + height)
    box.title("my box")

    #initialise
    params, checkButtons, intVars, strVars = [], [], [], []
    default_vals = list(df.default_vals)
    itemList = list(df.item)

    def trace_intVar(idx):        
        if intVars[idx].get() and not params[idx].get():
            params[idx].insert(0, df.default_vals[idx])

    def trace_strVar(idx):
        if strVars[idx].get():
            if not intVars[idx].get():
                intVars[idx].set(True)
        else:
            intVars[idx].set(False)


    for i,label in enumerate(itemList):
        Label(box, text = label).grid(row = i, sticky = W)
        strVars.append(StringVar())
        strVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_strVar(idx))
        params.append(Entry(box, textvariable=strVars[-1]))
        params[-1].grid(row = i, column = 1)
        #params[-1].insert(i, default_vals[i])  # <-- You don't need this any more
        intVars.append(IntVar())
        intVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_intVar(idx))
        checkButtons.append(Checkbutton(variable = intVars[-1]))
        checkButtons[-1].grid(row = i, column = 3)



    def sumbit(event=None):  
        global fields, checked
        fields = [params[i].get() for i in range(len(params))]
        checked = [intVars[i].get() for i in range(len(intVars))]
        box.destroy()

    #add submit button
    box.bind('<Return>', sumbit) 
    Button(box, text = "submit",
           command = sumbit).grid(row = df.shape[0]+3, sticky = W)                                          
    box.focus_force() 
    mainloop()        

    return fields, checked
r.ook
  • 13,466
  • 2
  • 22
  • 39
  • Thank you very much for your response! I will go through it step by step to understand what you're doing but on a first note, copying, pasting and running the complete implementation you're providing in the end produces the following error:`AttributeError: 'StringVar' object has no attribute 'trace_add'` Would you care to double check please? – Tony Nov 21 '18 at 18:46
  • What version of Python/Tkinter are you using? It works on mine (Python 3.7.1). Are you on Python 2.7? – r.ook Nov 21 '18 at 18:50
  • Try `trace('w', ...)` instead of `trace_add('write', ...)` and see if it works? Change it for both the `strVars` and `intVars`. – r.ook Nov 21 '18 at 18:52
  • yes it worked like magic! thank you. I will go through the code now to see if I get it. I have `Python 3.5`, not sure about `tkinter` as `.__version__` isn't available. Was it a `tkinter` version issue then? Based on what did you change `trace_add('write',..)` to `trace('w'..)` ? Thanks again – Tony Nov 21 '18 at 18:58
  • 1
    I forgot where I came across `trace_add` and can't find it at the moment, but I believe it's to replace `trace` in future versions (perhaps I got a deprecation warning somewhere). [Here's the relevant tkinter documentation on `trace`](http://effbot.org/tkinterbook/variable.htm), and [here's a relevant bug report](https://bugs.python.org/issue22115) on the proposal to add the various `trace_...` methods. – r.ook Nov 21 '18 at 19:10