Custom interfaces

The file examples/interface.py showcases the integration of the maze widget into a custom interface.

 1from PyQt5.QtCore import QTimer
 2from PyQt5.QtWidgets import (
 3    QWidget,
 4    QHBoxLayout,
 5    QFormLayout,
 6    QSpinBox,
 7    QDoubleSpinBox,
 8    QCheckBox,
 9    QComboBox,
10)
11
12import amaze

As usual we start by importing the required package. Here, however, we will explicitly qualify every amaze members to better distinguish from class imported from PyQT (which all start with a Q).

15class MainWindow(QWidget):

To make this page understandable, we define a class holding everything together. The main, presented at the bottom will only have to create our custom class and display it.

16    def __init__(self):
17        super().__init__()
18        self.resize(640, 480)
19
20        # Create horizontal layout
21        layout = QHBoxLayout()
22        self.setLayout(layout)
23
24        # Create dedicated widget for rendering mazes
25        self.maze_widget = amaze.MazeWidget(
26            self._maze_data(0, 5, 0, True, amaze.StartLocation.SOUTH_WEST),
27            amaze.Robot(amaze.Robot.BuildData.from_string("D")),
28            dict(robot=False),
29        )
30        layout.addWidget(self.maze_widget)
31
32        # Build content
33        self.widgets, sub_layout = self._create_widgets()
34        layout.addLayout(sub_layout)

First, the constructor delegates the bulk of creating a top-level widget to the PyQT library. We, then, create a horizontal layout to lay multiple items next to one another. In this primitive interface, we place a MazeWidget on the left while a secondary layout will hold the configuration widgets.

36    def _create_widgets(self):
37        # Create a secondary vertical layout for inputs
38        sub_layout = QFormLayout()
39
40        widgets = {}
41
42        def _add(cls, name, signal, func=None):
43            sub_layout.addRow(name, _w := cls())
44            if func is not None:
45                func(_w)
46            getattr(_w, signal).connect(self.reset_maze)
47            widgets[name] = _w
48
49        _add(QSpinBox, "Seed", "valueChanged")
50
51        _add(QSpinBox, "Size", "valueChanged", lambda w: w.setRange(5, 20))
52
53        _add(
54            QDoubleSpinBox,
55            "Lures",
56            "valueChanged",
57            lambda w: (w.setRange(0, 100), w.setSuffix("%")),
58        )
59
60        _add(QCheckBox, "Unicursive", "clicked", lambda w: w.setChecked(True))
61
62        _add(
63            QComboBox,
64            "Start",
65            "currentTextChanged",
66            lambda w: w.addItems([s.name for s in amaze.StartLocation]),
67        )
68
69        return widgets, sub_layout

We then create specific widgets to customize the maze thanks to the helper function _add. It’s job is to instantiate the widget and add it to the layout. The QFormLayout is a special case of vertical layout that places, next to one another, a widget and its string label. If the newly created widget needs further configuration we call the provided function to set up things like range or content. We then connect the widget’s signal to our reset_maze function so that whenever the user inputs new values, the maze is changed accordingly. Finally, we store everything for future reference.

For this interface we provide 5 configurable options of various types:

  • Two QSpinBox will provide integer input in a given range

  • One QDoubleSpinBox provides the same functionality but for float values

  • A QCheckBox allows binary input

  • A QComboBox lets the user choose from amongst a set of strings

71    def reset_maze(self):
72        self.maze_widget.set_maze(
73            self._maze_data(
74                self.widgets["Seed"].value(),
75                self.widgets["Size"].value(),
76                self.widgets["Lures"].value() / 100,
77                self.widgets["Unicursive"].isChecked(),
78                amaze.StartLocation[self.widgets["Start"].currentText()],
79            )
80        )

The function used to reset the maze is rather trivial as it only fetches values from the interface to populate a BuildData dataclass. However it also demonstrates of the specific widgets we used expose their values programatically: value for (Double)SpinBox, isChecked for CheckBox and currentText for ComboBox.

 99def main(is_test=False):
