tk.TopLevel is making two windows appear, but why?

72 Views Asked by At

The code below generates my main program (frmMain), which sometimes opens forms. But each time I open a form, the code below makes TWO forms open, the proper form, and one extra one, which is blank. The standard top-right close button only works on the proper form, and, the extra one is invulerable. But why does it appear, and why is it invulnerable?

frmMain=tk.Tk()

def createForm():
    lFrm = tk.Toplevel()    #Create a new form
    initForm(lFrm)          #tk.inits the form and sets the mass onFocus event bubble
    return lFrm
    
def initForm(pFrm):
    tk.Toplevel.__init__(pFrm) #instead of super
    #super().__init__(master=pFrm)
    setWindowFocusEvent(pFrm) #when any win gets focus, all windows come to fore in order (as per std MDI app behaviour)

An example of where I might call this is below, but it causes TWO windows to appear:

def listBands():
    global frmMain
    frmMain.lListBandsFrm = createForm()

There are some threads on SO about two windows appearing at once, but in those examples I can see what the cause was. But in my case, I really can't. I'm not calling any spurious tk.Tk(), and I'm only calling tk.Toplevel() once for each form. Stepping through the code reveals that both windows appear simultaneously when createForm finishes.

I've tried omitting the init() call, but that of course breaks everything, e.g., I can't call .bind until after init.

The answer is... not to fiddle with init, and to properly use inheritance and subclassing. Here it is:

class FrmMDI(tk.Toplevel):  #New form, with my own setup as required
    def __init__(self):     #Autocalled when obj is instantiated
        super().__init__()  #Calls the init() of the parent
        setWindowFocusEvent(self) #My own prep, to improve MDI in Windows

def createForm():      #This was giving me double-windows
    lFrm = FrmMDI()    #Create a new form using subclass of .Toplevel
    return lFrm
2

There are 2 best solutions below

1
Vexen Crabtree On BEST ANSWER

The answer is... not to fiddle with init, use the built-in and simpler super() instead. It was getting confusing which forms were calling my own custom init(), and when I needed to call it. And, rather than write your own Init() that you call from certain forms, it's better to make your own subclass. It then gets easy to understand, and init() works smoothly. Here's those things in code:

class FrmMDI(tk.Toplevel):  #New form based on Toplevel, with my own extra setup step
    def __init__(self):     #Autocalled when obj is instantiated
        super().__init__()  #Calls the init() of the parent
        setWindowFocusEvent(self) #My own prep, to improve MDI in Windows

def createForm():      #This was giving me double-windows
    lFrm = FrmMDI()    #Create a new form using subclass of .Toplevel
    return lFrm
12
wizzwizz4 On

Ah, the wonders of tkinter's Default Root… You're creating a root window with your first line. Try running this:

>>> import tkinter
>>> tkinter.Tk()
<tkinter.Tk object .>
>>> tkinter.Toplevel()
<tkinter.Toplevel object .!toplevel>

The first call creates the main Tk window, and the second creates a new Toplevel window. So… how about removing that first call?

RESTART: Shell
>>> import tkinter
>>> tkinter.Toplevel()
<tkinter.Toplevel object .!toplevel>

Two windows have appeared! What's going on here? Well, since we haven't created a root window, tkinter creates a root window for us implicitly. You can turn this off by adding a call to tkinter.NoDefaultRoot() right at the beginning:

RESTART: Shell
>>> import tkinter
>>> tkinter.NoDefaultRoot()
>>> tkinter.Toplevel()
Traceback (most recent call last):
  File "<pyshell#13>", line 1, in <module>
    tkinter.Toplevel()
  File "/usr/lib/python3.9/tkinter/__init__.py", line 2621, in __init__
    BaseWidget.__init__(self, master, 'toplevel', cnf, {}, extra)
  File "/usr/lib/python3.9/tkinter/__init__.py", line 2566, in __init__
    BaseWidget._setup(self, master, cnf)
  File "/usr/lib/python3.9/tkinter/__init__.py", line 2533, in _setup
    master = _get_default_root()
  File "/usr/lib/python3.9/tkinter/__init__.py", line 293, in _get_default_root
    raise RuntimeError("No master specified and tkinter is "
RuntimeError: No master specified and tkinter is configured to not support default root

Turns out that tkinter really wants to create a default root object. That's because in Tk, everything is part of a hierarchy – even windows. I would suggest one of three workarounds:

  • Use a separate tkinter.Tk instance for every window, rather than tkinter.Toplevel instances. (Simplest, but inelegant.)

  • Hide the root window. To ruthlessly steal from BuvinJ's answer:

    root = tkinter.Tk()
    root.overrideredirect(1)
    root.withdraw()
    

    (Slightly less inelegant, depending on your use-case.)

  • Subclass tkinter.Frame instead of tkinter.Toplevel, and handle the windowing separately using the master keyword. This is my preferred solution, though if you don't know which form will live the longest, you can't make any of them the root window, so you'll have to use one of the other two techniques.


Note: this isn't actually the issue in the question: that issue was duplicate calls to tkinter.Toplevel.__init__, which was resolved by subclassing tkinter.Toplevel properly. This answer matches the title, though.