Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Destroying a Combobox prevents Entry focus

355 views
Skip to first unread message

Jonathan Lahav

unread,
Dec 26, 2021, 4:46:57 AM12/26/21
to
I develop the GUI to a complex tool at work.
In one use case, an option selection in a combobox should recreate the GUI layout, so the widgets are removed, destroyed, and a new set is created.

The issue is that after the combobox is destroyed, widgets in the window can't get focus until an alt+tab to another window and back.
Buttons can be clicked, but entries, for example, can't be used becuase they need focus to write into them.

The issue happens on Windows, doesn't happen on Linux.

I have to apologize in advance, since I don't know TCL, and I use Python to access Tk. I know it might be impolite to give a Python example, but that's what I know, so again, sorry.
I created a minimal example to reproduce the issue. Even though it's in Python, the Tk calls should be understandable.
If someone could please let me know if I used Tk incorrectly or if it's Python's fault I'll appreciate it.

Thanks you!

The code:
import tkinter as tk
from tkinter import ttk

def combo_changed(_varname, _index, _operation):
print('changed')
reset()

# main window
window = tk.Tk()

# main frame
frame = ttk.Frame(window)
frame.grid()

# combobox
cb = ttk.Combobox(frame, values=['1','2'])
cb.grid()

# entry
entry = ttk.Entry(frame)
entry.grid()

# combobox var and event
tk_var = tk.StringVar()
cb.configure(textvariable=tk_var)
trace_id = tk_var.trace_add('write', combo_changed)

def reset():
tk_var.trace_remove('write', trace_id)
cb.grid_forget()
cb.destroy()

Jonathan Lahav

unread,
Dec 26, 2021, 4:50:49 AM12/26/21
to
An easier way to read the code:
https://pastebin.com/yP0JTFNi

Scott Pitcher

unread,
Dec 27, 2021, 5:32:51 AM12/27/21
to
On Sunday, December 26, 2021 at 8:50:49 PM UTC+11, .........@gmail.com wrote:
> An easier way to read the code:
> https://pastebin.com/yP0JTFNi

Please excuse me I parted ways with Python over 20 years ago. I had to fumble around and find a way of running the event loop which I've added at the bottom (window.mainloop()).
I've changed the call to reset() from combo_changed() and moved it to an after event (after(0...)). The entry box had focus and was editable after that with both options '1' and '2'. I'm hoping (guessing) that's what you want?

```import tkinter as tk
from tkinter import ttk

def combo_changed(_varname, _index, _operation):
print('changed')
window.after(0,reset)
#reset()

# main window
window = tk.Tk()

# main frame
frame = ttk.Frame(window)
frame.grid()

# combobox
cb = ttk.Combobox(frame, values=['1','2'])
cb.grid()

# entry
entry = ttk.Entry(frame)
entry.grid()

# combobox var and event
tk_var = tk.StringVar()
cb.configure(textvariable=tk_var)
trace_id = tk_var.trace_add('write', combo_changed)

def reset():
tk_var.trace_remove('write', trace_id)
cb.grid_forget()
cb.destroy()

window.mainloop()```

Jonathan Lahav

unread,
Dec 30, 2021, 7:54:03 AM12/30/21
to
Thanks a lot for making the effort to help despite the language barrier. I appreciate it!

1. The fact that it's possible to get to this state, that widgets can't get focus, isn't it a bug in itself? Should it be reported as such? Where?

2. I checked and the line that makes a difference is this:
window.after(0,reset)
Why does it help? What's happening here?

Jonathan

Scott Pitcher

unread,
Jan 2, 2022, 6:21:08 PM1/2/22
to
On Thursday, December 30, 2021 at 11:54:03 PM UTC+11, j.l...@gmail.com wrote:
> Why does it help? What's happening here?
>
> Jonathan

I coded the practical equivalent in tcl script:

```#!/usr/bin/wish
#
# TclTk equivalent of the python test script.
# From comp.lang.tcl 26DEC2021
# "Destroying a Combobox prevents Entry focus"

package require Tk
# import tkinter as tk
# from tkinter import ttk

proc combo_changed {varname index operation} {
puts "changed"
# after 0 reset
reset
}

# main window
set window "."

# main frame
set fr [ttk::frame "$window.f"]
grid $fr

# combobox
set cb [ttk::combobox "$fr.cb" -values {1 2}]
grid $cb

# entry
set en [ttk::entry $fr.en]
grid $en

# combobox var and event
set tk_var ""
$cb configure -textvariable tk_var
trace add variable tk_var write combo_changed

proc reset {} {
trace remove variable tk_var write combo_changed
grid forget $::cb
destroy $::cb
}
```

