How to recognize a button from its command? - python

I have created a list of entries in a for-loop. All entries are stored in a list so I can just obtain all of the inputs later:
inputs = [e.get() for e in self.entries]
However, I have also created a button next to each entry in the for-loop (so they each call the same function). How can I make it so that it recognizes which button belongs to which row/entry? Is there something I can do with event?
row = 0
self.entries = []
self.comments = []
for n in names:
e = Entry(self.top, bd = 5)
e.insert(0, n)
e.grid(column = 1, row = self.row, sticky = 'NSWE', padx = 5, pady = 5)
self.entries.append(e)
self.comments += [""]
commentButton = Button(self.top, text = "comment", command = self.commentSelected)
commentButton.grid(column = 3, row = self.row, sticky = 'NSWE', padx = 5, pady = 5)
self.row = self.row + 1

Yes -- use Callback Shims ( Currying Functions )
( courtesy Russell Owen )
I find I often wish to pass extra data to a callback function, in addition that that normally given. For instance the Button widget sends no arguments to its command callback, but I may want to use one callback function to handle multiple buttons, in which case I need to know which button was pressed.
The way to handle this is to define the callback function just before you pass it to the widget and include any extra information that you require. Unfortunately, like most languages, Python doesn't handle the mixing of early binding (information known when the function is defined) and late binding (informtation known when the function is called) particularly well. I personally find the easiest and cleanest solution is:
Write my callback function to take all desired data as arguments.
Use a callback shim class to create a callable object that stores my function and the extra arguments and does the right thing when called. In other words, it calls my function with the saved data plus the data that the caller supplies.
I hope the example given below makes this clearer.
The callback shim I use is RO.Alg.GenericCallback, which is available in my RO package. A simplified version that does not handle keyword arguments is given in the example below. All shim code is based on a python recipe by Scott David Daniels, who calls this "currying a function" (a term that is probably more common than "callback shim").
#!/usr/local/bin/Python
""" Example showing use of a callback shim"""
import Tkinter
def doButton(buttonName):
""" My desired callback.
I'll need a callback shim
because Button command callbacks receive no arguments.
"""
print buttonName, "pressed"
class SimpleCallback:
""" Create a callback shim.
Based on code by Scott David Daniels
(which also handles keyword arguments).
"""
def __init__(self, callback, *firstArgs):
self.__callback = callback
self.__firstArgs = firstArgs
def __call__(self, *args):
return self.__callback (*(self.__firstArgs + args))
root = Tkinter.Tk()
buttonNames = ( "Button 1", "Button 2", "Button 3" )
for name in buttonNames:
callback = SimpleCallback( doButton, name )
Tkinter.Button( root, text = name, command = callback ).pack()
root.mainloop()

You can also use lambda:
from tkinter import *
def bla(b):
...
root = Tk()
buttons = []
for i in range(...):
button = Button(root)
button.configure(command=lambda b=button: bla(b)) # Make sure the Button object already exists
buttons.append(button)
button.pack()
root.mainloop()
As far as I see, you can't create the buttons in a single list comprehension now, but it is simpler and more readable than a class imho.

Related

communication between Checkbutton and Entry(box)

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:
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?
It is possible, but let me preface my solution with a few cautions on your current code:
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()
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

Pausing from within a tkinter function

I'm trying to create a couple of functions which do things in a sequential order. First they need to open a new window and display a label, then they need to wait for some seconds, then they need to call another function. However, I'm struggling to get the functions to wait, all the methods I've tried (.after, .sleep, .wait_visibility) seem to be ignored and it just skips to the next function call without pausing.
Here's what I have (sorry if it's messy, I'm new to python):
from tkinter import *
import time
root =Tk()
root.geometry('600x600')
def scale_screen(event = None):
global s_screen
s_screen = Toplevel(root)
s_screen.title('Residual Inhibition Tester')
s_screen.geometry('600x600')
s_screen.transient(root)
s_screen.bind('<Return>', sel)
global var
var = IntVar()
scale = Scale(s_screen, variable = var, orient = HORIZONTAL, length = 1000)
scale.focus_set()
scale.pack(anchor=CENTER)
button = Button(s_screen, text="Select", command=sel)
button.pack(anchor=CENTER)
def sel(event = None):
label = Label(s_screen)
selection = "Value = " + str(var.get())
label.config(text = selection)
interval_screen()
def interval_screen():
global i_screen
i_screen = Toplevel(root)
i_screen.geometry('600x600')
i_screen.transient(root)
i_label = Label(i_screen, text = "Please Wait")
i_label.pack(anchor = CENTER)
s_screen.destroy()
i_screen.after(3000, masker_screen)
#time.sleep(3)
#i_screen.after(300,i_label.configure(text="Playing New Masker Noise"))
#root.wait_visibility(window = i_screen)
def masker_screen():
global m_screen
m_screen = Toplevel(root)
m_screen.geometry('600x600')
m_screen.transient(root)
m_label = Label(m_screen, text = "Playing New Masker Noise").pack(anchor = CENTER)
m_screen.after(3000, lambda: scale_screen(event = None))
i_screen.destroy()
b1 = Button(root, command = scale_screen).pack(anchor=CENTER)
root.bind('<Return>', scale_screen)
root.mainloop()
In this example, the program will run but just skip the interval_screen entirely and just do the masker_screen. I'm also not averse to just using one screen and using the .configure methods to change the label text if that's easier.
Thanks!
Without seeing all the ways you tried it, it's impossible to know what you did wrong. In general you should never call time.sleep and you should never call after with just a single argument. Also, when you use after with two arguments, the second argument must be a reference to a function.
The proper way to do this is to have your first function call your second function via after:
def interval_screen():
...
i_screen.after(3000, maker_screen)
def masker_screen():
...
m_screen.after(3000, lambda: scale_screen(event = None))
Note that in your updated question you're using after incorrectly:
m_screen.after(3000, scale_screen(event = None))
You're calling the function scale_screen(...) immediately, and giving the result of that to the after function. If you need to pass arguments to your function you must create another function that does not require arguments. The simplest way to do this is with lambda, though you can also use functools.partial or you can create your own function.

