Resizing Tkinter Frames with fixed aspect-ratio - python

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

Related

Tkinter Text Widget to be inside Notebook as resizable/streched/font changable

Why I cannot stretch Text widget inside Notebook widget(Tab) with sticky?
How to get fixed Text widget size while changing font, while grid_propagate doesn't give results.
How that same window can again be resizable (weight) altogether?
Thanks
import tkinter as tk
from tkinter import *
from tkinter import ttk, font
class TextInsideNotebook:
def __init__(self, main):
self.main = main
self.fontSizePx = -20
# Font
self.fontspecs = font.Font(family="consolas", size=self.fontSizePx)
# Notebook
self.tab = ttk.Notebook(main, width=800, height=600)
self.tab.grid(row=0, column=0, sticky="nsew")
# Tab
self.tab_frame = Frame(self.tab)
self.tab.grid(row=0, column=0, sticky="nsew")
self.tab.add(self.tab_frame, text=' NEW FILE ')
# Text Area
self.textarea = tk.Text(self.tab_frame, font=self.fontspecs)
self.textarea.grid(row=0, column=0, sticky="nsew")
self.tab_frame.grid_propagate(False)
# weights
main.columnconfigure(0, weight=1)
main.rowconfigure(0, weight=1)
# Bind
self.main.bind('<Control-MouseWheel>', self.new_font_size)
def new_font_size(self, event):
if event.delta > 0:
self.fontSizePx = self.fontSizePx - 2
else:
self.fontSizePx = self.fontSizePx + 2
self.fontspecs.config(size=self.fontSizePx)
if __name__ == "__main__":
main = tk.Tk()
agc = TextInsideNotebook(main)
main.mainloop()
Why I cannot stretch Text widget inside Notebook widget(Tab) with sticky?
The text does stick to the edges of the area you've allocated to it. However, you haven't given any rows or columns inside self.tab_frame a weight so that row and column is only as wide and tall as the text widget.
If you're only putting the text widget in the frame, it's much easier to use pack than grid since it takes only one line of code rather than three:
self.textarea.pack(fill="both", expand=True)
If you wish to stick to using grid for some reason, you must give the row and column that contains the text widget a non-zero weight
self.tab_frame.grid_rowconfigure(0, weight=1)
self.tab_frame.grid_columnconfigure(0, weight=1)
self.textarea.grid(row=0, column=0, sticky="nsew")
As a rule of thumb you should always give at least one row and one column a positive non-zero weight in any widget that has children managed by grid.
How to get fixed Text widget size while changing font
What I recommend is to give the widget a small size, such as 1x1, and then let the geometry manager (pack, place, or grid) stretch it to fit the space allocated to it.
self.textarea = tk.Text(..., width=1, height=1)
How that same window can again be resizable (weight) altogether?
I don't understand that question. It's never not resizable.

TKinter scrollbar for middle of screen, supporting resizeable screen with top bar and bottom bar

