Python Tkinter Grid Expand to window - python

I have a notebook that I played in grid (0;0). I want to the notebok to fill the entire screen even if its content (frames) would not fill the screen.
class App(Tk):
def __init__(self) -> None:
super().__init__()
# Widgets
self.notebook = Notebook(self) # <-- Widget I want to fill the window
self.notebook.grid(row=0, column=0)
self.control_frame = ControlFrame(self)
self.notebook.add(self.control_frame, text="Control")
class ControlFrame(Frame):
def __init__(self, master):
super().__init__(master)
self.control_bar = ControlBar(self)
self.control_bar.grid(row=0, column=0)
self.connect_btn = Button(self, text="Connect")
self.connect_btn.grid(row=1, column=0)
self.log = Text(self, width=100)
self.log.grid(row=2, column=0)

I think you need to use the sticky argument in you grid:
The width of a column (and height of a row) depends on all the widgets
contained in it. That means some widgets could be smaller than the
cells they are placed in. If so, where exactly should they be put
within their cells?
By default, if a cell is larger than the widget contained in it, the
widget will be centered within it, both horizontally and vertically.
The master's background color will display in the empty space around
the widget. In the figure below, the widget in the top right is
smaller than the cell allocated to it. The (white) background of the
master fills the rest of the cell.
The sticky option can change this default behavior. Its value is a
string of 0 or more of the compass directions nsew, specifying which
edges of the cell the widget should be "stuck" to. For example, a
value of n (north) will jam the widget up against the top side, with
any extra vertical space on the bottom; the widget will still be
centered horizontally. A value of nw (north-west) means the widget
will be stuck to the top left corner, with extra space on the bottom
and right.
So your code should look something like
self.notebook.grid(row=0, column=0, sticky='NSWE')
If you need more information check out this article
EDIT:
Thanks to #acw1668 for the hint!
I think the behaviour of the ttk.Notebook class is strange with the grid layout.
I manage to "solve" it using pack in the App() class.
Here is the code that worked for me:
class App(Tk):
def __init__(self) -> None:
super().__init__()
self.notebook = Notebook(self) # <-- Widget I want to fill the window
self.control_frame = ControlFrame(self.notebook)
self.notebook.add(self.control_frame, text="Control", sticky="NSWE")
self.notebook.pack(expand=True, fill=BOTH)
class ControlFrame(Frame):
def __init__(self, master):
super().__init__(master)
self.control_bar = ControlBar(self)
self.control_bar.grid(row=0, column=0)
self.grid(column=0, row=0, sticky="SNWE")
self.connect_btn = Button(self, text="Connect")
self.connect_btn.grid(row=1, column=0, sticky=N)
self.log = Text(self)
self.log.grid(row=2, column=0, sticky="NSWE")
self.grid_rowconfigure(0, weight=0)
self.grid_rowconfigure(1, weight=0)
self.grid_rowconfigure(2, weight=1)
self.grid_columnconfigure(0, weight=1)
Hope this solves your problem

Related

Tkinter window not updating after changing the window.columnconfigure()

