{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Graphical User Interfaces (GUIs)\n", "\n", "Make code user-friendly. \n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A Graphical User Interface (GUI) facilitates setting input variables of scripts. This is particularly useful for reusing a script that you have written a long time ago, without having to study the whole script again in detail. Although it is arguable whether GUIs are still appropriate in times of web applications, large and in particular copyrighted data must be processed locally. Ultimately, for local data processing, a GUI can be very convenient to run self-written, custom programs.\n", "\n", "Several GUI libraries (packages) are available for Python and this chapter builds on the [tkinter](https://docs.python.org/3/library/tkinter.html) library. Alternatives are, for instance, [*wxPython*](https://www.wxpython.org/) or [*Jython*](https://www.jython.org/) (a *Java* implementation of *Python 2*). `tkinter` is a standard library, which does not need to be installed additionally. For a quick example, type in the terminal (e.g., *Anaconda Prompt*, *PyCharm* or *Linux* terminal - not in the Python itself):" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```\n", "python -m tkinter\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **If you encounter troubles with `tkinter` on *Linux***, make sure that `tkinter` for Python is installed, either with <br>`sudo apt install python3-tk` or <br>`sudo apt install python3.X-tk` (replace `X` with your Python version) or<br> `sudo apt install tk8.6-dev` to install the library only (this should be sufficient). <br>If the above comments do not work, make sure that the `tkinter` repository is available to your system, for example on Debian: `sudo add-apt-repository ppa:deadsnakes/ppa` (the repository address may change and depends on your Linux and Python versions)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`tkinter` works on most popular platforms (*Linux*, *macOS*, *Windows*) and is not only available to Python, but also [Ruby](https://www.ruby-lang.org), [Perl](https://www.perl.org/), [Tcl](https://www.tcl-lang.org/) (the origin of `tkinter`), and many more languages. Because it supports languages like *Ruby* or *Perl*, `tkinter` can be used for local GUIs as well as for web applications.\n", "\n", "> **Tips:**\n", "> * All GUI codes featured in this chapter can be downloaded from the [course respository](https://github.com/hydro-informatics/jupyter-python-course/tree/main/gui).\n", "> * Consider using another IDE than Jupyter (e.g., PyCharm, Atom, or Spyder) for running the code blocks provided in this notebook." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The First GUI \n", "The very first step is to `import tkinter`, usually using the alias `as tk`. With `tk.Tk()`, a so-called parent window (e.g., `top`) can be created, in which further elements will be accommodated. All further elements are created as `tk` child-objects of the parent window and placed (arranged) in the parent window using the `pack()` or `grid()` method. Here, we will use `pack` most of the time and `grid` will be useful to place elements at an exact position on the window (e.g., `tk.ELEMENT.grid(row=INT, column=INT)`). To display the GUI, the parent window `top` must be launched with `top.mainloop()` after arranging all elements. The following code block shows how to create a parent window with a label element (`tk.Label`)." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import tkinter as tk\n", "top = tk.Tk()\n", "a_label = tk.Label(top, text=\"A label just shows some text.\")\n", "a_label.pack()\n", "top.mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After calling the `mainloop()` method, the window is in a *wait* state. That means the window is waiting for `events` being triggered through user action (e.g., a click on a button). This is called *event-driven programming*, where *event handlers* are called rather than a single linear flow in the form of a sequence of (Python) commands.\n", "\n", "For now, our window uses default values, for instance, for the window title, size, and background color. These window properties can be modified with the `title`, `minsize` or `maxsize`, and `configure` attributes of the `top` parent window:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "top = tk.Tk()\n", "a_label = tk.Label(top, text=\"A label just shows some text.\")\n", "a_label.pack()\n", "\n", "top.title(\"My first GUI App\")\n", "top.minsize(628, 382)\n", "top.configure(bg=\"sky blue\")\n", "top.mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Add a Button to Call a Function\n", " \n", "Currently, the window only shows a (boring) label and waits for events that do not exist. With a `tk.Button` we can add an event trigger, which still needs something to trigger. To this end, define a `call_back` function that creates an infobox, which is an object of `showinfo` from `tkinter.messagebox` (i.e., it needs to be imported)." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "from tkinter.messagebox import showinfo\n", "# more message boxes: askokcancel, askyesno\n", "\n", "def call_back(message):\n", " showinfo(\"This is an Infobox\", message)\n", "\n", "\n", "top = tk.Tk()\n", "a_label = tk.Label(top, text=\"Here is the button.\")\n", "a_label.pack()\n", "# add a button\n", "a_button = tk.Button(top, text = \">> Click <<\", command=lambda: call_back(\"Greetings from the Button.\"))\n", "a_button.pack()\n", "top.mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Note:** The `command` keyword receives a [lambda](https://hydro-informatics.com/python-basics/pyfun.html#lambda) function that links to the `call_back` function. Why do we need this complication? The answer is that the `call_back` function would be automatically triggered with the `mainloop()` method if we were not using a `lambda` function." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A Vanilla `tkinter` Program\n", "\n", "The above code blocks instantiate `tkinter` objects (*widgets*) in non-object-oriented script style. However, when we write a GUI, we most likely want to start an application (*App*) by running the script. This is why `tkinter` widgets are usually created as objects of custom classes that typically inherit from `tk.Frame`. Therefore, the following code block recasts the previous example into an object-oriented code with the template from the [section on Python classes](https://hydro-informatics.com/jupyter/classes.html#template).\n", "\n", "The below example creates a `VanillaApp`, which is a child of `tk.Frame` (`tkinters` *master* frame). The initialization method `__init__` needs to invoke `tk.Frame` and `pack()` it to initialize the window. After that, we can place other `widget`s such as labels and buttons as before. In the `VanillaApp`, we can also directly implement the above `call_back` function as a method. In addition, make the script run stand-alone (not supported by a beautiful Jupyter notebook) by adding an `if __name__ == \"__main__\": VanillaApp().mainloop()` statement at the bottom of the script (read more about the `__main__` statement in the [section on packages](https://hydro-informatics.com/jupyter/pypckg.html#standalone))." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# define the VanillaApp class\n", "class VanillaApp(tk.Frame):\n", " def __init__(self, master=None):\n", " tk.Frame.__init__(self, master)\n", " self.pack()\n", " \n", " table_label = tk.Label(master, text=\"Do you want vanilla ice?\")\n", " table_label.pack()\n", " vanilla_button = tk.Button(master, text=\"I want Vanilla\", command=lambda: self.call_back(\"Here is Vanilla!\"))\n", " vanilla_button.pack()\n", " no_vanilla_button = tk.Button(master, text=\"I want something else\", command=lambda: self.call_back(\"Here is bread!\"))\n", " no_vanilla_button.pack()\n", " \n", " def call_back(self, message):\n", " showinfo(\"This is an Infobox\", message)\n", "\n", "\n", "# instantiate a VanillaApp object\n", "if __name__ == \"__main__\":\n", " VanillaApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Make Script a Stand-alone App\n", "\n", "The above code block providing the `VanillaApp` class can be copied to any external Python file and saved as, for example, `vanilla_app.py`. Next, in *Anaconda Prompt* (Windows) or *Linux Terminal* start the GUI as follows (depending on the [Python installation](https://hydro-informatics.com/python-basics/pyinstall.html) used):\n", "\n", "1. Activate the required environment, for example:\n", " * Windows/Anaconda: `conda activate flussenv`\n", " * Linux/Virtualenv: `source vflussenv/bin/activate`\n", "2. Navigate to the directory where the script is located (use `cd` in [Windows](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cd) or [Linux/macOS](http://www.linfo.org/cd.html)).\n", "3. Type `python vanilla_app.py` (or `python -m vanilla_app.py`) to launch the GUI.\n", "\n", "This sequence of commands can also be written to a batch or bash file ([`.bat` on Windows](https://www.wikihow.com/Write-a-Batch-File)) or shell script ([.sh on Linux/macOS](https://www.linux.com/training-tutorials/writing-simple-bash-script/) - [alternative documentation](http://linuxcommand.org/lc3_writing_shell_scripts.php)). After writing and saving a batch or bash file, double-click on the file to start the Python-based GUI." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## More Widgets\n", "\n", "There are many more widgets than labels and buttons and the below figure features some of them including:\n", "\n", "* A definition of the GUI window name with `master.title(\"Window name\")`\n", "* A definition of the GUI window icon (ICO) with `master.iconbitmap(\"directory/icon-file.ico\")`\n", "\n", "> **Attention:** Some recent versions of `tkinter` cannot open icons because of an unknown error that might stem from relative path definitions in the library. Therefore, if you get an error message such as `TclError: bitmap \"gui/sample-icon.ico\" not defined`, the only solution might be to comment out the line `self.master.iconbitmap(\"gui/sample-icon.ico\")`.\n", "\n", "* `tk.Menu` with drop-down cascade\n", "* `tk.Label` (see above)\n", "* `tk.Button` (see above)\n", "* `tk.Entry` that is a blank field where users can enter values or words\n", "* `ttk.Combobox` that is a drop-down menu in the master frame ([tk-themed](https://docs.python.org/3/library/tkinter.ttk.html) `ttk` widget)\n", "* `tk.Listbox` with a `tk.Scrollbar`, where the scrollbar is required to navigate to listbox entries that are not in the visible range of the listbox size\n", "* `tk.Checkbutton` that can be checked (ticked) to set a `tk.BooleanVar()` object to `True` (default: not checked -> `False`)<br>Alternatively, have a look at [`tk.Radiobutton`](http://effbot.org/tkinterbook/radiobutton.htm) to enable selections from a multiple-choice frame (rather than the `False`-`True`-only frame of a checkbutton)\n", "* `tk.PhotoImage` to display a sub-sampled image in the GUI\n", "\n", "\n", "\n", "The next block features the code that creates the `tkinter` widgets in the figure (the [script](https://raw.githubusercontent.com/hydro-informatics/jupyter-python-course/main/gui/start_gui.py), [image](https://raw.githubusercontent.com/hydro-informatics/jupyter-python-course/main/gui/sunny-image.gif) and [icon](https://raw.githubusercontent.com/hydro-informatics/jupyter-python-course/main/gui/gui-icon.ico) are available at the course repository):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import tkinter as tk\n", "from tkinter.messagebox import showinfo\n", "from tkinter import ttk\n", "\n", "\n", "class MyApp(tk.Frame):\n", " def __init__(self, master=None):\n", " tk.Frame.__init__(self, master)\n", "\n", " self.master.title(\"Master Title\")\n", " self.master.iconbitmap(\"gui/sample-icon.ico\")\n", "\n", " # Set geometry: upper-left corner of the window\n", " ww = 628 # width\n", " wh = 382 # height\n", " wx = (self.master.winfo_screenwidth() - ww) / 2\n", " wy = (self.master.winfo_screenheight() - wh) / 2\n", " # assign geometry\n", " self.master.geometry(\"%dx%d+%d+%d\" % (ww, wh, wx, wy))\n", " # assign space holders around widgets\n", " self.dx = 5\n", " self.dy = 5\n", "\n", " # Menu Bar\n", " self.mbar = tk.Menu(self) # create standard menu bar\n", " self.master.config(menu=self.mbar) # make self.mbar standard menu bar\n", " # add menu entry\n", " self.ddmenu = tk.Menu(self, tearoff=0)\n", " self.mbar.add_cascade(label=\"A Drop Down Menu\", menu=self.ddmenu) # attach entry it to standard menu bar\n", " self.ddmenu.add_command(label=\"Drop Down Entry 1\", command=lambda: self.hello(\"Drop Down Menu!\"))\n", "\n", " # Label\n", " self.a_label = tk.Label(master, text=\"A Label\")\n", " self.a_label.grid(column=0, row=0, padx=self.dx, pady=self.dy)\n", "\n", " # Button\n", " self.a_button = tk.Button(master, text=\"A Button\", command=lambda: self.hello(\"The Button!\"))\n", " self.a_button.grid(column=0, row=1, padx=self.dx, pady=self.dy)\n", "\n", " # Entry\n", " self.an_entry = tk.Entry(master, width=20)\n", " self.an_entry.grid(column=0, row=2, padx=self.dx, pady=self.dy)\n", "\n", " # Combobox\n", " self.cbox = ttk.Combobox(master, width=20)\n", " self.cbox.grid(column=0, row=3, padx=self.dx, pady=self.dy)\n", " self.cbox['state'] = 'readonly'\n", " self.cbox['values'] = [\"Combobox Entry 1\", \"Combobox Entry 2\", \"Combobox Entry ...\"]\n", " self.cbox.set(\"Combobox Entry 1\")\n", " self.cbox_selection = self.cbox.get()\n", "\n", " # Listbox with Scrollbar\n", " self.scrlbar = tk.Scrollbar(master, orient=tk.VERTICAL)\n", " self.scrlbar.grid(stick=tk.W, column=1, row=4, padx=self.dx, pady=self.dy)\n", " self.lbox = tk.Listbox(master, height=3, width=20, yscrollcommand=self.scrlbar.set)\n", " for e in [\"Listbox Entry 1\", \"Listbox Entry 2\", \"With Scrollbar ->\", \"lb entry n\"]:\n", " self.lbox.insert(tk.END, e)\n", " self.lbox.grid(sticky=tk.E, column=0, row=4, padx=self.dx, pady=self.dy)\n", " self.scrlbar.config(command=self.lbox.yview)\n", " self.lbox_selection = self.lbox.get(0)\n", "\n", " # Checkbutton\n", " self.check_variable = tk.BooleanVar()\n", " self.cbutton = tk.Checkbutton(master, text=\"Tick this Checkbutton\", variable=self.check_variable)\n", " self.cbutton.grid(sticky=tk.E, column=2, row=0, padx=self.dx, pady=self.dy)\n", "\n", " # Image\n", " logo = tk.PhotoImage(file=\"gui/sunny-image.gif\")\n", " logo = logo.subsample(2, 2) # controls size\n", " self.l_img = tk.Label(master, image=logo)\n", " self.l_img.image = logo\n", " self.l_img.grid(row=1, column=2, rowspan=4)\n", " # create a placeholder to relax layout\n", " tk.Label(text=\" \").grid(row=0, column=1)\n", " \n", " @staticmethod\n", " def hello(message):\n", " showinfo(\"Got Message from ...\", message)\n", "\n", "\n", "if __name__ == '__main__':\n", " MyApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As usual in *Python*, there are many more options (widgets) available and the [effbot.org](http://effbot.org/tkinterbook/) offers a detailed overview of available `tkinter` objects." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `tkinter` Variables\n", "\n", "In the above example, the checkbox receives a `tk.BooleanVar()`, which takes a `True` value when a user checks the checkbox. There are more variables that can be associated with `tkinter` widgets (e.g., `tk.Entry`, `tk.Listbox`, or `ttk.Combobox`). `tkinter` variables correspond basically to [Python data types](https://hydro-informatics.com/jupyter/pybase.html#var) with special methods that are required to set or retrieve (get) user-defined values of these data types. Here is an overview of `tkinter` variables:\n", "\n", "* `tk.BooleanVar()` is a *boolean* that can be `True` or `False`\n", "* `tk.DoubleVar()` is a numeric floating point (*float*) variable\n", "* `tk.IntVar()` is a numeric *integer* variable\n", "* `tk.StringVar()` is a *string* (i.e., typically some text)\n", "\n", "How does Python know when to retrieve a user-defined value? Typically, we want to evaluate user-defined values when we call a function that receives user-defined values as input arguments. Default, predefined default values in a script can be set with `VARIABLE.set()` and the user settings can be retrieved with `VARIABLE.get()`. The following [script](https://raw.githubusercontent.com/hydro-informatics/jupyter-python-course/main/gui/variable_gui.py) and the [icon](https://raw.githubusercontent.com/hydro-informatics/jupyter-python-course/main/gui/sample-icon.ico) are available at the course repository." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import tkinter as tk\n", "from tkinter.messagebox import showinfo\n", "import random\n", "\n", "class MyApp(tk.Frame):\n", " def __init__(self, master=None):\n", " tk.Frame.__init__(self, master)\n", "\n", " self.master.title(\"GUI with variables\")\n", " self.master.iconbitmap(\"gui/sample-icon.ico\")\n", "\n", " # Set geometry: upper-left corner of the window\n", " ww = 628 # width\n", " wh = 100 # height\n", " wx = (self.master.winfo_screenwidth() - ww) / 2\n", " wy = (self.master.winfo_screenheight() - wh) / 2\n", " # assign geometry\n", " self.master.geometry(\"%dx%d+%d+%d\" % (ww, wh, wx, wy))\n", "\n", " self.a_label = tk.Label(master, text=\"Enter a value to call:\")\n", " self.a_label.grid(column=0, row=0, padx=5, pady=5)\n", " \n", " # define tk.StringVar() and assign it to an entry\n", " self.user_entry = tk.StringVar()\n", " self.an_entry = tk.Entry(master, width=20, textvariable=self.user_entry)\n", " self.an_entry.grid(column=1, row=0, padx=5, pady=5)\n", "\n", " # define Button to trigger call back\n", " self.a_button = tk.Button(master, text=\"Call Message!\", command=lambda: self.message_distributor())\n", " self.a_button.grid(column=2, row=0, padx=5, pady=5)\n", "\n", " # define a Checkbutton to use either user input or a random message\n", " self.check_variable = tk.BooleanVar()\n", " self.cbutton = tk.Checkbutton(master, text=\"Check this box to use a random message instead of the entry\", variable=self.check_variable)\n", " self.cbutton.grid(sticky=tk.E, column=0, columnspan=3, row=1, padx=5, pady=5)\n", " self.check_variable.set(False)\n", "\n", " \n", " def message_distributor(self):\n", " if not self.check_variable.get():\n", " showinfo(\"User message\", self.user_entry.get())\n", " else:\n", " showinfo(\"Random message\", self.random_message())\n", " \n", " def random_message(self):\n", " random_words = [\"summer\", \"winter\", \"is\", \"cold\", \"hot\", \"will be\"]\n", " return \" \".join(random.sample(random_words, 3))\n", "\n", "\n", "if __name__ == '__main__':\n", " MyApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Design, Place and Modify Widgets\n", "\n", "The above code examples use both the `OBJECT.grid()` and the `OBJECT.pack()` methods (geometry managers) to place widgets in the GUI. There is an additional geometry manager in the form of the `place` method. The choice of a convenient geometry manager depends on your preferences and there are pros and cons for the three geometry managers:\n", "\n", "* `pack`\n", " - automatically places widgets within a box\n", " - works best for simple GUIs, where all widgets are in one column or row\n", " - complex layouts can only be handled with complicated workarounds (tip: do not try)\n", "* `place`\n", " - places widgets at absolute or relative *x*-*y* positions\n", " - works well for graphical arrangements of widgets\n", "* `grid`\n", " - places widgets in columns and rows of a grid\n", " - works well with table-like apps and structured layouts\n", " \n", "To enable more graphical flexibility, widgets accept many optional keywords to change their foreground (`fg`) or background (`bg`) color. In addition, widgets can be modified with the `tk.OBJECT.config(PARAMETER_TO_CONMFIGURE=NEW_CONFIG)` method.\n", "\n", "The following sections provide more details on the `place` and `grid` geometry managers (the relevant functions of `pack` are already above shown: `pack()` - that is all) and illustrate keyword arguments along with widget methods to modify widgets.\n", "\n", "### Place with `place` and Use Object Colors\n", "\n", "The simplest geometry manager is the `pack` method, which works even without any keyword provided (see the very first examples in this section). With the `pack` method, widgets can be placed relatively in the window (`relx` and `rely`, where both must be < 1) or with absolute positions (`x` and `y`, where both should fit into the window dimensions defined through `self.config(width=INT, height=INT)`). The axis origin (zero positions of *x* and *y*) is determined with the `anchor` keyword." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class PlacedApp(tk.Frame):\n", " def __init__(self, master=None, **options):\n", " tk.Frame.__init__(self, master, **options)\n", " self.pack(expand=True, fill=tk.BOTH)\n", " self.config(width=628, height=100)\n", " self.master.title(\"A placed GUI\")\n", " tk.Label(self, text=\"Vanilla\", bg=\"goldenrod\", fg=\"dark slate gray\").place(anchor=tk.NW, relx=0.2, y=10)\n", " tk.Label(self, text=\"Green green tree\", bg=\"OliveDrab1\").place(anchor=tk.E, relx=0.8, rely=0.5)\n", " tk.Label(self, text=\"Blue sky\", bg=\"DeepSkyBlue4\", fg=\"floral white\").place(anchor=tk.CENTER, x=300, rely=0.8)\n", "\n", "\n", "if __name__ == '__main__':\n", " PlacedApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Note:** The above example does not create class objects of `tk.Labels`, which makes the labels non-modifiable. This definition of widgets is acceptable to shorten the often long GUI scripts, but only if the widgets should not be modified later." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Place Objects with `grid`\n", "In `grid`-ed GUIs, the widget alignment can be controlled with the `sticky` argument that uses cardinal directions (e.g., `sticky=tk.W` aligns or \"sticks\" a widget at the west, meaning, left side, of a GUI). The `padx` and `pady` keywords arguments enable the implementation of pixel space around widgets." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "from tkinter.messagebox import showinfo\n", "\n", "class GriddedApp(tk.Frame):\n", " def __init__(self, master=None, **options):\n", " tk.Frame.__init__(self, master, **options)\n", " self.pack(expand=True, fill=tk.BOTH)\n", " self.config(width=628, height=100)\n", " self.master.title(\"A grid GUI\")\n", " tk.Label(self, text=\"Enter name: \", bg=\"bisque2\", fg=\"gray21\").grid(sticky=tk.W, row=0, column=0, padx=10)\n", " tk.Entry(self, bg=\"gray76\", width=20).grid(stick=tk.EW, row=0, column=1, padx=5)\n", " tk.Button(self, text=\"Show message\", bg=\"pale turquoise\", fg=\"red4\", command=lambda: showinfo(\"Info\", \"Random message\")).grid(row=0, column=2, padx=5)\n", " tk.Checkbutton(self, text=\"A Checkbutton over multiple columns\").grid(stick=tk.E, row=1, column=0, columnspan=3, pady=15)\n", "\n", "\n", "if __name__ == '__main__':\n", " GriddedApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Configure Widgets\n", "Upon user action (an event), we may want to modify previously defined widgets. For instance, we may want to change the text of a label or the layout of a button to indicate successful or failed operations. For this purpose, `tkinter` objects can be modified with `tk.OBJECT.config(PARAMETER_TO_CONMFIGURE=NEW_CONFIG)`. Moreover, objects can be deleted (destroyed) with `tk.OBJECT.destroy()`, even though this is not an elegant method for any other widgets than pop-up windows (child frames of the master frame)." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "from tkinter.messagebox import showinfo, showerror\n", "\n", "class ReConfigApp(tk.Frame):\n", " def __init__(self, master=None, **options):\n", " tk.Frame.__init__(self, master, **options)\n", " self.config(width=628, height=100)\n", " self.pack()\n", " \n", " self.user_depth = tk.DoubleVar()\n", " self.kst = 40.0\n", " self.w = 5.0\n", " self.slope = 0.002\n", " \n", " \n", " self.master.title(\"A GUI that reconfigures its widgets\")\n", " tk.Label(self, text=\"Enter water depth (numeric, in meters): \", bg=\"powder blue\", fg=\"medium blue\").grid(sticky=tk.W, row=0, column=0, padx=10)\n", " tk.Entry(self, bg=\"alice blue\", width=20, textvariable=self.user_depth).grid(stick=tk.EW, row=0, column=1, padx=5)\n", " self.eval_button = tk.Button(self, text=\"Estimate flow velocity\", bg=\"snow2\", fg=\"dark violet\", command=lambda: self.call_estimator())\n", " self.eval_button.grid(row=0, column=2, padx=5)\n", " \n", " def call_estimator(self):\n", " try:\n", " flow_depth = float(self.user_depth.get())\n", " except tk.TclError:\n", " return showerror(\"ERROR\", \"Non-numeric value entered.\")\n", " self.eval_button.config(fg=\"green4\", bg=\"DarkSeaGreen1\")\n", " showinfo(\"Result\", \"The estimated flow velocity is: \" + str(self.estimate_u(flow_depth)))\n", " \n", " def estimate_u(self, h):\n", " try:\n", " return self.kst * h ** (2/3) * self.slope ** 0.5\n", " except ValueError:\n", " showerror(\"ERROR: Bad values defined.\")\n", " return None\n", " except TypeError:\n", " showerror(\"ERROR: Bad data types defined.\")\n", " return None\n", " \n", "\n", "if __name__ == '__main__':\n", " ReConfigApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "\n", "> **Challenges:**\n", "> 1. The roughness value varies as a function of surface characteristics. Can you implement a `ttk.Combobox` to let a user choose a Strickler *k<sub>st</sub>* roughness value between 10 and 85 (integers) and define the channel slope in a `tk.Entry` or a custom pop-up window (see below)?\n", "> 2. The cross-section averaged flow velocity also depends on the cross-section geometry. Can you implement `tkinter` widgets to enable the definition of a bank slope `m` and channel base width `w` for calculating the hydraulic radius?\n", "\n", "\n", "\n", "\n", "***" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pop-up Windows\n", "\n", "### Default Messages from `tkinter.messagebox`\n", "\n", "The `tkinter.messagebox` library provides standard pop-up windows, such as:\n", "\n", "* `showinfo(title=STR, message=STR)` that prints an information message (see above examples).\n", "* `showwarning(title=STR, message=STR)` that prints a warning message.\n", "* `showerror(title=STR, message=STR)` that prints an error message (see above example).\n", "* `askyesno(title=STR, message=STR)` that returns `False` or `True` depending on a user's answers to a *Yes-or-No* question.\n", "* `askretrycancel(title=STR, message=STR)` that returns `False` or `True`, or re-attempts to run an event (function) depending on a user's answers to a *Yes-or-No-or-Cancel* question.\n", "* `askokcancel(title=STR, message=STR)` that returns `False` or `True` depending on a user's answers to an *OK* question.\n", "\n", "Read more about default pop-up windows in the [Python docs](https://docs.python.org/3.9/library/tkinter.messagebox.html).\n", "\n", "### Top-level Custom Pop-ups\n", "The default windows may not meet the needs for every application, for instance, to invite users to enter a custom value. In this case, a `tk.Toplevel` object aids in producing a customized pop-up window, and the below example shows how a customized top-level pop-up window can be called within a method. With the `tk.Toplevel` widget and the `tk.Frame` (master) widgets, the code block produces two frames, in which buttons, labels, or any other `tkinter` object can be placed. The very first argument of any `tkinter` widget-object created determines whether the object is placed in the master or the top-level frame. For example, `tk.Entry(self).pack()` creates an entry in the master `tk.Frame`, and `tk.Entry(pop_up).pack()` creates an entry in the child `tk.Toplevel`." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from tkinter.messagebox import showwarning\n", "\n", "class PopApp(tk.Frame):\n", " def __init__(self, master=None, **options):\n", " tk.Frame.__init__(self, master, **options)\n", " self.config(width=628, height=50)\n", " self.pack()\n", " \n", " self.master.title(\"Custom pop-up GUI\")\n", " self.pop_button = tk.Button(self, text=\"Open pop-up window\", bg=\"cadet blue\", fg=\"white smoke\", command=lambda: self.new_window())\n", " self.pop_button.pack()\n", " \n", " def destroy_buttons(self):\n", " self.pop_button.destroy()\n", " self.p_button1.destroy()\n", " self.p_button2.destroy() \n", " showwarning(\"Congratulations\", \"This app is useless now. Don't press red-ish buttons ...'\")\n", " \n", " def new_window(self):\n", " pop_up = tk.Toplevel(master=self)\n", " # add two buttons to the new pop_up Toplevel object (window)\n", " self.p_button1 = tk.Button(pop_up, text=\"Destroy buttons (do not click here)\", fg=\"DarkOrchid4\",\n", " bg=\"HotPink1\", command=lambda: self.destroy_buttons())\n", " self.p_button2 = tk.Button(pop_up, text=\"Close window\", command=lambda: pop_up.quit()) \n", " self.p_button1.pack()\n", " self.p_button2.pack()\n", "\n", "\n", "if __name__ == '__main__':\n", " PopApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### File Dialog (Open ...)\n", "When a custom function's argument is a file or file name, we most likely want the user to be able to select the file type needed. The [tkinter.filedialog](https://docs.python.org/3.10/library/dialog.html#module-tkinter.filedialog) library provides methods to let users choose general or specific file types. Specific file types can be defined with the `filetypes=(\"Name\", \"*.ending\")` (or `filetypes=(\"Name\", \"*.ending1;*.ending2;...\")` for multiple file types) keyword argument. The following example illustrates the usage of `tkinter.filedialog`'s `askopenfilename`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "from tkinter.filedialog import askopenfilename\n", "from tkinter.messagebox import showinfo\n", "\n", "class OpenFileApp(tk.Frame):\n", " def __init__(self, master=None, **options):\n", " tk.Frame.__init__(self, master, **options)\n", " self.config(width=628, height=50)\n", " self.pack()\n", " \n", " self.master.title(\"GUI to open a file\")\n", " \n", " self.pop_button = tk.Button(self, text=\"Open a text file\", bg=\"light steel blue\", fg=\"dark slate gray\", command=lambda: self.open_file())\n", " self.pop_button.pack()\n", " \n", " def open_file(self):\n", " file_types = ((\"Text\", \"*.txt;*.csv;*.asc\"),) # equivalent to [(\"Text\", \"*.txt;*.csv;*.asc\")]\n", " file_name = askopenfilename(initialdir=\".\", title=\"Select a text file\", filetypes=file_types, parent=self)\n", " showinfo(\"File info\", \"You selected \" + str(file_name))\n", "\n", "\n", "if __name__ == '__main__':\n", " OpenFileApp().mainloop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Quit\n", "To cleanly quit a GUI, use `tk.Frame.quit()` (i.e., in a custom class, write `self.quit()` or `master.quit()`). The above example of the `PopApp` class also features the `destroy()` method, which helps to remove particular widgets.\n", "\n", "`tkinter` provides many more options such as the implementation of tabs with `ttk.Notebook()` (requires [binding](https://effbot.org/tkinterbook/tkinter-events-and-bindings.htm) of tab objects), tables (`from tkintertable import TableCanvas, TableModel`), or interactive graphic objects with [matplotib](https://hydro-informatics.com/jupyter/pyplot.html#matplotlib).\n", "\n", "```python\n", "import matplotlib\n", "matplotlib.use('TkAgg')\n", "import numpy as np\n", "from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg\n", "from matplotlib.figure import Figure\n", "```\n", "\n", "Enjoy creating your custom apps!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Exercise:** Get familiar with creating GUIs and object orientation in the [GUI exercise](https://hydro-informatics.com/jupyter/gui.html)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.12" } }, "nbformat": 4, "nbformat_minor": 4 }