Search code examples
python-3.xtkinterdeep-copy

Is there an alternative for deepcopy when working with tkinter?


Running into an issue while working with tkinter, here's a simple example of what I'm doing

import tkinter as tk
from tkinter import ttk
from typing import Any, Callable, Mapping, NamedTuple, Type


values = {
        'eat': ('something', 'nice'),
        'dirt': ('something', 'else'),
        'kid': ('something'), '': tuple()
    }

class MyTuple(NamedTuple):
    widget: Type[tk.Widget]
    config: Mapping[str, Any] = {}
    postcommand: Callable[[ttk.Combobox], None] = None


class Reg():
    def __init__(self, var):
        self.var = var
        self.widgets = (
                MyTuple(ttk.Label),
                MyTuple(ttk.Combobox, {'values': tuple(values.keys()), 'textvariable': self.var}),
                MyTuple(ttk.Combobox, postcommand=self.cb),
                MyTuple(ttk.Combobox, postcommand=self.other_cb),
            )

    def display(self, frame: tk.Tk):
        for w in self.widgets:
            widget = w.widget(frame, **w.config)
            widget.pack()
            if not w.postcommand is None:
                widget['postcommand'] = lambda widget=widget : w.postcommand(widget)

    def cb(self, combobox: ttk.Combobox):
        combobox['values'] = values[self.var.get()]

    def other_cb(self, combobox: ttk.Combobox):
        combobox['values'] = tuple(values.keys())


class GUI(tk.Tk):
    def __init__(self):
        super().__init__()
        v = tk.StringVar()
        reg = Reg(v)
        reg.display(self)

_ = GUI()
_.mainloop()

When running this, it seems that w gets overwritten and the same postcommand is set for all of the comboboxes. I tried changing display to look like this:

from copy import deepcopy

...

    def display(self, frame: tk.Tk):
        for w in self.widgets:
            widget = w.widget(frame, **w.config)
            widget.pack()
            if not w.postcommand is None:
                command = deepcopy(w.postcommand)
                widget['postcommand'] = lambda widget=widget : command(widget)

And I get the error TypeError: cannot pickle '_tkinter.tkapp' object.

Is there an alternative to deepcopy that I can use? All I could find was a way to overwrite the pickle function, but I can't figure out how to apply that here, since I can't even figure out what tkinter object is part of the deepcopy.

Alternatively, is there a way to fix the scoping issue with w?

(Using python 3.8.10)


Solution

  • The lambda that calls w.postcommand is not capturing the correct value of w. What happens if you change the lambda inside your loop to this?

    widget['postcommand'] = lambda widget=widget, w=w: w.postcommand(widget)
    

    Further reading: What do lambda function closures capture?