tkinter's .pack_propagate() method - python

I am experimenting with Tkinter, as I was trying to figure out is there a way to set the tkinter's window size without using canvas. I came upon this how to set frame size question on SO's Question & Answer. So I went ahead and test it by writing a very small program to display a text label. But I found out it is "missing", or disappear when I use frame.pack_propagate(0)
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root, width=400, height=400)
# Does not work at the moment, textBox is missing
# frame.pack_propagate(0)
frame.pack()
textBox = tk.Label(frame, text="(x,y): ")
textBox.pack()
root.mainloop()
So my question is, can you explain why my textBox (Label) is not appearing when I use the frame.pack_propagate(0) instead of frame.pack() method? And secondly, is there a way to set the window size without using a canvas? I want to know because I am writing a series of small programs to teach my friend about tkinter, before introducing canvas to him. It would be nice if the window size are all the same across my tkinter samples. And I am just wondering as well (curious). Thank you very much.
I am using python 3.2.2 on MAC OS 10.5.8.

pack_propagate only sets a flag, it doesn't cause the frame to be placed in the widget. It is not a substitute for calling pack.
In other words you must do this:
# put the frame in its parent
frame.pack()
# tell frame not to let its children control its size
frame.pack_propagate(0)
# put the textbox in the frame
textBox.pack()

To answer your second question: Yeah, there is a way.
tkinters Tk do have the Tk.geometry function. When you just call it without arguments, you will get the current geometry in form of 'widthxheight+x+y', so for example (on Windows 10) '200x200+26+26' when you create your first Tk window. Using that format you can resize the Tk by, e.g., writing: root.geometry('400x500+60+60') to set the width to 400, the height to 500 and place it at the coordinates (60|60).
This works for Tk alswell as for Toplevel. But Toplevel also takes the arguments height and width when initialized or configured. If you want them to keep their size when packing something inside just use root.pack_propagate(False) on them.
By the way there is something similar for the grid manager: root.grid_propagate(False)

Related

When designing a desktop app to be resizable, are there any hidden drawbacks to using two sequential Tk instances?

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.
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()
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.

How do you put multiple tkinter widigets into one .grid

I am trying to make a date chooser using python. I am using spinboxes, however I was wondering whether I could get all 5 widgets into one grid space, so it seemed like all 5 widgets are really one widget. Hopefully the following code articulates the problem better.
import tkinter as tk
root=tk.Tk()
Day=tk.IntVar()
Month=tk.IntVar()
Year=tk.IntVar()
Label1=tk.Label(root,text="Label Label Label Expanding Row")
Label1.grid(row=1,column=1)
DayEntry=tk.Spinbox(root,textvariable=Day,bg="white",from_=0, to_=31,width=2)
DayEntry.grid(row=2,column=1)
MonthEntry=tk.Spinbox(root,textvariable=Month,bg="white",from_=0, to_=12,width=2)
MonthEntry.grid(row=2,column=3)
YearEntry=tk.Spinbox(root,textvariable=Year,bg="white",from_=2000, to_=20019,width=4)
YearEntry.grid(row=2,column=5)
Divider1=tk.Label(root,text="/")
Divider1.grid(row=2,column=2)
Divider2=tk.Label(root,text="/")
Divider2.grid(row=2,column=4)
root.mainloop()
The solution is to put all of the widgets in a frame.
datepicker = tk.Frame(root)
datepicker.grid(row=2, column=0)
DayEntry=tk.Spinbox(datepicker,textvariable=Day,bg="white",from_=0, to_=31,width=2)
MonthEntry=tk.Spinbox(datepicker,textvariable=Month,bg="white",from_=0, to_=12,width=2)
YearEntry=tk.Spinbox(datepicker,textvariable=Year,bg="white",from_=2000, to_=20019,width=4)
Divider1=tk.Label(datepicker,text="/")
Divider2=tk.Label(datepicker,text="/")
DayEntry.grid(row=0,column=1)
Divider1.grid(row=0,column=2)
MonthEntry.grid(row=0,column=3)
Divider2.grid(row=0,column=4)
YearEntry.grid(row=0,column=5)

tkinter: Can't make frame scrollable

