tkinter - Layout

Gridlayout

Gridlayout

Das Grid-Layout basiert darauf, dass man Elemente in einer Art Tabelle anordnet. Es gibt Zeilen und Spalten. Elemente werden mit grid() in Zellen gelegt. Per rowspan und columnspan können mehrere Zeilen/Spalten von einem Widget überdeckt werden. Dann sorgt die Option sticky für das Ausdehnen. Dieses funktioniert mit Einschränkungen: Es müssen Elemente in den span-Zeilen enthalten sein, damit die Geometrie sichtbar funktioniert. Alternativ können Zeilen und Spalten konfiguriert werden, wie es in den nächsten Abschnitten gezeigt wird. Folgendes Beispiel verdeutlicht das:

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("300x200")
        self._createWidgets()

    def _createWidgets(self):
        label00 = ttk.Label(self, relief=tk.GROOVE, text="abc")
        label00.grid(row=0, column=0, sticky=tk.N+tk.S+tk.W+tk.E)
        
        label01 = ttk.Label(self,  relief=tk.GROOVE, text="def")
        label01.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W+tk.E)

        label02 = ttk.Label(self,  relief=tk.GROOVE, text='ghi')
        label02.grid(row=0, column=2, rowspan=2, sticky=tk.N+tk.S+tk.W+tk.E)

        # Kommentiert man dieses Label aus, dann wird auch label02
        # nicht mehr über 2 Reihen angezeigt
        label10 = ttk.Label(self,  relief=tk.GROOVE, text='aaa')
        label10.grid(row=1, column=0, sticky=tk.N+tk.S+tk.W+tk.E)

        button = ttk.Button(self, text='Quit', command=self.destroy)
        button.grid(row=2, column=0, columnspan=3, sticky=tk.W+tk.E)
        
if __name__ == '__main__':
    window = A()
    window.mainloop()

Parameter der Grid-Methode

Die Methode grid() kennt folgende Parameter:

ParameternameBeschreibung
columnSpaltennummer, optional, default=0
columnspanAnzahl der zusätzlich benötigten Spalten
in_Das Widget wird in ein anderes Widget eingefügt
ipadx, ipadyinneres Padding, zusätzlicher Platz links und rechts bzw. oben und unten innerhalb des Widgets. Diesen Platz sieht man nicht, wenn sich das Widget voll ausdehnt.
padx, padyäußeres Padding, zusätzlicher Platz links und rechts bzw. oben und unten. Diesen Platz sieht man auch, wenn das Widget sich voll ausdehnen darf.
rowZeilennummer, optional, default=0
rowspanAnzahl der zusätzlich benötigten Zeilen
stickyKombination aus tk.N, tk.S, tk.W und tk.E.
  • sticky=tk.N+tk.S dehnt ein Widget horizontal,
  • sticky=tk.E+tk.W dehnt ein Widget vertikal,
  • sticky=tk.N+tk.E+tk.S+tk.W dehnt in beide Richtungen

Das folgende Beispiel zeigt äußeres und inneres Padding: label00 wird mit einem Außenabstand zum Rand gezeichnet, obwohl es sich ausdehnen darf. label01 darf sich nicht ausdehnen, bekommt aber durch inneres Padding etwas mehr Innenraum. label02 hingegen darf sich ausdehnen, bekommt aber keinen Padding.

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("300x200")
        self._createWidgets()

    def _createWidgets(self):
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
        
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)

        label00 = ttk.Label(self, relief=tk.GROOVE, text="abc")
        label00.grid(row=0, column=0, sticky=tk.N+tk.S+tk.W+tk.E, padx=10, pady=10)
        
        label01 = ttk.Label(self,  relief=tk.GROOVE, text="def")
        label01.grid(row=0, column=1, ipadx=10, ipady=10)

        label02 = ttk.Label(self,  relief=tk.GROOVE, text='ghi')
        label02.grid(row=0, column=2, sticky=tk.N+tk.S+tk.W+tk.E)
        
        button = ttk.Button(self, text='Quit', command=self.destroy)
        button.grid(row=1, column=0, columnspan=3)       
        

