Göm meny
Gäller för: HT25

Förberedelsematerial 5


Alla program vi sett hittills har använt sig av ett rent textbaserat gränssnitt via terminalen. I verkligheten är det dock vanligare att program har ett grafiskt användargränssnitt (eng. Graphical User Interface, eller GUI). I detta förberedelsematerial ska vi bekanta oss med grunderna i att skapa GUI:er i Python med hjälp av biblioteket Tkinter.

Tanken här är inte att ge en generell introduktion till moderna GUI-ramverk utan att ge en introduktion till hur kod vi skriver i text kan generera något visuellt som användaren kan interagera med.

Vad är ett grafiskt användargränssnitt?

Ett gränssnitt (eng. interface) är platsen där två, ofta orelaterade, system möts och kommunicerar med varandra - något som ligger mellan två olika system och genom vilket systemen kommunicerar med varandra. Vi pratar ofta om olika typer av gränssnitt som exempelvis:

  • Hårdvarugränssnitt, dvs. fysiska anslutningar mellan apparater, t.ex. USB, SATA, XLR, etc.
  • Mjukvarugränssnitt, dvs. mellan olika program och/eller hårdvaror, t.ex. operativsystem, drivrutiner, API:er.
  • Nätverksgränssnitt, dvs. mellan apparat och nätverk, t.ex. Ethernet, WiFi, Fiber, etc. som kan vara både hårdvaru- och mjukvarubaserade.

När vi pratar om gränssnitt inom kognitionsvetenskapen menar vi oftast gränsnitt som på något sätt har ett mänskligt system, dvs. en människa, på ena sidan. När den människan är en användare av systemet så kallar vi det oftast för användargränssnitt.

Även användargränssnitt kan vara både hårdvaru- och mjukvarubaserade. Några exempel på olika typer av användargränssnitt är:

  • Rent elektroniska användargränssnitt som på de tidigaste datorerna. Dvs. med kommunikation mellan användare och maskin via koppling av kablar och indikatorlampor.
  • Fysiska gränssnitt med knappar, vred, analoga displayer, och digitala displayer, t.ex. i fordon, på köksapparater eller i kontrollrum för fabriker eller kraftverk.
  • Rena textgränssnitt med kommunikation i båda riktningarna enbart med text, som t.ex. skalet i Linux.
  • Grafiska gränssnitt där kommunikation sker genom grafiska element som fönster, menyer, ikoner, tabbar, widgets, osv. i kombination med text.
  • Haptiska gränssnitt där kommunikation sker via sensorer som känner av rörelser och aktuatorer som genererar rörelser. T.ex. telefoner som automatiskt anpassar skärmlayout beroende på hur du håller telefonen och använder vibrationer för att ge feedback vid knapptryckningar.
  • Röstbaserade gränssnitt där kommunikation sker via tal och ljud, t.ex. röstassistenter som Siri, Alexa, och Google Assistant.

De flesta system vi är vana vid är multimodala och använder en blandning av olika gränssnitt för att kommunicera men vi kommer att begränsa oss till grafiska gränssnitt här.

Det existerar olika interaktionsmodeller som lägger fokus på olika saker och fungerar olika bra med olika typer av input och output, oftast beskrivna med metaforer. Fönsterbaserade system var länge de mest dominerande och är fortfarande populära på persondatorer med större skärmar, mus och tangentbord. Tab- och menybaserade system är mer populärt på enheter med mindre men pekkänsliga skärmar, som t.ex. smartphones och tablets. Ofta har vi flera olika interaktionsmodeller i samma system, visuellt nästlade i varandra, som t.ex. fönster med menyer och tabbar.

Interaktionsflöden

I textbaserade gränssnitt (eng. Command Line Interface, eller CLI) är interaktionsflödet oftast linjärt och sekventiellt. Användaren får en fråga eller instruktion, användaren svarar, programmet processar svaret och ger ny output, och så vidare. Interaktionen sker främst med hjälp av tangentbordet, där användaren skriver in textkommandon som programmet tolkar och svarar på.

Idag är textbaserade gränssnitt sällan särskilt interaktiva utan används främst för att köra skript eller kommandon som inte kräver mycket användarinteraktion, som t.ex. kommandotolken i Linux. Vi kan dock tänka oss ett mer traditionellt textbaserat gränssnitt för t.ex. ett äventyrsspel där användaren skriver in kommandon för att navigera i spelet och interagera med objekt och karaktärer och väljer mellan olika alternativ. En sådan interaktion skulle kunna se ut ungefär så här:

Enter name: Amanda
Enter title: Giant Slayer
Enter gender: female

