I am currently having a problem with Tkinter.Variable
initialization and naming.
Setup:
I have a window, with a frame (window_frame
) in it and menubar at the top (tk.Menu
). Each command
on the menu opens a new app, which clears window_frame
and re-fills it with all new widgets. Some of the widgets have a tk.Variable
attached to them (ex. tk.Variable
, tk.StringVar
, etc).
Problem:
I am using customized widget classes (for global configuration), and originally, I had the variables initialized inside the widget with a name based on the widget name, for example:
<-SNIP->
# In the widgets module:
class CustomWidget(tk.Entry):
def __init__(self, master=None, cnf={}, **kw):
value = kw.pop('value', None)
tk.Entry.__init__(self, master, cnf, **kw)
self.variable = tk.Variable(self, name=self._name + '_var', value=value)
def destroy_widgets(master):
for child in master.winfo_children():
destroy_children(child)
child.destroy()
# In each app module:
def main(window_frame):
# Destroy the widgets in the window_frame:
destroy_widgets(window_frame)
# Add new widgets to the window_frame:
widgets = dict(
child1 = dict(widget = CustomWidget(name='child1', master=master)),
child2 = dict(widget = CustomWidget(name='child2', master=master)),
# etc
)
# Keep track of the variable in the dict as well:
for key in widgets:
widgets[key]['variable'] = widgets[key]['widget'].variable
# Notes:
# window_frame is populated with widgets & a button to re-run main().
# At root level, window_frame also has a menu to run main()
# from various apps.
<-SNIP->
The expected behavior was that after the Widget (and its Variable) were destroyed, each tk.Variable
would be re-initialized, and have the new value.
This appeared to be working for all of the widgets except for ones based on ttk.Checkbutton
, which had the checkbutton going into an "alternate" state on app change/reload.
However upon further inspection, all the Widgets were affected.
My understanding is that after destroying the Widgets in window_frame
, the named tk.Variable
s would still be "set" in Tcl
when the widgets (and their embedded tk.Variable
s) were initialized. Therefore tkinter
would re-use the Tcl
variables with those names instead of initializing new tk.Variable
instances. However, by the time the code attempted to get()
the values from the tk.Variable
s (for example, to display a checkbutton in "selected" or "!selected" state
) the tk.Variable
was no longer "set" in Tcl
and the widget would behave erratically, since the tk.Variable
was not accessible via tk
's setvar()
or getvar()
methods.
Proposed solution:
Someone recommended I keep track of the tk.Variable
names at the root level, for example in a dict
that would have the variable names passed to it. Then reuse variables as I go instead of creating new Variables with each Widget. (Note: I quickly discovered the tk.Variable
instances would have to be kept track of in this dict
, not just the name
of the Variable!)
However, before I move forward, is there something else I am missing that is causing the tk.Variable
instances to behave erratically?
For example, since I am keeping all the widget information, including a pointer to the widget, in a dict
that is initialized on each main()
call in each app module. Perhaps these dict
s are not being properly deleted by the garbage collector when I run a new app's main()
? Should I propagate the dictionaries upward, and update a dict
at the root level, rather than initializing a new dict
inside the main()
of each app?
Initial fix:
My initial fix was to remove all the name
s I assigned to all of my tk.Variable
instances. This allowed tkinter
to initialize a new Variable upon each Widget initialization ("PYVAR_1", "PYVAR_2", etc.).
However, when I added a print
statement in an app to track the number of Tcl
variables, the number rapidly increased with each app change/refresh. The number of variables does not appear to decrease at any point, whether I wait several seconds and then change/refresh the app, or force window_frame.update()
after destroying the child widgets.
Example app module:
<-SNIP->
# App window definition:
def main(window_frame, **kw):
print(root.tk.call("info", "vars")) # This number always increases on main() call
destroy_children(window_frame)
# Add new widgets here
<-SNIP->
After quite a bit of investigation, I've determined that the issue was in lingering pointers in Python that were preventing Tcl
from deleting tk.Variable
s.
The solution was to:
dict
s into a root level dict
.dict
, keep a list of tuples (child_key, child_widget)
.destroy_children
to use the master keys' child widget tuples
, in order to pop()
the variables from the widget __dict__
s and del
the variables explicity.Refactored code snippets (not including unchanged methods):
<-SNIP->
# In the widgets module:
def destroy_widgets(master_dict, master):
# 'master' is now a key in 'master_dict' rather than a widget!
# Use (child, child_widget) tuples instead of master.winfo_children()
# to cycle through the children of master:
for child, widget in master_dict[master].children:
# Pop the child key from the master_dict:
master_dict.pop(child)
# Remove the Variable pointer from the Widget:
variable = widget.__dict__.pop('variable')
# Destroy the widget:
widget.destroy()
# Delete the Variable:
del variable
# Clear the master's children:
master_dict[master].children = []
# At this point, no pointers to widgets or Variables exist in Python,
# so Tcl can "unset" the named Variables without issue
# In each app module:
def main(window_dict): # Note change to window_dict from window_frame
# The number of Tcl variables number no longer increases on main() call:
print(root.tk.call("info", "vars"))
# Destroy the widgets in the window_frame:
destroy_widgets(window_dict, 'window_frame')
# Add new widgets to the window_frame:
master = window_dict['window_frame']['widget']
widgets = dict(
child1 = dict(widget = CustomWidget(name='child1', master=master)),
child2 = dict(widget = CustomWidget(name='child2', master=master)),
# etc
)
# Propagate upwards:
window_dict.update(**widgets)
# Add the children to the window_frame key:
if window_dict['window_frame'].get('children') is None:
window_dict['window_frame']['children'] = []
for key in widgets.keys():
# Change: Don't need to keep track of the variable in the dict:
# window_dict[key]['variable'] = window_dict[key]['widget'].variable
# Add the (child, child_widget) tuples to the master key:
window_dict['window_frame']['children'].append(
(key, window_dict[key]['widget']))
<-SNIP->
With this, I am considering this issue closed. The Variables are being "unset" properly in Tcl
, allowing the same variable names to be reused without the erratic behavior seen before.