On python tkinter, I am using 2 different frames on a Toplevel window, one on the right and another on the left.
The frame which is on right side is not scrollable. I have created a canvas on top of the frame on that frame and one more frame on top of that canvas. I have made that canvas scrollable and pasted the widgets on that canvas but it's not scrollable. I am attaching the code of the scrollable part.
w1 = Canvas(frame2, width=600, height=300,background="white", scrollregion=(1500,1500,3000,3000))
scr_h1 = ttk.Scrollbar(frame2,orient=HORIZONTAL)
scr_h1.pack(side=BOTTOM,fill=X)
scr_h1.config(command=w1.xview)
scr_v1 = ttk.Scrollbar(frame2,orient=VERTICAL)
scr_v1.pack(side=RIGHT,fill=Y)
scr_v1.config(command=w1.yview)
w1.config(xscrollcommand=scr_h1.set,yscrollcommand=scr_v1.set)
w1.pack(fill=BOTH,expand=True)
This code works for me running Python 3.4 - a tkinter window pops up with a red oval (for testing), and the scrollbar allows you to navigate the frame. If you are using Python 2, change tkinter to Tkinter (capital T).
from tkinter import *
root = Tk()
frame2 = Frame(root)
frame2.pack(side=RIGHT)
w1 = Canvas(frame2, width=600, height=300,background="white", scrollregion=(0,0,3000,3000))
scr_h1 = Scrollbar(frame2,orient=HORIZONTAL)
scr_h1.pack(side=BOTTOM,fill=X)
scr_h1.config(command=w1.xview)
scr_v1 = Scrollbar(frame2,orient=VERTICAL)
scr_v1.pack(side=RIGHT,fill=Y)
scr_v1.config(command=w1.yview)
w1.config(xscrollcommand=scr_h1.set,yscrollcommand=scr_v1.set)
w1.pack(fill=BOTH,expand=True)
# inserted to see if it's actually scrolling
w1.create_oval(0,0,50,50,fill='red')
root.mainloop()
Two Possible Issues
Why were you using a ttk ScrollBar? The simple tkinter scroll bar will suffice for your code. When things aren't working, it might help to go back to the simpler model.
Why your starting scroll region was 1500 - any object placed on the canvas in the first 1500 units in either direction were not visible, with this setting, which may have given you the illusion that the scrollbar was not working. See http://effbot.org/zone/tkinter-scrollbar-patterns.htm for more information on using scroll bars.
It should not matter that there are two frames or their orientation, though you may run into problems if you try to mix managers (grid,pack,etc.). These problems are more along the lines of stalled programs, not stationary scrollbars.

Setting Tkinter/ttk Frame background color

I'm trying to change the background color of a ttk frame and I've looked up other examples, but none have seemed to work. This is my code so far:
from Tkinter import *
import ttk
p = Tk()
p.geometry('600x350')
p.configure(bg='#334353')
gui_style = ttk.Style()
gui_style.configure('My.TButton', foreground='#334353')
gui_style.configure('My.TFrame', background='#334353')
frame = ttk.Frame(p, style='My.TFrame')
frame.grid(column=1, row=1)
ttk.Button(frame, text='test', style='My.TButton').grid(column=0, row=0)
ttk.Button(frame, text='Test 2', style='My.TButton').grid(column=3, row=3)
p.mainloop()
The window has the background color that I want, but the frame still has the default gray background. Is there something i need to add differently? I want the entire window except for the buttons to be the color #334353. How do I do this?
EDIT: I've attached what my window looks like. I don't want the gray. :/ (Note. I don't have enough rep to post images apparently, so here is a link to imgur with my current window: http://imgur.com/KyhbdMB
Your frame is only sized to the minimum size required to hold the two child windows (the buttons). It seems like you want the frame to fill the main window. When you grid the frame you should add the sticky option to have it expand to fill the available space (eg: frame.grid(column=1,row=1,sticky='news')). Then you need to have the parent allocate all the space space to this grid cell. For that you want to use the grid_rowconfigure and grid_columnconfigure methods for the parent window. In this case:
p.grid_columnconfigure(1, weight=1)
p.grid_rowconfigure(1, weight=1)
which tells the main frame grid geometry manager that spare space should be given to the cell and row 1 column 1. This will lead to your frame expanding to fill the window.
It works on my PC!
Try this:
Update your Python environment(Tested under Py 3.4 Windows 32bit)
Install the lastest TTK package

Frame resizing issue in Tk/ttk python

I'm creating a GUI using Tkinter/ttk in Python 2.7, and I'm having an issue where a Frame will resize itself once a widget is placed inside of it. I am new to Python and haven't used Tkinter before.
example:
ROOT = Tk()
FRAME = ttk.Frame(ROOT, width=300, height=300, relief='groove')
FRAME.grid()
ROOT.mainloop()
will produce a grooved frame 300x300, if i place a widget inside it like so:
ROOT = Tk()
FRAME = ttk.Frame(ROOT, width=300, height=300, relief='groove')
BUTTON = ttk.Button(FRAME, text="DON'T READ THIS TEXT")
FRAME.grid()
BUTTON.grid()
ROOT.mainloop()
the frame will shrink down to fit the button. Any way to force the frame not to resize?
To force the frame to keep its original dimensions turn "geometry propagation" off. In your case you would call FRAME.grid_propagate(False).
Speaking as someone with over 15 years of Tk experience, might I suggest that you almost certainly don't need this feature. Tk's ability to "shrink to fit" is really great, and makes it really easy to create GUIs with proper resize behavior. Once you start turning geometry propagation off you'll find you'll either have GUIs with bad resize behavior, or you'll spend a lot of time tweaking sizes and widget placement.
if you want to add padding, then use widg.grid(ipadx=..., ipady=..., padx=..., pady=...)
otherwise, you will need to add more context on what layout you're trying to achieve
This works for me:
app = Application()
app.master.title('Example')
app.master.geometry('640x480')
app.mainloop()
Apllication is Frame in my case with grid layout. It resizes to size of master window, so we need to change size of master window.
This should do the job: ROOT.geometry("640x480")

Categories

Resources