{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "(chpt-gui)=\n", "# Graphical User Interfaces (GUIs)\n", "\n", "Make code user-friendly. For interactive reading and executing code blocks [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/hydro-informatics/hydro-informatics.github.io/main?filepath=jupyter) and find *b10-gui.ipynb*, or install {ref}`Python ` and {ref}`JupyterLab ` locally.\n", "\n", "```{image} ../img/python/hello-gui.png\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", "```{admonition} Watch this section as a video\n", ":class: tip, dropdown\n", "\n", "

Watch this section as a video on the @Hydro-Morphodynamics channel on YouTube.

\n", "```\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 *Python2*). `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": [ "```{admonition} Linux users\n", ":class: tip, dropdown\n", "**If you encounter troubles with `tkinter` on *Linux***, make sure that `tkinter` for Python is installed, either with
`sudo apt install python3-tk` or
`sudo apt install python3.X-tk` (replace `X` with your Python version) or
`sudo apt install tk8.6-dev` to install the library only (this should be sufficient).
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).\n", "```" ] }, { "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", "```{tip}\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.\n", "```" ] }, { "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": 1, "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": [ "```{figure} ../img/py-tk-first.png\n", ":alt: python GUI tkinter\n", ":name: py-tk-first\n", "\n", "The resulting GUI.\n", "```" ] }, { "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": 2, "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": [ "```{figure} ../img/py-tk-first-config.png\n", ":alt: python GUI tkinter with label config\n", ":name: py-tk-first-config\n", "\n", "The configured GUI.\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(add-button)=\n", "## 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": null, "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": [ "```{figure} ../img/py-tk-button.png\n", ":alt: python GUI tkinter with button\n", ":name: py-tk-button\n", "\n", "The GUI with a button calling an infobox.\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "The `command` keyword receives a {ref}`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.\n", "```" ] }, { "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 {ref}`section on Python classes