Resizing tkinter windows and contents - python

I am using python tkinter to build a ui containing a matplotlib figure and some buttons, but am having difficulty with resizing the window and it's contents. I've looked at some of the examples on this site and the docs and as I understand it, for a frame containing smaller frames to resize together they all need to be configured individually. Each one gets a weight applied to it to define how much of the available space it receives (is this correct?). However, when I try to apply this as shown below none of the frames resize.
Also if the wiegth is zero for columnconfigure and 1 for row configure does that mean it will only resize in one direction?
import Tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class Application():
def __init__(self, master):
frame2 = Tkinter.Frame(master, height=510, width=770, bg='red')
frame2.grid(row=0, column=0, sticky='nsew')
frame2.columnconfigure(0, weight=1)
frame2.rowconfigure(0, weight=1)
frame2a = Tkinter.Frame(frame2, height=80, width=770, bg='blue')
frame2a.grid(row=0, column=0, sticky='nsew')
frame2a.columnconfigure(0, weight=1)
frame2a.rowconfigure(0, weight=1)
frame2b = Tkinter.Frame(frame2, height=410, width=770, bg='green')
frame2b.grid(row=1, column= 0, sticky='nsew')
frame2b.columnconfigure(0, weight=1)
frame2b.rowconfigure(1, weight=1)
# add plot
fig = Figure(figsize=(9.5,5.2), facecolor='white')
fig.add_subplot(111)
canvas = FigureCanvasTkAgg(fig, master=frame2b)
canvas.show()
canvas.get_tk_widget().pack(side='top', fill='both', expand=1)
if __name__ == '__main__' :
root = Tkinter.Tk()
root.geometry("770x510")
app = Application(root)
root.mainloop()

Your approach using columnconfigure and rowconfigure is correct, but you forgot one thing: You haven't used the methods on the master window. So, you basically want to do this:
def __init__(self, master):
master.columnconfigure(0, weight=1)
master.rowconfigure(0, weight=1)
To answer your other question (Also if the wiegth is zero for columnconfigure and 1 for row configure does that mean it will only resize in one direction?): Yes, you're right, the widget/window would extend in just one or none direction then.
Additionally, since you are using grid for dynamic resizing, the height and width parameters are obsolete.

The rowconfigure needs to be applied to the container, not to the contained objects. You have frame 2 containing frame2a and frame2b. So along with frame2.rowconfigure(0, weight=1), giving a weight of 1 to row 0 (i.e. to frame2a), you need to add frame2.rowconfigure(1, weight=1) to weight the other row equally. On the other hand, the four lines of code of the form frame2[ab].[row|column]configure(...) are redundant.

Related

How do I get my frame sizes to match up to my root window size?