I just started using python and I am trying to make a simple gui that consists of 3 frames. One on the left, one on the right and one in the middle. Later I'd like to add buttons and stuff to those frames but for now that is all. I want the left frame to disappear or appear again if I press the escape key. To do this I have written the following code:
from tkinter import Tk, Button, Label, Frame
class Main:
def __init__(self):
self.root = Tk()
self.init_gui()
def init_gui(self):
self.root.title("Gui Testing")
self.root.minsize(900,600)
self.root.bind("<Escape>", self.toggle_left_menu)
self.root.grid_columnconfigure(0, minsize=200)
self.root.grid_columnconfigure(1, weight=1)
self.root.grid_columnconfigure(2, minsize=250)
self.root.grid_rowconfigure(0, weight=1)
# main 3 panels
self.left_menu_active = True
self.left_menu = Frame(self.root, bg="#333")
self.left_menu.grid(row=0, column=0, sticky="nsew")
self.center = Frame(self.root, bg="white")
self.center.grid(row=0, column=1, sticky="nsew")
self.right_menu = Frame(self.root, bg="#888")
self.right_menu.grid(row=0, column=2, sticky="nsew")
self.toggle_left_menu()
def toggle_left_menu(self, event=None):
if self.left_menu_active == True:
self.left_menu_active = False
self.root.grid_columnconfigure(0, minsize=0)
self.left_menu.grid_forget()
else:
self.left_menu_active = True
self.root.grid_columnconfigure(0, minsize=200)
self.left_menu.grid(row=0, column=0, sticky="nsew")
def start(self):
self.root.mainloop()
Main().start()
The problem is that when I press escape, nothing happens. However, when I then move the window by clicking on it and dragging it, it updates all of a sudden and it shows the window the way I want it. So the code seems to be working but the window isn't updating for some reason.
I don't know what I can do about that. I found out that it does update the grid positions of the left and the center frame, but the grid_configure() doesn't seem to update without moving the window.
Is there a way to update the frame or to achieve the frame toggling in some other way?
Edit:
The problem has been solved by adding a button to each frame. Now the frames are not empty anymore it seems works. I also edited the toggle_left_menu() function a bit. This is what I changed:
Added Buttons:
self.test_button1 = Button(self.left_menu, text="left", padx=10, pady=5)
self.test_button1.grid(row=0, column=0)
self.test_button2 = Button(self.center, text="center", padx=10, pady=5)
self.test_button2.grid(row=0, column=0)
self.test_button3 = Button(self.right_menu, text="right", padx=10, pady=5)
self.test_button3.grid(row=0, column=0)
Edited toggle_left_menu():
def toggle_left_menu(self, event=None):
if self.left_menu.winfo_viewable():
self.root.grid_columnconfigure(0, minsize=0)
self.left_menu.grid_remove()
else:
self.root.grid_columnconfigure(0, minsize=200)
self.left_menu.grid()
This worked for me, thanks!
Extending Bryan Oakley's example ~ you have to toggle the minsize, as well. As an aside, I classed out all of your gui and made Main the root. All that self.root.this and self.root.that is unnecessary this way. Also, you would have had to do this anyway, unless you intended to dump your entire gui contents into your init_gui method. If your app is large that would be a nightmare to keep track of. As an added bonus, I made the whole toggle_menu method dynamic so it can toggle either menu. You can change the key-bindings, to whatever. I used Escape then l for left_menu and Escape then r for right_menu.
from tkinter import Tk, Button, Label, Frame
class LeftMenu(Frame):
#property
def minsize(self):
return 200
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class RightMenu(Frame):
#property
def minsize(self):
return 250
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class Center(Frame):
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class Main(Tk):
def __init__(self):
Tk.__init__(self)
self.bind("<Escape><l>", self.toggle_menu)
self.bind("<Escape><r>", self.toggle_menu)
self.grid_columnconfigure(0, minsize=200)
self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure(2, minsize=250)
self.grid_rowconfigure(0, weight=1)
# main 3 panels
self.left_menu = LeftMenu(self, 0, 0, bg="#333")
self.center = Center(self, 0, 1, bg="white")
self.right_menu = RightMenu(self, 0, 2, bg="#888")
self.toggle_menu(menu=self.left_menu)
def toggle_menu(self, event=None, menu=None):
if event and event.char in 'lr':
menu = self.left_menu if event.char == 'l' else self.right_menu
if menu:
if menu.winfo_viewable():
self.grid_columnconfigure(menu.grid_info()['column'], minsize=0)
menu.grid_remove()
else:
menu.grid()
self.grid_columnconfigure(menu.grid_info()['column'], minsize=menu.minsize)
self.after_idle(self.event_generate, '<Configure>')
if __name__ == "__main__":
root = Main()
root.title("Gui Testing")
root.minsize(900,600)
root.mainloop()
Part of the problem is that minsize only affects the minimum size. If the left frame is visible and is more than zero pixels wide, setting the minsize to zero isn't going to make the frame smaller. So, one step is to remove the minsize=200 option for column 0.
Since you are using grid, the best way to hide or show a frame is to use grid_remove to remove the widget, and then grid to restore it. grid_remove will remove the widget from the window but remember all of its settings. When you subsequently call .grid(), all of the previous settings will be used.
You can also just check if the window is visible or not without having to manage a boolean flag since your function is a toggle. That simplifies the code a bit.
Also, I think there's a bug on some versions of tk (the library upon which tkinter is built) that prevents the window from refreshing in this specific type of situation. What works for me is to synthetically generate a <Configure> event on the root window.
Rolling all of that together, this version of your toggle function works for me on OSX without any other modifications to your code.
def toggle_left_menu(self, event=None):
if self.left_menu.winfo_viewable():
self.left_menu.grid_remove()
self.root.after_idle(self.root.event_generate, '<Configure>')
else:
self.left_menu.grid()
self.root.after_idle(self.root.event_generate, '<Configure>')

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