I am trying to make an application that displays a grid in the middle of the screen surrounded by two bars, a top bar and a bottom bar, which contain buttons for the user to press. These buttons should be able to display no matter where the user scrolls to on the grid and should not be cut off if the window is resized. I am struggling to configure the scrollbar to track the right area and to have the grid fall off the screen when the window is resized. Here is my code so far:
from tkinter import *
def add_row(event):
input_row = Entry(grid_frame, bd=1, text="", bg="white", relief="solid")
input_row.grid(row=grid_frame.rows, sticky=N+S+E+W)
Grid.rowconfigure(grid_frame, grid_frame.rows, weight=1)
grid_frame.rows = grid_frame.rows + 1
class GridFrame(Frame):
rows = 0
def __init__(self, root):
Frame.__init__(self, root, bd=1)
root = Tk(className="Main screen")
root.minsize(408, 80)
# size to quarter of screen
w, h = root.winfo_screenwidth() / 2, root.winfo_screenheight() / 2
root.geometry("%dx%d+0+0" % (w, h))
# grid_frame will resize and bars will not
Grid.rowconfigure(root, 1, weight=1)
Grid.columnconfigure(root, 0, weight=1)
myframe = Frame(root, bd=4, relief="groove")
myframe.grid(row=1, sticky=N + W + S + E)
canvas = Canvas(myframe)
grid_frame = GridFrame(canvas)
grid_frame.pack(fill=BOTH, expand=True)
grid_frame.bind("<Button-1>", add_row)
scrollbar = Scrollbar(myframe, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=RIGHT, fill=Y)
canvas.pack(side=LEFT, fill=BOTH, expand=True)
topBar = Frame(root, grid_frame)
label = Label(topBar, text="Top Text")
label.pack()
topBar.grid(row=0, sticky=W+N+E+S)
bottomFrame = Frame(root, grid_frame)
label = Label(bottomFrame, text="Bottom Text")
label.pack()
bottomFrame.grid(row=2, sticky=E+S+W)
mainloop()
The scrollregion I want to track is the myframe/canvas/grid_frame combination I read to use from this post. The current functionality is that the scrollbar is never in an "active" state and rows added to the grid merely shrink the grid for it to fit within the display. To add a new row, click within the grid_frame region. Any help would be greatly appreciated! Here are some images of the current UI:
UI display with only a few rows
UI display with many more rows
There are two major problems with your code.
First, for the canvas to be able to scroll the inner frame, the inner frame must be a canvas object created with create_window. You're adding it to the canvas with pack, which means the canvas cannot scroll it.
To fix that, use create_window instead of pack:
canvas.create_window(0, 0, anchor="nw", window=grid_frame)
Second, you must reset the scrollregion attribute whenever the contents inside the canvas change. Normally this is done in a <Configure> event handler on the frame, but you can just as easily call it in your add_row function.
For example, add the following line to the end of add_row:
canvas.configure(scrollregion=canvas.bbox("all"))
With those two changes, the scrollbars will start to work as soon as the inner frame is taller than the canvas.
The above solves the problem of the inner window being able to scroll when you add items. In the specific example of this test program, you also have the problem that your binding is on the frame. At startup the frame has a size of 1x1 so it's a bit hard to click on. Moving the binding to the canvas will make this specific demo program work better.

Python Tkinter - How to make a child frame with image inside a set width

I'm working on a program, which I'm building using python's tkinter gui library.
My 2 Problems
I'm trying to make a frame which will house a screenshot of the selected website. I have the frame and image created, but the frame is not anchoring to the right side of the window as expected. When the label element inside the frame has an image the frame is the expected length
But, when the selected site does not have an image yet the frame is not stretching all the way to the right side of the window.
Below is the portion of my code where I set up the image container/frame and respective image.
self.frame_ImageContainer = Frame(self.tab_Details, width=320, height=180, bg='black', bd=2)
self.frame_ImageContainer.grid(column=4, row=0, rowspan=11, columnspan=3, sticky=(N, S, E, W))
self.button_TakeScreenshot = Button(self.frame_ImageContainer, text='Take Screenshot', command=self.fn_RunScraper)
self.button_TakeScreenshot.grid(column=1, row=5, sticky=(E, W))
self.widget_Image = Label(self.frame_ImageContainer, compound='top')
self.widget_Image.grid(column=0, row=0, rowspan=12, columnspan=3, sticky=N)
self.frame_ImageContainer.grid_columnconfigure(0, weight=1)
self.frame_ImageContainer.grid_columnconfigure(1, weight=1)
self.frame_ImageContainer.grid_columnconfigure(2, weight=1)
self.frame_ImageContainer.grid_rowconfigure(0, weight=1)
self.frame_ImageContainer.grid_rowconfigure(1, weight=1)
self.frame_ImageContainer.grid_rowconfigure(2, weight=1)
self.frame_ImageContainer.grid_rowconfigure(3, weight=1)
self.frame_ImageContainer.grid_rowconfigure(4, weight=1)
self.frame_ImageContainer.grid_rowconfigure(5, weight=1)
self.frame_ImageContainer.grid_rowconfigure(6, weight=1)
self.frame_ImageContainer.grid_rowconfigure(7, weight=1)
self.frame_ImageContainer.grid_rowconfigure(8, weight=1)
self.frame_ImageContainer.grid_rowconfigure(9, weight=1)
self.frame_ImageContainer.grid_rowconfigure(10, weight=1)
self.frame_ImageContainer.grid_rowconfigure(11, weight=1)
The Take Screenshot button will not center both vertically & horizontally. I've attempted to center the Take Screenshot button in the middle of the frame, but it only applies vertically currently. I've also tried using .place(relx=.5, rely=.5, anchor=CENTER), but when I try that the frame/image doesn't even show up on the screen.
What I'm trying to accomplish
Making the image frame a set width
Showing a Take Screenshot button in the center of the image frame when a site does not have a screenshot yet.
In Question Form
How can I make the frame & image stay a consistent width & height (320x180)?
How can I center the Take Screenshot button both vertically and horizontally inside the Image frame?
If you want a frame to have a fixed size then simply unset its propagation for the layout manager its children use, to discard the size change based on its children's size demands, while defining setting its width and height:
frame = tk.Frame(..., width=320, height=180, ...)
#frame.grid_propagate(False) # uncomment if children use pack
frame.pack_propagate(False) # uncomment if children use grid
For centering a widget one easy way is using place:
widget.place(relx=.5, rely=.5, anchor='center')
A Minimal, Complete, and Verifiable example that accomplishes both:
from PIL import Image, ImageTk
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
if __name__ == '__main__':
root = tk.Tk()
frame = tk.Frame(root, width=320, height=180, bg='#f48024')
img = Image.new('RGB', (640, 360), color=(0, 121, 152))
frame.image = ImageTk.PhotoImage(img)
frame_s_label = tk.Label(frame, image=frame.image)
button = tk.Button(frame, text="Button")
button.place(relx=.5, rely=.5, anchor='center')
frame.grid()
#frame.grid_propagate(False) # uncomment if children use pack
frame.pack_propagate(False) # uncomment if children use grid
frame_s_label.pack()
tk.mainloop()
How can I make the frame & image stay a consistent width & height (320x180)?
If you want the frame to be a specific size, start by giving it a specific size. Next, either use place to add widgets to it, or turn geometry propagation off if using grid or pack
# if using pack:
self.frame_ImageContainer.pack_propagate(False)
# if using grid:
self.frame_ImageContainer.grid_propagate(False)
How can I center the Take Screenshot button both vertically and horizontally inside the Image frame?
The easiest way is to use place, since it won't affect the size of its master, and you can provide relative coordinates.
self.button_TakeScreenshot.place(relx=.5, rely=.5, anchor="center")

