Skip to content
Advertisement

How to make a Tkinter Button change its own text attribute?

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

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement