Tkinter grid spacing problems - python

I'm trying to understand how tk grid layouts work since the interface isn't looking the way I thought it would. I'm trying to place a label followed by 2 buttons on the same row, and a treeview on the next row that spans beyond the width of the label and buttons. The only way to get this to look how I want is if I use a huge value for the treeview's columnspan. Here is my code:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
columnHeadings = ("Heading 1", "Heading 2")
def printMsg():
print("Ok")
frame = ttk.Frame(root).grid(row=0, column=0)
label1 = tk.Label(frame, text="Label here").grid(row=0, column=0, columnspan=1)
button1 = tk.Button(frame, text="Yes", width=2, command=printMsg).grid(row=0, column=1)
button2 = tk.Button(frame, text="No", width=2, command=printMsg).grid(row=0, column=2)
#Label and buttons too far apart
#treeview1 = ttk.Treeview(frame, columns=columnHeadings, show='headings').grid(row=1,column=0, columnspan=3)
#Right distance but that's a huge columnspan
treeview1 = ttk.Treeview(frame, columns=columnHeadings, show='headings').grid(row=1,column=0, columnspan=100)
root.mainloop()
When columnspan is 3, the first row has a lot of spaces between the label and buttons. When columnspan is 100, the label and button spacing looks a lot better but I know this isn't the correct way. What's the correct way to do this?

You have several things conspiring against you in this little program. For one, frame is set to None, so you are actually putting all these widgets in the root window rather than the frame. This is because x=y().z() always sets x to the result of z, and grid() always returns None.
Second, a good rule of thumb for grid is that you need to give at least one (and usually exactly one) row and one column to have a weight, so that tkinter knows how to allocate extra space. You also need to use the sticky option so that your widgets expand to fill the space that's been given them. You are using neither of these techniques.
Third, in my experience I think it makes it very hard to debug layout problems when your layout statements are scattered throughout your code. It's best to group them altogether so you can more easily visualize your layout.
Solving the problem with grid
You can solve your problem by giving column 3 a weight of 1, and then having your treeview span that column. This prevents the columns that your buttons are in from expanding. If you also fix the problem with frame being None, and if you use the appropriate sticky options, you can get the look you want.
Here's an example:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
columnHeadings = ("Heading 1", "Heading 2")
def printMsg():
print("Ok")
frame = tk.Frame(root)
frame.grid(row=0, column=0, sticky="nsew")
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
label1 = tk.Label(frame, text="Label here")
button1 = tk.Button(frame, text="Yes", width=2, command=printMsg)
button2 = tk.Button(frame, text="No", width=2, command=printMsg)
treeview1 = ttk.Treeview(frame, columns=columnHeadings, show='headings')
label1.grid(row=0, column=0, columnspan=1)
button1.grid(row=0, column=1)
button2.grid(row=0, column=2)
treeview1.grid(row=1,column=0, columnspan=4, sticky="nsew")
frame.grid_columnconfigure(3, weight=1)
frame.grid_rowconfigure(1, weight=1)
root.mainloop()
An alternate solution, using pack
All that being said, I think there are better solutions than to try to get everything to fit in a grid. My philosophy is to use the right tool for the job, and in this case the right tool is pack because it excels at stacking things top-to-bottom OR left-to-right (and visa versa).
In your case, you have two distinct regions in your program: a toolbar across the top, and a treeview down below. Since that's all you have, it makes sense to use pack, to place one on top of the other. So I would start by creating two frames and packing them:
toolbar = ttk.Frame(root)
treeframe = ttk.Frame(root)
toolbar.pack(side="top", fill="x")
treeframe.pack(side="bottom", fill="both", expand=True)
Now, anything you put in toolbar will not affect things in treeframe and visa versa. You are free to do whatever you want in each of those frames. As your program grows, you'll find that having distinct regions makes layout problems much easier to solve.
Since the toolbar contains buttons that are left-justified, you can use pack there too. Just make sure their parent is the toolbar frame, and you can pack them like this:
label1 = tk.Label(toolbar, ...)
button1 = tk.Button(toolbar, ...)
button2 = tk.Button(toolbar, ...)
label1.pack(side="left")
button1.pack(side="left")
button2.pack(side="left")
Finally, if you only have a treeview in the bottom (ie: no scrollbars or other widgets), you can use pack. Make sure it's parent is treeframe. You can use grid if you want, but pack is a bit more straight-forward.
treeview1.pack(fill="both", expand=True)
The nice thing about pack is you can truly put everything on one line. You don't have to take the extra step and give a row or column a weight.
Here's a complete example:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
columnHeadings = ("Heading 1", "Heading 2")
def printMsg():
print("Ok")
toolbar = ttk.Frame(root)
treeframe = ttk.Frame(root)
toolbar.pack(side="top", fill="x")
treeframe.pack(side="bottom", fill="both", expand=True)
label1 = tk.Label(toolbar, text="Label here")
button1 = tk.Button(toolbar, text="Yes", width=2, command=printMsg)
button2 = tk.Button(toolbar, text="No", width=2, command=printMsg)
label1.pack(side="left")
button1.pack(side="left")
button2.pack(side="left")
treeview1 = ttk.Treeview(treeframe, columns=columnHeadings, show='headings')
treeview1.pack(side="top", fill="both", expand=True)
root.mainloop()