Choose appearance:
1. unwashed
2. sparkling
3. retro
Enter choice (1-3): 1

Do you have a magic bag (y/n)? y
Are you happy (y/n)? y
Do you have a secret (y/n)? n
Generate story (y=yes, n=no, s=start over)? y

Your name is Amanda, the Giant Slayer. You are [...]

Användarens input står här i varje fall efter antingen ett kolon eller ett frågetecken, och programmet har väntat på att användaren ska skriva in sin input innan det går vidare.

Vi kan tänka oss ett grafiskt gränssnitt för samma äventyrsspel som ovan, där användaren kan fylla i sina val i olika fält och menyer, och se en förhandsvisning av sin karaktär och historia i realtid. Ett sådant interface skulle kunna se ut ungefär så här:

I grafiska gränssnitt är interaktionsflödet ofta icke-linjärt och icke-sekventiellt. Användaren kan välja att interagera med olika delar av gränssnittet i valfri ordning, och programmet kan uppdatera olika delar av gränssnittet oberoende av varandra. Detta kräver att programmet kan hantera flera olika händelser och tillstånd samtidigt, och att användaren kan navigera och förstå gränssnittet på ett intuitivt sätt.

Ett CLI.
Olika widgets.

Interaktion sker ofta med hjälp av både mus och tangentbord, eller ett pekskärmsgränssnitt där användaren kan klicka på olika widgets som knappar, menyer, ikoner, och andra grafiska element för att utföra olika åtgärder och få feedback från programmet. Interaktionen är något mer begränsad i ett grafiskt gränssnitt jämfört med ett textbaserat gränssnitt, eftersom användaren oftast inte kan köra godtyckliga kommandon utan måste välja mellan de alternativ som finns representerader via widgets i gränssnittet. Å andra sidan är det ofta enklare och mer intuitivt att använda ett grafiskt gränssnitt, särskilt för användare som inte är vana vid textbaserade kommandon.

Ordet widget var ursprungligen synonymt med “gadget”, dvs manick, mojäng, grej, pryl, osv. Sedermera fick det den mer specifika definitionen “WIndow gaDGET”, när fönster-metaforen kom att dominera inom GUI-fältet. Vissa widgets är också behållare (eng. container widgets) som kan innehålla andra widgets, t.ex. fönster, paneler, och ramar.

Ett ramverk för grafiska gränssnitt behöver kunna hantera t.ex. layout av widgets, hantera händelser från användaren, och uppdatera gränssnittet i realtid. Vi kommer att använda Tkinter som är ett inbyggt ramverk i Python för att skapa grafiska gränssnitt.

Tkinter

Det går inte att köra exemplen nedan direkt via Remote-SSH eftersom Tkinter kräver tillgång till en grafisk display. Detta går att lösa men kräver extra konfiguration.

För att köra exemplen behöver ni antingen köra dem på en dator i ett Linux-PUL, på er egen dator om ni har python installerat, eller via en fjärransluten fönstermiljö, t.ex. via ThinLinc eller RDP.

Tkinter är ett inbyggt ramverk i Python för att skapa grafiska användargränssnitt. Det är en wrapper runt det en gång mycket populära Tk-ramverket, skrivet i programmeringsspråket Tcl (uttalas “tickle”), som erbjuder ett enkelt och lättanvänd API för att skapa fönster, knappar, etiketter, textfält, menyer, och andra widgets. Tkinter är plattformsoberoende och fungerar på Windows, macOS, och Linux, men är generellt sett inget under av modern grafisk design. Med andra ord, det ser ganska mossigt ut. När man bildgooglar tkinter widgets ser man inte många exempel som ser särskilt moderna ut och det är inte ovanligt att hitta exempel som ser ut som nedan:

För den som inte känner igen designspråket så är det från Windows XP, vars förlängda supportperiod slutade 2014, fem år efter att den ordinarie supportperioden upphörde 2009. Med andra ord, inte direkt modernt.

En annan nackdel med Tkinter är att det inte är konsekvent dokumenterat. Det finns en del dokumentation på Tkinter-sidan på Python.org och TkDocs är en bra resurs. Utöver det kan man också hitta många tutorials och exempel på nätet. Det finns dock ingen officiell heltäckande dokumentation för Tkinter, vilket kan göra det svårt att hitta information om specifika widgets och funktioner. Orsaken till detta är att Tkinter bara är en wrapper runt Tk, och i stor utsträckning består av automatgenererad kod som inte har någon egen dokumentation utöver den som finns för Tk.

