Context: this is part of a program I am making that builds a form with Tkinter that respects a specific json schema.
It works recursively and build_dict_form in called every time an “object” type (that actually corresponds to the dict type in python) is encountered in the schema. build_rec_form will call another function to complete that part of the form.
def build_dict_form(schema, master): answers = {} def btn_k_cmd_factory(k, k_properties, k_frame): def res(): if k in answers: del answers[k] #TODO: delete subframe of k_frame else: answers[k] = build_rec_form(k_properties, k_frame) # btn_k.text = "Cancel" return res properties = schema["properties"] if "maxProperties" not in schema: for k in properties: frm_k = tk.Frame(master=master, bd=4, highlightbackground="red", highlightthickness=0.5) lbl_k = tk.Label(master=frm_k, text=k) lbl_k.pack() answers[k] = build_rec_form(properties[k], frm_k) frm_k.pack() else: for k in properties: frm_k = tk.Frame(master=master, bd=4, highlightbackground="red", highlightthickness=0.5) lbl_k = tk.Label(master=frm_k, text=k) btn_k = tk.Button(master=frm_k, text="choose", command=btn_k_cmd_factory(k, properties[k], frm_k)) lbl_k.pack() btn_k.pack() frm_k.pack() return answers
My problem right now is to make it so that when I click on btn_k its text attribute changes to “Cancel” (the TODO is not part of this question).
- I can’t simply uncomment the
btn_k.text = "Cancel"
line since inside btn_k_cmd_factory I don’t have access to the btn_k object. - I can’t pass btn_k as a parameter for the factory since it is not yet created when the factory is called.
- For some reason I don’t understand I can’t either create the button without its command attribute and change it afterwards.
- I have to go through a factory, otherwise the different commands of the different buttons get scrambled, so tricks like this don’t work.
How can I make that work?
EDIT: if you need to make that run, you can use:
import tkinter as tk import jsonschema type_conversion = { "object": dict, "integer": int, "string": str, "array": list } def deep_eval(structure): t = type(structure) if t == dict: return {k: deep_eval(structure[k]) for k in structure} if t == list: return [deep_eval(k) for k in structure] else: return structure() def ask_new_item(schema): res = {} validated = {} def validate(): nonlocal validated validated = deep_eval(res) try: jsonschema.validate(instance=validated, schema=schema) window.destroy() except jsonschema.exceptions.ValidationError as e: print("schema not validated:") print(e) window = tk.Tk() btn_close = tk.Button(master=window, text="Validate", command=validate)#TODO: ["state"]=tk.DISABLED res = build_rec_form(schema, window) btn_close.pack() window.mainloop() return validated def build_rec_form(schema, master): frm_subform = tk.Frame(master=master, bd=4, highlightbackground="black", highlightthickness=0.5) helper_text = f"{schema.get('description', '<no description>')}, of type {schema['type']}" lbl_helper = tk.Label(master=frm_subform, text=helper_text) lbl_helper.pack() t_python = type_conversion[schema["type"]] res_method = None if t_python == str or t_python == int: untyped_method =build_str_form(schema, frm_subform) def res_method(): return t_python(untyped_method()) elif t_python == dict: res_method = build_dict_form(schema, frm_subform) elif t_python == list: res_method = build_list_form(schema, frm_subform) else: print(f"type not supported: {t_python}") frm_subform.pack() return res_method def build_dict_form(schema, master): answers = {} def btn_k_cmd_factory(k, k_properties, k_frame): def res(): if k in answers: del answers[k] #TODO: delete subframe of k_frame else: answers[k] = build_rec_form(k_properties, k_frame) # btn_k.text = "Cancel" return res properties = schema["properties"] if "maxProperties" not in schema: for k in properties: frm_k = tk.Frame(master=master, bd=4, highlightbackground="red", highlightthickness=0.5) lbl_k = tk.Label(master=frm_k, text=k) lbl_k.pack() answers[k] = build_rec_form(properties[k], frm_k) frm_k.pack() else: for k in properties: frm_k = tk.Frame(master=master, bd=4, highlightbackground="red", highlightthickness=0.5) lbl_k = tk.Label(master=frm_k, text=k) btn_k = tk.Button(master=frm_k, text="choose", command=btn_k_cmd_factory(k, properties[k], frm_k)) lbl_k.pack() btn_k.pack() frm_k.pack() return answers def build_list_form(schema, master): #TODO def res(): return [] return res def build_str_form(schema, master): ent_answer = tk.Entry(master=master) ent_answer.pack() return ent_answer.get if __name__ == "__main__": schema = { "type": "object", "properties": { "Prop1": { "type": "string" }, "Prop2": { "type": "integer" } } "maxProperties": 1 } print(ask_new_item(schema))
Advertisement
Answer
First, neither “text” nor “command” are an attribute of Button objects: accessing them requires going through btn['text']
and btn['command']
With that in mind it becomes possible to change the command of a button after it has been created:
btn_k = tk.Button(master=frm_k, text="choose") btn_k['command'] = btn_k_cmd_factory(k, properties[k], frm_k, btn_k)
and thus to have the button object as a parameter used in its own command function.
Based on @CoolCloud’s answer