Tkinter widget displacement with pack() - python

I want to pack two buttons (left and right) and a label (in the middle) in a frame. I want the label to fill the remaining space on the frame to both sides, but the widgets get displaced vertically with this code. What's the best way to do this? The widgets don't necessarily have to be packed on a frame but I want them to align horizontally while the text size of the label can change, but the buttons need to stay in place on the far left and right side. enter image description here
import tkinter as tk
root = tk.Tk()
root.geometry('600x800')
root.configure(background='#141414')
frm = tk.Frame(root)
frm.place(x=0, y=0, width=300, height=30)
btn1 = tk.Button(frm, text='button1')
lbl = tk.Label(frm, text='Lalalalalala')
btn2 = tk.Button(frm, text='button2')
btn1.pack(side='left')
lbl.pack(fill='x')
btn2.pack(side='right')
tk.mainloop()

You can solve this problem a couple of ways. One solution is to pack the label to one side or the other rather than the top.
btn1.pack(side='left')
lbl.pack(side='left', fill='x', expand=True)
btn2.pack(side='right')
Another is to pack the buttons first, and then pack the label. With pack the order matters.
btn1.pack(side='left')
btn2.pack(side='right')
lbl.pack(fill='x', expand=True)
For an illustrated explanation of how pack works see this answer to the question Tkinter pack method confusion

Related

How do I stop text from moving when I resize the screen in tkinter

I am creating a registration form, and I have coded labels to show next to the text box for someone's username and password. This is the code I am using to place the text boxes and labels:
usernamebx.place(relx=0.5, rely=0.5, width=225, height=25,
anchor= CENTER)
userbx_label.place(relx=0.1, rely=0.5, anchor=CENTER)
passwbx.place(relx=0.5, rely=0.6, width=225, height=25, anchor = CENTER)
passwbx_label.place(relx=0.1, rely=0.6, anchor=CENTER)
The code for usernamebx and passwbx means that the text boxes don't move when I resize the tkinter window. However, I have done the same with the labels for each but it doesn't work. Any help?
The code for usernamebx and passwbx means that the text boxes don't move when I resize the tkinter window.
Actually, they do move! If you put a widget at relx 0.5 in a window that is 200 pixels wide, that means the center of the widget will be 100 pixels from the left edge of the window. When you grow the window to 400 pixels wide, the center of the widget now will be 200 pixels from the left edge. It moved 100 pixels. You don't see it because it's symmetrical so it stays in the center.
The same happens with a widget that is at 0.1. on a 200 pixel wide window it's going to be 20 pixels from the left edge. When you make the window 400 pixels widget it's going to be 40 pixels from the edge.
This is the nature of relative coordinates -- they will always change when the window is resized.
It's hard to see what your actual requirement is, though I'm guessing you want the username label+entry and password label+entry to be co-aligned in the center of the window.
If that's the case, one simple solution is to put those widgets in a frame. Use grid internally since it appears that you are in fact creating a grid. Then, you can place the frame in the window as a separate step.
Here's an example of the technique. For illustrative purposes the frame has a visible border, but that's not strictly necessary. You can remove the border to make it blend in with the background.
This example uses place to put the frame in the center, though you can also use pack.
import tkinter as tk
root = tk.Tk()
root.geometry("400x400")
inner_frame = tk.Frame(root, bd=2, relief="groove")
usernamebx = tk.Entry(inner_frame)
userbx_label = tk.Label(inner_frame, text="Username:")
passwbx = tk.Entry(inner_frame)
passwbx_label = tk.Label(inner_frame, text="Password:")
inner_frame.grid_columnconfigure(0, weight=1)
userbx_label.grid(row=0, column=0, sticky="e")
usernamebx.grid(row=0,column=1, sticky="ew")
passwbx_label.grid(row=1, column=0, sticky="e")
passwbx.grid(row=1, column=1, sticky="ew")
inner_frame.place(relx=.5, rely=.5, anchor="center")
root.mainloop()
If you want to use pack rather than place, have the packer expand the allocated space to be the whole window, and the frame will automatically be centered. In this case the window will shrink to fit the frame plus the padding.
inner_frame.pack(side="top", expand=True, padx=10, pady=10)

Why is inner padding only applied on the right?