En stor fördel med Tkinter är dock att det är inbyggt i Python och inte kräver några externa installationer eller beroenden, vilket gör det enkelt att komma igång med. Det är också relativt enkelt att lära sig och att använda, särskilt för nybörjare inom grafiska gränssnitt. Mer moderna ramverk fokuserar ofta på responsiv design och separerar logik från layout och estetik på ett mer genomgripande sätt. Detta har stora fördelar i verkligheten, men är mer komplext att sätta sig in i. Vill ni lära er mer om teknikerna bakom sådana ramverk är ni välkomna till 729G87 Interaktionsprogrammering i termin 5.

I Tkinter är olika widgets representerade som klasser, och varje widget har sina egna metoder och attribut för att hantera dess utseende och beteende. Vi kan skapa en enkel Tkinter-applikation genom att importera Tkinter-biblioteket, skapa ett fönster, lägga till widgets, och starta huvudloopen för att hantera händelser från användaren.

Fönster i Tkinter

En enkel Tkinter-applikation som skapar ett fönster med en knapp som man kan trycka på men som inte har någon funktionalitet kan se ut så här:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Importera Tkinter-biblioteket
import tkinter as tk

# Skapa huvudfönstret
root = tk.Tk()
# Skapa en knappwidget som ligger i huvudfönstret root och har en text
button = tk.Button(root, text="Press me!")
# Placera knappen i fönstret
button.pack()

# Starta huvudloopen
tk.mainloop()

Kör vi ovanstående kod får vi upp ett fönster som ser ut ungefär så här:

Ett Tkinter-fönster i Windows.
Ett Tkinter-fönster i Windows 11.
Ett Tkinter-fönster i Xfce.
Ett Tkinter-fönster i Xfce (standard fönsterhanterare på LiUs Linux-system).
Ett Tkinter-fönster i Windows.
Ett Tkinter-fönster i Windows Subsystem for Linux 2 (WSL2).

Vi kan se att Tkinter-applikationen anpassar sig till det grafiska systemets utseende i någon utsträckning, t.ex. är titelfältet och knapparnas utseende och placering olika i Windows 11 jämfört med Xfce. Tittar vi noga ser vi också att det är olika mycket marginal runt knappen i de olika systemen och att typsnittet på texten på knappen skiljer sig åt.

Gemensamt för alla systemen är dock att intrycket är ganska enkelt och gammaldags. Det ger ungefär samma estetiska intryck som tentaklienten som användes under duggan, vilket kanske inte är en så smickrande jämförelse.

1
2
3
4
5
6
# importera Tkinter-modulen
import tkinter as tk
# skapa ett Tk-fönster
window = tk.Tk()
# starta GUI-loopen
tk.mainloop()

Det absolut enklaste Tkinter-programmet man kan tänka sig ser ut som ovan. Här har vi inte ens någon knapp, utan bara ett tomt fönster. I koden importerar vi Tkinter-biblioteket med det lokala namnet tk. Det här är en av de mest standardiserade konventionerna som vi nästan alltid använder för att slippa skriva ut tkinter hela tiden. Vi skapar sedan ett huvudfönster genom att instansiera klassen tk.Tk. Avslutningsvis startar vi huvudloopen med tk.mainloop(), vilket gör att fönstret visas och att programmet kan hantera händelser från användaren, som t.ex. knapptryckningar och fönsterhantering. Huvudloopen är en oändlig loop som körs tills användaren stänger fönstret, utan den så stängs fönstret direkt efter att det skapats, normalt sett innan vi ens hinner se det på skärmen.

Vi kan skapa flera fönster i en Tkinter-applikation genom att använda klassen tk.Toplevel, som skapar ett nytt fönster som är underordnat huvudfönstret men kan flyttas runt på skärmen oberoende av det. Exempelvis:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# windows.py
import tkinter as tk
# Skapa det första fönstret och ge det namet "Huvudfönster"
root = tk.Tk()
root.title("Huvudfönster")
# Skapa det andra fönstret och ge det namet "Sekundärt fönster"
top = tk.Toplevel()
top.title("Sekundärt fönster")
# Starta Tk-loopen
root.mainloop()

I koden ovan skapar vi två fönster, ett huvudfönster (root) och ett sekundärt fönster (top). Båda fönstren kan flyttas runt oberoende av varandra. Det händer dock lite olika saker beroende på vilket fönster vi stänger först. Stänger vi huvudfönstret stängs båda fönstren, medan om vi stänger det sekundära fönstret så fortsätter huvudfönstret att vara öppet. Skulle vi skapat enbart ett Toplevel-fönster utan ett Tk-fönster skulle det skapats ett Tk-fönster i bakgrunden automatiskt. Det beror på att det måste finnas ett Tk-fönster som huvudfönster i en Tkinter-applikation.

