Create a GUI#
Goals
This exercise features the creation of a Graphical User Interfaces (GUIs) based on the instructions in this eBook.
Requirements
Python libraries: tkinter, NumPy, and Pandas. Read and understand the creation of Graphical User Interfaces (GUIs).
Accomplish the sediment transport exercise.
Get ready by cloning the exercise repository:
git clone https://github.com/Ecohydraulics/Exercise-gui.git
Before getting started with the exercise, make sure to copy the code from the Python sediment transport exercise into the sediment_transport
sub-folder of the GUI exercise repository (i.e., overwrite bedload.py, fun.py, grains.py, hec.py, .py, main.py, and mpm.py with your code). If the file names are different from the default names used in the sediment transport exercise, adapt the __init__.py
file in the sediment_transport
sub-folder. Thus, we created a module called sediment_transport
, where the main.py
file requires some modifications.
Remove the
get_char_grain_size
function (will be replaced in the GUI).Add three optional arguments to the
main()
function:grain_file
to enable the selection of a user-defined csv file for"grains.csv"
hec_file
to enable the selection of a user-defined workbook for HEC-RAS output.out_folder
to enable the definition of a user-defined output directory for the bed load results workbook.
Modify the calls in the
main
function:
@log_actions
def main(D_char, hec_file, out_folder):
hec = HecSet(hec_file)
mpm_results = calculate_mpm(hec.hec_data, D_char)
mpm_results.to_excel(out_folder + "\\bed_load_mpm.xlsx")
Make the Application Frame#
Create a new Python file, call it gui.py
and import the following libraries:
import os
import tkinter as tk # standard widgets (Label, Button, etc.)
from tkinter import ttk # for Combobox widget
from tkinter.messagebox import askokcancel, showinfo # infoboxes
from tkinter.filedialog import askopenfilename, askdirectory # select files or folders
import webbrowser # open files or URLs from string-type directories
Moreover, we need to import the sediment transport code (converted to a module through the __init__.py
file in the sediment_transport
folder):
import sediment_transport as sed
tkinter
is tailored for object-oriented applications and this is why we create a new class called SediApp
as a child of tk.Frame
:
class SediApp(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
Set Window Geometry#
The initialization of the tk.Frame
parent class is the first and most important step that we have already implemented above. Next, define a window title and a window icon (use for example the provided icon graphs/icon.ico
in the exercise repository):
def __init__(self, master=None):
...
self.master.title("Sedi App")
self.master.iconbitmap("graphs/icon.ico")
TclError: bitmap “graphs/icon.ico” not defined
If you get this error message or similar, make sure the icon path is correct. In addition, recall that some recent versions versions of tkinter
cannot open icons because of an unknown error that might stem from relative path definitions in the library. Therefore, if you are sure the path is correct and the error message TclError: bitmap "graphs/icon.ico" not defined
persists, the only solution might be to comment out the line self.master.iconbitmap("graphs/icon.ico")
.
Assign a window geometry with window width and height, as well as x and y position on the screen in pixel units:
def __init__(self, master=None):
...
ww = 628 # width
wh = 382 # height
# screen position
wx = (self.master.winfo_screenwidth() - ww) / 2
wy = (self.master.winfo_screenheight() - wh) / 2
# assign geometry
self.master.geometry("%dx%d+%d+%d" % (ww, wh, wx, wy)
To relax the layout, we will use x and y pads later for the widgets (buttons, labels, and combobox). For this purpose, create two integer variables that define a buffer of 5 pixels around the widgets.
def __init__(self, master=None):
...
self.padx = 5
self.pady = 5
Add Methods (Commands) and Call them with Widgets#
The above-defined buttons call methods to open file names and directories (as string). As file selection dialogues are required twice (grains and HEC-RAS data), it makes sense to have a general function for selecting files. Therefore, add a new method to SediApp
and call it select_file
. The method uses askopenfilename
from tkinter.filedialog
and takes two input arguments. The first argument (description
) should be a (very) short description of the file to select. The second argument (file_type
) represents the file type (ending) that the user should look for. Both arguments are bound as a Tuple into a List of filetypes
that askopenfilename
uses to narrow down and clarify file selection options.
Note
The select_file
function could also be extended to multiple file types (e.g., include multiple types of workbooks or text files with filetypes=[('Workbook', 'xlsx; xlsx; ods'), ('Text file', '*.csv; *.txt')]
).
The initialdir
keyword argument defines the directory that opens up in the file dialogue window. The title
keyword argument sets the dialog window’s title and parent
defines the parent window or tk.Frame
(important when working with multiple tk.Frame
objects such as ttk.Notebook
tabs).
def select_file(self, description, file_type):
return askopenfilename(filetypes=[(description, file_type)],
initialdir=os.path.abspath(""),
title="Select a %s file" % file_type,
parent=self)
To enable the selection of a grain csv
file, write a set_grain_file
method as used with the above tk.Button
. The set_grain_file
method opens a file selection dialog and tries to open the file as a GrainReader
object (recall the sediment transport exercise). If it cannot open the selected grain size csv
file, the method falls into an OSError
statement and opens a showinfo
box (from tkinter.messagebox
) that notifies the user about the error. Otherwise (if everything is OK), the method updates the grain label (self.grain_label
) and the combobox (self.cbx_D_char
) with the information read from the grain size csv
file.
def set_grain_file(self):
self.grain_file = self.select_file("grain file", "csv")
try:
self.grain_info = sed.GrainReader(self.grain_file)
except OSError:
showinfo("ERROR", "Could not open %s." % self.grain_file)
self.grain_file = "SELECT"
return -1
# update grain label
self.grain_label.config(text="Grain file (csv): " + self.grain_file)
# update and enable combobox
self.cbx_D_char['state'] = 'readonly'
self.cbx_D_char['values'] = list(self.grain_info.size_classes.index)
self.cbx_D_char.set('D84')
To enable the selection of a HEC-RAS [USACoEngineeers16] output workbook, define a set_hec_file
method as used in the above tk.Button
. After the user’s file selection, the method needs to update the hec-label object (self.hec_label
).
def set_hec_file(self):
self.hec_file = self.select_file("HEC-RAS output file", "xlsx")
# update hec label
self.hec_label.config(text="HEC-RAS output file (xlsx): " + self.hec_file)
The selection of an output directory uses askdirectory
, which is another method from tkinter.filedialog
. After the user’s folder selection, the method needs to update the output folder label object (self.out_label
).
def select_out_directory(self):
self.out_folder = askdirectory()
# update output folder label
self.out_label.config(text="Output folder: " + self.out_folder)
Are all user inputs correctly defined?
Before running the bed load computation, we need to make sure that a grain size file, HEC-RAS workbook, and output directory are defined because the user can press the self.b_run
button at any time. To ensure that the necessary inputs are provided, parse self.grain_file
, self.hec_file
, and self.out_folder
for the string "SELECT"
, which is the default value of these variables (i.e., if the user did not make a choice, the variables contain the string "SELECT"
). Implement the validity check in a method called valid_selections
:
def valid_selections(self):
if "SELECT" in self.grain_file:
showinfo("ERROR", "Select grain size file.")
return False
if "SELECT" in self.hec_file:
showinfo("ERROR", "Select HEC-RAS output file.")
return False
if "SELECT" in self.out_folder:
showinfo("ERROR", "Select output folder.")
return False
return True
Define the Run Program Method#
To finalize the app, add a self.run_program
method corresponding to the command
function of the "Compute"
button (self.b_run
) . The run_program
method must ensure that the user has specified the necessary files and folders by calling the valid_selections
method (and return -1
otherwise). Then, the characteristic grain size selected by the user in the combobox is determined by self.cbx_D_char.get()
. If the provided grain csv
file has no valid numeric entry for the selected characteristic grain size, run_program
should fall into a ValueError
statement and inform the user about the issue in a showinfo
box.
An askokcancel
pop-up window (from tkinter.messagebox
) asks the user to press OK/Cancel to run/abort the program. If the user clicks OK, the pop-up window returns True
and starts the bed load computation through the main()
function of sed
(see above import of the sediment_transport
module).
After the successful run of the program, the run_program
method sets the foreground (text) color of the self.b_run
button to "forest green"
and adds the text "Success: Created %s" % str(self.out_folder + "/bed_load_mpm.xlsx")
to self.run_label
(defined in the __init__
method). The webbrowser
module’s open
method opens the newly produced Meyer-Peter and Müller [MPM48] bed load transport workbook (result of sed.main(...)
).
def run_program(self):
# ensure that user selected all necessary inputs
if not self.valid_selections():
return -1
# get selected characteristic grain size
try:
D_char = float(self.grain_info.size_classes["size"][str(self.cbx_D_char.get()])
except ValueError:
showinfo("ERROR", "The selected characteristic grain size is not correctly defined in the csv file (float?).")
return -1
if askokcancel("Start calculation?", "Click OK to start the calculation."):
sed.main(D_char, self.hec_file, self.out_folder)
self.b_run.config(fg="forest green")
self.run_label.config(text="Success: Created %s" % str(self.out_folder + "/bed_load_mpm.xlsx")
webbrowser.open(self.out_folder + "/bed_load_mpm.xlsx")
Make the Script Stand-alone#
To create the window, make gui.py
stand-alone executable by adding the following statement to the file bottom (recall the stand-alone descriptions):
if __name__ == '__main__':
SediApp().mainloop()
Launch the GUI#
Run the gui.py script (e.g., in PyCharm right-click in the gui.py
script and click > Run 'gui'
). If the script crashes or raises error messages, trace them back, and fix the issues. Otherwise, a tkinter
window opens:
Use the buttons to select a grain csv
file (e.g., grains.csv from the sediment transport exercise), a HEC-RAS output xlsx
workbook (e.g., HEC-RAS/output.xlsx from the sediment transport exercise), and define an output directory (e.g., …/Exercise-gui/). Make sure to select a characteristic grain size in the combobox (e.g., D84
) and click on the Compute
button.
After a successful run, the file bed_load_mpm.xlsx
opens, the Compute
button turns green, and the label below the button confirms the successful run (otherwise traceback errors and fix them). The GUI should now look like this:
Homework
Tweak the validity check of user inputs. Deactivate the self.b_run
button with self.b_run["state"] = "disabled"
and re-activate the button (self.b_run["state"] = "normal"
) if the user inputs are correct (result of valid_selections
). For this purpose, the call to valid_selections
must be moved outside the run_program
method.