Best Way to Structure a Tkinter Application

Best way to structure a tkinter application?

I advocate an object oriented approach. This is the template that I start out with:

# Use Tkinter for python 2, tkinter for python 3
import tkinter as tk

class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent

<create the rest of your GUI here>

if __name__ == "__main__":
root = tk.Tk()
MainApplication(root).pack(side="top", fill="both", expand=True)
root.mainloop()

The important things to notice are:

  • I don't use a wildcard import. I import the package as "tk", which requires that I prefix all commands with tk.. This prevents global namespace pollution, plus it makes the code completely obvious when you are using Tkinter classes, ttk classes, or some of your own.

  • The main application is a class. This gives you a private namespace for all of your callbacks and private functions, and just generally makes it easier to organize your code. In a procedural style you have to code top-down, defining functions before using them, etc. With this method you don't since you don't actually create the main window until the very last step. I prefer inheriting from tk.Frame just because I typically start by creating a frame, but it is by no means necessary.

If your app has additional toplevel windows, I recommend making each of those a separate class, inheriting from tk.Toplevel. This gives you all of the same advantages mentioned above -- the windows are atomic, they have their own namespace, and the code is well organized. Plus, it makes it easy to put each into its own module once the code starts to get large.

Finally, you might want to consider using classes for every major portion of your interface. For example, if you're creating an app with a toolbar, a navigation pane, a statusbar, and a main area, you could make each one of those classes. This makes your main code quite small and easy to understand:

class Navbar(tk.Frame): ...
class Toolbar(tk.Frame): ...
class Statusbar(tk.Frame): ...
class Main(tk.Frame): ...

class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.statusbar = Statusbar(self, ...)
self.toolbar = Toolbar(self, ...)
self.navbar = Navbar(self, ...)
self.main = Main(self, ...)

self.statusbar.pack(side="bottom", fill="x")
self.toolbar.pack(side="top", fill="x")
self.navbar.pack(side="left", fill="y")
self.main.pack(side="right", fill="both", expand=True)

Since all of those instances share a common parent, the parent effectively becomes the "controller" part of a model-view-controller architecture. So, for example, the main window could place something on the statusbar by calling self.parent.statusbar.set("Hello, world"). This allows you to define a simple interface between the components, helping to keep coupling to a minimun.

Tkinter - code structure, construction and organisation - application append sub-frame

The solution is to create two classes. Think of the first class as a custom widget that behaves much like a notebook. The main difference is that instead of tabs you have a vertical stack of frames. It creates the widgets in TopFrame from your drawing, and it has methods for adding and deleting the frames in the bottom section.

The second class represents one "tab" or one frame/combo/entry/button group. It is responsible for creating itself, and is given a reference to the first class in order to delete itself and do other things.

I've included some example code to get you started. It doesn't add the comboboxes in the top frame since I think it's somewhat irrelevant to the overall structure of the code which is what you're asking about.

In the following code, FrameStack is the first class. It creates TopFrame and GroupOfFrames from your diagram, and adds functions for adding and removing frames.

SubFrame represents the combination of a combobox, entry widget, a big button and a small button. Notice how the minus button calls back to the delete_frame method of the first class.

import tkinter as tk
from tkinter import ttk

