1

In order to ensure that my program will be compatible with any screen size, I first have a very small 'setup' window open when the program is launched, which will then allow the user to select the desired dimensions for the main window.

Once the 'finalize' button on the 'setup' window is pressed, the setup window disappears and the main window opens. This is handled by calling .destroy() on the setup window and creating a new instance with Tk() inside the button's command function.

To make the example code more compact and highlight the part which is relevant to this question, I left out the size selector and just set the main window to be a fixed 800x800 pixels here:

import tkinter as tk

# Start out small, to fit on any screen size
startWindow = tk.Tk()
startWindow['width'] = 400
startWindow['height'] = 200
startWindow.title("Setup")

# Use lists here so that the widgets created inside 'initializeMainWindow' will be
# accessible from the global scope.
mainWindow = [None]
mainWindowButtons = [None]

# Closes the 'setup' window and opens a new window which will be the main application.
# Also initializes all widgets which will belong to the new window.
def initializeMainWindow():
    startWindow.destroy()
    mainWindow[0] = tk.Tk()
    print("New window initialized.")
    mainWindow[0]['width'] = 800
    mainWindow[0]['height'] = 800
    mainWindow[0].title("Main Window")
    mainWindowButtons[0] = tk.Button(master=mainWindow[0], text="Test", command=testNewWindow)
    mainWindowButtons[0].place(x=350, y=375, width=100, height=50)
    mainWindow[0].bind('<Key>', test2)
    #mainWindow[0].mainloop()

# To demonstrate that the new window is interactive
def testNewWindow():
    print("Success!")

# Works whether or not 'mainloop' is called on the new window
def test2(e):
    print("Also success! '" + e.keysym + "' key pressed.")
    
setSizeButton = tk.Button(master=startWindow, text="Resize", command=initializeMainWindow)
setSizeButton.place(x=150, y=75, width=100, height=50)

# This prints BEFORE the 'setup' window is closed, as expected
# If 'mainloop' is uncommented, it works the same except IDLE won't show the '>>>' prompt
# after the text "Not yet initialized".
if startWindow:
    print("Not yet initialized.")
    #startWindow.mainloop()

