I wanted to have an example on managing geometry inside of frames independently and wrote the below code to have a checkered flag-like looking GUI.
import tkinter as tk
# a class that has 2 columns of frames inside
class TwoFrames(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# creates 2 frame objects and passes self as parent, which
# means object created using TwoFrames class is the parent
self.frame1 = tk.Frame(self)
self.frame2 = tk.Frame(self)
#manages 2 frames geometry
self.frame1.grid(column=0, sticky="nsew")
self.frame2.grid(column=1, sticky="nsew")
# enables resizing for 0th row, and 1st and 2nd columns of an
# object of this class
tk.Grid.rowconfigure(self, 0, weight=1)
tk.Grid.columnconfigure(self, 0, weight=1)
tk.Grid.columnconfigure(self, 1, weight=1)
class TwoLabels(tk.Frame):
def __init__(self, master=None, color=True):
super().__init__(master)
#creates 2 Label objects with TwoLabels object as parent
self.label1 = tk.Label(self)
self.label2 = tk.Label(self)
# configures the background color of labels for demonstrative
# purposes
if color:
#label1 will have red color
self.label1.configure(bg="black")
#label2 will have blue color
self.label2.configure(bg="white")
else:
#label1 will have blue color
self.label1.configure(bg="white")
#label2 will have red color
self.label2.configure(bg="black")
# manages the geometry
self.label1.grid(row=0, sticky="nsew")
self.label2.grid(row=1, sticky="nsew")
# enables resizing like above, but this time for 2 rows and 1
# column
tk.Grid.rowconfigure(self, 0, weight=1)
tk.Grid.rowconfigure(self, 1, weight=1)
tk.Grid.columnconfigure(self, 0, weight=1)
# creates the mainWindow
mainWindow = tk.Tk()
# creates a mainFrame that has 2 frames in it
mainFrame = TwoFrames(mainWindow)
# manages geometry of mainFrame and display it
mainFrame.pack(fill="both", expand=True)
# creates row_labels1 and row_labels2, both has 2 colored labels inside
row_labels1 = TwoLabels(mainFrame.frame1, True)
row_labels2 = TwoLabels(mainFrame.frame2, False)
# manages geometry of labels inside frames and displays them
row_labels1.pack(fill="both", expand=True)
row_labels2.pack(fill="both", expand=True)
# run the application
mainWindow.mainloop()
But ironically, the code instead produced a checkered flag that has its one vertical half on a second row, as if what I'm trying to do is not possible. Later on I changed the;
#manages 2 frames geometry
self.frame1.grid(column=0, sticky="nsew")
self.frame2.grid(column=1, sticky="nsew")
part with
#manages 2 frames geometry
self.frame1.grid(row=0, column=0, sticky="nsew")
self.frame2.grid(row=0, column=1, sticky="nsew")
and it worked as I first intended. I'm glad it works but;
I'm not sure if the geometry is being managed on at least the class
basis or not. Is it?
What does passing, that I assumed to be equal to what I pass anyway,
row numbers change?
Also I'd be glad if you could review my code in Code Review.
I'm not sure if the geometry is being managed on at least the class basis or not. Is it?
Geometry management never is managed on a class basis. Geometry management is based on a parent/child structure of the widgets themselves. If widgets in a class are children of the class (eg: the class is a frame), then yes, the management can be seen as being done on a class basis. Tkinter doesn't know or care about your class structure, it only cares about widget parent/child relationships.
What does passing, that I assumed to be equal to what I pass anyway, row numbers change?
Your assumption is false. If you don't specify a row, it is auto-incremented by one each time you call grid.
In other words, this:
self.frame1.grid(column=0, sticky="nsew")
self.frame2.grid(column=1, sticky="nsew")
... is functionally identical to this (pay close attention to the value for row in each line):
self.frame1.grid(row=0, column=0, sticky="nsew")
self.frame2.grid(row=1, column=1, sticky="nsew")
Although if you don't specify a column, it is by default equal to 0 even after you call grid multiple times. In other words:
self.label1.grid(row=0, sticky="nsew")
self.label2.grid(row=1, sticky="nsew")
is functionally identical to this (pay attention to column in each line):
self.label1.grid(column=0, row=0, sticky="nsew")
self.label2.grid(column=0, row=1, sticky="nsew")
As a rule of thumb (and for the sake of clarity) you should always supply both the row and column number. Doing so removes all ambiguity of where a widget is positioned.
Related
I'm new to Python and trying to understand the code below. This code should create 3 frame objects that can be rotated to the front to swap pages.
The APP class should create these 3 new objects. I'm not sure that it is.
What I am trying to be is modify a label on the Dashboard class through a function in that class.
i.e. Dashboard.update()
Can someone please explain how the APP class is creating frame objects for the 3 windows. I'm now sure that it is and I think I am trying to update text in the class and not a object of that class.
### Import libaries
import requests
import pyodbc
import tkinter as tk
from tkinter import *
from tkinter import messagebox, ttk
### Set global fonts
TITLE_FONT = ("Verdana", 12)
### Define the applicaiton class
class APP (Frame):
### Build the init function to create the container and windows
def __init__ (self, master=None ):
Frame.__init__(self, master)
self.grid()
# Set the application window title
self.master.title("Playing Around with Classes")
# set the size of the row height for the application
self.master.rowconfigure(0, weight=1)
self.master.rowconfigure(1, weight=35)
self.master.rowconfigure(2, weight=1)
self.master.rowconfigure(3, weight=1)
#Row 0 - Title area
label = tk.Label(master, text="Playing Around with Classes", font=TITLE_FONT)
label.grid(row=0, columnspan=3, sticky="nsew")
# Main presentation are
Frame2 = Frame(master, bg="#263D42")
Frame2.grid(row = 1, column = 0, rowspan = 1, columnspan = 3, sticky = "nsew")
# List of pages
self.frames = {}
# i think this loop defines the class objects
for F in (NetworkMap,AuthorPage,Dashboard):
frame = F(Frame2, self)
self.frames[F] = frame
frame.grid(row=0, column=1, sticky="nsew")
self.show_frame(Dashboard)
### Define the show_frame function that will bring the selected fram to the front so it can be viewed
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
### Create a class for the Dashboard page. This will also be the start page when the application starts
class Dashboard (tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self,parent, bg="#263D42")
label = tk.Label(self, text="Text to change", font=TITLE_FONT, bg="#263D42", fg="white", pady = 20)
label.grid(row=0, column=0, sticky="nsew")
def update(self):
self.allPapersLabel.config(text="Changed Text")
### Create a page to get the Author detasil
class AuthorPage (tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self,parent)
label = tk.Label(self, text="Get Author", font=TITLE_FONT)
label.grid(row=0, column=0, sticky="nsew")
class NetworkMap (tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self,parent)
label = tk.Label(self, text="Network Map", font=TITLE_FONT)
label.grid(row=0, column=0, sticky="nsew")
def changeText():
Dashboard.update()
changeText()
root = tk.Tk()
root.geometry("600x800+100+100")
app = APP(master=root)
app.mainloop()
Can someone please explain how the APP class is creating frame objects for the 3 windows
The crux is here:
for F in (NetworkMap,AuthorPage,Dashboard):
frame = F(Frame2, self)
self.frames[F] = frame
frame.grid(row=0, column=1, sticky="nsew")
Keep in mind that NetworkMap, AuthorPage and Dashboard are classes. Classes are callables that function as a factory for new instances of the particular type.
So basically the for-loop makes F an alias (or label) for each of those classes and calls them in turn to instantiate an object.
Keep in mind that what we call "variables" in most languages are refered to as names in Python. From the language manual:
Names refer to objects. Names are introduced by name binding operations.
So F is nothing more than a handy label to refer to the three classes. The for-loop header binds the name to the classes.
BTW: This looks like a re-implementation of a ttk.Notebook. I would suggest to use that instead.
Edit
The frames are saved into the frames dictionary the App object. So in all of the methods of App instances you can access self.frames to get the individual frames.
The somewhat weird (to me at least) thing is that the class object of the frame is used as the key for selecting from the dictionary.
So using self.frames[AuthorPage] in methods of App should return the AuthorPage frame.
In simply trying to work on organizing my code, I've found online that it seems to be best to put much of your code into classes when needed. So in doing that, I'd figure that I'll try to create a frame class with create_labels and create_buttons methods.
My goal is to be able to create 2 or more seperate frames that are similar in style (hence why I find it best to make a frame class). Then, using methods, create labels, buttons, and other widgets and allow them to move around with ease within their respective frames.
So here's my code:
import tkinter as tk
window = tk.Tk()
class MyFrame(tk.Frame):
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent)
self.parent = parent
self.layout(**kwargs)
def labels(self, text, **kwargs):
tk.Label.__init__(self, text=text)
self.layout(**kwargs)
def buttons(self, text, command, **kwargs):
tk.Button.__init__(self, text=text, command=command)
self.layout(**kwargs)
def layout(self, row=0, column=0, columnspan=None, row_weight=None, column_weight=None, color=None, sticky=None, ipadx=None, padx=None, ipady=None, pady=None):
self.grid(row=row, column=column, columnspan=columnspan, sticky=sticky, ipadx=ipadx, padx=padx, ipady=ipady, pady=pady)
self.grid_rowconfigure(row, weight=row_weight)
self.grid_columnconfigure(column, weight=column_weight)
self.config(bg=color)
frame_1 = MyFrame(window, row=0, column=0, sticky="news", color="pink")
frame_1.buttons("Btn_1/Frme_1", quit, row=0, column=0)
frame_1.buttons("Btn_2/Frme_1", quit, row=0, column=1)
frame_2 = MyFrame(window, row=1, column=0, sticky="news", color="green")
frame_2.buttons("Btn_1/Frme_2", quit, row=0, column=0)
frame_2.buttons("Btn_2/Frme_2", quit, row=0, column=1)
window.grid_columnconfigure(0, weight=1)
window.grid_columnconfigure(1, weight=1)
window.grid_rowconfigure(1, weight=1)
window.grid_rowconfigure(0, weight=1)
window.mainloop()
Now I think a problem of mine is during the __init__ method because there should be 2 frames and 2 buttons per frame. However, there's no errors which makes it harder to find out for sure of that's why only the latest buttons and frames exist. I don't even think it's a case of one frame or widget 'covering' another. I think the second frame/widgets seem to be overwriting the first frame/widgets.
Any help is appreciated.
The issue lies with your layout function. Both the frames are being grided on the row=0 and column=0, as you are not passing any specific row and column to the function. Hence, the overwriting of the frames can be seen.
Another issue (possible) in your code is that the frame_1 and frame_2 buttons do not belong to the Frame widget but to the root window
I'm pretty new to Tkinter and I build a little window with different widgets.
My Code looks like this:
import tkinter as tk
from tkinter import ttk
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
self.master.geometry("800x600")
self.master.title("Tkinter Sandbox")
self.master.grid_rowconfigure(0, weight=1)
self.master.grid_columnconfigure(1, weight=1)
self._create_left_frame()
self._create_button_bar()
self._create_label_frame()
def _create_left_frame(self):
frame = tk.Frame(self.master, bg="red")
tree_view = ttk.Treeview(frame)
tree_view.column("#0", stretch=tk.NO)
tree_view.heading("#0", text="Treeview")
tree_view.pack(fill=tk.Y, expand=1)
frame.grid(row=0, column=0, rowspan=2, sticky=tk.N + tk.S)
def _create_button_bar(self):
frame = tk.Frame(self.master, bg="blue")
button_run_single = tk.Button(frame, text="Button 1")
button_run_all = tk.Button(frame, text="Button 2")
button_details = tk.Button(frame, text="Button 3")
button_run_single.grid(row=0, column=0)
button_run_all.grid(row=0, column=1, padx=(35, 35))
button_details.grid(row=0, column=2)
frame.grid(row=0, column=1, sticky=tk.N)
def _create_label_frame(self):
frame = tk.Frame(self.master, bg="blue")
name_label = tk.Label(frame, text="Label 1")
performance_label = tk.Label(frame, text="Label 2")
name_entry = tk.Entry(frame)
performance_entry = tk.Entry(frame)
name_label.grid(row=0, column=0)
name_entry.grid(row=0, column=1)
performance_label.grid(row=1, column=0)
performance_entry.grid(row=1, column=1)
frame.grid(row=1, column=1)
if __name__ == '__main__':
root = tk.Tk()
app = Application(root)
app.mainloop()
Between the three buttons and the label + entry frame is a huge space. I want the button and label + entry frame right under each other, without the huge space but the treeview should also expand vertically over the whole application window.
I think the problem might be my row and column configuration but I don't know how to solve this problem.
The way you've structured your code makes it hard to see the problem. As a good general rule of thumb, all calls to grid or pack for widgets within a single parent should be in one place. Otherwise, you create dependencies between functions that are hard to see and understand.
I recommend having each of your helper functions return the frame rather than calling grid on the frame. That way you give control to Application.__init__ for the layout of the main sections of the window.
For example:
left_frame = self._create_left_frame()
button_bar = self._create_button_bar()
label_frame = self._create_label_frame()
left_frame.pack(side="left", fill="y")
button_bar.pack(side="top", fill="x")
label_frame.pack(side="top", fill="both", expand=True)
I used pack here because it requires less code than grid for this type of layout. However, if you choose to switch to grid, or wish to add more widgets to the root window later, you only have to modify this one function rather than modify the grid calls in multiple functions.
Note: this requires that your functions each do return frame to pass the frame back to the __init__ method. You also need to remove frame.grid from each of your helper functions.
With just that simple change you end up with the button bar and label/entry combinations at the top of the section on the right. In the following screenshot I changed the background of the button_bar to green so you can see that it fills the top of the right side of the UI.
You need to change line
self.master.grid_rowconfigure(0, weight=1)
to
self.master.grid_rowconfigure(1, weight=1)
so that the second row takes all the space. Then you need to stick widgets from the label frame to its top by adding sticky parameter to the grid call in _create_label_frame:
frame.grid(row=1, column=1, sticky=tk.N)
I prefer to use the Pack Function since it gives a more open window - its easy to configure. When you use Pack() you can use labels with no text and just spaces to create a spacer, by doing this you won't run into the problem your facing.
this program works functionality wise so far anyway so that's not my issue. My issue is something to do with how the alignment works for widgets with multiple frames. If I make the widgets on one frame longer width wise, than the other frames, it will center all the frames based on the widest frame rather than each individual frame. The widest frame right now in this code is 'ChangePasswordPage' as it will almost always use a long string for the label; this causes all of the other frames to shift to the left. If I remove the 'sticky="nsew"' from frame.grid(column=0, row=0, sticky="nsew") it will allign everything properly but the frame doesn't fill outwards so you can see the other frames behind each other.
I am not sure how to fix this and would love some help. I have been trying different ways such as unloading each widget and then reloading it but getting errors there too. Any help will be highly appreciated.
Here's my condensed code:
import tkinter as tk
from tkinter import ttk, messagebox
class CCTV(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack()
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (LoginPage, ChangePasswordPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(column=0, row=0, sticky="nsew")
self.creatingAccount()
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def creatingAccount(self):
self.show_frame(LoginPage)
class LoginPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.createView()
def createView(self):
self.labelPassword = ttk.Label(self, text="Password")
self.entryPassword = ttk.Entry(self, show = "*")
self.buttonLogin = ttk.Button(self, text="Login", command=lambda: self.controller.show_frame(ChangePasswordPage))
self.labelPassword.grid(row=2, column=3, sticky="w")
self.entryPassword.grid(row=2, column=4, sticky="e")
self.buttonLogin.grid(row=3, columnspan=6, pady=10)
class ChangePasswordPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.createView()
def createView(self):
self.labelSecurityQuestion = ttk.Label(self, text="A very very very very very very very very very very long string")
self.entrySecurityQuestion = ttk.Entry(self)
self.buttonCreateAccount = ttk.Button(self, text="Change Password", command=lambda: self.controller.show_frame(LoginPage))
self.labelSecurityQuestion.grid(row=3, column=0, sticky="w")
self.entrySecurityQuestion.grid(row=3, column=1, sticky="e")
self.buttonCreateAccount.grid(row=5, columnspan=2, pady=10)
app = CCTV()
app.geometry("800x600")
app.mainloop()
There are usually many ways to solve layout problems. It all depends on what you actually want to do, what actual widgets you're using, and how you expect widgets react when their containing windows or frames change.
When you use grid, by default it gives widgets only as much space as they need. Any extra space within the containing widget will go unused. If you want grid to use all available space you must tell it how to allocate the extra space by giving rows and columns "weight". You are not doing so, so extra space is not being used, and thus, your widgets aren't being centered.
As a rule of thumb, for any container (typically, a Frame) that uses grid, you must give a weight to at least one row, and one column. If you have a canvas or text widget, usually the row and column it is in is the one you want to get the un-allocated space. If you have entry widgets that you want to grow and shrink, you'll often give a weight to the column(s) that contain the entry widgets. Often you'll have multiple rows and/or columns that you want to be given extra space, though there are also times where you want everything centered, with extra space being allocated along the edges.
In your specific case, to center everything in the password screen there are several solutions. I will detail a couple.
Using only grid, and placing all widgets directly on the page
When you want to arrange absolutely all of your widgets in one frame and manage them with grid, and you want everything centered in the window, you can give all of the weight to rows and columns that surround your content.
Here's an example. Notice that the widgets are in rows 1 and 2, and columns 1 and 2, and all extra space is being given to rows 0 and 3, and columns 0 and 3.
def createView(self):
...
self.labelPassword.grid(row=1, column=1, sticky="e")
self.entryPassword.grid(row=1, column=2, sticky="ew")
self.buttonLogin.grid(row=2, column=1, columnspan=2)
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(3, weight=1)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(3, weight=1)
Using a nested frame
Another approach is to place the widgets into an inner frame that exactly fits its contents. When you do that, you only have to worry about centering that one frame within the "page" since, when you center the frame, by definition everything in the frame will also be centered relative to the window as a whole.
When you do that you can skip worrying about row and column weights because you want the inner frame to shrink to fit the contents and thus not have to worry about un-allocated space. However, I personally think it's good to always give at least one row and one column weight.
Here's an example using the inner frame technique. Notice how the widgets are placed in the inner frame rather than self, and the inner frame uses pack with no options which causes it to be centered at the top of its parent. You could also use place, which is particularly convenient if you want the password prompt centered vertically as well.
def createView(self):
inner_frame = tk.Frame(self)
inner_frame.pack(side="top", fill="none")
self.labelPassword = ttk.Label(inner_frame, text="Password")
self.entryPassword = ttk.Entry(inner_frame, show = "*")
self.buttonLogin = ttk.Button(inner_frame, text="Login", command=lambda: self.controller.show_frame(ChangePasswordPage))
self.labelPassword.grid(row=1, column=1, sticky="e")
self.entryPassword.grid(row=1, column=2, sticky="ew")
self.buttonLogin.grid(row=2, column=1, columnspan=2)
Summary
Layout out widgets requires a methodical approach. It helps to temporarily give each frame a distinct color so that you can see which frames are growing or shrinking to fit their contents and/or their parents. Often times that is enough to see whether the problem is with a frame, the contents in the frame, or the contents in the containing frame.
Also, you should almost never use pack, place or grid without giving explicit arguments. When you use grid, always give at least one row and one column a weight.
What I want in Tkinter in Python 2.7 is the following grid layout:
However once, I start using the grid() functions instead of pack() functions, nothing is showing on running the script. The following is what I am stuck with:
import Tkinter, ttk
class App(Tkinter.Frame):
def __init__(self,parent):
Tkinter.Frame.__init__(self, parent, relief=Tkinter.SUNKEN, bd=2)
self.parent = parent
self.grid(row=0, column=0, sticky="nsew")
self.menubar = Tkinter.Menu(self)
try:
self.parent.config(menu=self.menubar)
except AttributeError:
self.tk.call(self.parent, "config", "-menu", self.menubar)
self.tree = ttk.Treeview(self.parent)
self.tree.grid(row=0, column=0, sticky="nsew")
self.yscrollbar = ttk.Scrollbar(self, orient='vertical', command=self.tree.yview)
self.yscrollbar.grid(row=0, column=1, sticky='nse')
self.tree.configure(yscrollcommand=self.yscrollbar.set)
self.yscrollbar.configure(command=self.tree.yview)
if __name__ == "__main__":
root = Tkinter.Tk()
root.title("MyApp")
app = App(root)
app.pack()
app.mainloop()
Any help will be highly appreciated.
You have several problems that are affecting your layout.
First, some of the widgets inside App use self as the parent, some use self.parent. They should all use self in this particular case. So, the first thing to do is change the parent option of the Treeview to self.
self.tree = ttk.Treeview(self)
Second, since your main code is calling app.pack(), you shouldn't be calling self.grid. Remove the line `self.grid(row=0, column=0, sticky="nsew"). It's redundant.
Third, you are using very unusual code to add the menubar. You need to configure the menu of the root window. There's no need to put this in a try/except block, and there's no reason to use self.tk.call. Simply do this:
self.parent.configure(menu=self.menubar)
This assumes that self.parent is indeed the root window. If you don't want to force that assumption you can use winfo_toplevel() which will always return the top-most window:
self.parent.winfo_toplevel().configure(menu=self.menubar)
Finally, since you are using grid, you need to give at least one row and one column a "weight" so tkinter knows how to allocate extra space (such as when the user resizes a window).
In your case you want to give all of the weight to row and column 0 (zero), since that's where you've placed the widget which needs the most space:
def __init__(self, parent):
...
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
Note: you'll also want to make sure when you call app.pack() that you give it parameters that makes it fill any extra space, too. Otherwise the tree will fill "app", but "app" would not fill the window.
app.pack(fill="both", expand=True)
Here is a fully working example with all of those changes. I grouped the main layout code together since that makes the code easier to visualize and easier to maintain:
import Tkinter, ttk
class App(Tkinter.Frame):
def __init__(self,parent):
Tkinter.Frame.__init__(self, parent, relief=Tkinter.SUNKEN, bd=2)
self.parent = parent
self.menubar = Tkinter.Menu(self)
self.parent.winfo_toplevel().configure(menu=self.menubar)
self.tree = ttk.Treeview(self)
self.yscrollbar = ttk.Scrollbar(self, orient='vertical', command=self.tree.yview)
self.tree.configure(yscrollcommand=self.yscrollbar.set)
self.tree.grid(row=0, column=0, sticky="nsew")
self.yscrollbar.grid(row=0, column=1, sticky='nse')
self.yscrollbar.configure(command=self.tree.yview)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
if __name__ == "__main__":
root = Tkinter.Tk()
root.title("MyApp")
app = App(root)
app.pack(fill="both", expand=True)
app.mainloop()
Your question mentioned grid, but in this case you could save a few lines of code by using pack. pack excels in layouts like this, where your gui is aligned top-to-bottom and/or left-to-right. All you need to do is replace the last five lines (the calls to grid, grid_rowconfigureandgrid_columnconfigure`) with these two lines:
self.yscrollbar.pack(side="right", fill="y")
self.tree.pack(side="left", fill="both", expand=True)