if __name__ == '__main__':
    window = A()
    window.mainloop()

Gridlayout bei variabler Fenstergröße

Das eigentliche Ziel des Layouts ist es, die Widgets in einem Fenster so anzuordnen, dass auch bei sich ändernder Fenstergröße die Widgets passend angeordnet werden. Das Ziel kann leicht erreicht werden, wenn man mit rowconfigure() und columnconfigure() Zeilen und Spalten eine Größe oder alternativ ein Gewicht zuordnen. Gewicht bedeutet eine relative Breite oder Höhe.

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("300x200")
        self._createWidgets()

    def _createWidgets(self):
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)

        label00 = ttk.Label(self, relief=tk.GROOVE, text="abc")
        label00.grid(row=0, column=0, sticky=tk.N+tk.S+tk.W+tk.E)
        
        label01 = ttk.Label(self,  relief=tk.GROOVE, text="def")
        label01.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W+tk.E)

        label02 = ttk.Label(self,  relief=tk.GROOVE, text='ghi')
        label02.grid(row=0, column=2, rowspan=2, sticky=tk.N+tk.S+tk.W+tk.E)

        # Unabhängig von den folgenden beiden Zeilen wird label02
        # angezeigt
        #label10 = ttk.Label(self,  relief=tk.GROOVE, text='aaa')
        #label10.grid(row=1, column=0, sticky=tk.N+tk.S+tk.W+tk.E)

        button = ttk.Button(self, text='Quit', command=self.destroy)
        button.grid(row=2, column=0, columnspan=3, sticky=tk.W+tk.E)
        
if __name__ == '__main__':
    window = A()
    window.mainloop()

Gridlayoutmethoden im Überblick

FunktionParameterRückgabewertBeschreibung
w.columnconfigure(spalte, option=wert, …)spalte: Nummer der Spalte, option=wert: Optionen für Spalte. Optionen können minsize (Mindestbreite), pad (Extraabstand in Pixeln) oder weight (Relative Breite Spalte) sein -Verändert die Konfiguration der Spalte
w.grid_bbox(spalte1, reihe1, spalte2, reihe2) spalte, reihe optionale Spalten- und Reihennummern 4-TupelGibt die Geometrie als Rechteck der Elemente in den angegebenen Spalten/Reihen zurück
w.grid_forget()--w wird aus dem Grid gelöst, ist aber weiterhin da und kann per grid() wieder eingefügt werden
w.grid_info()-DictionaryBeschreibt die Lage eines Widgets, beispielsweise eines Labels, innerhalb des Grid-Layouts. Es werden alle Gridattribute mit ihren zugehörigen Werten zurückgeliefert.
w.grid_location(x, y)x, y Screenkoordinaten2-TupelGibt die Zelle (spalte, reihe) zurück
w.grid_propagate(prop)True oder False-False: Das Widget teilt dem Fenster seine Größe nicht mit. Es wird also nicht in die Gridlayout-Größenberechnung miteinbezogen. Nützlich, wenn man festgelegte konstante Breiten/Höhen erreichen will.
w.grid_remove()--wie grid_forget(), die Gridproportionen werden aber gemerkt und genutzt, wenn das Widget erneut eingefügt wird
w.grid_size()-2-TupelGibt die Anzahl der Spalten und Reihen im Grid-Layout von w an
w.grid_slaves(reihe, spalte)reihe: optionale Reihe, spalte: optionale SpalteListe von WidgetsGibt eine Liste von Widgets zurück, die sich in der Reihe/Spalte oder im gesamten Grid befinden
w.rowconfigure(zeile, option=wert, …)wie w.columnconfigure(…), nur mit Zeilen

Packlayout

Packlayout