Widgets i Tkinter

Widget Description
Button A simple button, used to execute a command or other operation.
Canvas Structured graphics. This widget can be used to draw graphs and plots, create graphics editors, and to implement custom widgets.
Checkbutton Represents a variable that can have two distinct values. Clicking the button toggles between the values.
Entry A text entry field.
Frame A container widget. The frame can have a border and a background, and is used to group other widgets when creating an application or dialog layout.
Label Displays a text or an image.
Listbox Displays a list of alternatives. The listbox can be configured to get radiobutton or checklist behavior.
Menu A menu pane. Used to implement pulldown and popup menus.
Menubutton A menubutton. Used to implement pulldown menus.
Message Display a text. Similar to the label widget, but can automatically wrap text to a given width or aspect ratio.
OptionMenu A dropdown menu widget that allows the user to select one value from a list of options.
Radiobutton Represents one value of a variable that can have one of many values. Clicking the button sets the variable to that value, and clears all other radiobuttons associated with the same variable.
Scale Allows you to set a numerical value by dragging a “slider”.
Scrollbar Standard scrollbars for use with canvas, entry, listbox, and text widgets.
Spinbox Allows the user to input a custom value or select a value from a fixed set of values by clicking arrows to increment or decrement the value.
Text Formatted text display. Allows you to display and edit text with various styles and attributes. Also supports embedded images and windows.
Toplevel A container widget displayed as a separate, top-level window.

I tabellen ovan ser vi många av de widgets som finns i Tkinter. Det finns ytterligare widgets i submoduler och tilläggsmoduler, t.ex. ttk (Themed Tkinter), men de ligger utanför ramarna idag.

Button

Klassen tkinter.Button har vi redan sett att det är en widget som representerar en knapp i ett grafiskt användargränssnitt. En sådan knapp har oftast text som beskriver vad knappen gör, men kan också ha en bild eller ikon. Kan inaktiveras (gråas ut) för att indikera att den av något skäl inte är klickbar just nu.

Läs mer: https://dafarry.github.io/tkinterbook/button.htm

Exempel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# importera Tkinter-modulen
import tkinter as tk

# skapa ett rot-fönster
root = tk.Tk()
button = tk.Button(root, text="Press me!")
button.pack()

# starta GUI-loopen
root.mainloop()

(button.py)

Vi kommer tillbaka till widgets men först ska vi titta på hur man får något att hända när man klickar på en knapp.

Händelsehantering i Tkinter

Vi kopplar beteende till en widget genom att koppla ett funktionsobjekt/metodobjekt till den händelse (eng. event) som inträffar när användaren interagerar med widgeten. Ett funktionsobjekt är en referens till en funktion eller metod som ska anropas t.ex. när användaren klickar på en knapp eller skriver in text i ett textfält.

För en knapp kan vi koppla en funktion till händelsen att knappen klickas på genom att använda argumentet command när vi skapar knappen:

button = tk.Button(root, text="OK", command=do_something)

Där do_something är namnet på en funktion som vi har definierat tidigare i koden. När användaren klickar på knappen kommer funktionen do_something att anropas. Ofta kallar vi sådana funktioner som anropas via interaktion med GUI-komponenter för callback-funktioner eller bara callbacks.

En callback-funktion kan ta noll eller flera argument, beroende på vilken typ av händelse den är kopplad till. För knappar så ska callback-funktionen ta noll argument. I exemplet nedan definierar vi en enkel callback-funktion som skriver ut en sträng när den anropas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# importera Tkinter-modulen
import tkinter as tk

def callback():
    """Print a set string when called."""
    print("Something happened!")

# skapa ett Tk-fönster
root = tk.Tk()
button = tk.Button(root, text="Press Me!", command=callback)
button.pack()

# starta GUI-loopen
root.mainloop()

(button-command.py)

När vi kör ovanstående kod och klickar på knappen i fönstret kommer texten “Something happened!” att skrivas ut i terminalen där vi körde programmet.

Förutom att vissa widgets har inbyggda sätt att koppla callbacks till händelser, kan vi också använda metoden bind för att koppla en callback till en specifik händelse (Event) på en widget. Detta är användbart när vi vill hantera mer komplexa händelser utöver standardinteraktionen. En sådan callback-funktion måste ta ett argument, som är ett tkinter.Event-objekt med information om händelsen som inträffade.

