Bud bud abbul abbuw! ”Wubba lubba dub dub”, baklänges. Tredje Dev Bloggen, om man inkluderar #0!
Om ni minns förra Dev Bloggen (som bör ha lästs före du läser detta), så byggde vi programmets grunder. Vi valde först språk; Python. Vi använde oss sedan av TkInter, för att göra ett fönster och ett kalkylark. I dag, som jag lovade i Dev Blog #1, ska jag visa hur man gör TkInter knappar. En knapp som lägger till rader, en knapp som tar bort rader, och en knapp som sparar data.
Innehåll
Boken om Cirus och Lucas
Jag skriver för närvarande en bok, vid namn Boken om Cirus och Lucas. Det är en sci-fi-roman, som jag har skrivit 23 tusen ord på hittills. Målet är någonstans runt 75 tusen ord, men jag bryr mig inte direkt om det blir 70 eller 80. Förra Dev Bloggen sa jag att jag skulle berätta mer om boken detta inlägg, så här har ni ett utdrag från kapitel ett:
Cirus hade precis sprängt Jorden, och den ende som hade följt med honom var Människo-hanen Lucas från arten Homo sapiens.
”Du, Cirus”, frågade Lucas.
”Mm.”
”Var … är jag? Och varför dricker du hela tiden?”
Den första frågan var en väldigt bra fråga att ställa, då Lucas inte visste var han befann sig för tillfället. Den andra frågan var dock en väldigt dålig fråga att fråga, då det var Cirus grej att dricka. Alla visste det.
”Du är här. Och att dricka är min grej, det vet du”, svarade Cirus.
Lucas visste mycket riktigt att det var Cirus grej att dricka, men frågade ändå. En av människosläktets många dåliga egenskaper. Det var ju givetvis Cirus grej att dricka (no shit, Lucas). För närvarandet drack han en 55-procentig, polsk vodka.
Herre Gud, Lucas! Alla vet att Cirus dricker! Det är ju i och för sig Cirus grej att dricka, Lucas. Nej, men det är i alla fall en seriös-ish-komedi-i-bland-bok. Det är en blandning av alla möjliga genrer och ideér.
WRITe
Tillbaka till ämnet: knappar. Detta är lite av en tutorial på hur man skapar TkInter knappar. Koden är dock kommenterad på engelska — fast allt annat är på svenska! Du kommer dessutom att lära dig ytterst lite om lambda funktioner i Python 3.x.
Korrigeringar
Först måste jag dock ändra lite på koden. Detta är en del av den gamla koden, som vi blev klara med förra Dev Bloggen:
33 34 35 36 37 38 39 | def addEntry( self, row: int, column: int, # Default validation command: True if int. validationCommand=(lambda x: True if x is int else False), ): |
Rad 38 (den nedre, markerade) har ingen anledning att ha en default-parameter, och den fungerar dessutom inte. lambda x: True if x is int else False betyder True om inputen är int… Förutom att den inte fungerar. x är en str, så det kommer alltid vara False. ( lambda-guider finns här, här, och till och med här! (Om du har tur finns det även här…)) Även om det hade fungerat, vilket den inte gör, så finns SimpleTableInput.Validate()-metoden:
48 49 50 51 52 53 54 55 56 57 58 59 60 | def validate(self, P): """ Perform input validation. Allow only an empty value, or a value that can be converted to an int. """ try: int(P) except ValueError: return False return True |
Som alltid har varit där … och dessutom använts. Jag märkte inte att det var fel, då det som var fel inte användes. Yikes.
Ja, ja. Sånt händer. Den reviderade koden finns nedan:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | import tkinter import threading ENTRIES = {} # Entry box dictionary, containing all the entry boxes ROWS = 5 COLUMNS = 3 class SimpleTableInput(threading.Thread, tkinter.Frame): def __init__(self, parent): tkinter.Frame.__init__(self, parent) # Validation command; command to validate entry box input (only # numbers are allowed) global validationCommand validationCommand = (self.register(self.validate), "%P") # Create integer-only entry boxes for row in range(ROWS): for column in range(COLUMNS): self.addEntry(row, column, validationCommand) # Create column headers headers = ["Date", "Word Count", "Word Increase"] for column in range(COLUMNS): index = (0, column) entry = ENTRIES[index] header = tkinter.StringVar(parent, value=headers[column]) entry.config(textvariable=header) entry.config(state="readonly") def addEntry(self, row: int, column: int, validationCommand): entryBox = tkinter.Entry( self, validate="key", validatecommand=validationCommand ) entryBox.grid(row=row, column=column, stick="nsew") index = (row, column) ENTRIES[index] = entryBox def validate(self, P): """ Perform input validation. Allow only an empty value, or a value that can be converted to an int. """ if P == "": return True try: int(P) except ValueError: return False return True class App(tkinter.Frame): def __init__(self, parent): super().__init__() tkinter.Frame.__init__(self, parent) self.table = SimpleTableInput(self) self.table.grid(row=1, column=1, sticky="nsew") root = tkinter.Tk() root.title("WRITe") root.resizable(width=False, height=False) App(root).pack(side="top", fill="both", expand=True) root.mainloop() |
Jag ber om ursäkt för detta. Dags för knappar!
Hur man skapar TkInter knappar!
Knappar. Det finns många sådana. Det finns stora, små, gula, lila, små och gula. Till och med små, gula och lila knappar finns!!! De vi kommer att skapa är dock inte gula eller lila. En knappar kommer att vara grön, en annan röd, och en vit.
Den gröna knappen kommer att lägga till rader, den röda ta bort rader, och den vita spar data.
Först och främst ska jag visa hur man skapar TkInter knappar. Vi använder oss av tkinter.Button-klassen för det.
1 2 3 4 5 6 | button = tkinter.Button( text="Exempel", command=exampelFunktion, bg="green", ) button.pack() |
Detta är en TkInter knapp, med texten ”Exempel”. När man trycker på den utförs exampelFunktion()-funktionen, som kan vara vad man vill. Den har även en bakgrundsfärg, grön, som defineras med bg-attributen
Först skapar vi våra knappar i App():s __init__(), och tkinter.pack():ar dem, så att de syns i mitten av skärmen:
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | class App(tkinter.Frame): def __init__(self, parent): super().__init__() tkinter.Frame.__init__(self, parent) self.table = SimpleTableInput(self) self.table.grid(row=1, column=1, sticky="nsew") saveButton = tkinter.Button( text="Save Data", ) saveButton.pack() addRowButton = tkinter.Button( text="Add Row", # Green bg="#2fba54", ) addRowButton.pack() deleteRowButton = tkinter.Button( text="Delete Row", # Red bg="#ff5242", ) deleteRowButton.pack() |
Då får vi detta:
Snyggt? Nej. Fult? Ja. Jag vet inte vad ni tycker, men det hade sett bättre ut i fall knapparna låg sida vid sida. Detta gör vi med tkinter.Frame() och tkinter.grid(). tkinter.grid() rätar upp widgets (det vill säga TkInter-objekt). Kalkylarket (rutorna, alltså), exempelvis, använder sig av tkinter.grid() — så att du vet hur det ser ut. Vi kommer att utnyttja ett sådant för att våra knappar ska se snygga ut. Först och främst måste vi skapa en tkinter.Frame() i App(), som vi kan lägga tktiner.grid():en i, som vi sedan kommer lägga knapparna i:
1 2 | buttonFrame = tkinter.Frame(root) buttonFrame.pack() |
Vi tkinter.pack():ar den, så att den syns i fönstret över huvud taget. Grejen är den att, TkInter har tre inbyggda layouthanteringsmetoder: tkinter.grid(), tkinter.pack() och tkinter.place(). Man måste använda någon av dem för att en widget ska synas, annars är den inte med på skärmen. Nu har vi vår tkinter.Frame(). I denna måste vi lägga ett tkinter.grid(), som innehåller knapparna. Nu har vi detta:
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | class App(tkinter.Frame): def __init__(self, parent): super().__init__() tkinter.Frame.__init__(self, parent) self.table = SimpleTableInput(self) self.table.grid(row=1, column=1, sticky="nsew") buttonFrame = tkinter.Frame(root) buttonFrame.pack() saveButton = tkinter.Button( text="Save Data", ) saveButton.pack() addRowButton = tkinter.Button( text="Add Row", # Green bg="#2fba54", ) addRowButton.pack() deleteRowButton = tkinter.Button( text="Delete Row", # Red bg="#ff5242", ) deleteRowButton.pack() |
De markerade raderna är knapparna, och raderna mellan knapparna tkinter.pack():ar knapparna. För att lägga in knapparna i buttonFrame, måste vi lägga till buttonFrame som knapparnas parent:s. Vilket vi enkelt gör genom att sätta buttonFrame som master. self är master som default, men nu så vill vi ju inte det … för då blir jag ledsen ;(… Koden nu:
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | class App(tkinter.Frame): def __init__(self, parent): super().__init__() tkinter.Frame.__init__(self, parent) self.table = SimpleTableInput(self) self.table.grid(row=2, column=1, sticky="nsew") buttonFrame = tkinter.Frame(root) buttonFrame.pack() saveButton = tkinter.Button( buttonFrame, text="Save Data", ) saveButton.grid(row=0, column=0) addRowButton = tkinter.Button( buttonFrame, text="Add Row", # Green bg="#2fba54", ) addRowButton.grid(row=0, column=1) deleteRowButton = tkinter.Button( buttonFrame, text="Delete Row", # Red bg="#ff5242", ) deleteRowButton.grid(row=0, column=2) |
Om vi nu kör koden, så får vi detta:
Bra, men vi har ett steg kvar. Som ni kan se, så är knapparna inte lika stora, vilket resulterar i att de inte är symmetriskt upplagda. Så, vi ändrar knapparnas width-attribut till ett värde. Detta värde, buttonWidth, sätter jag på 9. Det markerade är ändringarna:
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | class App(tkinter.Frame): def __init__(self, parent): super().__init__() tkinter.Frame.__init__(self, parent) self.table = SimpleTableInput(self) self.table.grid(row=2, column=1, sticky="nsew") buttonFrame = tkinter.Frame(root) buttonFrame.pack() buttonWidth = 9 saveButton = tkinter.Button( buttonFrame, text="Save Data", width=buttonWidth, ) saveButton.grid(row=0, column=0) addRowButton = tkinter.Button( buttonFrame, text="Add Row", # Green bg="#2fba54", width=buttonWidth, ) addRowButton.grid(row=0, column=1) deleteRowButton = tkinter.Button( buttonFrame, text="Delete Row", # Red bg="#ff5242", width=buttonWidth, ) deleteRowButton.grid(row=0, column=2) |
Perfekt! Det var en pärs. Det var inte så komplicerat, men det var långdraget! Men, som du kanske märkt, så händer inget när man trycker på knapparna. Detta beror på att de inte har några funktioner länkade till dem.
Spara knappen
För att en knapp ska vara länkad till en funktion (eller rättare sagt en metod, då metoden existerar inuti SimpleTableInput(), en klass), så måste man först ha en metod. Spara knappen ska vara länkad till en metod som sparar data. För detta kommer vi använda en simpel .csv-fil. Jag kom fram till detta:
58 59 60 61 62 63 64 65 66 67 68 69 70 71 | def save(self): string = "" data = [] for row in range(ROWS): currentRow = [] for column in range(COLUMNS): currentRow.append(ENTRIES[(row, column)].get()) data.append(currentRow) del data[0] # Remove headers from data for row in data: string += ";".join(row) + "\n" with open("data.csv", "w+") as file: file.write(string) |
Det är relativt simpel kod, som går igenom alla rader. Den går sedan igenom alla dessa raders kolumner, och gör en list med radernas innehåll. Efter den har gått igenom alla rader, skriver den dem rad för rad till data.csv. Men, då vi ändå vet vilka rubrikerna är (på rad 1), så exkluderar vi rad 1 från filen, då de tar upp onödig plats. Eller rättare sagt, de är onödiga. Den platsen är nödig; det är upptagandet som är onödigt.
Men, vi behöver även kunna ladda denna sparfilen. Detta görs lätt, genom att lägga till inladdningen i SimpleTableInput.__init__(), så att den körs när programmet startas. Sådan kod hade kunnat se ut så här:
12 13 14 15 16 17 18 19 20 21 22 23 | global ROWS tkinter.Frame.__init__(self, parent) # Load save file (data.csv) try: with open("data.csv", "r") as file: fileContents = file.readlines() rowCount = len(fileContents) if rowCount > 0: ROWS = rowCount + 1 except FileNotFoundError: fileContents = [] |
Det är relativt simpel kod, som går igenom alla rader. Den går sedan igenom alla dess raders kolumner, och gör en list med radernas innehåll. Efter den har gått igenom alla rader, anger den dem rad för rad i kalkylarket.
Sedan är det dags att ange command i Spara knappen:
104 105 106 107 108 109 | saveButton = tkinter.Button( buttonFrame, text="Save Data", command=self.table.save, width=buttonWidth, ) |
command är nu self.table.save().
Schkablam!
Ny rad knappen
För att skapa en ny rad ska vi göra en metod i SimpleTableInpt(). En metod, som använder sig av SimpleTableInput.addEntry()-metoden. Vad funktionen ska göra är ohyggligt simpelt: den ska skapa en ny tkinter.Entry(), COLUMN (3) gånger. Sedan ökar den globala ROWS med 1, det vill säga antalet rader ökar med 1.
61 62 63 64 65 66 67 | def addRow(self): global ROWS for column in range(COLUMNS): self.addEntry(ROWS, column, validationCommand) ROWS += 1 |
Och addRowButton:s command-attribut:
118 119 120 121 122 123 124 125 | addRowButton = tkinter.Button( buttonFrame, text="Add Row", command=self.table.addRow, # Green bg="#2fba54", width=buttonWidth, ) |
Det var lätt!
Radera rad knappen
Att radera en rad är som att lägga till en rad, fast baklänges. Alltså, i stället för att skapa en tkinter.Entry(), så använder vi tkinter.Entry():s destroy()-metod. Vi gör detta i en ny SimpleTableInput()-metod:
69 70 71 72 73 74 75 | def deleteRow(self): global ROWS if ROWS > 1: # Don't delete the headers for column in range(COLUMNS): index = (ROWS - 1, column) ENTRIES[index].destroy() |
Perfekt!
Avslut
Vi har lärt oss hur man skapar TkInter knappar, och nu har vi gjort ett sparsystem, samt knappar som lägger till och raderar rader!
Här är den slutgiltiga koden:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | import tkinter import threading ENTRIES = {} # Entry box dictionary, containing all the entry boxes ROWS = 5 COLUMNS = 3 class SimpleTableInput(threading.Thread, tkinter.Frame): def __init__(self, parent): global ROWS tkinter.Frame.__init__(self, parent) # Load save file (data.csv) try: with open("data.csv", "r") as file: fileContents = file.readlines() rowCount = len(fileContents) if rowCount > 0: ROWS = rowCount + 1 except FileNotFoundError: fileContents = [] # Validation command; command to validate entry box input (only # numbers are allowed) global validationCommand validationCommand = (self.register(self.validate), "%P") # Create integer-only entry boxes for row in range(ROWS): for column in range(COLUMNS): self.addEntry(row, column, validationCommand) # Re-create data from save file (data.csv) currentRow = 0 for row in fileContents: currentRow += 1 row = row.split(";") for column in range(COLUMNS): ENTRIES[(currentRow, column)].insert(0, row[column]) # Create column headers headers = ["Date", "Word Count", "Word Increase"] for column in range(COLUMNS): index = (0, column) entry = ENTRIES[index] header = tkinter.StringVar(parent, value=headers[column]) entry.config(textvariable=header) entry.config(state="readonly") def addEntry(self, row: int, column: int, validationCommand): entryBox = tkinter.Entry( self, validate="key", validatecommand=validationCommand ) entryBox.grid(row=row, column=column, stick="nsew") index = (row, column) ENTRIES[index] = entryBox def addRow(self): global ROWS for column in range(COLUMNS): self.addEntry(ROWS, column, validationCommand) ROWS += 1 def deleteRow(self): global ROWS if ROWS > 1: # Don't delete the headers for column in range(COLUMNS): index = (ROWS - 1, column) ENTRIES[index].destroy() ROWS -= 1 def save(self): string = "" data = [] for row in range(ROWS): currentRow = [] for column in range(COLUMNS): currentRow.append(ENTRIES[(row, column)].get()) data.append(currentRow) del data[0] # Remove headers from data for row in data: string += ";".join(row) + "\n" with open("data.csv", "w+") as file: file.write(string) def validate(self, P): """ Perform input validation. Allow only an empty value, or a value that can be converted to an int. """ if P == "": return True try: int(P) except ValueError: return False return True class App(tkinter.Frame): def __init__(self, parent): super().__init__() tkinter.Frame.__init__(self, parent) self.table = SimpleTableInput(self) self.table.grid(row=2, column=1, sticky="nsew") buttonFrame = tkinter.Frame(root) buttonFrame.pack() buttonWidth = 9 saveButton = tkinter.Button( buttonFrame, text="Save Data", command=self.table.save, width=buttonWidth, ) saveButton.grid(row=0, column=0) addRowButton = tkinter.Button( buttonFrame, text="Add Row", command=self.table.addRow, # Green bg="#2fba54", width=buttonWidth, ) addRowButton.grid(row=0, column=1) deleteRowButton = tkinter.Button( buttonFrame, text="Delete Row", command=self.table.deleteRow, # Red bg="#ff5242", width=buttonWidth, ) deleteRowButton.grid(row=0, column=2) root = tkinter.Tk() root.title("WRITe") root.resizable(width=False, height=False) App(root).pack(side="top", fill="both", expand=True) root.mainloop() |
Jag är i alla fall nöjd. Nästa gång kommer vi att implementera datumsystemet, det vill säga en inbyggd kalender … och sånt. Glöm inte att prenumerera på vårt nyhetsbrev, så att du aldrig missar ett inlägg! Glöm dessutom inte att all kod finns på min GitHub!
Tobbe out.