tkinter deforms my frames after inserting objects - python

So I want to insert some objects in a frame, but when I firstly added a button the frames where were they weren't suppoused to.
Before
After
And this is the code:
import tkinter as tk
root = tk.Tk()
root.geometry("1200x700")
# Main frames
frame1 = tk.Frame(root, width=1200, height=625)
frame2 = tk.Frame(root, width=1200, bg="black", height=75)
frame1.grid(row=1, column=1)
frame2.grid(row=2, column=1)
# Secondary frames
frame1browser = tk.Frame(frame1, height=625, width=850, bg="grey")
frame1a = tk.Frame(frame1,height=625, width=(1200-850))
frame1browser.grid(row=1, column=1)
frame1a.grid(row=1, column=2)
# Last frames
frame1aa = tk.Frame(frame1a, width=(1200-850),height=525, bg="green")
frame1ab = tk.Frame(frame1a, width=(1200-850),height=100, bg="yellow")
frame1aa.grid(row=1, column=1)
frame1ab.grid(row=2, column=1, sticky="nswe")
# Elements that are not frames
Button1 = tk.Button(frame1ab, text="ur mother")
Button1.grid(column=1, row=1)
root.mainloop()

The frame ignores the width/height explicitly given if there is a widget inside it, by default. AFAIK, It finds and uses the minimum size required to fit all the widgets, also accommodating to extra properties like sticky, expand and so on.
To override this behavior, you will have to use the <grid/pack>_propagate(False) depending on whether you use pack or grid on the items inside the frame. Now the frame will grow/shrink as much as the size you specify.
frame1ab.grid_propagate(False)

Related

Tkinter frame, how to make it resize after destroying child widgets and repopulating with new widgets