En händelse kan vara t.ex. ett musklick, en tangenttryckning, eller en musrörelse. Vi kan specificera vilken typ av händelse vi vill hantera genom att använda en sträng som beskriver händelsen, t.ex. "<Key>" för tangenttryckningar. Några exempel exempel på händelser man kan binda är:

Händelse Beskrivning
"<Enter>" när musen förs in i över en widget
"<Leave>" när musen lämnar en widget
"<KeyPress>" när en tangent trycks ner
"<KeyRelease>" när en tangent släpps upp igen
"<Button-1>" när musknapp 1 trycks
"<Button-2>" när musknapp 2 trycks

För den som vill läsa mer:

Exempel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tkinter as tk

def callback():
    print("Something happened!", end=' ')

def over_me(event):
    print("You are over me!", end=' ')

def left_me(event):
    print("You left me!", end=' ')

# skapa ett Tk-fönster och knapp
root = tk.Tk()
button = tk.Button(root, text="Press Me!", command=callback)

# bind funktioner till Enter/Leave-händelserna
button.bind("<Enter>", over_me)
button.bind("<Leave>", left_me)
button.pack()

# starta GUI-loopen
root.mainloop()

(button-event.py)

Här ser vi två funktioner over_me och left_me som tar ett argument event. Detta argument är ett tkinter.Event-objekt som innehåller information om händelsen som inträffade, t.ex. vilken widget händelsen inträffade på, vilken typ av händelse det var, och andra relevanta data. När vi kör ovanstående kod och för muspekaren över knappen kommer texten “You are over me!” att skrivas ut i terminalen. När vi för muspekaren bort från knappen kommer texten “You left me!” att skrivas ut.

Mer om Event-klassen: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/event-handlers.html

I exemplen ovan har vi bara sett namngivna funktioner användas som callbacks. Det går dock ofta lika bra att använda anonyma funktioner, dvs. lambda-uttryck. Dessa är ju till för små funktioner som bara används på ett enda ställe i koden. Det är just den situationen vi ofta har med callbacks och därför är det inte ovanligt att se lambda-uttryck användas som callbacks i Tkinter-kod.

Widgets, fortsättning

Label

En tkinter.Label-instans är kanske den enklade widgeten i Tkinter. Som namnet antyder används den som en etikett i ett GUI, t.ex. för att berätta vad som ska skrivas i ett textfält. Man kan välja om texten i etiketten ska var centrerad eller höger- eller vänsterjusterad. Standard är vänsterjusterad.

Entry

En Entry-widget är ett textfält som har en rad, något vi ofta ser i enkla formulär när vi ska skriva in ett namn eller en mailadress. Vi läser texten från ett Entry genom att anropa dess metod get(). Vi kan också ändra texten som står i ett entry genom att använda metoderna insert och delete.

Metoden insert behöver ett start-index och en sträng, t.ex. entry1.insert(0, "hej"). Metoden delete() behöver ett index eller ett start- och slutindex, t.ex. entry1.delete(0, tk.END) för att radera all text i entryn. Notera att vi använder tk.END för att referera till slutet av texten i entryn istället för -1 som vi gjort i vanliga Python. Det beror på att Tkinter inte använder vanliga Python-index utan sina egna index baserade på hur Tcl/Tk fungerar.

Exempel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import tkinter as tk

# skapa och placera en frame i ett fönster
root = tk.Tk()

def key(event):
    """Skriv ut debugutskrifter för textfält."""
    if event.widget == entry1:
        print(entry1.get())
    if event.widget == entry2:
        print("Entry 2 changed")
        
# Entry 1
entry1 = tk.Entry(root)
entry1.bind("<KeyRelease>", key)
entry1.pack()

# Entry 2
entry2 = tk.Entry(root)
entry2.bind("<KeyRelease>", key)
entry2.pack()

root.mainloop()

(entry.py)

Här redigerar vi inte texten i entry-fälten utan skriver bara ut debugutskrifter när texten ändras. När vi kör koden och skriver i något av fälten kommer det aktuella innehållet i det första fältet att skrivas ut i terminalen, medan för det andra fältet skrivs bara “Entry 2 changed” ut varje gång texten ändras.

Frame

En tkinter.Frame-widget är en behållarwidget som kan innehålla andra widgets. Den används för att gruppera widgets när man skapar en applikation eller dialoglayout. En Frame kan ha en ram och en bakgrund, och kan användas för att organisera widgets i ett fönster på ett strukturerat sätt. Vi kan skapa en Frame genom att anropa tkinter.Frame-klassen och placera den i ett fönster eller en annan Frame.