I'm making a text game. I used an entry widget for player input, a text widget for the game's output, and put them into frames. I set the root window's geometry and the frame sizes to fit into that geometry. However, the frame sizes are smaller than expected. Specifically, my story_text_frame is shorter than expected. I have done a tutorial, and am not sure what I am missing now.
import tkinter as tk
class Game(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
self.geometry('1280x720')
self.player_input_frame = tk.Frame(self, height=20, width=625)
self.player_input_field = tk.Entry(self.player_input_frame, background='black', foreground='white', relief='flat')
self.player_input_field.grid(row=0, column=0)
self.player_input_frame.grid(row=2, column=1)
self.story_text_frame = tk.Frame(self, height=670, width=625)
self.story_text_field = tk.Text(self.story_text_frame, background='grey', foreground='white')
self.story_text_field.grid(row=0, column=0)
self.story_text_frame.grid(row=1, column=1)
To have a widget size follow the size of a master widget or a window you must specify how this is to be done with columnconfigure() and rowconfigure(). Then you must expand the widget to fill the available cell space by instructing grid() to stick to the edges; sticky='nsew'. See my example:
import tkinter as tk
class Game(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
self.geometry('800x600')
self.columnconfigure(0, weight=1) # Specify how columns and rows will
self.rowconfigure(0, weight=1) # change when window size changes.
# The game's output Text() widget
self.story_text_field = tk.Text(self)
self.story_text_field.grid(row=0, column=0, sticky='nsew',
pady=10, padx=10) # Sticky expands the widget to fill the cell
# The player input Entry() widget
self.player_input_field = tk.Entry(self)
self.player_input_field.grid(row=1, column=0, pady=(0,10))
Game().mainloop()
I have removed the Frames to make the construction clearer. Also I removed the coloring of the widgets. I introduced some padding to make the result more pleasing to the eye and changed the window size to be easier to handle on my tiny screen.
For further information on grid() I can recommend effbot's The Tkinter Grid Geometry Manager. You can also read Mike - SMT's answer to Find position of another anchor than the anchor already used which elaborates on both grid() and pack().

Disable tree view from filling the window after update

I have very simple grid layout with two columns, where first column should display some text, and the second to show tree view:
#! python3
from random import randint
import tkinter as tk
from tkinter import ttk
from tkinter.constants import *
class Application(ttk.Frame):
def __init__(self, root):
self.root = root
self.root.resizable(0, 0)
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=3)
self.init_widgets()
self.arrange_grid()
def init_widgets(self):
self.text_frame = ttk.Labelframe(self.root, text='Info')
self.button = ttk.Button(self.root, text='Process', command=self.on_button)
self.tree = ttk.Treeview(self.root)
self.scroll = ttk.Scrollbar(self.root, orient=HORIZONTAL, command=self.tree.xview)
self.tree.configure(xscrollcommand=self.scroll.set)
def arrange_grid(self):
self.text_frame.grid(row=0, sticky=NSEW)
self.button.grid(row=0, sticky=N, pady=32)
self.tree.grid(row=0, column=1, sticky=NSEW)
self.scroll.grid(row=0, column=1, sticky=(S, W, E))
def on_button(self):
headers = list(range(20))
rows = [[randint(0, 100)] * len(headers) for i in headers]
self.tree["columns"] = headers
for i, row in enumerate(rows):
self.tree.insert("", i, values=row)
if __name__ == '__main__':
root = tk.Tk()
app = Application(root)
root.mainloop()
When I click on a "Process" button, tree view is populated with data, but at the same time it resizes the root window and fills whole space.
How can I instruct ttk tree view, to remain it's size after populating with data?
The treeview will grow to fit all of its columns, unless constrained by the window. The window will grow to fit all of it children unless you give it a fixed size. What is happening is that you're giving the treeview many columns, causing it to grow. Because it grows, the window grows because you haven't constraint its growth.
There are several solutions. Perhaps the simplest solution is to put the tree in a frame so that you can give it an explicit width and height. The key to this is to make the frame control the size of its children rather than the other way around. This is done by turning geometry propagation off.
First, start by creating a frame, and then putting the tree in the frame. We can also put the scrollbar in the frame so that we can treat the tree and scrollbar as a single unit.
self.tree_frame = tk.Frame(self.root, width=400, height=200)
self.tree = ttk.Treeview(self.treeframe)
self.scroll = ttk.Scrollbar(self.tree_frame, orient=HORIZONTAL, command=self.tree.xview)
self.tree.configure(xscrollcommand=self.scroll.set)
Next, add the treeview and scrollbar to the frame. You can use any of pack, place or grid; I find pack superior for a top-to-bottom layout. We also use pack_propagate to turn off geometry propagation (meaning: the frame width and height are honored):
self.tree_frame.pack_propagate(0)
self.scroll.pack(side="bottom", fill="x")
self.tree.pack(side="top", fill="both", expand=True)
With that, you need to modify your arrange_grid to put the frame in the root window, and then ignore the scrollbar since it's already packed in the frame:
def arrange_grid(self):
self.text_frame.grid(row=0, sticky=NSEW)
self.button.grid(row=0, sticky=N, pady=32)
self.tree_frame.grid(row=0, column=1, sticky=NSEW)
Note: you've turned off the ability for the user to resize the window. I recommend avoiding this -- the user usually knows better what size they want the window. Instead, you should configure your GUI to properly resize when the user resizes the window.
Since you're using grid, all you have to do is tell tkinter which rows and columns get any extra space caused by the user resizing the window. Since everything is in a single row, you merely need to give that row a weight:
root.grid_rowconfigure(0, weight=1)

Scrollbar and resizing won't work with tkinter grid array and treeview

I have written and application frontend with tkinter that uses a page manager layout and would like to lay it out with a grid system instead of pack. I could not get the scrollbar function to work at all with the grid layout so I set it up with pack. This generally works, but the layout I want will require a grid (or a bunch of extra frames while using pack).
Can someone take a look to see what I'm not getting? I have included code using the pack system that gives something close to what I want to achieve with grid. The corresponding grid commands that I thought would work are commented out.
I also took out the page management in the main class and reduced it to one page to make the issues more clear. I want to keep page management in the long run...
Thanks for looking!
import tkinter as tk
from tkinter import ttk
import tkinter.font as tkFont
colHeadings = ['car', 'repair']
itemList = [('Hyundai', 'brakes'),('Honda', 'light'),
('Lexus', 'battery'), ('Benz', 'wiper'),
('Ford', 'tire'), ('Chevy', 'air'),
('Chrysler', 'piston'), ('Toyota', 'brake pedal'),
('BMW', 'seat')]
def buildtree(thetree, colHeadings, itemList):
thetree['columns'] = colHeadings
for each in colHeadings:
thetree.column(each, width=tkFont.Font().measure(each.title()))
thetree.heading(each, text=each.title())
for item in itemList:
thetree.insert('', 'end', values=item)
for i, each in enumerate(item):
colwidth = int(1.3*tkFont.Font().measure(each))
if thetree.column(colHeadings[i], width=None) < colwidth:
thetree.column(colHeadings[i], width=colwidth)
class AppBT(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = ttk.Frame(self)
frame = StatsTree(container)
container.pack(side="top", fill="both", expand=1)
frame.pack(side="top", fill="y",expand=1)
# container.grid(row=0, column=0, sticky="nsew")
# container.grid_rowconfigure(0, weight=1)
# container.grid_columnconfigure(0, weight=1)
# frame.grid(row=0, column=0, sticky="ns")
class StatsTree(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent)
tframe = ttk.Frame(self)
yscrollbar = ttk.Scrollbar(tframe, orient="vertical")
tree = ttk.Treeview(tframe)
yscrollbar.config(command=tree.yview)
tree.config(yscrollcommand=yscrollbar.set)
buildtree(tree, colHeadings, itemList)
tree.pack(side="left", fill="y")
yscrollbar.pack(side="right", fill="y")
tframe.pack(side="top", fill="y", expand=1, padx=10, pady=10)
# tframe.grid(row=0, column=0, sticky="ns", padx=10, pady=10)
# tframe.grid_rowconfigure(0, weight=1)
# tframe.grid_columnconfigure(0, weight=1)
# tree.grid(row=0, column=0, sticky="ns")
# tree.grid_rowconfigure(0, weight=1)
# tree.grid_columnconfigure(0, weight=1)
# yscrollbar.grid(row=0, column=1, sticky="ns")
# yscrollbar.grid_rowconfigure(0, weight=1)
# yscrollbar.grid_columnconfigure(1, weight=1)
app = AppBT()
app.geometry("800x600")
app.mainloop()
First off, there's nothing wrong with how the scrollbars work. If you are able to shrink the window so that some of the items are obscured, the scrollbars will work as expected. There is nothing you need to do to fix the scrollbar behavior.
As a general rule of thumb, whenever you place widgets into another widget using grid, you must give at least one row and one column of the parent widget a positive weight. You are forgetting to do that in two places.
The first problem is that you're using grid to place container in the root window, but you haven't given a weight to the rows and columns in the root window. Therefore, container will not expand to fill the window as a whole, so nothing in container can expand, either.
Since this is the only widget in the root window, in my opinion it's a bit easier to use pack. Since you want to use grid you need to add the following in AppBT.__init__:
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
You repeat the same problem with StatsTree - you are using grid to place tframe in this frame, but you aren't configuring the weight of the rows and columns in StatsTree. That prevents tframe from expanding, which in turn prevents the tree and scrollbar from expanding.
You need to add the following to StatsTree.__init__ if you wish to use grid for the child widgets in StatsTree
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1

If I'm using grid(), why the widgets don't scale when I resize the window?

I'm using tkinter with Python 3.4 in Windows 7.
I'm positioning in a non-absolute way (I'm not using place, I'm using grid), and therefore, the widgets should scale when I resize the window automatically. Nevertheless, that does not happen, and I'm failing to grasp the point. Here's my code:
import tkinter as tk
class App(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.config()
self.grid()
self.create_widgets()
def config(self):
self.master.title("Pykipedia Explorer")
def create_widgets(self):
self.search_label = tk.Label(self, text="Search: ")
self.search_label.grid(row=0, column=0, sticky=tk.N+tk.SW)
self.search_entry = tk.Entry(self)
self.search_entry.grid(row=0, column=0, padx=60, sticky=tk.N+tk.SW)
self.search_button = tk.Button(self, text="Explore!")
self.search_button.grid(row=0, column=0, padx=232, sticky=tk.SW)
self.content_area = tk.Text(self)
self.content_area.grid(row=1, column=0)
self.content_scroll_bar = tk.Scrollbar(self, command=self.content_area.yview)
self.content_scroll_bar.grid(row=1, column=1, sticky=tk.NW+tk.S+tk.W)
self.content_area["yscrollcommand"] = self.content_scroll_bar.set
self.quit_button = tk.Button(self, text="Quit", command=self.quit)
self.quit_button.grid(row=2, column=0, sticky=tk.SW)
def main():
app = App()
app.mainloop()
return 0
if __name__ == '__main__':
main()
Why??
Also, I've tried to use grid_columnconfigure and grid_rowconfigure just like in this answer, and it fails miserably.
You have several problems in your code that are working together to prevent the widgets from scaling (and by "widgets", I assume you mean the text widget).
First, you use grid to put the instance of App in the root window. However, you haven't set the sticky attribute, so the app won't grow and shrink. If it doesn't grow and shrink, neither will its contents.
Also, because you're using grid, you need to give row zero and column zero a positive weight so that tkinter will allocate extra space to it. However, since this is the only widget in the root window, you can use pack and solve the problem by replacing the call to grid with a call to pack:
self.pack(fill="both", expand=True)
Next, you use grid to add the text widget to the canvas. You haven't used the sticky option, so even if the space allocated to it grows, the widget will stay centered in the space. You need to use the sticky attribute to tell it to "stick" to all sides of the area it is given:
self.content_area.grid(row=1, column=0, sticky="nsew")
Finally, you haven't given any columns any "weight", which tells tkinter how to allocate extra space. When a window managed by grid resizes, any extra space is given to the rows and columns according to their weight. By default a row and column has zero weight, so it does not get any extra space.
To get the text area to grow as the window grows, you need to give column zero and row one a positive weight:
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)

Resizing Tkinter Frames with fixed aspect-ratio

I'm looking for a way to make Tkinter Frames behave somewhat like this while resizing:
from Tkinter import *
w = Tk()
w.aspect(1,1,1,1)
w.mainloop()
So I'd like this code here:
import Tkinter as tk
from Tkconstants import *
r=tk.Tk()
content_frame=tk.Frame(r,borderwidth=5,relief=GROOVE)
content_frame.grid(row=0,column=0,sticky=(N,S,E,W))
tk.Label(content_frame,text='content').pack()
pad_frame=tk.Frame(r,borderwidth=5,relief=GROOVE)
pad_frame.grid(row=1,column=0,sticky=(N,S,E,W))
tk.Label(pad_frame,text='-pad-').pack()
r.rowconfigure(0, weight=1)
r.rowconfigure(1, weight=1)
r.columnconfigure(0, weight=1)
r.mainloop()
which basically creates 2 frames, one that's supposed to hold some content (in my application this frame holds a mathplotlib graph that can be resized) and one simply for padding.
To behave on resizing following these rules:
the content frame resizes with a fixed aspect ration (let's say it always needs to be square)
the pad frame takes up the remaining (vertical) space
Any ideas? I've been reading manuals for a while now and can't seem to find something fitting.
-Daniel
There are at least a couple ways to solve this. The simplest, IMO, is to have your padding widget be a container for your content widget, and then you explicitly set the width and height of the content widget using place. This is one of the edge cases where place is preferred over grid or pack.
In the following example I've created a function which lets you pass in a content frame, a padding frame, and an aspect ratio. It then constrains the size of the content frame by the aspect ratio and the size of the container. It will make the content window fill the container in the X dimension and then set the height appropriately. If the resulting window is too tall to be visible, it sets the max height to the height of the container and adjusts the width instead.
I've tried to keep most of the code from the question intact, even though this isn't exactly how I would normally choose to code it. I've given the widgets distinct colors so it's easier to see what is happening.
import Tkinter as tk
from Tkconstants import *
r=tk.Tk()
def set_aspect(content_frame, pad_frame, aspect_ratio):
# a function which places a frame within a containing frame, and
# then forces the inner frame to keep a specific aspect ratio
def enforce_aspect_ratio(event):
# when the pad window resizes, fit the content into it,
# either by fixing the width or the height and then
# adjusting the height or width based on the aspect ratio.
# start by using the width as the controlling dimension
desired_width = event.width
desired_height = int(event.width / aspect_ratio)
# if the window is too tall to fit, use the height as
# the controlling dimension
if desired_height > event.height:
desired_height = event.height
desired_width = int(event.height * aspect_ratio)
# place the window, giving it an explicit size
content_frame.place(in_=pad_frame, x=0, y=0,
width=desired_width, height=desired_height)
pad_frame.bind("<Configure>", enforce_aspect_ratio)
pad_frame = tk.Frame(borderwidth=0, background="bisque", width=200, height=200)
pad_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=20)
content_frame=tk.Frame(r,borderwidth=5,relief=GROOVE, background="blue")
tk.Label(content_frame,text='content').pack()
set_aspect(content_frame, pad_frame, aspect_ratio=2.0/1.0)
r.rowconfigure(0, weight=1)
r.columnconfigure(0, weight=1)
r.mainloop()
This will work best if the containing widget has contents that easily adjust to the size of the container. If there is a complex layout of widgets within, some widgets could get chopped off if they don't fit when the window is shrunk below its natural size.
You could bind a function to the <Configure> event for a Frame which contains the content and padding frames. The <Configure> event will be fired when you resize a window. Use the event's width and height attributes to fix the size of the content frame by updating the weights of the rows and columns using rowconfigure and columnconfigure
You will need two rows and two columns in the container frame to have a square content frame. With a tall window, you need padding in the second row. And with a wide window you need padding in the second column.
A working example:
import Tkinter as tk
from Tkconstants import *
class Application(tk.Frame):
def __init__(self, master, width, height):
tk.Frame.__init__(self, master)
self.grid(sticky=N + S + E + W)
master.rowconfigure(0, weight=1)
master.columnconfigure(0, weight=1)
self._create_widgets()
self.bind('<Configure>', self._resize)
self.winfo_toplevel().minsize(150, 150)
def _create_widgets(self):
self.content = tk.Frame(self, bg='blue')
self.content.grid(row=0, column=0, sticky=N + S + E + W)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
def _resize(self, event):
'''Modify padding when window is resized.'''
w, h = event.width, event.height
w1, h1 = self.content.winfo_width(), self.content.winfo_height()
print w1, h1 # should be equal
if w > h:
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=0)
self.columnconfigure(0, weight=h)
self.columnconfigure(1, weight=w - h)
elif w < h:
self.rowconfigure(0, weight=w)
self.rowconfigure(1, weight=h - w)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=0)
else:
# width = height
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=0)
self.rowconfigure(0, weight=1)
self.columnconfigure(1, weight=0)
root = tk.Tk()
app = Application(master=root, width=100, height=100)
app.mainloop()

Categories