Python 3 How do I temporarily disable tracing variables

I am using Python 3.4.1 on windows, if that should help.
Q1: How do I temporarily disable tracing variables
I have a variable field with at trace, and I would like to temporarily disable the trace so that I can change the value of the field without triggering the call to the trace function.
Does it make sense?
And it might be that I am doing it all wrong (and I join a snippet of code):
I have a drop down list that shows a list of items I can choose from.
I have a second drop down list that shows, for each of the items in the first drop down menu, a list of "sub items", which of course must be updated when I change the first drop down menu.
Q2: The question is, how do I "repack" the second drop down menu when the first one is changed?
Here is the code:
import tkinter as tk
WORKINGWINDOWWIDTH = 800 # Width for the working window
WORKINGWINDOWHEIGHT = 800 # Height for the working window
root = tk.Tk()
w = tk.Canvas(root, width=WORKINGWINDOWWIDTH - 10, height=WORKINGWINDOWHEIGHT - 10, bg="darkred")
def display_parameters(*args):
print("args: {0}, and I have the following option: {1}".format(args, functionChoiceVar.get()))
if functionChoiceVar.get() == "Option 1":
print("I picked the first one...")
print("How do I repack the presets?")
elif functionChoiceVar.get() == "Option 2":
print("I picked the second one...")
return
def display_options(*args):
print("args: {0}, and I have the following suboption: {1}".format(args, presetChoiceVar.get()))
return
functionChoiceVar = tk.StringVar(root)
functionChoices = ['Option 1', 'Option 2']
functionOption = tk.OptionMenu(root, functionChoiceVar, *functionChoices)
functionOption.pack(side='left', padx=10, pady=10)
functionOption.place(x= 10, y=10)
functionChoiceVar.set('Option 1')
functionChoiceVar.trace("w", display_parameters)
presetChoiceVar = tk.StringVar(root)
presetChoices11 = ['Suboption 11', 'Suboption 12', 'Suboption 13', 'Suboption 14','Suboption 15']
presetChoices12 = ['Suboption 21', 'Suboption 22', 'Suboption 23', 'Suboption 24','Suboption 25']
presetOption = tk.OptionMenu(root, presetChoiceVar, *presetChoices11)
presetOption.pack(side='left', padx=10, pady=10)
presetOption.place(x= 100, y=10)
presetChoiceVar.set('Suboption 11')
presetChoiceVar.trace("w", display_options)
When you set a trace, tkinter will return an id which you can use to later remove the trace with the .trace_vdelete() method. To restart the trace, simply do what you did the first time.
An easy way to keep track of the trace id is to store it as an attribute right to the instance of a StringVar.
For example:
functionChoiceVar.trace_id = functionChoiceVar.trace("w", display_parameters)
...
functionChoiceVar.trace_vdelete("w", functionChoiceVar.trace_id)
(by the way, unrelated to the question that was asked, calling .pack() and then immediately calling .place() serves no purpose. You can remove the call to .pack() because it gets negated by the call to .place() )
A1: how to remove tracing temporarily & return it afterwards
aWriteTracerID = presetChoiceVar.trace( "w", aWriteHANDLER ) # SAVE <<aTracer>> ID#
# setup code continues
presetChoiceVar.trace_vdelete( "w", aWriteTracerID ) # REMOVE <<aTracer>>
# code
# that needs the write-tracer
# to have been removed
# ...
aWriteTracerID = presetChoiceVar.trace( "w", aWriteHANDLER ) # SET AGAIN
I ran into this myself while creating a new class which inherits from a base class taking a variable as an input keyword argument. So I developed the below routine which attempts to recover a named callback function from the globals() based on the fact that the Trace ID is nothing more than some numerical digits prepended to the original callback function name. I expect that those digits have some meaning, but I was not able to ascertain them. Here's the routine:
def get_observer_callback(id):
func = None
funcname = id.lstrip('0123456789')
if funcname != '<lambda>' and funcname in globals():
func = globals().get(funcname)
if type(func) is not type(lambda:()):
func = None
return func
While testing, a trace_id was 50360848my_callback and the callback function was identified as
<function my_callback at 0x03064370>
Interestingly, 50360848 from the wrapper is hex 0x03007210, which is in the same ballpark as hex 0x03064370 from the function description. I couldn't find a hard relationship between the two beyond that...
I would describe the above routine as a kludge, but maybe, given the Tk implementation, the above kludge is sufficient for named functions. Clearly it is not helpful for lambda functions. I expect that within Tk there's a registration table which holds all the desired information, including references to the function objects. Ideally a call which returns the function object from within the Tk internal table would be best. Any input is welcome.
The easiest way to remove a trace would be to say:
idx = 0 # This is the index of the first trace on tk_variable.
tk_variable.trace_remove(*tk_variable.trace_info()[idx])
To add the trace back, you can say:
tk_variable.trace_add('write', your_function)
To check if a variable is traced, you can say:
if tk_variable.trace_info():
# do something
pass

