Basic tkinter usage for Python 3
I am offering a job online and was flooded with applicants. I needed a way to cut and paste the data and then save it for later processing.
Lessons in this Tutorial
- tkinter basics
- Classes
- Basic flow control in loops
- Python Dictionaries
- List Splicing
- json writes
- Regex
- Field validation
- Grid layout in tkinter
Tkinter or tkinter?
Among the most popular gui frameworks for python such as Tkinter, wxWidgets, Qt, Gtk+, Kivy, FLTK, OpenGL Tkinter will prove to be the easiest to learn and license. Once you dig in a bit, you’ll see also that there are tutorials for both Tkinter and tkinter. The difference is Tkinter is for python 2 and tkinter is for python 3. Both are just a wrapper for Tk, a very old, but universal gui framework. I chose to learn and use tkinter because it’s easy enough to learn in an afternoon (specifically this is 1 afternoon of playing with tkinter).
tkinter Layout Systems
When you design your GUI you will need to pick either “pack” or “grid” layouts. I found that grid is easier and more precise. Pack tends to jumble things around, especially as you change resolutions. This tutorial will specifically demonstrate the grid layout system and ignore pack.
- tkinter Grid Layout
- Image 1990 table layout. Grid layout merges columns and rows just like old school HTML. I like it because it’s super simple and reliable.
- tkinter Pack Layout
- I didn’t find an advantage to pack except you don’t have to think much about the simple layouts. For some GUI apps, this might be plenty. You have about as much control as trying to organize a suitcase – it works great until you go through TSA and you have to reorganize it. I hate playing with pack, but not near as much as I hate TSA!
More about tkinter
Instead of rewriting clear, but dated docs on tkinter that will no doubt change by the time you read this, I’ll link to the python 3 tkinter docs. I’m sure you’ll find them as obfuscated as I did.
Form Parsing
Back to “why” I wrote this. I have an email form that collects data. It looks something like this in my email client:
I’ll be honest, I hate cut and paste anything. Moreover I hate typing things into a form. I needed to track their data though and process it after they applied for a job. I thought it would be a great exercise and save time and frustration to parse out the data I needed using python.
You’ll find that if you can easily manipulate data from spreadsheets, cut and paste, emails, and arbitrary inputs – people will think you are much smarter than you actually are. Form parsing (especially with a GUI), happens to be one of those magically creative things that people like.
Basic Steps to GUI Design
- Decide on the Scope/Purpose
- Sketch out the Inputs/Outputs
- Consider ways it needs error checking
- Code it and document as you go
- Fix up your code as required
Anyway, that’s how you probably should do it. More often it looks like this:
- Waste time coding a GUI first
- spend extra time on the widgets of features you won’t ever use
- Decide what you want the GUI to do
- Rewrite it to account for the errors you didn’t think about
- Pretend you know how to code
- Go back after a few months and forget why you did things, then decide you should of documented it
For illustration purpose, assume I did the first way to create this script!
Error Checking
There isn’t much fantastic about the specific error checking I’m using, but it is a good illustration of how to validate form fields. (I didn’t need to validate these, but wanted to learn more about tkinter, so I added these tkinter field checks).
- Name (must be longer than 5 chars)
- Email (extremely basic regex, not a full email validator, just helps with typos)
- PHone (must be 10 digits, strips and rebuilds to xxx-xxx-xxxx)
Knowing how to tie in a regex makes it easy enough to find other regex checks you might want for other apps (such as validating an IP address, network, port numbers, etc)
Just Gimme the Form Parser Script!
If the comments don’t help enough, feel free to ask questions. There are probably more “pythonic” ways of doing what I did below, but it works for my one afternoon of playing with tkinter.
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
''' Form to JSON Parser ''' import tkinter as tk import tkinter.messagebox as mb import json import re # true email validation probably not going to happen # but this at least checks the domain for basic stuff # decided not to use it and instead use dumb regex #from validate_email import validate_email # general appearances opts1 = { 'ipadx': 5, 'ipady': 5 , 'sticky': 'nswe' } # centered opts2 = { 'ipadx': 5, 'ipady': 5 , 'sticky': 'e' } # right justified opts3 = { 'ipadx': 5, 'ipady': 5 , 'sticky': 'w' } # left justified # other options to play with: # -column, -columnspan, -in, -ipadx, -ipady, -padx, -pady, -row, -rowspan, or -sticky bgcolor = '#ECECEC' white = '#FFFFFF' class App(tk.Tk): def __init__(self): super().__init__() self.title("Form to JSON Parser") # corners for spacing/layout self.label_nw = tk.Label(self, text=" ", bg=bgcolor) self.label_nw.grid(row=0, column=0, **opts1) self.label_ne = tk.Label(self, text=" ", bg=bgcolor) self.label_ne.grid(row=0, column=9, **opts1) # first header self.label_a = tk.Label(self, text="Paste Form Info Below", bg=bgcolor) self.label_a.grid(row=0, column=1, columnspan=8, **opts1) # variables we are using/looking for self.name = tk.StringVar() self.mail = tk.StringVar() self.cell = tk.StringVar() self.text = tk.Text(self, width=50, height=10, bg=white) # go ahead and focus on the paste to field self.text.focus_set() # text box placed after defined self.text.grid(row=1, column=1, columnspan=8, rowspan=5, **opts1) # second header self.label_b = tk.Label(self, text="Parse or Save Data as JSON", bg=bgcolor) self.label_b.grid(row=7, column=1, columnspan=8, **opts1) # manual form tk.Label(self, text="Name").grid(row=8, column=1, columnspan=3, **opts2) tk.Entry(self, textvariable=self.name).grid(row=8, column=4, columnspan=5, **opts3) tk.Label(self, text="Email").grid(row=9, column=1, columnspan=3, **opts2) tk.Entry(self, textvariable=self.mail).grid(row=9, column=4, columnspan=5, **opts3) tk.Label(self, text="Cell Phone").grid(row=10, column=1, columnspan=3, **opts2) tk.Entry(self, textvariable=self.cell).grid(row=10, column=4, columnspan=5, **opts3) # third header/blank self.label_c = tk.Label(self, text=" ", bg=bgcolor) self.label_c.grid(row=11, column=1, columnspan=8, **opts1) # buttons self.btn_clear = tk.Button(self, text="Clear",command=self.clear_text, bg=bgcolor) self.btn_clear.grid(row=12,column=1, columnspan=2, **opts1) self.btn_parse = tk.Button(self, text="Parse",command=self.parse_text, bg=bgcolor) self.btn_parse.grid(row=12,column=3, columnspan=2, **opts1) self.btn_print = tk.Button(self, text="Print",command=self.print_selection, bg=bgcolor) self.btn_print.grid(row=12,column=5, columnspan=2, **opts1) self.btn_save = tk.Button(self, text="Export",command=self.save_selection, bg=bgcolor) self.btn_save.grid(row=12, column=7, columnspan=2, **opts1) # final header self.label_b = tk.Label(self, text=" ", bg=bgcolor) self.label_b.grid(row=13, column=1, columnspan=8, **opts1) # Functions for buttons def clear_text(self): # clear out the trash self.text.delete("1.0", tk.END) self.name.set("") self.mail.set("") self.cell.set("") # load sample data sample = ''' Name John Smith Email jsmith@gmail.com Cell Phone 9725551212 Applying For: Full Time ''' # how to load data into a textarea self.text.insert(tk.INSERT, sample) print("Sample data loaded") def parse_text(self): # parses big block and fills out fields with data we weanted # text widget adds a new line, so check if right before that there is nothing # couldn't get this to work either. todo if len(self.text.get("1.0", "end-1c")) == 0: mb.showinfo("Information", "No Data to Parse. Click 'Clear' for Sample Data") print("Try clicking 'CLEAR', to get sample data") else: # load up our data data = self.text.get("1.0", tk.END) lines = data.split("\n") # skip through weird sometimes blank lines iter = 0 for line in lines: if (len(line) < 3): iter = iter + 1 continue else: break # ok we think this is the right stuff name = lines[iter + 1] mail = lines[iter + 3] cell = lines[iter + 5] self.name.set(name) if (len(name) < 5): mb.showinfo("Information", "Name seems TOO SHORT. Verify it") # ask the host SMTP if the email exists, but don't send email #if not validate_email(mail,verify=False): # kinda does nothing #if not validate_email(mail,verify=True): # adds 2 second delay, not 100% anyway if not re.match(r"[^@]+@[^@]+\.[^@]+", mail): # this one kinda works, but allows some weird mb.showinfo("Information", "Email Seems WRONG. Verify it") else: self.mail.set(mail) # couldn't get email validaiton or regexes to work! todo #self.mail.set(mail) # set phone properly though phone = re.sub('\D', '', cell) # digits only, erase the rest # rebuild number to our spec, assuming it's right # will error here if it's wrong on CLI and later via gui # index out of bounds if it's too short, didn't want to error check 2x a = phone[0:3] b = phone[3:6] c = phone[6:10] # assume it's right, rebuild rebuilt = a + "-" + b + "-" + c # now see if it still is likely a phone number or not if (len(phone) == 10): print(rebuilt + ": parse successful") else: mb.showinfo("Information", "Phone numer (" + rebuilt + ") is WRONG for some reason, verify it") # it's probably right, or you've been warned at least self.cell.set(rebuilt) def print_selection(self): # dumps selection under mouse, or defaults to form fields to screen # put in place for error checking, not really needed for any function # kept for demonsttraion purposes selection = self.text.tag_ranges(tk.SEL) # IF you have something selected if selection: content = self.text.get(*selection) print(content) # ELSE just print the parse else: content = self.name.get() + "\n" + self.mail.get() + "\n" + self.cell.get() print(content) def save_selection(self): # dumps captured data to json file # if error detected, flag will set to 0 flag = 1 name = self.name.get() mail = self.mail.get() cell = self.cell.get() if (len(name) < 3): mb.showinfo("Information", "Name Field Empty?") flag = 0 if (len(mail) < 3): mb.showinfo("Information", "Mail Field Empty?") flag = 0 if (len(cell) < 3): mb.showinfo("Information", "Cell Field Empty?") flag = 0 if (flag == 1): data = { "name": self.name.get(), "mail": self.mail.get(), "cell": self.cell.get() } # build our json file name file = self.cell.get() + ".json" # dump to json, clobber existing file of same name with open(file, 'w') as outfile: json.dump(data, outfile) # explain to user what happened print(file + " updated") else: # or don't mb.showinfo("Information", "FILE NOT SAVED, FIELDS MIGHT BE EMPTY") # ok, do the stuff above if __name__ == "__main__": app = App() app.mainloop() |