Beim Packlayout werden die Widgets von links nach rechts, oder oben nach unten mit pack()eingefügt. Der Parameter fill bestimmt die Richtung, in die sich ein Widget ausdehnen darf, der Parameter expand bestimmt, ob das Widget extra Raum bekommen darf.

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("300x200")
        self._createWidgets()

    def _createWidgets(self):
        label00 = ttk.Label(self, relief=tk.GROOVE, text="abc")
        label00.pack(fill=tk.X)
        
        label01 = ttk.Label(self,  relief=tk.GROOVE, text="def")
        label01.pack(fill=tk.Y, expand=tk.YES)

        label02 = ttk.Label(self,  relief=tk.GROOVE, text='ghi')
        label02.pack(fill=tk.BOTH, expand=tk.YES)
        
        button = ttk.Button(self, text='Quit', command=self.destroy)
        button.pack()

if __name__ == '__main__':
    window = A()
    window.mainloop()

Hier darf sich label00 horizontal ausdehnen und den verfügbaren Platz einnehmen, label01 und label02 dürfen sich vertikal ausdehnen und konkurrieren um den verfügbaren Platz. Während label01 sich nicht horizontal ausdehnen darf, darf label02 sich in beide Richtungen erstrecken.

Parameter der Pack-Methode

ParameternameBeschreibung
after, beforeÄndert die Pack-Reihenfolge. Dieses Widget wird nach / vor einem mit after / before anzugebenen Widget gepackt
anchorGibt es mehrere Möglichkeiten für ein Widget, sich zu plazieren, kann mit anchor bestimmt werden, wohin es plaziert wird. tk.N, tk.S, tk.W, tk.E, tk.CENTER (default)
expandtk.YES, tk.NO (default), gibt an, ob das Widget seinen Freiraum nutzen darf
filltk.NONE: nichts ausfüllen (default), tk.X: in X-Richtung den Platz füllen, tk.Y: in Y-Richtung, tk.BOTH: in X- und Y-Richtung den Platz ausfüllen
in_Das Widget wird in ein anderes Widget eingefügt
ipadx, ipadyZusätzlicher Innenabstand
padx, padyZusätzlicher Außenabstand
sideTK.BOTTOM, tk.TOP (default), tk.LEFT, tk.RIGHT, Gibt die Seite an, an die das Widget gepackt wird

Im folgenden Beispiel kommt es sehr auf die Reihenfolge der Labels an. Ändert man diese bei gleichen Pack-Parametern, dann ändert sich auch gleichzeitig die Ausdehnung:

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("300x200")
        self._createWidgets()

    def _createWidgets(self):
        labelLeft = ttk.Label(self, relief=tk.GROOVE, text="LEFT")
        labelLeft.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES)

        labelRight = ttk.Label(self,  relief=tk.GROOVE, text='RIGHT')
        labelRight.pack(side=tk.RIGHT, fill=tk.BOTH, expand=tk.YES)
        
        labelTop = ttk.Label(self,  relief=tk.GROOVE, text="TOP")
        labelTop.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)

        labelCenter = ttk.Label(self, relief=tk.GROOVE, text="CENTER")
        labelCenter.pack(anchor=tk.CENTER, fill=tk.BOTH, expand=tk.YES)
        
        labelBottom = ttk.Label(self,  relief=tk.GROOVE, text='BOTTOM')
        labelBottom.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=tk.YES)

if __name__ == '__main__':
    window = A()
    window.mainloop()

Packlayoutmethoden im Überblick

FunktionParameterRückgabewertBeschreibung
w.pack_forget(), w.forget()--Entfernt das Widget aus dem Layout und vergisst dabei alle pack()-Parameter.
w.info()-Dictionary mit allen möglichen Pack-ParameternGibt die aktuelle Pack-Konfiguration zurück.
l.pack_propagate(prop)True / False, tk.YES / tk.NO, 1/0- True: Größe des Layout-Widgets ändert sich, wenn Widgets dem Layout hinzugefügt werden. (Default) Sonst nicht
l.pack_slaves(), l.slaves()-Array von Widgets Gibt ein Array zurück, das alle im Layout-Widget enthaltenen Widgets zurückliefert.

