Skip to content

tblanke/ScenarioGUI

Repository files navigation

ScenarioGUI: An open-source tool for an easy way to create graphical user interfaces

PyPI version Tests codecov Downloads Downloads Documentation Status

What is ScenarioGUI?

ScenarioGUI is a Python package that contains many functionalities to design your own Graphical User Interfaces (GUI). It should help to create and maintain scenario based GUIs.

Read The Docs

ScenarioGUI has an elaborate documentation were all the functionalities of the tool are explained, with examples. This can be found on ScenarioGUI.readthedocs.io.

Requirements

This code is tested with Python 3.10 until 3.12 and requires the following libraries (the versions mentioned are the ones with which the code is tested)

  • PySide6>=6.6.0
  • matplotlib>=3.5.2
  • numpy>=1.23.1
  • pandas>=1.4.3
  • black>=23.1.0

For the tests

  • pytest>=7.1.2
  • pytest-cov>=3.0.0
  • pytest-timeout>=2.1.0
  • pytest-qt>=4.1.0
  • keyboard>=0.13.5

Quick start

Installation

One can install ScenarioGUI by running Pip and running the command

pip install ScenarioGUI

Developers can clone this repository.

It is a good practise to use virtual environments (venv) when working on a (new) Python project so different Python and package versions don't conflict with eachother. For ScenarioGUI, Python 3.10 or higher is recommended. General information about Python virtual environments can be found here and in this article.

Get started with ScenarioGUI

The GUI can be customized using a gui_config.ini file. There the font and font size can be set. Furthermore, the gui name, icon, version, saving file extension can be set. Several colors like the DARK background and the LIGHT Foreground color can be set as well. A folder containing an icons folder can be given. This one needs to contain at least the icon contained under ScenarioGUI/icons.

[COLORS]
WHITE: rgb(255, 255, 255)
LIGHT: rgb(84, 188, 235)
LIGHT_SELECT: rgb(42, 126, 179)
DARK: rgb(0, 64, 122)
GREY: rgb(100, 100, 100)
WARNING: rgb(255, 200, 87)
BLACK: rgb(0, 0, 0)

[DEFAULT]
FILE_EXTENSION: scenario
GUI_NAME: Scenario GUI
ICON_NAME: icon.svg
PATH_2_ICONS: ./ScenarioGUI/
FONT_WINDOWS: Arial
FONT_MAC: Helvetica
FONT_SIZE_WINDOWS: 12
FONT_SIZE_MAC: 14

To create your own GUI part you can inherit from the GuiStructure provided by this lib and add more pages, categories and input field as you like.

from ScenarioGUI import GuiStructure
from ScenarioGUI import elements as els
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import PySide6.QtWidgets as QtW
    from examples.translation_class_creation.translation_class import Translations


class GUI(GuiStructure):
    """your own customized GUI"""

    def __init__(self, default_parent: QtW.QWidget, translations: Translations):
        # first init the parent clas
        super().__init__(default_parent, translations)
        # add a first page called "Inputs" and has a button name of "Input" and has an icon "Add.svg"
        self.page_inputs = els.Page(name="Inputs", button_name="Input", icon="Add.svg")
        # Then several aims can be added to the page with different names and icons
        self.aim_add = els.Aim(label="Adding", icon="Add", page=self.page_inputs)
        self.aim_sub = els.Aim(label="Substract", icon="Delete", page=self.page_inputs)
        self.aim_plot = els.Aim(label="Plot", icon="Parameters", page=self.page_inputs)
        # this three aims can appear in one row by setting:
        self.page_inputs.aims_in_row = 3
        # a category with the label "Inputs" can be added to the inputs page like:
        self.category_inputs = els.Category(label="Inputs", page=self.page_inputs)
        # an integer box can be added with different options like this (some of these options are optional):
        self.int_a = els.IntBox(label="a", default_value=2, minimal_value=0, maximal_value=200, step=2, category=self.category_inputs)
        # a float box can be added with different options like this (some of these options are optional):
        self.float_b = els.FloatBox(
            label="b",
            default_value=100,
            minimal_value=0,
            maximal_value=1000,
            decimal_number=2,
            step=0.5,
            category=self.category_inputs,
        )
        # a button box can be added with different options like this
        self.button_box = els.ButtonBox(label="a or b?", default_index=0, entries=["a", "b"], category=self.category_inputs)
        # the button box can also be a list box for many options
        self.list_box = els.ListBox(label="a or b?", default_index=0, entries=["a", "b"], category=self.category_inputs)
        # a filename box can be added with different options like this
        file = "./example_data.csv"
        self.filename = els.FileNameBox(label="Filename", default_value=file, dialog_text="Hello", error_text="no file found", category=self.category_inputs)
        # a function button can be implemented like this:
        self.function_button = els.FunctionButton(button_text="function", icon="Add", category=self.category_inputs)
        # the function ("func") which will be called every time the button is clicked can be defined as follows:
        self.page_inputs.add_function_called_if_button_clicked(func)
        # A Hint can be implemented (if warning is True the option is displayed in WARNING color) like:
        self.hint = els.Hint(hint="Very important hint", category=self.category_inputs, warning=False)
        # The results page must be created like this:
        self.create_results_page()
        # then a category for numerical results can be added
        self.numerical_results = els.Category(page=self.page_result, label="Numerical results")
        # A text result calling the get_results function from the ResultsClass and rounding it to 2 decimals can be set like this: 
        self.result_text_add = els.ResultText("Result", category=self.numerical_results, prefix="Result: ", suffix="m")
        self.result_text_add.text_to_be_shown("ResultsClass", "get_result")
        self.result_text_add.function_to_convert_to_text(lambda x: round(x, 2))
        # a results figure calling the create_plot function from ResultsClass which is returning a tuple of a plt.Figure and plt.Axes can be implemented 
        # like this:
        self.figure_results = els.ResultFigure(label="Plot", page=self.page_result)
        self.figure_results.fig_to_be_shown(class_name="ResultsClass", function_name="create_plot")
        # this figure can then be linked to an option to display the legend like this:
        self.legend_figure_results = els.FigureOption(
            category=self.figure_results, label="Legend on", param="legend", default=0, entries=["No", "Yes"], entries_values=[False, True]
        )
        # with this function the results options will be displayed if one of the aims is selected
        self.aim_add.add_link_2_show(self.result_text_add)
        self.aim_plot.add_link_2_show(self.figure_results)
        # The settings page must be created like this:
        self.create_settings_page()
        # This function needs to be called to update the page, category and option lists
        self.create_lists()
        # links to next or previous pages can be set like this:
        self.page_inputs.set_next_page(self.page_result)
        self.page_result.set_previous_page(self.page_inputs)
        self.page_result.set_next_page(self.page_settings)
        self.page_result.set_previous_page(self.page_result)