Tkinter Text Widget Horizontal Fill

I have a small python program to build a GUI. I'm trying to use a text widget to create an easily scrollable window that contains vertically stacked frames. A frame is created on button press and added to the bottom of the text widget. This works fine; however, I'm struggling to get these frames to stretch to fill the text box horizontally.
import Tkinter as tk
class NewEntry(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
#self.pack(fill="x", expand=True) #possible error source
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
textField = tk.Entry(self)
textField.grid(row=1, column=0, padx=2, pady=1, sticky="ew")
addButton = tk.Button(self, text="Add", cursor="arrow")
addButton.grid(row=1, column=1, padx=10, pady=2, sticky="ew")
newLabel = tk.Label(self, text="Test", bg="#5522FF")
newLabel.grid(row=0, padx=2, pady=2, sticky="ew", columnspan=2)
newLabel.columnconfigure(0, weight=1)
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent)
self.parent=parent
self.grid()
self.columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
self.text = tk.Text(self, wrap="none", bg="#AA3333")
vsb = tk.Scrollbar(orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.text.pack(fill="both", expand=True)
b = tk.Button(self, text="Button #%s" % 1, command=self.OnButtonClick)
self.text.window_create("end", window=b)
self.text.insert("end", "\n")
def OnButtonClick(self):
self.text.configure(state="normal")
panel = NewEntry(self.text, bg="#FF1111")
self.text.window_create("end", window=panel)
self.text.insert("end", "\n")
self.text.configure(state="disabled")
if __name__=="__main__":
root = tk.Tk()
root.resizable(True, True)
appinstance=MainApplication(root)
appinstance.pack(fill="both", expand=True)
root.mainloop()
I've read many different posts talking about grid_columnconfigure, fill options, sticky options, etc, but I haven't been able to get it filling properly. I am wondering if the window_create() method of the Text widget creates some sort of size limitation? It seems as though my code in NewEntry class is properly filling the space allowed by the window_create method, but I don't know how to create a "panel" to fill the width of the text box.
I am also aware of the possibility of using a canvas instead of text box (I'm wanting to maintain dynamic size and scrollability, though). I read from several different posts that a text widget is easiest if you have a simple stack of widgets, though. I will accept any recommendation, though.
The root of the problem is that a frame typically wants to fit its contents. So, when you add the label, entry widget, and button to the frame, it will shrink to fit. Any size you give to the frame will be ignored.
There are several solutions to this problem. What many people do is turn geometry propagation off (eg: self.grid_propagate(False)) but that means you have to manage both the width and height when all you really want is to control the width.
Another solution is to put something inside the panel that can be configured with an explicit width, and which will then cause the containing frame to grow to fit. For example, you can add an invisible frame in row 0 that sits behind the other widgets in row 0. When you change the width of this frame it will cause the containing frame to grow:
class NewEntry(tk.Frame):
def __init__(...):
...
# create this _before_ creating the other widgets
# so it is lowest in the stacking order
self.sizer = tk.Frame(self, height=1)
self.sizer.grid(row=0, columnspan=2)
...
With that, you can force the panel to be any width you want by setting the width of the sizer, but tkinter will continue to compute the optimum height for the widget:
self.sizer.configure(width=200)
Now you just need to set up bindings so that whenever the text widget changes size, you call this function to resize each entry.
For example, you might want to save all of the panels in a list so that you can iterate over them later.
class MainApplication(...):
def __init__(...):
...
self.panels = []
...
def OnButtonClick(...):
...
panel = NewEntry(...)
self.panels.append(panel)
...
With that, you can set up a binding that triggers whenever the window resizes:
class MainApplication(...):
def __init__(...):
...
self.text.bind("<Configure>", self.OnConfigure)
...
def OnConfigure(self, event):
for panel in self.panels:
panel.sizer.configure(width=event.width)
I wouldn't do it precisely like that since it tightly couples the implementation of the panel to the main window, but it illustrates the general technique of explicitly controlling the width of an embedded window.
Other solutions involve putting the panels inside a containing frame, and make that frame be the only widget added to the text widget. You could also use a canvas instead of a text widget since it allows you to explicitly set the width and height of embedded windows.

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)

python 3.2.5 grid sticky/weight not working

Below is a simple illustration of the problem. It consists of a grid layout where I've placed a button widget in row 0 and a text widget in row 1.
What I want is for the text widget in row 1 to expand with the form while keeping the top of the text widget anchored NW (row 0 not expanding). The problem is that the horizontal(column expands correctly, but the text widget row does not. If I get rid of the button the text widget expands correctly. Also the original form I've put together is more involved and is best served using a grid. So basically using pack isn't a solution. Any help would be appreciated.
################################################
from tkinter import *
class Application(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initGui()
# ##################################################################
# Initialize GUI widgets
def initGui(self):
self.parent.title("Test Grid")
self.parent.resizable(width=TRUE, height=TRUE)
self.grid(sticky=W+E+N+S, padx=20, pady=20)
self.parent.columnconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.parent.rowconfigure(1, weight=1)
self.rowconfigure(1, weight=1)
# Add a button to row 0
self.btn = Button(self, text="Button", width=20)
self.btn.grid(row=0, column=0, padx=(0,10),pady=(0,10), sticky=N+W)
# Add a text box and v-scrollbar to row 1
self.txtOut = Text(self, width=80, height=20)
self.scrOut = Scrollbar(self)
self.txtOut.grid(row=1,column=0,padx=(0,18),sticky=N+E+S+W)
self.scrOut.grid(row=1,column=0,sticky=N+S+E)
self.scrOut.config(command=self.txtOut.yview)
self.txtOut.config(yscrollcommand=self.scrOut.set)
print(self.grid_size())
def main():
root = Tk()
app = Application(parent=root)
app.mainloop()
if __name__ == '__main__':
main()
There's a case where using pack probably is a solution. It won't cause any conflicts if you pack the instance of the Frame (Application) inside the root window, then grid widgets inside that frame. It'll cut down on the headache of all the rowconfigure and columnconfigure methods and just makes more sense to me.
def initGui(self):
self.parent.title("Test Grid")
self.parent.resizable(width=TRUE, height=TRUE)
self.pack(fill=BOTH, expand=1, padx=20, pady=20)
self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
The core problem is that you are putting the application frame in row 0 (zero), but you are giving row 1 (one) a weight of 1 (one). That is why the text widget doesn't expand when you resize the window.
This is really hard to see because you mix grid commands of a widget in its parent, along with its own children widgets. It makes it hard to quickly scan the code to see what the layout options are, because you can't assume all of the grid, pack or place commands only affect child widgets.
Another minor problem with your code is that you've put the text widget and scrollbar in the same column.
Here's how I would change the code: remove the self.grid and self.parent.*configure commands from initGui. Then, use pack to add the application frame to the root window at the point where it's created. Also, move the scrollbar to column 1.
class Application(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initGui()
# ##################################################################
# Initialize GUI widgets
def initGui(self):
self.parent.title("Test Grid")
self.parent.resizable(width=TRUE, height=TRUE)
self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
# Add a button to row 0
self.btn = Button(self, text="Button", width=20)
self.btn.grid(row=0, column=0, padx=(0,10),pady=(0,10), sticky=N+W)
# Add a text box and v-scrollbar to row 1
self.txtOut = Text(self, width=80, height=20)
self.scrOut = Scrollbar(self)
self.txtOut.grid(row=1,column=0,padx=(0,18),sticky=N+E+S+W)
self.scrOut.grid(row=1,column=1,sticky=N+S)
self.scrOut.config(command=self.txtOut.yview)
self.txtOut.config(yscrollcommand=self.scrOut.set)
print(self.grid_size())
def main():
root = Tk()
app = Application(parent=root)
app.pack(fill="both", expand=True)
app.mainloop()
if __name__ == '__main__':
main()

Categories