Im folgenden Beispiel bekommt man nichts zu sehen, wenn der Parameter von f.pack_propagate(1) auf "0" gesetzt wird: Die Größe des Frames ändert sich dann nicht, wenn die Labels hinzugefügt werden:

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")
        self._createWidgets()

    def _createWidgets(self):
        f = ttk.Frame(self, relief=tk.SUNKEN)
        f.pack()
        f.pack_propagate(1)
        
        label1 = ttk.Label(f, relief=tk.GROOVE, text="Label1")
        label1.pack(padx=10, pady=10, side=tk.LEFT, expand=tk.YES, fill=tk.BOTH)
        
        label2 = ttk.Label(f,  relief=tk.GROOVE, text='Label2')
        label2.pack(padx=10, pady=10, side=tk.LEFT, expand=tk.YES, fill=tk.BOTH)
        
if __name__ == '__main__':
    window = A()
    window.mainloop()

Gibt eine Liste von im Layout enthaltener Widgets aus:

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")
        self._createWidgets()

    def _createWidgets(self):
        f = ttk.Frame(self, relief=tk.SUNKEN)
        f.pack(side=tk.LEFT, expand=tk.YES, fill=tk.BOTH)
        
        label1 = ttk.Label(f, relief=tk.GROOVE, text="Label1")
        label1.pack(padx=10, pady=10, side=tk.LEFT, expand=tk.YES, fill=tk.BOTH)
        
        label2 = ttk.Label(f,  relief=tk.GROOVE, text='Label2')
        label2.pack(padx=10, pady=10, side=tk.LEFT, expand=tk.YES, fill=tk.BOTH)
        
        print(f.pack_slaves())
        
if __name__ == '__main__':
    window = A()
    window.mainloop()

Placelayout

Placelayout - absolut

Das Placelayout basiert darauf, dass Widgets mit place() punktgenau positioniert werden. Tkinter braucht hierfür nichts weiter zu berechnen und der Anwender ist für Größe und Position der Widgets veantwortlich. Ferner gibt es die Möglichkeit, ein Widget relativ zur Größe des Layout-Widgets zu positionieren.

Im folgenden Beispiel werden zwei Widgets positioniert. Eines oben links, das Andere unten rechts. Der Parameter anchor bestimmt, welcher Ort des Labels an den angegebenen Punkt plaziert wird.

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")
        self._createWidgets()

    def _createWidgets(self):
        label1 = ttk.Label(self, relief=tk.GROOVE, text="Label1")
        label1.place(x=0, y=0)
        
        label2 = ttk.Label(self, relief=tk.GROOVE, text="Label2")
        label2.place(anchor=tk.SE, x=200, y=200)
        
if __name__ == '__main__':
    window = A()
    window.mainloop()

Placelayout - relativ

Neben der absoluten Positionierung bietet place() die Möglichkeit, ein Widget relativ zur Größe des Layout-Widgets zu positionieren.

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")
        self._createWidgets()

    def _createWidgets(self):
        label1 = ttk.Label(self, relief=tk.GROOVE, text="Oben-Mitte")
        label1.place(relx=0.5, y=0)
        
        label2 = ttk.Label(self, relief=tk.GROOVE, text="Links-Mitte")
        label2.place(x=0, rely=0.5)

        label3 = ttk.Label(self, relief=tk.GROOVE,
                           text="Dieses Widget liegt in der Mitte")
        label3.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
        
        label4 = ttk.Label(self, relief=tk.GROOVE, text="Unten-Mitte")
        label4.place(relx=0.5, rely=1, anchor=tk.S)
        
if __name__ == '__main__':
    window = A()
    window.mainloop()