Layout i Tkinter

I Tkinter finns tre olika så kallade geometrihanterare (eng. geometry managers) som används för att hantera layout av widgets i ett fönster eller en Frame: pack, grid, och place. Varje metod har sina egna fördelar och nackdelar, och valet av metod beror på vilken typ av layout man vill skapa och hur komplex layouten är. De tidigare exemplen vi sett har använt pack som helt enkelt placerar widgets i den ordning de läggs till. Vi kommer främst att fokusera på grid här och i sista delen av temauppgiften kommer ni att arbeta med place.

grid

Layoutmetoden grid organiserar våra widgets i en matris/tabell/rutnät. Vi börjar direkt med ett exempel så att vi har något att referera till. Två saker är viktiga att notera. För det första så kommer exemplet se olika ut beroende på vilken fönstermiljö vi kör det i. Bilden här nedan är rent konceptuell och speglar inte exakt hur det kommer se ut i något system. För det andra är det bra att veta att ett Tk-fönster skapas automatiskt när vi skapar en Frame. Om vi inte vill göra några särskilda inställningar av själva fönstret behöver vi alltså bara arbeta med en Frame.

Widgets placed with a grid layout. Widgets placed with a grid layout.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import tkinter as tk

frame = tk.Frame()
frame.pack()

name_label = tk.Label(frame, text="Name")
name_label.grid(row=1, column=0, padx=10)
name_entry = tk.Entry(frame)
name_entry.grid(
    row=1, column=1,
    padx=20, pady=5
)
game_label = tk.Label(frame, text="Game")
game_label.grid(row=2, column=0, padx=10)
game_entry = tk.Entry(frame)
game_entry.grid(
    row=2, column=1,
    padx=20, pady=5
)
button = tk.Button(frame, text="Ok")
button.grid(
    row=3, column=1,
    padx=5, pady=(0, 5),
    sticky=tk.E
)
title_label = tk.Label(
    frame,
    text="The best game character ever"
)
title_label.grid(
    row=0, column=0,
    columnspan=2, pady=(10, 0)
)
frame.mainloop()

(grid.py)

En enskild widgets placering i en grid specificeras med rad- och kolumnindex, där både rader och kolumner indexeras från 0. Dessa sätts med hjälp av argumenten row och column när grid-metoden på widgeten anropas. T.ex. name_label.grid(row=1, column=0, padx=10) placerar name_label, etiketten “Name”, i cellen på rad 1 och kolumn 0. (Vi kommer till vad padx=10 gör strax.)

Vi ser i exemplet att våra widgets har placerats i en “tabell” med 4 rader och 2 kolumner. Vi behöver dock inte ange detta i förväg eftersom Tkinter automatiskt skapar så många rader och kolumner som behövs baserat på vad vi angivit när vi placerat våra widgets. Även storleken på rader och kolumner anpassas automatiskt efter innehållet i cellerna, om inget annat anges.

Vi kan lägga till extra marginaler och justeringar med hjälp av olika argument till grid-metoden, som t.ex. padx och pady som lägger till extra marginaler till vänster och höger respektive över och under en widget. Vi såg i exemplet på Tkinter-fönster i Windows 11 respektive Xfce att det var olika standardinställningar för hur mycket padding det skulle vara runt knappen i de olika systemen. Dessa inställningar kan vi alltså ändra själva med padx och pady.

Beroende på om vi sätter padx eller pady till ett enskilt värde eller till en 2-tupel så får vi lite olika beteenden. Sätter vi t.ex. pady=5 på en widget, som på rad 18 i exemplet, så läggs det till 5 pixlar extra marginal både över och under widgeten. Sätter vi pady=(10, 0), som på rad 32 i exemplet, så läggs det till 10 pixlar över men 0 pixlar under widgeten.

Vi kan också låta en widget uppta flera rader eller kolumner med hjälp av argumenten rowspan och columnspan, som t.ex. rubriken “The best game character ever” som upptar både kolumn 0 och 1 i rad 0.

Slutligen kan vi styra hur widgeten ska justeras inom sin cell med hjälp av argumentet sticky, som t.ex. knappen “Ok” som är justerad till höger i sin cell med sticky=tk.E (east). Vi kan kombinera flera riktningar med hjälp av bokstäverna N, S, E, och W för att specificera norr (upp), söder (ned), öster (höger), och väster (vänster). T.ex. sticky=tk.EW skulle justera widgeten både till höger och vänster i sin cell, vilket skulle innebära att den sträcktes ut över hela cellens bredd.

Tk-variabler