The ResultsClass needs to have a "_to_dict", "_from_dict" and all function defined in the ResulText and ResultFigure options. Furthermore, it needs to be creatable without any inputs.

from collections.abc import Callable
import matplotlib.pyplot as plt

class ResultsClass:
    """Example results class"""
    def __init__(self, a: int = 1, b: int = 2):
        self.a = a
        self.b = b
        self.result = None

    def adding(self):
        """adding a and b"""
        self.result = self.a + self.b

    def get_result(self) -> float:
        """returns the result"""
        return self.result

    def create_plot(self, legend: bool = False) -> tuple[plt.Figure, plt.Axes]:
        """Creates a plot"""
        fig = plt.figure()
        ax = fig.add_subplot(111)
        # set axes labels
        ax.set_xlabel(r"Time (year)")
        ax.set_ylabel(r"Temperature ($^\circ C$)")
        ax.hlines(self.a, 0, self.b, colors="r", linestyles="dashed", label="line", lw=1)
        if legend:
            ax.legend()
        return fig, ax

    def _to_dict(self) -> dict:
        """creates a dictionary from the class variables"""
        return {"a": self.a, "b": self.b, "result": self.result}

    def _from_dict(self, dictionary: dict):
        """creates the class from a dictionary"""
        self.a = dictionary["a"]
        self.b = dictionary["b"]
        self.result = dictionary["result"]

def data_2_results(data) -> tuple[ResultsClass, Callable[[], None]]:
    """casts the data in the Datastorage to the results class and the function which should be called"""
    result = ResultsClass(data.int_a, data.float_b)
    return result, result.adding

The gui can then be start like this:

from sys import argv, exit as sys_exit

def run(path_list=None):  # pragma: no cover
    import PySide6.QtWidgets as QtW

    from ScenarioGUI.global_settings import FILE_EXTENSION
    from ScenarioGUI import MainWindow
    # import your own Translation class a script to create one from a csv file is given as well
    from ScenarioGUI.gui_classes.translation_class import Translations

    # init application
    app = QtW.QApplication()
    # init window
    window = QtW.QMainWindow()
    # init gui window
    main_window = MainWindow(window, app, GUI, Translations, result_creating_class=ResultsClass, data_2_results_function=data_2_results)
    # load file if it is in path list
    if path_list is not None:
        main_window.filename = (
            [path for path in path_list if path.endswith(f".{FILE_EXTENSION}")][0],
            0,
        )
        main_window.fun_load_known_filename()

    # show window
    window.showMaximized()
    # close app
    sys_exit(app.exec())


if __name__ == "__main__":  # pragma: no cover
    # pass system args like a file to read
    run(argv if len(argv) > 1 else None)

A full list of functionalities is given below.

Functionalities

ScenarioGUI offers functionalities of value to all different disciplines which would like to create a GUI for different scenarios. These scenario can thern be easily compared. For more information about the functionalities of ScenarioGUI, please visit the ReadTheDocs.

License

ScenarioGUI is licensed under the terms of the 3-clause BSD-license. See ScenarioGUI license.

Contact ScenarioGUI

  • Do you want to contribute to ScenarioGUI?
  • Do you have a great idea for a new feature?
  • Do you have a specific remark/problem?

Please do contact us at blanke@fh-aachen.de.