tkinter button commands with lambda in Python

ive tried searching for a solution but couldn't find one that works. I have a 2d list of tkinter Buttons, and i want to change their Text when it is clicked by the mouse. I tried doing this:
def create_board(number):
print(number)
for i in range (0,number):
buttonList.append([])
for j in range(0,number):
print(i,j)
buttonList[i].append(Button(root, text = " ", command = lambda: update_binary_text(i,j)))
buttonList[i][j].pack()
Then when it is clicked it calls this function:
def update_binary_text(first,second):
print(first,second)
buttonList[first][second]["text"] = "1"
When i click a button, it simply does nothing, i had the program display the indexes of the button that was clicked, and they ALL show 4, 4 (this is when the variable number=5) Is there an solution to this?
this is my first python attempt for a class.
Thanks
You can fix this problem by creating a closure for i and j with the creation of each lambda:
command = lambda i=i, j=j: update_binary_text(i, j)
You could also create a callback factory with references to the button objects themselves:
def callback_factory(button):
return lambda: button["text"] = "1"
And then in your initialization code:
for j in range(0, number):
new_button = Button(root, text=" ")
new_button.configure(command=callback_factory(new_button))
new_button.pack()
buttonList.append(new_button)
Whenever I need a collection of similar widgets, I find it's simplest to enclose them in an object and pass a bound-method as callback rather than playing tricks with lambda. So, instead of having a list like buttonList[] with widgets, create an object:
class MyButton(object):
def __init__(self, i, j):
self.i = i
self.j = j
self.button = Button(..., command = self.callback)
def callback(self):
. . .
Now, you have a list buttonList[] of these objects, rather than the widgets themselves. To update the text, either provide a method for that, or else access the member directly: buttonList[i].button.configure(. . .) And when the callback is activated, it has the entire object and whatever attributes you might need in self.

Combined Event Handling of widgets in TKinter

I am making a GUI Program in Tkinter and am running into problems.What I want to do is draw 2 checkboxes and a button. According to the user input next steps should take place. A part of my code has been shown below :-
CheckVar1 = IntVar()
CheckVar2 = IntVar()
self.C1 = Checkbutton(root, text = "C Classifier", variable = CheckVar1, onvalue = 1, offvalue = 0, height=5,width = 20).grid(row=4)
self.C2 = Checkbutton(root, text = "GClassifier", variable = CheckVar2, onvalue = 1, offvalue = 0, height=5, width = 20).grid(row=5)
self.proceed1 = Button(root,text = "\n Proceed",command = self.proceed(CheckVar1.get(),CheckVar2.get())).grid(row=6)
# where proceed prints the combined values of 2 checkboxes
The error that I am getting is typical ie a default value of both the selected checkboxes gets printed up and then there is no further input. The error that I get is NullType Object is not callable.
I searched on the net and I think the answer is related to lambda events or curry.
Please help ..
You're passing the value of self.proceed(CheckVar1.get(),CheckVar2.get()) to the Button constructor, but presumably what you want is for command to be set to a function which will call self.proceed(CheckVar1.get(),CheckVar2.get()) and return a new, possibly different value every time the button is pressed. You can fix that with a lambda, or by wrapping the call in a short callback function. For example, replace the last line with:
def callback():
return self.proceed(CheckVar1.get(), CheckVar2.get())
self.proceed1 = Button(root, text="\n Proceed", command=callback).grid(row=6)
This is pretty typical Tkinter. Remember: when you see a variable called command in Tkinter, it's looking for a function, not a value.
EDIT: to be clear: you're getting 'NullType Object is not callable' because you've set command to equal the return value of a single call to self.proceed (that's the NullType Object). self.proceed is a function, but its return value is not. What you need is to set command to be a function which calls self.proceed.
Like Peter Milley said, the command option needs a reference to a function (ie: give it a function name (ie: no parenthesis). Don't try to "inline" something, create a special function. Your code will be easier to understand and to maintain.

Categories

Resources