python, tkinter - Frame height and width parameter values have no effect

I am trying to create 16 buttons which will completely occupy the bottom 3/4 th of the window. But, the frame height and width values don't seem to have any effect. How to get the behaviour I want?
from tkinter import *
class Application(object):
def __init__(self):
# Creating the main window
self.window = Tk()
self.window.geometry('{}x{}'.format(400, 400))
# frame where numbered buttons will be
frame = Frame(self.window)
frame.configure(height=300, width=300)
frame.pack(side=BOTTOM)
# add buttons
for i in range(4):
for j in range(4):
Button(frame, text=str(4*i + j + 1)).grid(row=i, column=j, sticky=N+E+S+W)
self.window.mainloop()
def main():
app = Application()
main()
The reason for the behavior you are seeing is that tkinter widgets are designed to shrink or expand to exactly fit around their children when using grid or pack. 99.99% of the time this is the exact right behavior, because it results in GUIs that are responsive to changes in font size, screen resolution, and window size.
If your goal is to divide the screen into two parts, where one part takes up 1/4 of the screen and one part takes up 3/4, the best solution is to use grid or place since those both make it easy to set relative sizes.
I don't normally recommend place, so here's a solution using grid. Note the use of grid.rowconfigure and grid.columnconfigure
from tkinter import *
class Application(object):
def __init__(self):
self.window = Tk()
self.window.geometry('{}x{}'.format(400, 400))
self.window.grid_rowconfigure(0, weight=1)
self.window.grid_rowconfigure(1, weight=3)
self.window.grid_columnconfigure(0, weight=1)
frame = Frame(self.window, background="pink")
frame.grid(row=1, column=0, sticky="nsew")
for row in range(4):
frame.grid_rowconfigure(row, weight=1)
for column in range(4):
frame.grid_columnconfigure(column, weight=1)
# add buttons
for i in range(4):
for j in range(4):
button = Button(frame, text=str(4*i + j + 1))
button.grid(row=i, column=j, sticky=N+E+S+W)
self.window.mainloop()
def main():
app = Application()
main()
With this example, row 0 (zero) in the root window can be used for anything you want. Tkinter will try its best to always make that part of the GUI 1/4 the height of the window. I recommend putting a frame in that row, and then any other widgets inside that frame.

Resizing tkinter windows and contents

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.

Categories