100    # Create main QT objects
101    app = amaze.qt_application()
102    window = MainWindow()
103    window.show()
104
105    if is_test:
106        QTimer.singleShot(1000, lambda: window.close())
107
108    # Run
109    app.exec()

Finally, as stated above, the consists only of creating an application and our primitive main window, requesting it to be shown and letting PyQT handle the rest.

As before, the full listing of the example is provided below.

Full listing for examples/interface.py
  1from PyQt5.QtCore import QTimer
  2from PyQt5.QtWidgets import (
  3    QWidget,
  4    QHBoxLayout,
  5    QFormLayout,
  6    QSpinBox,
  7    QDoubleSpinBox,
  8    QCheckBox,
  9    QComboBox,
 10)
 11
 12import amaze
 13
 14
 15class MainWindow(QWidget):
 16    def __init__(self):
 17        super().__init__()
 18        self.resize(640, 480)
 19
 20        # Create horizontal layout
 21        layout = QHBoxLayout()
 22        self.setLayout(layout)
 23
 24        # Create dedicated widget for rendering mazes
 25        self.maze_widget = amaze.MazeWidget(
 26            self._maze_data(0, 5, 0, True, amaze.StartLocation.SOUTH_WEST),
 27            amaze.Robot(amaze.Robot.BuildData.from_string("D")),
 28            dict(robot=False),
 29        )
 30        layout.addWidget(self.maze_widget)
 31
 32        # Build content
 33        self.widgets, sub_layout = self._create_widgets()
 34        layout.addLayout(sub_layout)
 35
 36    def _create_widgets(self):
 37        # Create a secondary vertical layout for inputs
 38        sub_layout = QFormLayout()
 39
 40        widgets = {}
 41
 42        def _add(cls, name, signal, func=None):
 43            sub_layout.addRow(name, _w := cls())
 44            if func is not None:
 45                func(_w)
 46            getattr(_w, signal).connect(self.reset_maze)
 47            widgets[name] = _w
 48
 49        _add(QSpinBox, "Seed", "valueChanged")
 50
 51        _add(QSpinBox, "Size", "valueChanged", lambda w: w.setRange(5, 20))
 52
 53        _add(
 54            QDoubleSpinBox,
 55            "Lures",
 56            "valueChanged",
 57            lambda w: (w.setRange(0, 100), w.setSuffix("%")),
 58        )
 59
 60        _add(QCheckBox, "Unicursive", "clicked", lambda w: w.setChecked(True))
 61
 62        _add(
 63            QComboBox,
 64            "Start",
 65            "currentTextChanged",
 66            lambda w: w.addItems([s.name for s in amaze.StartLocation]),
 67        )
 68
 69        return widgets, sub_layout
 70
 71    def reset_maze(self):
 72        self.maze_widget.set_maze(
 73            self._maze_data(
 74                self.widgets["Seed"].value(),
 75                self.widgets["Size"].value(),
 76                self.widgets["Lures"].value() / 100,
 77                self.widgets["Unicursive"].isChecked(),
 78                amaze.StartLocation[self.widgets["Start"].currentText()],
 79            )
 80        )
 81
 82    @staticmethod
 83    def _maze_data(seed, size, p_lure, easy, start):
 84        return amaze.Maze.BuildData(
 85            seed=seed,
 86            width=size,
 87            height=size,
 88            unicursive=easy,
 89            rotated=True,
 90            start=start,
 91            clue=[amaze.Sign(value=1)],
 92            p_lure=p_lure,
 93            lure=[amaze.Sign(value=0.5)],
 94            p_trap=0,
 95            trap=[],
 96        )
 97
 98
 99def main(is_test=False):
100    # Create main QT objects
101    app = amaze.qt_application()
102    window = MainWindow()
103    window.show()
104
105    if is_test:
106        QTimer.singleShot(1000, lambda: window.close())
107
108    # Run
109    app.exec()
110
111
112if __name__ == "__main__":
113    main()