A Simple GUI and Command-line Python Program with a file browser! and an exit button!! zomg!!!
I needed to make a simple GUI for translating comma-separated value
input into a reStructuredText table, and ended up writing a simple
Python program that might be a useful example for you of Tkinter,
tkFileDialog
, and a combination command line and GUI program.
What is this for?
I needed a simple program to convert CSV files into reStructuredText tables for a group of people who write in RST and don't want to be bothered to create RST tables by hand (which really is a pain unless you’re using Emacs and its text-based tables package).
I started with the command-line version to get the functionality then added the GUI elements.
def write_table(outputfile, table_contents): """ Write out the .rst file with the table in it """ with open(outputfile, "wb") as output_file: try: output_file.write(tabulate(table_contents, tablefmt="grid", headers="firstrow")) except: return False return True def command_line(args): """ Run the command-line version """ if args.output is None: args.output = get_output_filename(args.input) table_contents = read_csv(args.input) if write_table(args.output, table_contents): print "rst table is in file `{}'".format(args.output) else: print "Writing file `{}' did not succeed.".format(args.output) def read_csv(filename): """ Read the CSV file This fails pretty silently on any exception at all """ try: with open(filename, 'rb') as csvfile: dialect = csv.Sniffer().sniff(csvfile.read(1024)) csvfile.seek(0) reader = csv.reader(csvfile, dialect) r = [] for row in reader: r.append(row) except: return None return r def get_parser(): """ The argument parser of the command-line version """ parser = argparse.ArgumentParser(description=('convert csv to rst table')) parser.add_argument('--input', '-F', help='name of the intput file') parser.add_argument('--output', '-O', help=("name of the output file; " + "defaults to <inputfilename>.rst")) return parser if __name__ == "__main__": """ Run as a stand-alone script """ parser = get_parser() # Start the command-line argument parsing args = parser.parse_args() # Read the command-line arguments if args.input: # If there is an argument, command_line(args) # run the command-line version else: gui() # otherwise run the GUI version
Tkinter
There are many resources for Python’s Tk integration library (Tkinter) on the Internet, but the basics are:
- create the widgets (buttons, labels, text entry fields, etc.) you want, and use the pack() function to get them arranged.
- add functions that are called when the buttons are pressed; these are called callback functions because once the loop is started in the next step, the only way out of the loop is to take a brief detour from the loop to the function associated with the widget (typically buttons)
- call the function
mainloop()
As a side note, this is how most graphical user interfaces work; in Microsoft Windows this is called
GetMessage()
, in Mac OS X it isCFRunLoopRun()
, in Android apps it isandroid.os.Looper
.
With a text entry field and a Go button to process the file, this little program could be considered complete.
def gui(): """make the GUI version of this command that is run if no options are provided on the command line""" def button_go_callback(): """ what to do when the "Go" button is pressed """ input_file = entry.get() if input_file.rsplit(".")[-1] != "csv": statusText.set("Filename must end in `.csv'") message.configure(fg="red") return else: table_contents = read_csv(input_file) if table_contents is None: statusText.set("Error reading file `{}'".format(input_file)) message.configure(fg="red") return output_file = get_output_filename(input_file) if write_table(output_file, table_contents): statusText.set("Output is in {}".format(output_file)) message.configure(fg="black") else: statusText.set("Writing file " "`{}' did not succeed".format(output_file)) message.configure(fg="red") root = Tk() frame = Frame(root) frame.pack() statusText = StringVar(root) statusText.set("Enter CSV filename, " "then press the Go button") label = Label(root, text="CSV file: ") label.pack() entry = Entry(root, width=50) entry.pack() button_go = Button(root, text="Go", command=button_go_callback) button_go.pack() message = Label(root, textvariable=statusText) message.pack() mainloop()
This code has two parts:
- The main function that creates a text entry field and a Go
button then calls
mainLoop()
- A sub-function (or nested function or inner function) that calls the same functions as the command-line version of the program and updates the status line in the GUI as appropriate.
Using this, however, means you have to know the path to the CSV file and type it in to the text entry box. That is not how most modern applications work, usually there is a file browser…
tkFileDialog
The tkFileDialog
presents an OS-native file browsing and selection
dialog. This script takes the selected file name and populates the
text entry box for the CSV file with the full path to the selected
file. The nicest part of this is that it only takes a few lines of
Python to do this.
We add a Browse button and another nested function to be its
callback function. The callback function simply lets
tkFileDialog.askopenfilename()
give us the name of the file the
user selected from the file browser and then fills in the entry
field (cleverly named entry
in our program) with the full path
and file name.
When the Browse button is pressed the browse_button_callback
function is called because the button was created with:
button_browse = Button(root, text="Browse", command=button_browse_callback)
and the file name entry field was created with:
entry = Entry(root, width=50)
then the filename comes from the askopenfilename
function in
tkFileDialog
and is used to populate the text entry field.
def button_browse_callback(): """ What to do when the Browse button is pressed """ filename = tkFileDialog.askopenfilename() entry.delete(0, END) entry.insert(0, filename)
What do we have now?
Now we have pretty simply Python program that:
can be run from the command line using standard command line options and offering a help menu when the command line option is
--help
$ ./makersttable.py --help usage: makersttable.py [-h] [--input INPUT] [--output OUTPUT] convert csv to rst table optional arguments: -h, --help show this help message and exit --input INPUT, -F INPUT name of the intput file --output OUTPUT, -O OUTPUT name of the output file; defaults to <inputfilename>.rst
- in the absence of command line options, a graphical application is started that allows the user to type in a file name or select one from a file browser, then click Go
- either option results in the creation of a file with a reStructuredText table in it based on the contents of a file with comma-separated values (CSV) in it
With a simple input file formatted like this:
Header Col 1, Header Col 2, Header Col3 This is the first value, This is the second value, This is the third value Red, Blue, Green 42, 10, 1
the Python program can be run from the command line like:
$ ./makersttable.py -F test.csv rst table is in file `test.rst'
and the resulting test.rst
file looks like:
+-------------------------+--------------------------+-------------------------+ | Header Col 1 | Header Col 2 | Header Col3 | +=========================+==========================+=========================+ | This is the first value | This is the second value | This is the third value | +-------------------------+--------------------------+-------------------------+ | Red | Blue | Green | +-------------------------+--------------------------+-------------------------+ | 42 | 10 | 1 | +-------------------------+--------------------------+-------------------------+
Or, the Python program can be run like:
$ ./makersttable.py
and you’ll get a graphical interface that looks like:
Figure 1: Initial GUI Screen
Pressing the Browse button will present a file dialog:
Figure 2: The file browsing dialog box
Selecting a file will populate the entry field:
Figure 3: The selected filename shown in the entry field
And pressing the Go button converts the file; the path to which is in the status message area of the GUI:
Figure 4: The path to rST file is shown in the status area
The Whole Python Program
#!/usr/bin/env python """Convert CSV to reStructuredText tables A command-line and PythonTk GUI program to do a simple conversion from CSV files to reStructuredText tables A. Caird (acaird@gmail.com) 2016 """ import argparse import csv from tabulate import tabulate import tkFileDialog from Tkinter import * def get_output_filename(input_file_name): """ replace the suffix of the file with .rst """ return input_file_name.rpartition(".")[0] + ".rst" def gui(): """make the GUI version of this command that is run if no options are provided on the command line""" def button_go_callback(): """ what to do when the "Go" button is pressed """ input_file = entry.get() if input_file.rsplit(".")[-1] != "csv": statusText.set("Filename must end in `.csv'") message.configure(fg="red") return else: table_contents = read_csv(input_file) if table_contents is None: statusText.set("Error reading file `{}'".format(input_file)) message.configure(fg="red") return output_file = get_output_filename(input_file) if write_table(output_file, table_contents): statusText.set("Output is in {}".format(output_file)) message.configure(fg="black") else: statusText.set("Writing file " "`{}' did not succeed".format(output_file)) message.configure(fg="red") def button_browse_callback(): """ What to do when the Browse button is pressed """ filename = tkFileDialog.askopenfilename() entry.delete(0, END) entry.insert(0, filename) root = Tk() frame = Frame(root) frame.pack() statusText = StringVar(root) statusText.set("Press Browse button or enter CSV filename, " "then press the Go button") label = Label(root, text="CSV file: ") label.pack() entry = Entry(root, width=50) entry.pack() separator = Frame(root, height=2, bd=1, relief=SUNKEN) separator.pack(fill=X, padx=5, pady=5) button_go = Button(root, text="Go", command=button_go_callback) button_browse = Button(root, text="Browse", command=button_browse_callback) button_exit = Button(root, text="Exit", command=sys.exit) button_go.pack() button_browse.pack() button_exit.pack() separator = Frame(root, height=2, bd=1, relief=SUNKEN) separator.pack(fill=X, padx=5, pady=5) message = Label(root, textvariable=statusText) message.pack() mainloop() def write_table(outputfile, table_contents): """ Write out the .rst file with the table in it """ with open(outputfile, "wb") as output_file: try: output_file.write(tabulate(table_contents, tablefmt="grid", headers="firstrow")) except: return False return True def command_line(args): """ Run the command-line version """ if args.output is None: args.output = get_output_filename(args.input) table_contents = read_csv(args.input) if write_table(args.output, table_contents): print "rst table is in file `{}'".format(args.output) else: print "Writing file `{}' did not succeed.".format(args.output) def read_csv(filename): """ Read the CSV file This fails pretty silently on any exception at all """ try: with open(filename, 'rb') as csvfile: dialect = csv.Sniffer().sniff(csvfile.read(1024)) csvfile.seek(0) reader = csv.reader(csvfile, dialect) r = [] for row in reader: r.append(row) except: return None return r def get_parser(): """ The argument parser of the command-line version """ parser = argparse.ArgumentParser(description=('convert csv to rst table')) parser.add_argument('--input', '-F', help='name of the intput file') parser.add_argument('--output', '-O', help=("name of the output file; " + "defaults to <inputfilename>.rst")) return parser if __name__ == "__main__": """ Run as a stand-alone script """ parser = get_parser() # Start the command-line argument parsing args = parser.parse_args() # Read the command-line arguments if args.input: # If there is an argument, command_line(args) # run the command-line version else: gui() # otherwise run the GUI version