This works exactly as I intend it to so far. However I'm aware that when an application has more than one window, it's standard to use Toplevel() and not create multiple instances of Tk(). But this example isn't trying to run multiple instances of Tk() at once, instead, they're sequential: think of it as a separate 'launcher' program that then opens the main program, as is common on many desktop games. (This is exactly how I'm using it in the full program).

Before building on a potentially flawed foundation, I'd like to know if there are any hidden problems which could surface later with this approach. If the consensus is that it's better to switch to using Toplevel() or even have two separate Python files, I'd rather find out sooner than later!

I have already viewed this question and answer: What's the difference between tkinter's Tk and Toplevel classes? but they don't cover this specific question.

A related issue: I also experimented with calling .mainloop() vs. not calling it, and at least with the Mac version of IDLE and using Python 3.9.4, it seems to be optional. This was discussed here When do I need to call mainloop in a Tkinter application? and it looks like the reason omitting it still works is that IDLE has its own event loop (credit to Ori for this solution https://stackoverflow.com/a/8684277/18248018).

If this is the case, is it advisable to explicitly call .mainloop() anyway (where I have it commented out in the example code) for reliability? I haven't tested this yet, but if it's something IDLE does, I'd guess the automatic event loop functionality might not transfer over when I convert the program to a standalone app using py2app, without explicit calls to .mainloop() in the .py file.

Quack E. Duck
  • 594
  • 1
  • 4
  • 20
  • 2
    Instead of using a Toplevel, and instead of destroying and recreating the root window, have you considered reusuing the existing window? That seems like the most efficient solution. – Bryan Oakley Apr 27 '22 at 19:23
  • @BryanOakley Yes, I have used the method you suggested in the past, by simply resetting the window's 'width' and 'height' properties. Unfortunately the resizing itself happens very jerkily and looks unsightly, although this only takes a fraction of a second, and the program works fine once it does. I was hoping this method would avoid the ugly/unprofessional looking visuals. If it has the potential to cause issues though, I will stick with resetting the original `Tk()` instance's 'width' and 'height' - it is a lot simpler! – Quack E. Duck Apr 27 '22 at 19:31
  • 1
    Another alternative (that also only uses one `Tk()`) would be to create two separate `Frame`s and switch between them as demonstrated in [this answer](https://stackoverflow.com/a/7557028/355230) of Bryan's to another question. – martineau Apr 27 '22 at 20:33
  • @martineau thanks for the reference! I don't know that it would work in this case though, as he states in the answer it's best when the frames are the same size, and the larger frame's size would still be dependent on user input – Quack E. Duck Apr 27 '22 at 20:37
  • 1
    I think something similar could be done for unequally sized frames, but instead of "raising" the newly active frame to the top, you could *hide* the currently visible one and make the new one visible to replace it. – martineau Apr 27 '22 at 21:08
  • 1
    See answers to question [Showing and Hiding widgets](https://stackoverflow.com/questions/10267465/showing-and-hiding-widgets) on how to show & hide a `Frame` (and other widgets). – martineau Apr 27 '22 at 22:07
  • @martineau I tried your solution and it works perfectly (although now the frames have to be added to the window using `pack` and not `place` in order to set the window's size). This seems better than having a second `Tk()` instance, so I'll use your method. Thanks for the suggestion! – Quack E. Duck May 01 '22 at 18:55
  • 1
    That's good to hear, thanks for LMK. I'd like to see what you came up with — so I have one more suggestion — why don't you post an answer here to your own question and accept it (which is allowed). – martineau May 01 '22 at 19:39
  • @martineau I posted it, but didn't accept it as it doesn't really answer the theoretical part of the question: what would happen if someone **did** use the original approach? Although I kind of doubt anyone will answer that because it seems like there's no reason to ever do it that way... – Quack E. Duck May 01 '22 at 22:46
  • Doing it with a single ``Tk` instance is generally preferable, although I don't know if the reasons not to apply in this scenario. See [Why are multiple instances of Tk discouraged?](https://stackoverflow.com/questions/48045401/why-are-multiple-instances-of-tk-discouraged) I think there can be issues trying to share tkinter variables between the two, as well as running two `mainloops` at once. but again those doesn't seem to appy to what you were trying to do. – martineau May 01 '22 at 23:47

2 Answers2

1

Here's the code in your answer with a minor change — the finalFrame doesn't get created until the openMainWindow() function is called since it's not needed until then. I think this is a little more logical instead of having creation and usage scattered about.

import tkinter as tk

window = tk.Tk()

# Start out with a small popup, so it will fit on any screen size.
# Width and height must be specified as attributes of the frame, so that the
# frame can set the window's size.
setupFrame = tk.Frame(master=window, bg='green', width=400, height=200)

# Using 'pack' will cause the window's size to be equal to setupFrame's size
# Using 'place' here would not work: the window would open with the default size
# (small and square) and cut off the frame.
setupFrame.pack()

# In the actual program, these values will be determined by user input
W = 800
H = 800

def openMainWindow(w, h):
#    finalFrame  # Uncomment if ever needed.
    # Destroy the 'launcher' frame once it is no longer needed.
    setupFrame.destroy()

    # This frame will reset the size of the window and will display the
    # program's main content.
    finalFrame = tk.Frame(master=window, bg='lightblue', width=w, height=h)
    sizeScalingExample = tk.Label(master=finalFrame, bg='purple', fg='white',
                                  text="This label's size is set with `place` and "
                                       "depends on the frame's size.")
    finalFrame.pack()
    sizeScalingExample.place(x = 0.125*w, y = 0.375*h, width=0.75*w, height=0.25*h)

resizeButton = tk.Button(master=setupFrame, text="Finalize", bg="yellow",
                         fg="darkblue", command=lambda: openMainWindow(W, H))
resizeButton.place(x=150, y=75, width=100, height=50)

window.mainloop()

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Looking at your edit description just now, I noticed you had to add `window.mainloop()` to get the code to run. I suppose that's confirmation that it shouldn't be omitted even though for some reason it's working for me without it – Quack E. Duck May 02 '22 at 17:25
  • 1
    AFAIK you always need to call `mainloop()` to get your GUI to run — although I have seen a few instances where folks will call `window.update()` repeated to kind of do the same thing, Anyway, it's what allows `tkinter` process user input (and do various other internal things like updating how the GUI looks). So I strongly suspect you are mistaken about it working without you having called it. – martineau May 02 '22 at 18:41
  • That's what I've always heard too, but I did confirm just now (by commenting `window.mainloop()` out) that it runs the same with or without it under these specific conditions at least: Python 3.9.4 with IDLE on macOS Monterey 12.3.1. The issue of code working (or not working) with/without `mainloop` is discussed here: https://stackoverflow.com/questions/8683217/when-do-i-need-to-call-mainloop-in-a-tkinter-application?noredirect=1&lq=1 and it seems to be very unpredictable. I'll follow your advice and always use it, as it seems like a rare quirk of a specific environment/IDE not to need it – Quack E. Duck May 02 '22 at 22:00
  • 1
    In my (considerable) experience, it's needed pretty predictably, so I wouldn't focus on the topic too much and spend a lot of time trying to figure out whether I needed one or not, and just always use put one in — it's never wrong to do so. – martineau May 02 '22 at 23:17
0

Following martineau's suggestion in the comments of switching between two different Frame widgets, I rewrote the code in my question as the following. This achieves the same visual effect as the original code, and eliminates any need for a second Tk() instance.

This is a minimal example of the method I will be using in my resizable application:

import tkinter as tk

window = tk.Tk()

# Start out with a small popup, so it will fit on any screen size.
# Width and height must be specified as attributes of the frame, so that the
# frame can set the window's size.
setupFrame = tk.Frame(master=window, bg='green', width=400, height=200)

# Using 'pack' will cause the window's size to be equal to setupFrame's size
# Using 'place' here would not work: the window would open with the default size
# (small and square) and cut off the frame.
setupFrame.pack()

# This frame will reset the size of the window and will display the program's
# main content
finalFrame = tk.Frame(master=window, bg='lightblue')
sizeScalingExample = tk.Label(master=finalFrame, bg='purple', fg='white',
                              text="This label's size is set with `place` and "
                                   "depends on the frame's size.")

# In the actual program, these values will be determined by user input
W = 800
H = 800

def openMainWindow(w, h):
    # Hide the 'launcher' once it is no longer needed
    setupFrame.pack_forget()
    finalFrame['width'] = w
    finalFrame['height'] = h
    finalFrame.pack()
    sizeScalingExample.place(x = 0.125*w, y = 0.375*h, width=0.75*w, height=0.25*h)

resizeButton = tk.Button(master=setupFrame, text="Finalize", bg="yellow",
                         fg="darkblue", command=lambda: openMainWindow(W, H))
resizeButton.place(x=150, y=75, width=100, height=50)

window.mainloop()

Although the ability to implement the same functionality using only Frame widgets means there is no practical reason to use a second Tk() instance, I'd still be interested from a theoretical perspective to learn about any unexpected outcomes which could result from using the original approach.

martineau
  • 119,623
  • 25
  • 170
  • 301
Quack E. Duck
  • 594
  • 1
  • 4
  • 20
  • 1
    Note you could simply call `setupFrame.destroy()` instead of `setupFrame.pack_forget()` here since you don't plan on redisplaying the `setupFrame` again (which if you were would make using `pack_forget()` handy). – martineau May 02 '22 at 00:11
  • 1
    Off-topic: I strongly suggest you read and start following the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code, especially the part about [maximum line length](https://peps.python.org/pep-0008/#maximum-line-length) (I use 90) and [naming conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions). – martineau May 02 '22 at 00:20