Und schließlich lässt sich noch die Größe des Widgets relativ zum Layout-Widget per relwidth, relheightangeben:

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")
        self._createWidgets()

    def _createWidgets(self):
        label1 = ttk.Label(self, relief=tk.GROOVE, text="Oben-Mitte")
        label1.place(x=0, y=0, relwidth=0.5, relheight=0.5)
        
        label2 = ttk.Label(self, relief=tk.GROOVE, text="Unten")
        label2.place(relx=0.5, rely=1, relwidth=0.5, anchor=tk.S)

if __name__ == '__main__':
    window = A()
    window.mainloop()

Parameter der Place-Methode

ParameternameBeschreibung
anchor Ort im Widget, der positioniert werden soll: tk.NW, …, tk.SE, tk.CENTER
bordermode Bei tk.INSIDE (default): Rahmen des Layout-Widgets wird berücksichtigt, tk.OUTSIDE: Rahmen im Layout-Widget bleibt komplett unberücksichtigt
height, widthAbsolute Höhe/Breite eines Widgets
in_Angabe eines anderen Layout-Widgets, in welches das Widget eingefügt werden soll
relheight, relwidth Relative Höhe/Breite eines Widgets, 0..1
relx, relyRelative Positionsangabe eines Widgets, 0..1
x, yAbsolute Positionsangabe eines Widgets

Im folgenden Beispiel werden Label in einem Frame mit einem 10 Pixel Rahmen plaziert. Die Label unterscheiden sich im bordermode-Parameter:

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")
        self._createWidgets()

    def _createWidgets(self):
        f = ttk.Frame(self, relief=tk.RAISED, borderwidth=10)
        f.pack(fill=tk.BOTH, expand=tk.YES)
        
        label1 = ttk.Label(f, relief=tk.GROOVE, text="INSIDE")
        label1.place(x=0, y=0, bordermode=tk.INSIDE)
        
        label2 = ttk.Label(f, relief=tk.GROOVE, text="OUTSIDE")
        label2.place(x=0, rely=0.5, bordermode=tk.OUTSIDE)

if __name__ == '__main__':
    window = A()
    window.mainloop()

Packlayoutmethoden im Überblick

FunktionParameterRückgabewertBeschreibung
w.place_forget()--Entfert Widget aus dem Layout
w.place_info()-Dictionary mit Place-OptionenGibt ein Dictionary mit allen Place-Optionen zurück.
w.place_slaves()-Array mit WidgetsGibt ein Array mit allen Widgets im aktuellen Layout zurück.

Fensterlayout

Fensterlayout - kein echtes Layout

Das Fensterlayout ist kein echtes Layout, weil es sich nur auf die Verwaltung eines einzelnen Fensters stützt. Zentrale Methode ist hier geometry() mit folgenden Möglichkeiten:

Hat man mehrere Fenster und möchte deren Anordnung über- oder untereinander festlegen ("Stacking Order"), helfen die folgenden Funktionen:

Folgendes Programm stellt drei Toplevel-Fenster vor, die nach jeweils 500 ms neu übereinander angeordnet werden:

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('400x200+100+100')
        self._createWidgets()

    def _createWidgets(self):
        self._currentTop = 0
        # drei Toplevel-Fenster mit unterschiedlichem Hintergrund
        self.w1 = tk.Toplevel(self, background='green')
        self.w2 = tk.Toplevel(self, background='red')
        self.w3 = tk.Toplevel(self, background='blue')
        # Geometrie, sollte überlappen
        self.w1.geometry('100x100+120+120')
        self.w2.geometry('100x100+150+150')
        self.w3.geometry('100x100+180+180')
        # Hauptfenster unter das erste Toplevel schieben 
        self.lower(belowThis=self.w1)
        # Starte den Timer
        self.after(500, self._lifter)

    def _lifter(self):
        self._currentTop = (self._currentTop + 1) % 3
        if self._currentTop == 0:
            self.w1.tkraise()
        elif self._currentTop == 1:
            self.w2.tkraise()
        else:
            self.w3.tkraise()
        self.after(500, self._lifter)
        
if __name__ == '__main__':
    window = A()
    window.mainloop()