My app displays data from a sqlite database. After displaying the initial data, say 50 records when my program destroys the widgets associated with that initial display and repopulates based on a search function the frame container no longer adapts to the size of the new widgets placed in it.
To make this simpler I've created smaller simpler version of the problem. My initial version of this post was from that actual app and difficult understand. Lesson learned. Running this code defaults to showin 50 rows initially, Then try entering 100 in the search field and click the button and see that the window does not expand to fit. Then can then try entering 40 and see that the window doesn't shrink. I'm using Python v3.11.
Here's the simplified code that captures the essence of my problem:
from tkinter import *
from tkinter import ttk
def displayData():
for widgets in second_frame.winfo_children():
widgets.destroy()
Count = Search.get()
print("displayData entered with value of " + str(Count))
nRows = int(Count)
rows = []
for i in range(nRows):
label_list = Label(second_frame, text='Row '+str(i), relief=GROOVE, font=("Arial 11"), width=17, anchor='w', justify=LEFT)
label_list.grid(row=i, column=0, sticky=NSEW)
rows.append(label_list)
def _on_mousewheel(event):
my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
# Set up tkinter GUI
root = Tk()
root.geometry("1250x810+100+0")
root.title("Test")
# Control Frame
control_frame = Frame(root, height=10, highlightbackground="blue", highlightthickness=2, pady=3, padx=3)
control_frame.pack(fill=X)
Search = StringVar()
Search.set('50')
ent_search = ttk.Entry(control_frame, width=15, textvariable=Search)
ent_search.pack(padx=5, pady=5, side=RIGHT)
# Create a Main Frame
main_frame = Frame(root, highlightbackground="yellow", highlightthickness=2)
main_frame.pack(fill=BOTH, expand=1)
# Create a Canvas
my_canvas = Canvas(main_frame)
my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
my_canvas.bind_all("<MouseWheel>", _on_mousewheel)
# Add a Scrollbar to the Canvas
my_scrollbar = ttk.Scrollbar(main_frame, orient=VERTICAL, command=my_canvas.yview)
my_scrollbar.pack(side=RIGHT, fill=Y)
# Configure the Canvas
my_canvas.configure(yscrollcommand=my_scrollbar.set)
my_canvas.bind('<Configure>', lambda e: my_canvas.configure(scrollregion = my_canvas.bbox('all')))
# Create Another Frame inside the Canvas
second_frame = Frame(my_canvas)
# Add the New Frame to a Window inside the Canvas
my_canvas.create_window((0,100), window=second_frame, anchor='nw')
btn_search = Button(control_frame, text='Search', command=displayData)
btn_search.pack(padx=5, pady=5, side=RIGHT)
rows = []
displayData()
root.mainloop()
```
FYI: my actual app is displaying database records based on search parameters. The programs works fine except for the fact once the initial size of 'second_frame' is set up it never changes. So if a search happens to display more records than that initial display, those records will be hidden. e.g. Initial display shows 50 records, if a search asks to display 75 records, 25 of them will not be visible...So the second_frame doesn't resize to show the added widgets in the search.
My workaround for now is just to initially display more records then I anticipate most searches will need to display.
How can I make 'second_frame' adapt to new amounts of widgets on new searches? The simplified code above emulates my issue.
Yahoo, found a solution to this issue. Essentially one must first destroy and recreate the canvas used to display the records. Some complications due to the scrollbar widget being linked to the frame containing the canvas. I solved this by creating an intermediate frame to contain the frame referenced by the scrollbar and the used the methods winfo_childred() and destroy() to delete the frames/canvas/widgets before recreating them anew to display the new data.
Here's the modified example code that now works correctly:
from tkinter import *
from tkinter import ttk
def makeCanvas():
# Create a Canvas
global my_canvas
global second_frame
global my_scrollbar
global main_frame
mid_frame = Frame(main_frame)
mid_frame.pack(fill=BOTH, expand=1)
my_canvas = Canvas(mid_frame)
my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
my_canvas.bind_all("<MouseWheel>", _on_mousewheel)
# Add a Scrollbar to the Canvas
my_scrollbar = ttk.Scrollbar(mid_frame, orient=VERTICAL, command=my_canvas.yview)
my_scrollbar.pack(side=RIGHT, fill=Y)
# Configure the Canvas
my_canvas.configure(yscrollcommand=my_scrollbar.set)
my_canvas.bind('<Configure>', lambda e: my_canvas.configure(scrollregion = my_canvas.bbox('all')))
# Create Another Frame inside the Canvas
second_frame = Frame(my_canvas)
# Add the New Frame to a Window inside the Canvas
my_canvas.create_window((0,0), window=second_frame, anchor='nw')
def displayData():
global displayCnt
print("displayData entered with displayCnt of " + str(displayCnt))
if displayCnt != 0:
for widgets in main_frame.winfo_children():
widgets.destroy()
Count = Search.get()
makeCanvas()
print("displayData entered with value of " + str(Count))
nRows = int(Count)
rows = []
for i in range(nRows):
label_list = Label(second_frame, text='Row '+str(i), relief=GROOVE, font=("Arial 11"), width=17, anchor='w', justify=LEFT)
label_list.grid(row=i, column=0, sticky=NSEW)
rows.append(label_list)
displayCnt += 1
def _on_mousewheel(event):
my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
# Set up tkinter GUI
root = Tk()
root.geometry("1250x810+100+0")
root.title("Test")
# Control Frame
control_frame = Frame(root, height=10, highlightbackground="blue", highlightthickness=2, pady=3, padx=3)
control_frame.pack(fill=X)
Search = StringVar()
Search.set('50')
ent_search = ttk.Entry(control_frame, width=15, textvariable=Search)
ent_search.pack(padx=5, pady=5, side=RIGHT)
# Create a Main Frame
main_frame = Frame(root, highlightbackground="yellow", highlightthickness=2)
main_frame.pack(fill=BOTH, expand=1)
# Create a Canvas
#makeCanvas()
btn_search = Button(control_frame, text='Search', command=displayData)
btn_search.pack(padx=5, pady=5, side=RIGHT)
rows = []
displayCnt = 0
displayData()
root.mainloop()
I ended up creating a separate function makeCanvas() to recreate the frame/canvas structure upon each new display of data. For this much simplified example the data consists simply of numbered Label widgets.

How do you make a child label smaller than its parent frame?

There is only one frame in my GUI, and it resizes itself to the size of the window. The frame has a child label, and I want the label to always be 1/3 the height of the frame and 1/1.5 the width of the frame. The code below tries to do that but the label always resizes itself to the size of the frame.
import tkinter
tk = tkinter.Tk()
tk.geometry("400x400")
f = tkinter.Frame(tk, bd=5, bg="white")
f.pack(padx=10, pady=10)
def callback(event):
f.config(height=tk.winfo_height(), width=tk.winfo_width())
l.config(width=int(f.winfo_width()/1.5), height=int(f.winfo_height()/3))
l = tkinter.Label(f, text="lead me lord", bg="yellow", relief=tkinter.RAISED, bd=5)
l.pack(side="bottom")
tk.bind("<Configure>", callback)
tk.mainloop()
The width and height of the label are in characters. In order to use pixels, you need to add an empty image to the label:
img = tkinter.PhotoImage() # an image of size 0
l = tkinter.Label(f, text="lead me lord", bg="yellow", relief=tkinter.RAISED, bd=5,
image=img, compound='center')
Actually you don't need to resize the frame in the callback if you add fill="both", expand=1 into f.pack(...):
import tkinter
tk = tkinter.Tk()
tk.geometry("400x400")
f = tkinter.Frame(tk, bd=5, bg="white")
f.pack(padx=10, pady=10, fill="both", expand=1)
def callback(event):
l.config(width=int(f.winfo_width()/1.5), height=int(f.winfo_height()/3))
#l.config(width=event.width*2//3, height=event.height//3) # same as above line if bind on frame
img = tkinter.PhotoImage()
l = tkinter.Label(f, text="lead me lord", bg="yellow", relief=tkinter.RAISED, bd=5,
image=img, compound='center')
l.pack(side="bottom")
f.bind("<Configure>", callback) # bind on frame instead of root window
tk.mainloop()
Given your precise specifications, the best solution is to use place since it lets you use relative widths and heights. However, if you plan to have other widgets in the window, place is rarely the right choice.
This example will do exactly what you asked: place the label at the bottom with 1/3 the height and 1/1.5 the width. There is no need to have a callback for when the window changes size.
Note: I had to change the call to pack for the frame. The text of your question said it would expand to fill the window but the code you had wasn't doing that. I added the fill and expand options.
import tkinter
tk = tkinter.Tk()
tk.geometry("400x400")
f = tkinter.Frame(tk, bd=5, bg="white")
f.pack(padx=10, pady=10, fill="both", expand=True)
l = tkinter.Label(f, text="lead me lord", bg="yellow", relief=tkinter.RAISED, bd=5)
l.place(relx=.5, rely=1.0, anchor="s", relheight=1/3., relwidth=1/1.5)
tkinter.mainloop()

Can't prevent label from extending the main window

Example
(mimics the relevant parts of the layout in my real code)
import Tkinter as tk
import ttk
# set up root
root = tk.Tk()
root.minsize(300, 50)
frame = ttk.Frame(root)
frame.grid(row=0, column=0, sticky=tk.EW)
# set up buttons that insert a short or a long string
textvar = tk.StringVar(value='foo')
def insert_short():
textvar.set('foo')
def insert_long():
textvar.set('foo'*30)
button_short = ttk.Button(frame, text='short', command=insert_short)
button_short.grid(row=0, column=0)
button_long = ttk.Button(frame, text='long', command=insert_long)
button_short.grid(row=0, column=0)
button_long.grid(row=0, column=1)
# set up label
# border for label to see its size
style = ttk.Style()
style.configure(
'Bordered.TLabel', foreground='black', borderwidth=1, relief='solid')
# make label extend to the right
frame.columnconfigure(2, weight=1)
# place label
label = ttk.Label(frame, textvariable=textvar, style='Bordered.TLabel')
label.grid(row=0, column=2, sticky=tk.EW)
# place some other widget under label to mimic my real code
ttk.Button(frame, text='some other widget').grid(row=1, column=2)
# TRIED, NOT WORKING:
#root.resizable(False, False)
#frame.propagate(False)
#frame.grid_propagate(False)
#label.propagate(False)
#label.grid_propagate(False)
root.mainloop()
Output
Question
How do I prevent label from extending the main window?
(Bonus question, but not important: is there a way to make the label scrollable if it gets too long?)
Attempts
I tried the following commands:
root.resizable(False, False)
frame.propagate(False)
frame.grid_propagate(False)
label.propagate(False)
label.grid_propagate(False)
You can create a scrollable label using an Entry in a read-only state and by using scrolling it will prevent the widget from extending the main window.
Try replacing your label definition with the following code:
child_frm = ttk.Frame(frame)
label = ttk.Entry(child_frm, textvariable=textvar, style='Bordered.TLabel', state='readonly')
scroll = ttk.Scrollbar(child_frm, orient='horizontal', command=label.xview)
label.config(xscrollcommand=scroll.set)
label.grid(row=0, sticky=tk.EW)
scroll.grid(row=1, sticky=tk.EW)
child_frm.grid(row=0, column=2)
By default, the width of a Label is calculated based on its contents. You can override this behavior by specifying a value for width when creating the Label.
label = ttk.Label(frame, textvariable=textvar, style='Bordered.TLabel', width=1)
Much to my surprise, when I update your code with this, the label doesn't shrink to a size suitable for displaying exactly one character. It appears that the sticky=tk.EW argument of your grid call ensures that the label stays as wide as the widest element in the column.

Tkinter frame problem : resizing does not work

I want to be able to use nested frames but there is a weird behavior : when I enter the height and width parameters they seem to not work. I use .grid() Is that what is causing the problem ? I use ttk Frame, is there some behavior I do not know about ?
I looked at the documentation but nothing seemed to be helping. I tried changing the parameters but I didn't help either.
from tkinter import *
from tkinter import ttk
root = Tk()
root.title("Tk test")
root.geometry("800x800")
frame_1 = ttk.Frame(root, height=400, width=400, relief="sunken")\
frame_1.grid(row=0, column=0, rowspan=1, columnspan=1)
frame_2 = ttk.Frame(frame_1, height=200, width=200, relief="sunken")\
frame_2.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="N, S, W, E")
label_1 = ttk.Label(frame_2, text="Text")
label_1.grid(row=0, column=0, sticky="S, W, N, E")
root.mainloop()
Expected result : there is a sunken frame inside another sunken frame. Inside the nested frame there is a label named "Text"
Actual result : The label is always in the upper left corner and does not want to move.
You can give cells on a grid a minimum size using the grid_columnconfigure() and grid_rowconfigure methods, as documented here.
Applied to your code (along with other corrections & improvements):
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Tk test")
root.geometry("800x800")
frame_1 = ttk.Frame(root, height=400, width=400, relief="sunken")
frame_1.grid(row=0, column=0)
frame_2 = ttk.Frame(frame_1, height=200, width=200, relief="sunken")
frame_2.grid(row=0, column=0, sticky="NSWE")
frame_2.grid_rowconfigure(0, minsize=200)
frame_2.grid_columnconfigure(0, minsize=200)
label_1 = ttk.Label(frame_2, text="Text")
label_1.grid(row=0, column=0, sticky="NW")
root.mainloop()
Since the grid manager doesn't know how many rows and columns there are to be on the main window, it doesn't allot the frames with the defined height and width.
If you add padding to each frame, you will see that the Text widget in not the upper left corner. But the Text widget will always be in the upper left corner as it has been placed on the 0th row and column.
Also, use rowconfigure and columnconfigure to ensure that the frames take the space specified by you on the main window.

Expand a listbox over 2 columns Tkinter

I have the following interface: 3 frames from which 2 are on the left-hand, frame1 and frame2 and another one which is on the right, frame3.
I want to create another frame, frame4 which will have the label Output and under it there should be a Listbox. I want that both these widgets to span over the previous frames, each being places in a cell by using the grid manager.
Also I am not sure if Listbox is the widget I should be using. I want something which will contain the output of the program I will run through my application. I also thought of Entry but I need something in which I can display more than one line.
This is the code I have so far:
from Tkinter import *
root = Tk()
frame1 = Frame(root)
frame1.grid(row=0,column=0)
frame2 = Frame(root)
frame2.grid(row=1,column=0)
frame3 = Frame(root)
frame3.grid(row=0,column=1)
frame4 = Frame(root)
frame4.grid(row=2,columnspan=2)
l5 = Label(frame4, text='Output:').grid(row=2,columnspan=2)
output = Listbox(frame4, height=5)
#output.grid(row=2,column=0,columnspan=2)
#output.pack(side=LEFT, fill=BOTH, expand=1)
root.mainloop()
I managed to make the label to span across the other frames, but when I uncommented either of the last 2 lines, the interface didn't open and the program just froze.
How can I solve this?
I slightly ammended your code:
from tkinter import * # for python 2.7 use Tkinter
root = Tk()
frame1 = Frame(root, bg='red', height=20)
frame1.grid(row=0,column=0, sticky=W+E)
frame3 = Frame(root, bg='blue', height=20)
frame3.grid(row=0,column=1, sticky=W+E)
frame2 = Frame(root, bg='green', height=20)
frame2.grid(row=1,column=0, sticky=W+E)
frame4 = Frame(root)
frame4.grid(row=2,columnspan=2, sticky=E+W)
l5 = Label(frame4, text='Output:', bg='orange').grid(row=0, column=0, sticky=E+W)
output = Listbox(frame4, height=5, width=50)
output.grid(row=1,column=0)
#output.pack(side=LEFT, fill=BOTH, expand=1)
root.mainloop()
This results in:
So basically what I did was to add bg, height and sticky parameters to frames and label to easily visual what is happening and how they frames are laid out. Also I modified grid parameters listbox and label.
Hope this is what you are after, or it will help you to get there.
If you use both .grid() and .pack() Tkinter (python 2.x) or tkinter (python 3.x) will happily spend the rest of your life trying to find a way to satisfy both, you must use one within the same window or Frame, see http://www.openbookproject.net/py4fun/gui/tkPhone.html

Categories

Resources