Information Technology Grimoire

Version .0.0.1

IT Notes from various projects because I forget, and hopefully they help you too.

Basic tkinter usage for Python3

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: Sample Form Data

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.

'''
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()

Example of Usage

Last updated on 12 Feb 2020
Published on 12 Feb 2020