class FrameStack(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.subframes = []
self.topFrame = tk.Frame(self)
self.groupOfFrames = tk.Frame(self, height=200)
self.topFrame.pack(side="top", fill="x")
self.groupOfFrames.pack(side="top", fill="both", expand=True)

self.add = tk.Button(self.topFrame, text="+", command=self.add_frame)
self.add.pack(side="right")

def delete_frame(self, frame):
self.subframes.remove(frame)
frame.destroy()

def add_frame(self):
f = SubFrame(parent=self.groupOfFrames, controller=self)
self.subframes.append(f)
f.pack(side="top", fill="x")

class SubFrame(tk.Frame):
def __init__(self, parent, controller):
super().__init__(parent)
self.parent = parent
self.controller = controller

self.cb = ttk.Combobox(self)
self.entry = tk.Entry(self)
self.main_button = tk.Button(self, width=10)
self.remove_button = tk.Button(self, text="-", command=self.remove)

self.grid_rowconfigure(0, weight=1)
self.cb.grid(row=0, column=0, sticky="ew")
self.entry.grid(row=1, column=0, sticky="ew")
self.main_button.grid(row=0, column=1, rowspan=2, sticky="nsew")
self.remove_button.grid(row=1, column=2, sticky="se")

def remove(self):
self.controller.delete_frame(self)

root = tk.Tk()
root.geometry("400x400")

fs = FrameStack(root)
fs.pack(fill="both", expand=True)
root.mainloop()

How to structure tkinter for a single window with multiple frames with multiple buttons inside each frames

You should probably use a class for each group. The most common way to do this is to have the class inherit from Frame. That lets you treat the whole group of widgets as a single compound widget when trying to lay everything out.

For example, one of the frames might look like this:

class Frame1(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
label1 = tk.Label(self, text = "this is frame1")
button1 = tk.Button(self)

label1.pack()
button1.pack()

Notice that all of the child widgets are in self rather than in the root window.

You can then treat this class as if it were a regular widget. For example:

root = tk.Tk()
frame1 = Frame1(root)
frame1.pack()

If each of your frames is identical, you can create a base class from which all other frames inherit. If they are all different, you don't need the base class. Even if the frames share almost nothing in common, using classes is a good way to logically treat a group of widgets as a single object.


After I wrote this answer you modified your question to say "my question is whether using class necessary when the only similarity between frames are the frame itself and a label, and nothing else."

The similarity isn't the issue. Classes are useful whether they share a common base or not. Classes make it possible to encapsulate a group of related functionality into a single entity.

tkinter application structure - using LabelFrames and Class

As a general rule of thumb when inheriting from a frame, all child widgets should be a child of the class itself, not its parent. Otherwise, there's zero advantage of inheriting from the frame class. In other words, make the parent of the label frames self rather than self.parent.

You then need to call pack, place, or grid on your classes so that they appear.

Also, you need to either give the label frames a size or put one or more widgets in it. Otherwise, the frames will be 1x1 pixel and virtually impossible to see.

Finally, if you're only putting a single widget in a frame, such as putting the label frames in self, it's easiest to use pack rather than grid since you can get it to fill the frame with a single command and without having to remember to give the rows and columns weight.

Here's your code with all of those changes:

import tkinter as tk
#-------------------------------------------------------------------------------
# CLASS
class ModeWin(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self,parent,bg='light blue')
self.parent = parent
self.lblFrame = tk.LabelFrame(self, text="Mode", padx=20, pady=20)
self.lblFrame.pack(fill="both", expand=True)

self.label = tk.Label(self.lblFrame, text="Text in ModeWin")
self.label.pack(side="top")

class StatusWin(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self,parent,bg='light green')
self.parent = parent
self.lblFrame = tk.LabelFrame(self, text="Status", padx=20, pady=20)
self.lblFrame.pack(fill="both", expand=True)

self.label = tk.Label(self.lblFrame, text="Text in StatusWin")
self.label.pack(side="top")

class MainWin(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)

self.modeWin = ModeWin(self)
self.statusWin = StatusWin(self)

self.modeWin.grid(row=0, column=0, sticky="ew")
self.statusWin.grid(row=1, column=0, sticky="ew")

def start(self):
self.mainloop()

#-------------------------------------------------------------------------------
# MAIN
def main():
app = MainWin()
app.start()

if __name__ == '__main__':
main()

screenshot

How do I organize my tkinter appllication?

Basic knowlege about tkinters geometry management

The geometry management of tkinter is characterized by this Quote here:

By default a top-level window appears on the screen in its natural size, which is the one determined internally by its widgets and geometry managers.



Toplevels

Your Toplevel is the first question you should have to answer with:

  • wm_geometry: size, position in your screen?
  • wm_minsize \ wm_maxsize are there minimal or maximal bounderies?
  • wm_resizable has the user the ability to resize it?
  • wm_attributes are there attributes like topmost or fullscreen?
  • pack_propagate \ grid_propagate ignore requested width and height of children.

Note: You can skip this question and let the process decide what will be needed after all.



Arrange children

To arrange your children you've got 3 options, each of them are designed to satisfy specific needs:

The packer:

The pack command is used to communicate with the packer, a geometry
manager that arranges the children of a parent by packing them in
order around the edges of the parent.

-> I use pack to arrange quickly a few widgets beside eachother in the master.

The placer

The placer is a geometry manager for Tk. It provides simple fixed
placement of windows, where you specify the exact size and location of
one window, called the slave, within another window, called the
master. The placer also provides rubber-sheet placement, where you
specify the size and location of the slave in terms of the dimensions
of the master, so that the slave changes size and location in response
to changes in the size of the master. Lastly, the placer allows you to
mix these styles of placement so that, for example, the slave has a
fixed width and height but is centered inside the master.

-> I use place sometimes for One-Sheet applications or to set a background image.

The gridder

The grid command is used to communicate with the grid geometry manager
that arranges widgets in rows and columns inside of another window,
called the geometry master (or master window).

-> Grid is the best choice for more complex applications that contains many widgets.

So the question you need to answer here, before picking one of these managers is, how do I organise my application in the best way?

Note:

Warning: Never mix grid and pack in the same master window. Tkinter
will happily spend the rest of your lifetime trying to negotiate a
solution that both managers are happy with. Instead of waiting, kill
the application, and take another look at your code. A common mistake
is to use the wrong parent for some of the widgets.

-> You can create a nested layout, in each master(window/frame) you've freedom of choice



Most important features

Most important features of each manger can help to answer your question. Because you will need to know if the manager can do what you wanna do.

For pack I think it is:

  1. fill stretch the slave horizontally, vertically or both
  2. expand The slaves should be expanded to consume extra space in their master.
  3. side Specifies which side of the master the slave(s) will be packed against.
  4. anchor it specifies where to position each slave in its parcel.

For place it should be:

  1. relheight -relheight=1.0, -height=-2 makes the slave 2 pixels shorter than the master.
  2. relwidth -relwidth=1.0, -width=5 makes the slave 5 pixels wider than the master.
  3. relx -relx=0.5, -x=-2 positions the left edge of the slave 2 pixels to the left out of the center.
  4. rely -rely=0.5, -x=3 positions the top edge of the slave 3 pixels below the center of its master.

And for grid it should be:

  1. columnspan Insert the slave so that it occupies n columns in the grid.
  2. rowspan Insert the slave so that it occupies n rows in the grid.
  3. sticky this option may be used to position (or stretch) the slave within its cell.
  4. grid_remove the configuration options for that window are remembered
  5. grid_columnconfigure
  6. grid_rowconfigure

for the last two options I recommend this answer here.


Read the docs

A working exampel to play with can be found here:

Sample Image

import tkinter as tk

root=tk.Tk()

holderframe = tk.Frame(root,bg='red')
holderframe.pack()

display = tk.Frame(holderframe, width=600, height=25,bg='green')
display2 = tk.Frame(holderframe, width=300, height=145,bg='orange')
display3 = tk.Frame(holderframe, width=300, height=300,bg='black')
display4 = tk.Frame(holderframe, width=300, height=20,bg='yellow')
display5 = tk.Frame(holderframe, bg='purple')


##display_green
display.grid(column = 0, row = 0, columnspan=3)
display.pack_propagate(0) #when using pack inside of the display
#display.grid_propagate(0) #when using grid inside of the display

#left
b =tk.Button(display, width =10,text='b')
b1 =tk.Button(display, width =10,text='b1')

b.pack(side='left')
b1.pack(side='left')
#right
b2 =tk.Button(display, width =20,text='b2')
b2.pack(side='right')
#center
l = tk.Label(display, text ='My_Layout',bg='grey')
l.pack(fill='both',expand=1)

#the order by using pack can be important.
#you will notice if you swip right with center.


##display2_orange
display2.grid(column=0,row=1, sticky='n')
display2.grid_propagate(0)

#column0
lab = tk.Label(display2, text='test2')
lab1 = tk.Label(display2, text='test2')
lab2 = tk.Label(display2, text='test2')
lab3 = tk.Label(display2, text='test2')
lab4 = tk.Label(display2, text='test2')
lab5 = tk.Label(display2, text='test2')
lab6 = tk.Label(display2, text='test2')

lab.grid(column=0,row=0)
lab1.grid(column=0,row=1)
lab2.grid(column=0,row=2)
lab3.grid(column=0,row=3)
lab4.grid(column=0,row=4)
lab5.grid(column=0,row=5)
lab6.grid(column=0,row=6)

#column1
lab10 = tk.Label(display2, text='test2')
lab11 = tk.Label(display2, text='test2')
lab12 = tk.Label(display2, text='test2')
lab13 = tk.Label(display2, text='test2')
lab14 = tk.Label(display2, text='test2')
lab15 = tk.Label(display2, text='test2')
lab16 = tk.Label(display2, text='test2')

lab10.grid(column=2,row=0)
lab11.grid(column=2,row=1)
lab12.grid(column=2,row=2)
lab13.grid(column=2,row=3)
lab14.grid(column=2,row=4)
lab15.grid(column=2,row=5)
lab16.grid(column=2,row=6)

display2.grid_columnconfigure(1, weight=1)
#the empty column gets the space for left and right effect

##display3_black
display3.grid(column=1,row=1,sticky='nswe')
display3.grid_propagate(0)

##display4_yellow
display4.grid(column=0,row=1,sticky='s')
display4.grid_propagate(0)

lab20 = tk.Label(display4, bg='black')
lab21 = tk.Label(display4, bg='red')
lab22 = tk.Label(display4, bg='orange')
lab23 = tk.Label(display4, bg='grey')

lab20.grid(column=0,row=0,sticky='ew')
lab21.grid(column=1,row=0,stick='e')
lab22.grid(column=2,row=0,sticky='e')
lab23.grid(column=3,row=0,stick='ew')

display4.grid_columnconfigure(0, weight=4)
display4.grid_columnconfigure(1, weight=2)
display4.grid_columnconfigure(2, weight=2)
display4.grid_columnconfigure(3, weight=1)

##display5_purple
display5.place(x=0,y=170,relwidth=0.5,height=20)
display5.grid_propagate(0)


root.mainloop()


Related Topics



Leave a reply



Submit