Related

Python Tkinter - full width frames not working

In short, I have such a code (it is a fragment, but the important one).
class DF(tk.Toplevel):
def __init__(self, master,width):
tk.Toplevel.__init__(self)
self.root_frame = Frame(self, bg="red")
self.root_frame.grid(row=0, column=0, padx=20, pady=20, sticky="ne")
bottom_frame = Frame(self)
bottom_frame.grid(row=1, column=0, padx=5, pady=5, sticky="ne")
cancel_button = Button(master=bottom_frame)
cancel_button.pack(side="right")
For example, this is how I dynamically create consecutive _main_form elements.
def add(self):
label = tk.Label(master=self._main_frame)
label.grid(row=self._rows, column=0, columnspan=2, sticky="we")
tk.Entry(master=self._main_frame)
textbox.grid(row=self._rows, column=2, columnspan=2, sticky="ne")
However, the window does not adjust to the window's width. What can I do in such a situation? Thanks to bg you can see as if these frames were not at all in their entire width. I also add a screen.
The root of the problem is that you're using grid in the Toplevel, but you haven't told grid what to do with extra space. As a rule of thumb, you should always call rowconfigure and columnconfigure to give a positive weight to at least one row and one column.
self.grid_columnconfigure(0, weight=1)
In this case, however, since you only have two full-width frames inside the Toplevel, it's simpler to use pack for both the top and bottom frames.
bottom_frame.pack(side="bottom", fill="x")
self._main_frame.pack(side="top", fill="both", expand=True)
Either of those solutions solve the problem of the bottom and top frame not taking the full width of the window. You need to be equally diligent to add weight to columns and rows inside of self._main_frame.
For a deeper description of how the weight option works, along with some visual aids, see What does 'weight' do in tkinter?

Scrollbar in Tkinter