Tk-variabler används av Tkinter för att interagera med variablerna i det underliggande Tcl/Tk-systemet. Eftersom Tk egentligen körs i ett helt annat programmeringsspråk (Tcl) kan vi inte koppla vanliga Python-variabler direkt till widgets utan vi måste skapa dessa särskilda objekt för att interagera med Tcl-variablerna som används av Tk. Vi måste explicit säga vilken typ av data som ska lagras i en Tk-variabel genom att använda olika klasser som är specialiserade för olika datatyper. T.ex. finns

  • tkinter.StringVar som tar hand om strängar
  • tkinter.IntVar som tar hand om heltal
  • tkinter.DoubleVar som tar hand om flyttal
  • tkinter.BooleanVar som tar hand om sanningsvärden, representerade som 0, för falskt, och 1, för sant.

För att skapa en Tk-variabel skapar vi en instans av någon av dessa klasser. Vi kan sedan använda metoderna get() och set() för att läsa respektive skriva värden till variabeln. Det kan se ut ungefär så här:

1
2
3
4
5
import tkinter as tk
root = tk.Tk()
s = tk.StringVar()
s.set("hej")
print(s.get())

Notera att vi måste ha ett Tk-fönster skapat innan vi skapar en Tk-variabel, annars får vi ett felmeddelande. Det beror på att Tk-variabler är kopplade till Tkinter-applikationens rotobjekt och inte kan existera utan ett sådant. Vi behöver dock inte köra huvudloopen och faktiskt visa fönstret på skärmen (dvs. anropa root.mainloop()) för att skapa och använda Tk-variabler, det räcker med att vi skapat ett Tk-objekt.

Vi kan också ge den underliggande Tcl-variabeln ett namn när vi skapar Tk-variabeln genom att använda argumentet name:

1
s = tk.StringVar(name="my_string_var")

Detta kan lätt bli förvirrande eftersom vi har en Python-variabel som heter s, som refererar till en Tk-variabel, en StringVar i det här fallet, som vi använder för att interagera med en variabel i Tcl som heter my_string_var. Vissa förordar att man alltid ska namnge sina Tk-variabler för att undvika förvirring, medan andra menar att det i sig skapar lika mycket förvirring. Oavsett vilket är det viktigt att vara medveten om skillnaden mellan de olika nivåerna av variabler här.

Widgets, fortsättning

Radiobutton

Används när vi vill låta användaren välja exakt ett av flera alternativ. Skapas med

tk.Radiobutton(
    parent_container,
    text="Description of associated value",
    variable=associated_tk_variable,
    value=associated_value,
    command=some_function
)

Dessa radioknappar grupperas genom att de tilldelas samma Tk-variabel att lagra sitt värde i. Vi kan alltså ha flera grupper av radioknappar som är oberoende av varandra genom att de olika grupperna refererar till olika Tk-variabler. När en viss radioknapp i en grupp väljs så uppdateras värdet på den gemensamma Tk-variabeln till det värde som är associerat med den valda radioknappen. Alla andra radioknappar i samma grupp avmarkeras automatiskt.

Man kan koppla olika funktionsobjekt som kommandon till varje radioknapp men i exemplet nedan kopplar vi samma funktion till alla radioknappar.

Exempel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import tkinter as tk

root = tk.Tk()


def radio():
    """Skriv ut värdet hos radio-knappen."""
    print(radio_value.get())


# Tk-variabel att lagra radioknapp-gruppens värde i
radio_value = tk.StringVar()
radio_value.set("svejsan")

# knapp 1
radio_button1 = tk.Radiobutton(
    root, text="Hejsan", variable=radio_value, value="hejsan", command=radio
)
radio_button1.pack()

# knapp 2
radio_button2 = tk.Radiobutton(
    root, text="Hoppsan", variable=radio_value, value="hoppsan", command=radio
)
radio_button2.pack()

# knapp 3
radio_button3 = tk.Radiobutton(
    root, text="Svejsan", variable=radio_value, value="svejsan", command=radio
)
radio_button3.pack()

root.mainloop()

(radio.py)

Här ser vi att alla tre radioknappar refererar till samma Tk-variabel radio_value. När vi kör koden och klickar på någon av knapparna kommer värdet i radio_value att uppdateras till det värde som är associerat med den valda knappen, och funktionen radio kommer att anropas för att skriva ut det aktuella värdet i terminalen.

Checkbutton

En Checkbutton-widget, eller check box som vi ofta kallar den, representerar en variabel som kan ha två distinkta värden, t.ex. sant/falskt eller på/av. När användaren klickar på knappen växlar variabeln mellan de två värdena. Vi skapar en Checkbutton med:

tk.Checkbutton(
    parent_container,
    text="Description of associated variable",
    variable=associated_tk_variable,
    onvalue=associated_value_when_checked,
    offvalue=associated_value_when_unchecked,
    command=some_function
)

Ofta är det mest lämpliga att vi använder en tk.BooleanVar för att lagra värdet, då sätter vi onvalue=True och offvalue=False. Vi kan dock invertera True och False eller använda andra typer av Tk-variabler också, t.ex. tk.IntVar eller tk.StringVar, beroende på vad som passar bäst för vår applikation.

Mer om Tk-variabler

En Tk-variabel kan anropa ett funktionsobjekt när en interaktion med variabeln sker. Detta görs genom att använda metoden trace_add på Tk-variabeln på följande sätt:

some_tk_variable.trace_add(mode="write", callback=some_function)

I exemplet säger vi att funktionen some_function ska anropas varje gång värdet på variabeln some_tk_variable ändras. Att funktionen anropas när värdet ändras specificeras med argumentet mode="write". Andra möjliga värden för mode är "read" och "unset", som anger att funktionen ska anropas när värdet läses respektive när variabelns värde tas bort.

Funktionen som anropas måste ta tre argument: name, index, och mode. Argumentet name är namnet på den underliggande Tcl-variabeln som ändrades, index är indexet i variabeln som ändrades (används bara för list-variabler och kan oftast ignoreras), och mode är den typ av operation som orsakade callbacken att anropas (dvs. "write", "read", eller "unset"). Ofta är det bara name-argumentet som är av intresse men de andra måste vara med i funktionssignaturen ändå.

Exempel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import tkinter as tk

root = tk.Tk()

def check_changed(name, index, mode):
    """Skriv ut debug-utskrifter för checkboxar."""
    # name är namnet på den variabeln som triggade check_changed
    # i detta exempel finns två kandidater, check_val1 som har
    # namnet "check1" och check_val2 som har namnet "check2".
    if name == "check1":
        print("check 1: " + str(check_val1.get()), end='  |  ')
    elif name == "check2":
        print("check 2: " + str(check_val2.get()), end='  |  ')
        
# skapa en instans av IntVar som får namnet "check1"
check_val1 = tk.IntVar(name="check1")

# när värdet på check_val1 ändras, kör funktionen check_changed()
check_val1.trace('w', check_changed)

# skapa en instans av IntVar som får namnet "check2"
check_val2 = tk.IntVar(name="check2")

# när värdet på check_val2 ändras, kör funktionen check_changed()
check_val2.trace('w', check_changed)

# skapa två checkbuttons, koppla ihop dem med var sin variabel
checkbutton1 = tk.Checkbutton(root, variable=check_val1,
                              text="box of check I am")
checkbutton2 = tk.Checkbutton(root, variable=check_val2,
                              text="box of check I am too")

# lägg till knapparna till layouten
checkbutton1.pack()
checkbutton2.pack()

root.mainloop()

(checkbox.py)

OptionMenu

Klassen OptionMenu används generellt sett för att hantera “rullgardinsmenyer” där användaren kan välja ett alternativ från en lista med fördefinierade alternativ. Man kan alltså se OptionMenu som ett alternativ till en grupp Radiobutton-widgetar som refererar till samma Tk-variabel.

tk.OptionMenu(
    parent_container, 
    some_tk_variable, 
    value1,
    value2, 
    value3,
    ...
)

En OptionMenu är ofta mer platsbesparande än en grupp radioknappar, särskilt när det finns många alternativ att välja mellan. Den har dock begränsningen att den endast kan kopplas till en Tk-variabel av typen StringVar, vilket innebär att alla alternativ måste vara strängar.

Exempel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import tkinter as tk

root = tk.Tk()

var = tk.StringVar(root)
var.set("one") # initial value

option = tk.OptionMenu(root, var, "one", "two", "three", "four")
option.pack()

def ok():
    print("value is", var.get(), end='  |  ')

button = tk.Button(root, text="OK", command=ok)
button.pack()

tk.mainloop()

(optionmenu.py)

I exemplet ovan skapar vi en OptionMenu med fyra alternativ: “one”, “two”, “three”, och “four”. Den är kopplad till en StringVar-variabel var, som lagrar det valda alternativet. När användaren klickar på knappen “OK” skrivs det aktuella värdet i var ut i terminalen.

Quiz

Testa din förståelse av materialet med tillhörande quiz


Sidansvarig: Johan Falkenjack
Senast uppdaterad: 2025-11-04