When i ran this code, again on Windows 7 with tcl 8.6.11 I found a background error. I added a bgerror handler (not sure how you do that in python:

```proc bgerror {message} {
puts "Background error: $message"
puts "errorInfo: $::errorInfo"
}
```

And this is the text output to the console:
Background error:
errorInfo:
while executing
"$cb current $index"
(procedure "SelectEntry" line 2)
invoked from within
"SelectEntry $cb [lindex $selection 0]"
(procedure "LBSelect" line 5)
invoked from within
"LBSelect $lb"
(procedure "ttk::combobox::LBSelected" line 3)
invoked from within
"ttk::combobox::LBSelected .f.cb.popdown.f.l "
(command bound to event)


I'm not familiar with ttk, but it looks like the combobox is trying to select the current entry, but, I daresay it's already been destroyed. I changed reset to this:
```proc reset {} {
trace remove variable tk_var write combo_changed
grid forget $::cb
puts "Calling destroy"
destroy $::cb
puts "destroy returned"
}
```
... and I see this output now:
changed
Calling destroy
destroy returned
Background error:
errorInfo:
while executing
"$cb current $index"
(procedure "SelectEntry" line 2)
invoked from within
"SelectEntry $cb [lindex $selection 0]"
(procedure "LBSelect" line 5)
invoked from within
"LBSelect $lb"
(procedure "ttk::combobox::LBSelected" line 3)
invoked from within
"ttk::combobox::LBSelected .f.cb.popdown.f.l "
(command bound to event)

I probably wouldn't have called destroy. It's sufficient to call "grid forget" and let the combo box disappear.

Is it a bug? I don't think so. When you can destroy your deleting the widget and if events trigger during this time and invoke the widget or try to, then background errors might arise. In tcl I would probably make more checks like wrapping the destroy calls in "if {[winfo exists $cb]} ....". Often a simple catch can trap the error like ```catch "destroy $::cb"```.

Usually for production code some of these methods have to be used to ensure the app works cleanly, that you don't get error dialogs when the user closes windows and things like that.

Just some general tcl advice I hope might be useful. I'm not sure how you do those things in Python.

Scott


Scott Pitcher

unread,
Jan 2, 2022, 10:07:16 PM1/2/22
to
On Thursday, December 30, 2021 at 11:54:03 PM UTC+11, j.l...@gmail.com wrote:
> 2. I checked and the line that makes a difference is this:
> window.after(0,reset)
> Why does it help? What's happening here?

To understand what is happening, you have to understand the flow of execution. Please bear with me here, I'm not familiar with the ttk::xxx widgets or the code, but the one good thing about ttk is that we can examine a lot of the event binding code as the modules are all pure tcl. So I had a look at the lib/tk8.6/ttk/combobox.tcl module. It's about 12K and 450 lines long but here are some important ones:

Around line 63 we have -
```### Combobox listbox bindings.
#
bind ComboboxListbox <ButtonRelease-1> { ttk::combobox::LBSelected %W }
bind ComboboxListbox <Return> { ttk::combobox::LBSelected %W }
```

and at line 93 -
```## LBSelected $lb -- Activation binding for listbox
# Set the combobox value to the currently-selected listbox value
# and unpost the listbox.
#
proc ttk::combobox::LBSelected {lb} {
set cb [LBMaster $lb]
LBSelect $lb
Unpost $cb
focus $cb
}```

This is probably the function that runs when you release the mouse after clicking on the listbox, and it will then go ahead and make a new selection in the widget. I inserted some debug output at each line here so I could "checkpoint" and see where the error was popping up:
```proc ttk::combobox::LBSelected {lb} {
puts "ttk::combobox::LBSelected lb=$lb"
set cb [LBMaster $lb]
puts " calling LBSelect"
LBSelect $lb
puts " calling Unpost"
Unpost $cb
puts " calling focus"
focus $cb
puts " ---LBSelected finished!"
}
```

I also took the LBSelect proc and inserted some there:
```## LBSelect $lb --
# Transfer listbox selection to combobox value.
#
proc ttk::combobox::LBSelect {lb} {
puts "ttk::combobox::LBSelect lb=$lb"
set cb [LBMaster $lb]
puts " calling curselection"
set selection [$lb curselection]
if {[llength $selection] == 1} {
puts " calling SelectEntry"
SelectEntry $cb [lindex $selection 0]
}
puts " ---LBSelect finished"
}
```

When I ran the test script again-
ttk::combobox::LBSelected lb=.f.cb.popdown.f.l
calling LBSelect
ttk::combobox::LBSelect lb=.f.cb.popdown.f.l
calling curselection
calling SelectEntry
changed
Calling destroy
destroy returned
Background error:
errorInfo:
while executing
"$cb current $index"
(procedure "SelectEntry" line 2)
invoked from within
"SelectEntry $cb [lindex $selection 0]"
(procedure "LBSelect" line 8)
invoked from within
"LBSelect $lb"
(procedure "ttk::combobox::LBSelected" line 5)
invoked from within
"ttk::combobox::LBSelected .f.cb.popdown.f.l "
(command bound to event)

So what's happening is that the user is clicking on the combobox, the event runs and sets the selection and during the SelectEntry proc sets your variable is set, which activates the trace and calls combo_changed, which calls reset, which destroys the combobox, and when execution returns to SelectEntry, the comboxbox no longer exists and we get a background error and the focus is never set to the entry box.

By putting the reset call into the after event ("after 0" basically translates to something like "after return") we decouple the combobox work from your reset function and the destroy will run later, no longer upsetting the combobox. I'll insert the after 0 and try it again and it all runs properly now:
scottyw@officewinvm MINGW32 /n/Projects, Software/python/Test.comp.lang.tcl.26DEC2021
$ wish script.tcl
ttk::combobox::LBSelected lb=.f.cb.popdown.f.l
calling LBSelect
ttk::combobox::LBSelect lb=.f.cb.popdown.f.l
calling curselection
calling SelectEntry
changed
---LBSelect finished
calling Unpost
calling focus
---LBSelected finished!
Calling destroy
destroy returned

The thing to remember it that these actions are all happening as events, and if you want to do something drastic like destroy the widget that is calling you, during the call back itself, your best to place that outside the widget's execution in something like an "after 0" call back. If you must do it immediately then as a safety you should wrap the calls in catch "..." however in this case it's not going to help you, because the call to destroy during the trace call back is the problem itself.

Hope these ramblings make sense. I'm 7 days into a very very bad flu I don't think what I've written it all that coherent. Normally when I'm trying to get to the bottom of problems with event code I'll use lots of puts "..." messages to see who is calling who and when. I had major headaches with a snit "messageentry" widget I'd made a couple of years ago in one commercial application, and that widget had become a real swiss army knife and now included popup suggestions list using a listbox that pops up dynamically. I found it most difficult to catch things like the user clicking away from the application and getting the button click in the widget to work correctly. It was all the added debug output from the event handlers that helped or at least, gave me enough information to pour over and sort out how to get it working.

Scott

Jonathan Lahav

unread,
Jan 13, 2022, 4:32:06 AM1/13/22
to
These are not ramblings and thanks to you I solved the issue in our application. Thanks a lot for your help!
Sorry to hear about the flu. Yes, it's terrible these days. Feels like a third of the population either got Covid or something else.

I agree that it's probably a good idea to put the destroy() inside an after(0,) and indeed, this is what solved the issue in my application.

However, it feels wrong that getting to a state where you can't click on any widget until alt+tab is even possible. The UI is in a weird state.
I can leave it here, sure, but I'll take your word here as you know much more than I do.
Should I report it or leave it?

Thanks again
Jonathan

Scott Pitcher

unread,
Jan 17, 2022, 3:38:09 PM1/17/22
to
On Thursday, January 13, 2022 at 8:32:06 PM UTC+11, j.l...@gmail.com wrote:
> However, it feels wrong that getting to a state where you can't click on any widget until alt+tab is even possible. The UI is in a weird state.
> I can leave it here, sure, but I'll take your word here as you know much more than I do.
> Should I report it or leave it?

Reporting it is probably a good idea. At the very least the widget should probably check it's state after it sets the variable's value.

Kind regards,
Scott
0 new messages