I tried many things to do 2 things :
center my content (horizontal)
have a scrollbar right and bottom
Here my code :
# encoding: utf8
from tkinter import *
from tkinter import ttk
class Program:
def __init__(self):
# Tk.__init__(self)
# Fill the content of the window
self.window = Tk()
self.window.geometry("1080x720")
self.createFrameWithScrollbar()
self.content()
def createFrameWithScrollbar(self):
# Create a Main Frame
self.mainFrame = Frame(self.window)
self.mainFrame.pack(fill=BOTH, expand=True)
# Create a canvas
self.canvas = Canvas(self.mainFrame)
self.canvas.pack(side=LEFT, fill=BOTH, expand=True)
# Add a scrobar
yScrollbar = ttk.Scrollbar(self.mainFrame, orient=VERTICAL, command=self.canvas.yview)
yScrollbar.pack(side=RIGHT, fill=Y)
xScrollbar = ttk.Scrollbar(self.mainFrame, orient=HORIZONTAL, command=self.canvas.xview)
xScrollbar.pack(side=BOTTOM, fill=X)
# Configure the canvas
self.canvas.configure(yscrollcommand=yScrollbar.set)
self.canvas.configure(xscrollcommand=xScrollbar.set)
self.canvas.bind('<Configure>', lambda e: self.canvas.configure(scrollregion = self.canvas.bbox("all")))
self.frame = Frame(self.canvas)
self.canvas.create_window((0,0), window=self.frame, anchor="nw")
self.currentFrame = Frame(self.frame)
self.currentFrame.configure(bg="red")
self.currentFrame.pack(fill=BOTH, expand=1)
def content(self):
label_title = Label(self.currentFrame, text="Title")
label_title.grid(column=0, row=0, sticky=NSEW)
label_description = Label(self.currentFrame, text="Title")
label_description.grid(column=0, row=1, sticky=NSEW)
label_version = Label(self.currentFrame, text="0.2 beta [Novembre 2020]")
label_version.grid(column=0, row=2, sticky=NSEW)
# Lauch the program
app = Program()
app.window.mainloop()
So the result is the following :
So any suggestion ?
The bottom sidebar is so small ! I don't know why.
Are regarding the text, it's not center at all ;(
I want to have it center and changing position if I rezize the window
Thanks a lot
pack works by allocating space along an entire empty side of the master, so the order of when you call pack matters. When you pack the canvas before packing the scrollbar, the canvas takes up the entire left side from top to bottom.
The simple solution is to pack the scrollbars before you pack the canvas.
yScrollbar.pack(side=RIGHT, fill=Y)
xScrollbar.pack(side=BOTTOM, fill=X)
self.canvas.pack(side=LEFT, fill=BOTH, expand=True)
To make the UI look a bit more professional, grid is usually a better choice when laying out scrollbars. With pack, either one edge of the horizontal scrollbar will be below the vertical scrollbar, or the edge of the vertical scrollbar will be directly beside the horizontal one. That, or you have to add a little bit of padding so the scrollbars don't seem to overlap.
When I have a scrollable widget, I almost always put it and the one or two scrollbars together in a frame using grid. Then, I can treat them all as if they were a single widget when adding them to the rest of the UI.
In a comment you ask about how to do it in a grid. You simply need to place the widgets in the appropriate rows and columns, and make sure that the row and column with the scrollable widget has a non-zero weight so any extra space is allocated to it.
self.mainFrame.grid_rowconfigure(0, weight=1)
self.mainFrame.grid_columnconfigure(0, weight=1)
yScrollbar.grid(row=0, column=1, sticky="ns")
xScrollbar.grid(row=1, column=0, sticky="ew")
self.canvas.grid(row=0, column=0, sticky="nsew")

Is their a grid_remember()? Reversible grid_forget()?

I am confused on the documentation surrounding the tkinter "grid_forget()"
I know that this function does not permanently delete the widget ascribed to it, however I do not know how to call it again. Further, if the widget is forgotten in a frame, can it be called back to the same the frame?
You can call grid() with no parameters to reverse the effects of grid_remove().
In the following example there is a label that is placed at the top of the window with grid. There is a toggle button that will alternate between calling grid and grid_remove to show that calling grid with no parameters will restore the message exactly as it was.
Notice, for example, that both the row, column, and columnspan attributes are remembered when the message reappears.
import tkinter as tk
class Example():
def __init__(self):
self.root = tk.Tk()
self.root.grid_rowconfigure(2, weight=1)
self.root.grid_columnconfigure(1, weight=1)
self.toolbar = tk.Frame(self.root)
self.toggle = tk.Button(self.toolbar, text="Toggle the message",
command=self.toggle_message)
self.toggle.pack(side="left")
# simulate a typical app with a navigation area on the left and a main
# working area on the right
self.navpanel = tk.Frame(self.root, background="bisque", width=100, height=200)
self.main = tk.Frame(self.root, background="white", width=300, height=200, bd=1, relief='sunken')
self.message = tk.Label(self.root, text="Hello, world!")
self.toolbar.grid(row=0, column=0, columnspan=2)
self.message.grid(row=1, column=0, columnspan=2)
self.navpanel.grid(row=2, column=0, sticky="nsew")
self.main.grid(row=2, column=1, sticky="nsew")
def start(self):
self.root.mainloop()
def toggle_message(self):
if self.message.winfo_viewable():
self.message.grid_remove()
else:
self.message.grid()
if __name__ == "__main__":
Example().start()
If you change the code from using grid_remove to using grid_forget, restoring the label will not put it back in the same place or with the same options. That is the main distinction between grid_remove and grid_forget -- grid_forget literally forgets the grid options whereas grid_remove removes the widget but remembers the settings.
Here is a simple example to illustrate what is happening when you remove a widget from the grid then re-grid it. You simply need to re apply the grid the same way you would have done in the first place. You can even chose a different grid location if you like. Though I am not sure if you can change the container it was originally assigned to. If not then it will only be able to be re-added to the original container the widget was assigned to.
import tkinter as tk
root = tk.Tk()
some_label = tk.Label(root, text="IM HERE!")
some_label.grid(row=0, column=0, columnspan=2)
def forget_label():
some_label.grid_forget()
def return_label():
some_label.grid(row=0, column=0, columnspan=2)
tk.Button(root, text="Forget Label", command=forget_label).grid(row=1, column=0)
tk.Button(root, text="Return Label", command=return_label).grid(row=1, column=1)
root.mainloop()