MCVE
import Tkinter as tk
import ttk
root = tk.Tk()
root.minsize(200, 100)
inner_frame = ttk.LabelFrame(root, text='inner_frame')
inner_frame.grid(row=0, column=0)
button = ttk.Button(inner_frame, text='this is a button')
button.grid(row=0, column=0)
# this does not work as expected
inner_frame.grid_configure(ipadx=20)
root.mainloop()
Output
Question
Why is the inner padding for inner_frame only applied on the right? How do I apply it on both sides?
Interestingly, and I don't know if this is a bug, your problem is solved by making the column containing the button expand past its minimum horizontal size.
The minimum horizontal size of column 0 in the inner frame is the horizontal size of its content, which is the button.
If you add a inner_frame.columnconfigure(0, weight=1), then the internal padding works as expected:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
inner_frame = ttk.LabelFrame(root_frame, text="inner_frame")
inner_frame.grid(row=0, column=0)
inner_frame.columnconfigure(0, weight=1)
button = ttk.Button(inner_frame, text="this is a button")
button.grid(row=0, column=0)
# this works expected:
inner_frame.grid_configure(ipadx=20)
root.mainloop()
Note that grid_configure changes column 0 by default, so here we're adding internal padding to the cells in column 0.
I do not know exactly why this happens. It is not mentioned in the Tcl grid docs (http://tcl.tk/man/tcl8.5/TkCmd/grid.htm#M13).
Interestingly, this link does mention some special case around size of the frame using internal padding (low down the page, in the "Internal Padding" section): https://tkdocs.com/tutorial/grid.html
The difference can be subtle. Let's say you have a frame that's 20x20, and specify normal (external) padding of 5 pixels on each side. The frame will request a 20x20 rectangle (its natural size) from the geometry manager. Normally, that's what it will be granted, so it'll get a 20x20 rectangle for the frame, surrounded by a 5-pixel border.
With internal padding, the geometry manager will effectively add the extra padding to the widget when figuring out its natural size, as if the widget has requested a 30x30 rectangle. If the frame is centered, or attached to a single side or corner (using "sticky"), you'll end up with a 20x20 frame with extra space around it. If however the frame is set to stretch (i.e. a "sticky" value of "we", "ns", or "nwes") it will fill the extra space, resulting in a 30x30 frame, with no border.
But it is confusingly written, and I can't fully understand what they mean (or even if they are correct in modern Python). If someone knows what that paragraph above means, by all means comment below and let us know!
Adding internal padding to the frame as a whole
You can add internal padding by using styles or passing it directly as an argument to the frame's constructor:
ttk.LabelFrame(root, text="inner", padding=(20, 0))
The padding value there can take either:
One value, for padding on all sides
Two values, for x and y (in that order)
Four values, for padding starting at the left and going clockwise.
In your case, ipadx isn't failing. It's working as designed, it's just that the way it works isn't very intuitive, especially when you apply it to a frame.
To better visualize what is happening, lets apply the ipadx value to the button rather than the frame. That way we can see the padding relative to the label on the button.
For example, add two buttons instead of one. Give one an ipadx of 20, and give the other an ipadx of 0.
button1 = ttk.Button(inner_frame, text='this is a button')
button2 = ttk.Button(inner_frame, text='this is a button')
button1.grid(row=0, column=0, ipadx=20)
button2.grid(row=1, column=0, ipadx=0)
Notice that the button with ipadx=20 is wider, and the extra space is inside the button rather than as a margin surrounding the button.
The same thing is happening with inner_frame: when it is added to its parent, the extra space is being added inside the frame, effectively making inner_frame wider. You can't see it because it's added to the empty space already inside the frame.
Here's the important part: if you add a widget to inner_frame, grid doesn't know anything about the ipadx values applied to inner_frame -- that ipadx option only applies to inner_frame and its parent, not its children. At the point of adding widgets inside of inner_frame, grid only knows that inner_frame is X pixels wide.
To illustrate, we can add a label to the button, similar to how your original code adds a button to the frame. (note: we'll turn geometry propagation off so that it doesn't cause the button to shrink).
button1.grid_propagate(False)
label = ttk.Label(button1, text="x")
label.grid(row=0, column=0)
You should see a window that looks something like this:
See how the "x" is at the far left edge of the button? That is because it doesn't know anything about the ipadx value applied to its parent. All it knows is that the button is X pixels widget, and that it's supposed to be on the left edge of the button.
That is what's happening with you original frame and button - the button is being added inside the frame, making use of all of the space inside the frame.
So far I have figured out that using padding=... in the LabelFrame constructor produces the correct result.
If you delete the line
inner_frame.grid_configure(ipadx=20)
and use
inner_frame = ttk.LabelFrame(root, text='inner_frame', padding=[20, 0])
or alternatively
inner_frame['padding'] = [20, 0]
the result looks like this:
I have no clue why using ipadx through grid_configure does not work as expected.

TK, understanding pack and expanding space

I have a window that is resizeable, where i want to have two listboxes with scroll bars that expand to fill all space available.
when i have only one listbox packed as fill=both, expand=1, side=left and one scrollbar packed as fill=Y, expand=0, side=right then it will expand only horizontally, even though it is set to fill both directions. when i resize the window, the listbox only fill the sides. the bottom of the window remain empty.
then i moved on to add another listbox. Now instead of packing the scroll bar on right, i packed everything to left, so they are stacked. the listboxes continue to have fill=both, expand=1. Now when i resize the window both list boxes only fill vertically! the horizontal space remains empty.
what is going on? why does it ignore the vertical space with one element packed left and another right? and why it refuses to fill horizontally when everything is stacked left?
the fact that once it fill the vertical or the horizontal space leads me to believe the parent frame is expanding fine... or should i investigate that more as well?
Without seeing your actual code it's impossible to know what you're doing wrong. Here's an example to prove that pack works as documented:
import Tkinter as tk
root = tk.Tk()
lb1 = tk.Listbox(root)
lb2 = tk.Listbox(root)
vsb1 = tk.Scrollbar(root, orient="vertical", command=lb1.yview)
vsb2 = tk.Scrollbar(root, orient="vertical", command=lb2.yview)
lb1.configure(yscrollcommand=vsb1.set)
lb2.configure(yscrollcommand=vsb2.set)
lb1.pack(side="left", fill="both", expand=True)
vsb1.pack(side="left", fill="y", expand=False)
lb2.pack(side="left", fill="both", expand=True)
vsb2.pack(side="left", fill="y", expand=False)
root.mainloop()
workaround i'm using (downvote if not the right tk way)
i created two frames, both side=LEFT, expand=1, fill=BOTH and then put each pair of listbox+scrollbar there. now everything expands/fills just fine.
previously the listboxes were in the yellow frame. The ones i just created are the the blue and green.
still not sure with the pack manager would not expand the listboxes when they had scrollbars without expansion between them...

Is it possible to easily center side-stacked frames in a frame in tkinter?

I am faced with the problem to center side-stacked frames in a parent frame. I know how to center a single frame in a frame but I did not find a simple way to do this for several of them.
I get the following window
from the code below:
import Tkinter as tk
root = tk.Tk()
root.geometry("200x200")
# main frame
f = tk.Frame(root, background='black')
f.pack(expand=True, fill="both")
# two side-by-side frames inside, they fill up their space
f1 = tk.Frame(f, background='green')
f1.pack(side=tk.LEFT, expand=True, fill="both")
f2 = tk.Frame(f, background='red')
f2.pack(side=tk.LEFT, expand=True, fill="both")
# three fixed-size frames in the left frame above; I would like them to be centered in the frame
tk.Frame(f1, width=20, height=20, background="orange").pack(side=tk.LEFT, fill=None, expand=False)
tk.Frame(f1, width=20, height=20, background="white").pack(side=tk.LEFT, fill=None, expand=False)
tk.Frame(f1, width=20, height=20, background="gray50").pack(side=tk.LEFT, fill=None, expand=False)
root.mainloop()
I would like the three square frames to be centered in the green one. I had to use tk.LEFT to position them, otherwise they would have been stacked up by default.
In my complete program, the green frame is there to exclusively contain the three square frames.
What is the most standard way to center the three square frames in the green one?
While thinking about furas's comment I realized that I did not understand the true difference between expand and fill (it is still a bit vague). It is possible to center the three frames by changing the f1.pack() line to:
f1.pack(side=tk.LEFT, expand=True, fill=None)
The f1 frame is tight around the three square (fill=None) ones buts tries to take as much space as possible in all directions (expand=True), effectively being centered. Note that the green background is not visible, the frame being tight around its content.

Scrollbar not stretching to fit the Text widget

I was able to get the Scrollbar to work with a Text widget, but for some reason it isn't stretching to fit the text box.
Does anyone know of any way to change the height of the scrollbar widget or something to that effect?
txt = Text(frame, height=15, width=55)
scr = Scrollbar(frame)
scr.config(command=txt.yview)
txt.config(yscrollcommand=scr.set)
txt.pack(side=LEFT)
In your question you're using pack. pack has options to tell it to grow or shrink in either or both the x and y axis. Vertical scrollbars should normally grow/shrink in the y axis, and horizontal ones in the x axis. Text widgets should usually fill in both directions.
For doing a text widget and scrollbar in a frame you would typically do something like this:
scr.pack(side="right", fill="y", expand=False)
text.pack(side="left", fill="both", expand=True)
The above says the following things:
scrollbar is on the right (side="right")
scrollbar should stretch to fill any extra space in the y axis (fill="y")
the text widget is on the left (side="left")
the text widget should stretch to fill any extra space in the x and y axis (fill="both")
the text widget will expand to take up all remaining space in the containing frame (expand=True)
For more information see http://effbot.org/tkinterbook/pack.htm
Here is an example:
from Tkinter import *
root = Tk()
text = Text(root)
text.grid()
scrl = Scrollbar(root, command=text.yview)
text.config(yscrollcommand=scrl.set)
scrl.grid(row=0, column=1, sticky='ns')
root.mainloop()
this makes a text box and the sticky='ns' makes the scrollbar go all the way up and down the window
Easy solution to use a textbox with an integrated scrollbar:
Python 3:
#Python 3
import tkinter
import tkinter.scrolledtext
tk = tkinter.Tk()
text = tkinter.scrolledtext.ScrolledText(tk)
text.pack()
tk.mainloop()
To read the textbox:
string = text.get("1.0","end") # reads from the beginning to the end
Of course you can shorten the imports if you want.
In Python 2 you import ScrolledText instead.

Categories