Is there any better method to manage place in Tkinter in Python?

Now I only use the method place() to manipulate objects place. Like this:
self.s_date_label = Label(self, text = 'Start Date: ')
self.s_date_label.place(x=0,y=0)
self.start_date = Entry(self, bd=1)
self.start_date.place(x=70,y=0)
self.s_date_label2 = Label(self, text = 'example: 20160101'
self.s_date_label2.place(x=130,y=0)
However, I believe this is a stupid way.
Because when I have to control a lot of objects in the same line. All I can do is only control them by the parameter x in place().
Is there any better method to manage the location of objects?
The two alternatives are to use grid and pack
grid
grid is best if you need a table-like layout using rows and columns. You can specify specific cells, and you can have items span multiple rows and/or multiple columns.
For example:
l1 = tk.Label(parent, text="Username:")
l2 = tk.Label(parent, text='Password:")
username_entry = tk.Entry(parent)
password_entry = tk.Entry(parent)
l1.grid(row=1, column=0, sticky="e")
username_entry.grid(row=1, column=1, sticky="ew")
l2.grid(row=2, column=0, sticky="e")
password_entry.grid(row=2, column=1, sticky="ew")
parent.grid_rowconfigure(3, weight=1)
parent..grid_columnconfigure(1, weight=1)
For more information see http://effbot.org/tkinterbook/pack.htm
pack
pack is great for laying out objects in horizontal or vertical groups. pack is great for toolbars, and and is often what I use for the overall layout where you have a toolbar at the top, a statusbar at the bottom, and content in the middle.
For example:
toolbar_frame = tk.Frame(root)
statusbar_frame = tk.Frame(root)
content_frame = tk.Frame(root)
toolbar_frame.pack(side="top", fill="x")
statusbar_frame.pack(side="bottom", fill="x")
content_frame.pack(side="top", fill="both", expand=True)
For more information see http://effbot.org/tkinterbook/pack.htm
Mixing pack and grid
You can (and should) use both grid and pack in the same application. However, you cannot use both on widgets that share a common parent.
This won't work because toolbar and statusbar have the same parent:
toolbar = tk.Frame(root)
sstatusbar = tk.Frame(root)
toolbar.grid(...)
statusbar.pack(...)
This will work, because toolbar and save_button have different parents.
toolbar = tk.Frame(root)
save_button = tk.Button(toolbar, ...)
toolbar.pack(side="top", fill="x")
save_button.pack(side="left")
Use the grid() method to place the objects in a layout.
With the help of padding (padx and pady) you can also space your objects.
For details check: grid and sample

Tkinter Button Alignment in Grid

I am attempting to fit two buttons on a grid within a frame, that takes up the entire row, no matter the size of the root frame. So essentially one button takes up half of the row, while the other takes the other half. Here's my code:
self.button_frame = tk.Frame(self)
self.button_frame.pack(fill=tk.X, side=tk.BOTTOM)
self.reset_button = tk.Button(self.button_frame, text='Reset')
self.run_button = tk.Button(self.button_frame, text='Run')
self.reset_button.grid(row=0, column=0)
self.run_button.grid(row=0, column=1)
Not really sure where to go from here. Any suggestions would be greatly appreciated. Thanks!
Use columnconfigure to set the weight of your columns. Then, when the window stretches, so will the columns. Give your buttons W and E sticky values, so that when the cells stretch, so do the buttons.
import Tkinter as tk
root = tk.Tk()
button_frame = tk.Frame(root)
button_frame.pack(fill=tk.X, side=tk.BOTTOM)
reset_button = tk.Button(button_frame, text='Reset')
run_button = tk.Button(button_frame, text='Run')
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1)
reset_button.grid(row=0, column=0, sticky=tk.W+tk.E)
run_button.grid(row=0, column=1, sticky=tk.W+tk.E)
root.mainloop()
Result:

Categories