Skip to content

peterhinch/micropython-micro-gui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

micropython-micro-gui

This is a lightweight, portable, MicroPython GUI library for displays having drivers subclassed from framebuf. Written in Python it runs under a standard MicroPython firmware build. Options for data input comprise:

  • Two pushbuttons: limited capabilities with some widgets unusable for input.
  • Three pushbuttons with full capability.
  • Five pushbuttons: full capability, less "modal" interface.
  • A switch-based navigation joystick: another way to implement five buttons.
  • Via two pushbuttons and a rotary encoder such as this one. An intuitive interface.
  • On ESP32 physical buttons may be replaced with touchpads.

It is larger and more complex than nano-gui owing to the support for input. It enables switching between screens and launching modal windows. Widgets are a substantial superset of nano-gui widgets.

It is compatible with all display drivers for nano-gui so is portable to a wide range of displays. It is also portable between hosts.

Image
Raspberry Pico with an ILI9341 from eBay.

Image
TTGO T-Display. A joystick switch and an SIL resistor make a simple inexpensive and WiFi-capable system.

Image
micro_gui now has limited support for ePaper.

Rationale

Touch GUI's are supported by micropython-touch. This GUI provides an alternative for displays without a touch overlay. A non-touch solution avoids the need for calibration and can also save cost. Cheap Chinese touch displays often marry a good display to a poor touch overlay. It can make sense to use such a screen with micro-gui, ignoring the touch overlay. For touch support it is worth spending money on a good quality device (for example Adafruit).

The micro-gui input options work well and can yield inexpensive solutions. A network-connected board with a 135x240 color display can be built for under £20 ($20?) using the TTGO T-Display. The test board shown above has a 320x240 display from eBay with a Pi Pico and has a component cost of well below £20.

The following are similar GUI repos with differing objectives.

  • nano-gui Extremely low RAM usage but display-only with no provision for input.
  • LCD160cr Touch GUI for the official display.
  • RA8875 Touch GUI for displays with RA8875 controller. Supports large displays, e.g. from Adafruit.
  • SSD1963 Touch GUI for displays based on SSD1963 and XPT2046. High performance on large displays due to the parallel interface. Specific to STM hosts.

LVGL is a pretty icon-based GUI library. It is written in C with MicroPython bindings; consequently it requires the build system for your target and a C device driver (unless you can acquire a suitable binary).

Project status

April 2024: Add screen replace feature for non-tree navigation. Sept 2023: Add "encoder only" mode suggested by @eudoxos.
April 2023: Add limited ePaper support, grid widget, calendar and epaper demos. Now requires firmware >= V1.20.
July 2022: Add ESP32 touch pad support.
June 2022: Add QRMap and BitMap widgets.
March 2022: Add latency control for hosts with SPIRAM.
February 2022: Supports use with only three buttons devised by Bart Cerneels. Simplified widget import. Existing users should replace the entire gui tree.

Code has been tested on ESP32, ESP32-S2, ESP32-S3, Pi Pico and Pyboard. This is under development so check for updates.

0. Contents

  1. Basic concepts Including "Hello world" script.
    1.1 Coordinates The GUI's coordinate system.
    1.2 Screen Window and Widget objects Basic GUI classes.
    1.3 Fonts
    1.4 Navigation Options for hardware. How the GUI navigates between widgets.
         1.4.1 Encoder-only mode Using only an encoder for navigation.
    1.5 Hardware definition How to configure your hardware.
    1.6 Quick hardware check Testing the hardware config. Please do this first.
    1.7 Installation Installing the library.
    1.8 Performance and hardware notes
    1.9 Firmware and dependencies
    1.10 Supported hosts and displays
    1.11 Files Discussion of the files in the library.
         1.11.1 Demos Simple demos showing coding techniques.
         1.11.2 Test scripts GUI tests, some needing larger displays
    1.12 Floating Point Widgets How to input floating point data.
  2. Usage Application design.
    2.1 Program structure and operation A simple demo of navigation and use.
    2.2 Callbacks
    2.3 Colors
         2.3.1 Monochrome displays
  3. The ssd and display objects
    3.1 SSD class Instantiation in hardware_setup.
    3.2 Display class Instantiation in hardware_setup.py.
         3.2.1 Encoder usage
         3.2.2 Encoder only mode
  4. Screen class Full screen window.
    4.1 Class methods
    4.2 Constructor
    4.3 Callback methods Methods which run in response to events.
    4.4 Method Optional interface to asyncio code.
    4.5 Class variable Control latency caused by garbage collection.
    4.6 Usage Accessing data created in a screen.
  5. Window class
    5.1 Constructor
    5.2 Class method
    5.3 Popup windows
  6. Widgets Displayable objects.
    6.1 Label widget Single line text display.
         6.1.1 Grid widget A spreadsheet-like array of labels.
    6.2 LED widget Display Boolean values.
    6.3 Checkbox widget Enter Boolean values.
    6.4 Button and CloseButton widgets Pushbutton emulation.
    6.5 ButtonList object Pushbuttons with multiple states.
    6.6 RadioButtons object One-of-N pushbuttons.
    6.7 Listbox widget
    6.8 Dropdown widget Dropdown lists.
    6.9 DialogBox class Pop-up modal dialog boxes.
    6.10 Textbox widget Scrolling text display.
    6.11 Meter widget Display floats on an analog meter, with data driven callbacks.
         6.11.1 Region class
    6.12 Slider and HorizSlider widgets Linear potentiometer float data entry and display
    6.13 Scale widget High precision float entry and display.
    6.14 ScaleLog widget Wide dynamic range float entry and display.
    6.15 Dial widget Display multiple vectors.
    6.16 Knob widget Rotary potentiometer float entry.
    6.17 Adjuster widget Space saving way to enter floats.
    6.18 Menu class
    6.19 BitMap widget Draw bitmaps from files.
    6.20 QRMap widget Draw QR codes created by uQR.
  7. Graph plotting Widgets for Cartesian and polar graphs.
    7.1 Concepts
         7.1.1 Graph classes
         7.1.2 Curve classes
         7.1.3 Coordinates
    7.2 Graph classes
         7.2.1 Class CartesianGraph
         7.2.2 Class PolarGraph
    7.3 Curve classes
         7.3.1 Class Curve
         7.3.2 Class PolarCurve
    7.4 Class TSequence Plotting realtime, time sequential data.
  8. ESP32 touch pads Replacing buttons with touch pads.
  9. Realtime applications Accommodating tasks requiring fast RT performance.
  10. ePaper displays Guidance on using ePaper displays.

Appendix 1 Application design Tab order, button layout, encoder interface, use of graphics primitives, more on callbacks. Appendix 2 Freezing bytecode Optional way to save RAM.
Appendix 3 Cross compiling Another way to save RAM.

1. Basic concepts

Internally micro-gui uses asyncio. It presents a conventional callback based interface; knowledge of asyncio is not required for its use. Display refresh is handled automatically. Widgets are drawn using graphics primitives rather than icons. This makes them efficiently scalable and minimises RAM usage compared to icon-based graphics. It also facilitates the provision of extra visual information. For example the color of all or part of a widget may be changed programmatically, for example to highlight an overrange condition. There is limited support for icons in pushbuttons via icon fonts, also via the BitMap widget.

The following, taken from gui.demos.simple.py, is a complete application. It shows a message and has "Yes" and "No" buttons which trigger a callback.

import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton
# from gui.core.writer import Writer  # Monochrome display
from gui.core.writer import CWriter
# Font for CWriter or Writer
import gui.fonts.arial10 as arial10
from gui.core.colors import *


class BaseScreen(Screen):

    def __init__(self):

        def my_callback(button, arg):
            print('Button pressed', arg)

        super().__init__()
        # wri = Writer(ssd, arial10, verbose=False)  # Monochrome display
        wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)

        col = 2
        row = 2
        Label(wri, row, col, 'Simple Demo')
        row = 50
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 60
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))
        CloseButton(wri)  # Quit the application

def test():
    print('Simple demo: button presses print to REPL.')
    Screen.change(BaseScreen)  # A class is passed here, not an instance.

test()

Notes:

  • Monochrome displays use the Writer class rather than CWriter to render fonts, as per the commented-out code above.
  • Hardware is defined by a single small file hardware_setup.py which the user must edit.

1.1 Coordinates

These are defined as row and col values where row==0 and col==0 corresponds to the top left most pixel. Rows increase downwards and columns increase to the right. The graph plotting widget uses normal mathematical conventions within graphs.

1.2 Screen Window and Widget objects

A Screen is a window which occupies the entire display. A Screen can overlay another, replacing all its contents. When closed, the Screen below is re-displayed. This default method of navigation results in a tree structure of Screen instances where the screen below retains state. An alternative allows a Screen to replace another, allowing Screen instances to be navigated in an arbitrary way. For example a set of Screen instances might be navigated in a circular fashion. The penalty is that, to save RAM, state is not retained when a Screen is replaced

A Window is a subclass of Screen but is smaller, with size and location attributes. It can overlay part of an underlying Screen and is typically used for dialog boxes. Window objects are modal: a Window can overlay a Screen but cannot overlay another Window.

A Widget is an object capable of displaying data. Some are also capable of data input: such a widget is defined as active. A passive widget can only display data. An active widget can acquire focus. The widget with focus is able to respond to user input. See navigation. Widget objects have dimensions defined as height and width. The space requred by them exceeds these dimensions by two pixels all round. This is because micro-gui displays a surrounding white border to show which object currently has focus. Thus to place a Widget at the extreme top left, row and col values should be 2.

1.3 Fonts

Python font files are in the gui/fonts directory. The easiest way to conserve RAM is to freeze them which is highly recommended. In doing so the directory structure must be maintained.

To create alternatives, Python fonts may be generated from industry standard font files with font_to_py.py. The -x option for horizontal mapping must be specified. If fixed pitch rendering is required -f is also required. Supplied examples are:

  • arial10.py Variable pitch Arial. 10 pixels high.
  • arial35.py Arial 35 high.
  • arial_50.py Arial 50 high.
  • courier20.py Fixed pitch Courier, 20 high.
  • font6.py FreeSans 14 high.
  • font10.py FreeSans 17 high.
  • freesans20.py FreeSans 20 high.

The directory gui/fonts/bitmaps is only required for the bitmap.py demo.

1.4 Navigation

The GUI requires from 2 to 5 pushbuttons for control. These are:

  1. Next Move to the next widget.
  2. Select Operate the currently selected widget.
  3. Prev Move to the previous widget.
  4. Increase Move within the widget (i.e. adjust its value).
  5. Decrease Move within the widget.

An alternative is to replace buttons 4 and 5 with a quadrature encoder knob such as this one. That device has a switch which operates when the knob is pressed: this may be wired for the Select button. This provides the most intuitive operation.

Many widgets such as Pushbutton or Checkbox objects require only the Select button to operate: it is possible to design an interface with a subset of micro-gui widgets which requires only the first two buttons. With three buttons all widgets may be used without restriction.

Widgets such as Listbox objects, dropdown lists (Dropdown), and those for floating point data entry can use the Increase and Decrease buttons (or an encoder) to select a data item or to adjust the linear value. If three buttons are provided, the GUI will enter "adjust" mode in response to a double-click of Select. In this mode Prev and Next act to decrease and increase the widget's value. A further double-click restores normal navigation. This is discussed in Floating Point Widgets.

The currently selected Widget is identified by a white border: the focus moves between widgets via Next and Prev. Only active Widget instances (those that can accept input) can receive the focus. Widgets are defined as active or passive in the constructor, and this status cannot be changed. In some cases the state can be specified as a constructor arg, but other widgets have a predefined state. An active widget can be disabled and re-enabled at runtime. A disabled active widget is shown "greyed-out" and cannot accept the focus until re-enabled.

1.4.1 Encoder only mode

This uses a rotary encoder with a built-in pushbutton as the sole means of navigation, a mode suggested by @eudoxos. By default, turning the dial moves the currency between widgets; the widget with the focus has a white border. Widgets for numeric entry such as sliders and scales may be put into "adjust" mode with a double click. In that mode turning the dial adjusts the widget. Floating Point Widgets can enter "precision" adjustment mode with a long press of the button. "Adjust" and "precision" modes are cleared with a short button press.

This mode works well and its use is quite intuitive. Navigation by turning a dial makes it particularly useful when a screen has a large number of widgets.

1.5 Hardware definition

A file hardware_setup.py must exist in the GUI root directory. This defines the connections to the display, the display driver, and pins used for the pushbuttons. Example files may be found in the setup_examples directory. Further examples (without pin definitions) are in this nano-gui directory.

The following is a typical example for a Raspberry Pi Pico driving an ILI9341 display:

from machine import Pin, SPI, freq
import gc

from drivers.ili93xx.ili9341 import ILI9341 as SSD
freq(250_000_000)  # RP2 overclock
# Create and export an SSD instance
pdc = Pin(8, Pin.OUT, value=0)  # Arbitrary pins
prst = Pin(9, Pin.OUT, value=1)
pcs = Pin(10, Pin.OUT, value=1)
spi = SPI(0, baudrate=30_000_000)
gc.collect()  # Precaution before instantiating framebuf
# Instantiate display and assign to ssd. For args see display drivers doc.
ssd = SSD(spi, pcs, pdc, prst, usd=True)
# The following import must occur after ssd is instantiated.
from gui.core.ugui import Display, quiet
# quiet()
# Define control buttons
nxt = Pin(19, Pin.IN, Pin.PULL_UP)  # Move to next control
sel = Pin(16, Pin.IN, Pin.PULL_UP)  # Operate current control
prev = Pin(18, Pin.IN, Pin.PULL_UP)  # Move to previous control
increase = Pin(20, Pin.IN, Pin.PULL_UP)  # Increase control's value
decrease = Pin(17, Pin.IN, Pin.PULL_UP)  # Decrease control's value
# Create a Display instance and assign to display.
display = Display(ssd, nxt, sel, prev, increase, decrease)

Where an encoder replaces the increase and decrease buttons, only the final line needs to be changed to provide an extra arg:

display = Display(ssd, nxt, sel, prev, increase, decrease, 4)

The final arg specifies the sensitivity of the attached encoder, the higher the value the more the knob has to be turned for a desired effect. A value of 1 provides the highest sensitivity, being the native rate of the encoder. Many encoders have mechanical detents: a value of 4 matches the click rate of most devices.

The commented-out quiet() line provides a means of suppressing diagnostic messages.

Instantiation of SSD and Display classes is detailed in section 3.

Display drivers are documented here.

1.6 Quick hardware check

The following may be pasted at the REPL to verify correct connection to the display. It also confirms that hardware_setup.py is specifying a suitable display driver.

from hardware_setup import ssd  # Create a display instance
from gui.core.colors import *
ssd.fill(0)
ssd.line(0, 0, ssd.width - 1, ssd.height - 1, GREEN)  # Green diagonal corner-to-corner
ssd.rect(0, 0, 15, 15, RED)  # Red square at top left
ssd.rect(ssd.width -15, ssd.height -15, 15, 15, BLUE)  # Blue square at bottom right
ssd.show()

1.7 Installation

Please ensure device firmware is up to date. Clone the repo to the PC with:

$ git clone https://github.com/peterhinch/micropython-micro-gui
$ cd micropython-micro-gui

In the micropython-micro-gui directory edit hardware_setup.py to match the hardware in use.

The official mpremote tool is recommended. Install with:

$ pip3 install mpremote

There are several options for installation

  1. Using mpremote to run the GUI demos via the PC without installing.
  2. Subtractive. Installing the entire GUI, then (optionally) removing unused components.
  3. Additive. Installing a minimal subset and manually adding extra components.
  4. Using frozen bytecode.

Testing without installing

The easy way to start is to use mpremote which allows a directory on your PC to be mounted on the host. In this way the filesystem on the host is left unchanged. This is at some cost in loading speed, especially on ESP32. In the micropython-micro-gui directory run:

$ mpremote mount .

This should provide a REPL. Run the minimal demo:

>>> import gui.demos.simple

If this runs the hardware is correctly configured and other demos should run.

Installing a display driver

It is necessary to install a display driver prior to any GUI installation. On networked hardware a display driver may be installed as follows (example is for ST7789):

>>> mip.install("github:peterhinch/micropython-nano-gui/drivers/st7789")

The last part of the addresss (st7789) is the name of the directory holding drivers for the display in use. In cases where the directory holds more than one driver all will be installed. Unused drivers may be deleted.

Install using mpremote on the PC as follows:

$ mpremote mip install "github:peterhinch/micropython-nano-gui/drivers/st7789"

Full installation (subtractive)

The entire GUI is large. It is possible to install it all from the PC clone by issuing:

$ cd micropython-micro-gui
$ mpremote cp -r gui :
$ mpremote cp hardware_setup.py :

This is rather profligate with Flash storage. There is great scope for discarding unused fonts, demos and widgets. As an alternative to installing everything and pruning, an additive approach may be used where a minimal subset is installed with extra fonts and widgets being added as required.

Minimal installation (additive)

This installs a subset adequate to run the simple.py demo. It comprises:
Image
Note that mip and mpremote mip install to /lib/ which therefore becomes the root of the above tree. The subset is installed with (on the device):

>>> mip.install("github:peterhinch/micropython-micro-gui")

or (on the PC):

$ mpremote mip install "github:peterhinch/micropython-micro-gui"

In both cases the edited hardware_setup.py must be copied from the PC:

$ cd micropython-micro-gui
$ mpremote cp hardware_setup.py :

When adding components the directory structure must be maintained. For example, in the micropython-micro-gui directory:

$ mpremote cp gui/fonts/font10.py :/gui/fonts/
$ mpremote cp gui/widgets/checkbox.py :/gui/widgets/

Freezing bytecode

There is scope for speeding loading and saving RAM by using frozen bytecode. The entire gui tree may be frozen but the directory structure must be maintained. For reasons that are unclear freezing display drivers may not work. For fexibility, consider keeping hardware_setup.py in the filesystem. See Appendix 2 Freezing bytecode.

1.8 Performance and hardware notes

RAM usage

Running the linked_sliders demo, the code uses about 23,000 bytes with frozen bytecode and 55,000 bytes without. To this must be added the size of the frame buffer. This can readily be calculated. For example in the case of the ILI9341 (a 240x320 pixel unit whose driver uses 4-bit color) the buffer size is
240x320/2 = 38,400 bytes.

A Pico shows ~182000 bytes free with no code running. With linked_sliders running on an ILI9341 display, it shows 120,896 bytes free with frozen bytecode and 88,640 bytes free without.

With multi-pixel displays the size of the frame buffer can prevent the GUI from compiling. If frozen bytecode is impractical, consider cross-compiling. See Appendix 3 Cross compiling.

Speed

The consequence of inadequate speed is that brief button presses can be missed. This is because display update blocks for tens of milliseconds, during which time the pushbuttons are not polled. This can be an issue in displays with a large number of pixels, multi-byte colors and/or slow SPI clock rates. In high resolution cases the device driver has specfic asyncio support whereby the driver yields to the scheduler a few times during the refresh.Currently this exists on ILI9486, ILI9341 and ST7789 (e.g. TTGO T-Display). By my calculations and measurements this should be unnecessary on other drivers, but please report any tendency to miss button presses and I will investigate.

This may be mitigated by two approaches:

  1. Clocking the SPI bus as fast as possible. This is discussed in the drivers doc.
  2. Clocking the host fast (machine.freq).

Platform notes

On ESP32 (including the TTGO T-Display) note that pins 36-39 are input-only and do not have pullup support: if these are used for pushbutton input, physical pullups to 3.3V should be used. See ref.

On a Pyboard 1.1 with 320x240 ili9341 display it was necessary to use frozen bytecode: in this configuration running the various.py demo there was 29K of free RAM. Note that, at 37.5KiB, this display is the worst-case in terms of RAM usage. A smaller display or a Pyboard D would offer more headroom. Frozen bytecode was also necessary on an RP2 running an ILI9486: a 480x320 display requires a 76,800 byte frame buffer.

1.9 Firmware and dependencies

Firmware should be V1.17 or later. The source tree includes all dependencies. These are listed to enable users to check for newer versions or to read docs:

  • writer.py Provides text rendering of Python font files.
  • SSD1306 driver. A copy of the official driver for OLED displays using the SSD1306 chip is provided. The link is to the official file.
  • Synchronisation primitives. The link is to my asyncio support repo.
  • PCD8544/Nokia 5110. Displays based on the Nokia 5110 (PCD8544 chip) require this driver. It is not provided in this repo. The link is to its source.

1.10 Supported hosts and displays

Development was done using a Raspberry Pi Pico connected to a cheap ILI9341 320x240 display. I have also tested a TTGO T-Display (an ESP32 host) and a Pyboard. Code is written with portability as an aim, but MicroPython configs vary between platforms and I can't guarantee that every widget will work on every platform. For example, some use the cmath module which may be absent on some builds.

Supported displays are as per the nano-gui list. In general ePaper and Sharp displays are unlikely to yield good results because of slow and visually intrusive refreshing. However there is an exception: the Waveshare pico_epaper_42. See 10. ePaper displays.

Display drivers are documented here.

1.11 Files

Display drivers may be found in the drivers directory. These are copies of those in nano-gui, included for convenience. Note the file drivers/boolpalette.py, required by all color drivers.

The system is organised as a Python package with the root being gui. Core files in gui/core are:

  • colors.py Constants including colors and shapes.
  • ugui.py The main GUI code.
  • writer.py Supports the Writer and CWriter classes.

The gui/primitives directory contains the following files:

  • pushbutton.py Interface to physical pushbuttons and ESP32 touch pads.
  • delay_ms.py A software triggerable timer.
  • encoder.py Driver for a quadrature encoder. This offers an alternative interface - see Appendix 1.

The gui/demos directory contains a variety of demos and tests described below.

1.11.1 Demos

Demos are run by issuing (for example):

>>> import gui.demos.simple

If shut down cleanly with the "close" button a demo can be re-run with (e.g.):

gui.demos.simple.test()

Before running a different demo the host should be reset (ctrl-d) to clear RAM.

These will run on screens of 128x128 pixels or above. The initial ones are minimal and aim to demonstrate a single technique.

  • simple.py Minimal demo discussed below. Button presses print to REPL.
  • checkbox.py A Checkbox controlling an LED.
  • slider.py A Slider whose color varies with its value.
  • slider_label.py A Slider updating a Label. Good for trying precision mode.
  • linked_sliders.py One Slider updating two others, and a coding "wrinkle" required for doing this.
  • dropdown.py A dropdown list (with scrolling) updates a Label.
  • listbox.py A listbox with scrolling.
  • dialog.py DialogBox demo. Illustrates the screen change mechanism.
  • screen_change.py A Pushbutton causing a screen change using a re-usable "forward" button.
  • screen_replace.py A more complex (non-tree) screen layout.
  • primitives.py Use of graphics primitives.
  • aclock.py An analog clock using the Dial vector display. Also shows screen layout using widget metrics. Has a simple asyncio task.
  • tbox.py Text boxes and user-controlled scrolling.
  • tstat.py A demo of the Meter class with data sensitive regions.
  • menu.py A multi-level menu.
  • adjuster.py Simple demo of the Adjuster control.
  • adjust_vec.py A pair of Adjusters vary a vector.
  • bitmap.py Demo of the BitMap widget showing a changing image.
  • qrcode.py Display a QR code. Requires the uQR module.
  • calendar.py Demo of grid widget.
  • epaper.py Warts-and-all demo for an ePaper display. Currently the only supported display is the Waveshare pico_epaper_42 with Pico or other host.

1.11.2 Test scripts

These more complex demos are run in the same way by issuing (for example):

>>> import gui.demos.active

Some of these require larger screens. Required sizes are specified as (height x width).

  • active.py Demonstrates active controls providing floating point input (240x320).
  • plot.py Graph plotting (128x200).
  • screens.py Listbox, dropdown and dialog boxes (128x240).
  • various.py Assorted widgets including the different types of pushbutton (240x320).
  • vtest.py Clock and compass styles of vector display (240x320).
  • calendar.py Demo of grid control (240x320 - but could be reduced).

1.12 Floating Point Widgets

Some applications need to adjust a data value with an extremely large dynamic range. This is the ratio of the data value's total range to the smallest adjustment that can be made. The mechanism currently implemented enables a precision of 0.05%.

Floating point widgets respond to a brief press of the increase or decrease buttons by adjusting the value by a small amount. A continued press causes the value to be repeatedly adjusted, with the amount of the adjustment increasing with time. This enables the entire range of the control to be accessed quickly, while allowing small changes of 0.5%. This works well. In many cases the level of precision will suffice. An encoder provides similar performance.

Fine adjustments may be achieved by pressing the select button for at least one second. The GUI will respond by changing the border color from white (i.e. has focus) to yellow. In this mode a brief press of increase or decrease or small movement of an encoder will have a reduced effect (0.05%). Fine mode may be cancelled by pressing select or by moving the focus to another control. This also works in three-button mode, with Next and Prev performing the adjustments.

In the case of slider and knob controls the precision of fine mode exceeds that of the visual appearance of the widget: fine changes can be too small to see. Options are to use the Scale widget or to have a linked Label showing the widget's exact value.

The callback runs whenever the widget's value changes. This causes the callback to run repeatedly while the user adjusts the widget. This is required if there is a linked Label to update.

2. Usage

2.1 Program structure and operation

The following is a minimal script (found in gui.demos.simple.py) which will run on a minimal system with a small display and two pushbuttons. Commented out code shows changes for monochrome displays.

The demo provides two Button widgets with "Yes" and "No" legends. It may be run by issuing at the REPL:

>>> import gui.demos.simple

Note that the import of hardware_setup.py is the first line of code. This is because the frame buffer is created here, with a need for a substantial block of contiguous RAM.

import hardware_setup  # Instantiate display, setup color LUT (if present)
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton
# from gui.core.writer import Writer  # Monochrome display
from gui.core.writer import CWriter

# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *


class BaseScreen(Screen):

    def __init__(self):

        def my_callback(button, arg):
            print('Button pressed', arg)

        super().__init__()
        # wri = Writer(ssd, arial10, verbose=False)
        wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)

        col = 2
        row = 2
        Label(wri, row, col, 'Simple Demo')
        row = 20
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 60
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))
        CloseButton(wri)  # Quit the application

def test():
    print('Testing micro-gui...')
    Screen.change(BaseScreen)

test()

Note how the Next pushbutton moves the focus between the two buttons and the "X" close button. The focus does not move to the "Simple Demo" widget because it is not active: a Label cannot accept user input. Pushing the Select pushbutton while the focus is on a Pushbutton causes the callback to run.

Applications start by performing Screen.change() to a user-defined Screen object. This must be subclassed from the GUI's Screen class. Note that Screen.change accepts a class name, not a class instance.

The user defined BaseScreen class constructor instantiates all widgets to be displayed and typically associates them with callback functions - which may be bound methods. Screens typically have a CloseButton widget. This is a special Pushbutton subclass which displays as an "X" at the top right corner of the physical display and closes the current screen, showing the one below. If used on the bottom level Screen (as above) it closes the application.

The CWriter instance wri associates a widget with a font. Constructors for all widgets have three mandatory positional args. These are a CWriter instance followed by row and col. These args are followed by a number of optional keyword args. These have (hopefully) sensible defaults enabling you to get started easily. Monochrome displays use the simpler Writer class.

2.2 Callbacks

The interface is event driven. Widgets may have optional callbacks which will be executed when a given event occurs. Events occur when a widget's properties are changed programmatically, and also (in the case of active widgets) in response to user input.

A callback function receives positional arguments. The first is a reference to the object raising the callback. Subsequent arguments are user defined, and are specified as a tuple or list of items. Callbacks and their argument lists are optional: a default null function and empty tuple are provided. Callbacks may optionally be written as bound methods. This facilitates communication between widgets.

When writing callbacks take care to ensure that the correct number of arguments are passed, bearing in mind the first arg described above. An incorrect argument count results in puzzling tracebacks which appear to implicate the GUI code. This is because it is the GUI which actually executes the callbacks.

Callbacks should complete quickly. See Appendix 1 Application design for discussion of this.

2.3 Colors

The file gui/core/colors.py defines a set of color constants which may be used with any display driver. This section describes how to change these or to create additional colors. Most of the color display drivers define colors as 8-bit or larger values. For the larger displays 4-bit drivers are provided with the aim of conserving RAM.

In the 4-bit case colors are assigned to a lookup table (LUT) with 16 entries. The frame buffer stores 4-bit color values, which are converted to the correct color depth for the hardware when the display is refreshed. Of the 16 possible colors 13 are assigned in gui/core/colors.py, leaving color numbers 12, 13 and 14 free.

The following code is portable between displays and creates a user defined color PALE_YELLOW.

from gui.core.colors import *  # Imports the create_color function
PALE_YELLOW = create_color(12, 150, 150, 0)  # index, r, g, b

If a 4-bit driver is in use, the color rgb(150, 150, 0) will be assigned to "spare" color number 12. Any color number in range 0 <= n <= 15 may be used, implying that predefined colors may be reassigned. It is recommended that BLACK (0) and WHITE (15) are not changed. If an 8-bit or larger driver is in use, the color number is ignored and there is no practical restriction on the number of colors that may be created.

In the above example, regardless of the display driver, the PALE_YELLOW variable may be used to refer to the color. An example of custom color definition may be found in this nano-gui demo.

There are five default colors which are defined by a color_map list. These may be reassigned in user code. For example the following will cause the border of any control with the focus to be red:

from colors import *
color_map[FOCUS] = RED

The color_map index constants and default colors (defined in colors.py) are:

Index Color Purpose
FOCUS WHITE Border of control with focus
PRECISION YELLOW Border in precision mode
FG WHITE Window foreground default
BG BLACK Background default including screen clear
GREY_OUT GREY Color to render greyed-out controls

2.3.1 Monochrome displays

Most widgets work on monochrome displays if color settings are left at default values. If a color is specified, drivers in this repo will convert it to black or white depending on its level of saturation. A low level will produce the background color, a high level the foreground.

At the bit level 1 represents the foreground. This is white on an emitting display such as an OLED. On a Sharp display it indicates reflection.

There is an issue regarding ePaper displays discussed here. The driver for the Waveshare pico_epaper_42 renders colored objects as black on white.

3. The ssd and display objects

The following code, issued as the first executable lines of an application, initialises the display.

import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd, display  # display symbol is seldom needed

The hardware_setup file creates singleton instances of SSD and Display classes. These instances are made available via ugui. Normal GUI applications only need to import ssd. This refererence to the display driver is used to initialise Writer objects. Bound variables ssd.height and ssd.width may be read to determine the dimensions of the display hardware.

The display object is only needed in applications which use graphics primitives to write directly to the screen. See Appendix 1 Application design.

3.1 SSD class

This is instantiated in hardware_setup.py. The specific class must match the display hardware in use. Display drivers are documented here.

3.2 Display class

This is instantiated in hardware_setup.py. It registers the SSD instance along with the Pin instances used for input; also whether an encoder is used. Pins are arbitrary, but should be defined as inputs with pullups. Pushbuttons are connected between Gnd and the relevant pin.

The constructor takes the following positional args:

  1. objssd The SSD instance. A reference to the display driver.
  2. nxt A Pin instance for the next button.
  3. sel A Pin instance for the select button.
  4. prev=None A Pin instance for the previous button (if used).
  5. incr=None A Pin instance for the increase button (if used).
  6. decr=None A Pin instance for the decrease button (if used).
  7. encoder=False If an encoder is used, an integer must be passed.
  8. touch=False Supply an integer to use ESP32 TouchPad instances in place of all physical pushbuttons. See ESP32 touch pads.

Class variables:

  • verbose=True Causes a message to be printed indicating whether an encoder was specified.

3.2.1 Encoder usage

If an encoder is used, it should be connected to the pins assigned to increase and decrease. If the direction of movement is wrong, these pins should be transposed (physically or in code).

To specify to the GUI that an encoder is in use an integer should be passed to the Display constructor encoder arg. Its value represents the division ratio. A value of 1 defines the native rate of the encoder; if the native rate is 32 pulses per revolution, a value of 4 would yield a virtual device with 8 pulses per rev. A value of 4 matches most encoders with mechanical detents.

If an encoder is used but the encoder arg is False, response to the encoder will be erratic.

3.2.2 Encoder only mode

This uses an encoder with an included pushbutton as the sole means of control. To use this mode, constructor args should be:

  1. objssd The SSD instance. A reference to the display driver.
  2. nxt A Pin instance attached to the encoder X pin.
  3. sel A Pin instance attached to the encoder button.
  4. prev A Pin instance attached to the encoder Y pin.
  5. incr=False. Must set False.
  6. decr=None.
  7. encoder An int defining the division ratio as above.

4. Screen class

The Screen class presents a full-screen canvas onto which displayable objects are rendered. Before instantiating widgets a Screen instance must be created. This will be current until another is instantiated. When a widget is instantiated it is associated with the current screen.

All applications require the creation of at least one user screen. This is done by subclassing the Screen class. Widgets are instantiated in the Screen constructor. Widgets may be assigned to bound variable: this facilitates communication between them.

4.1 Class methods

In normal use only change and back are required, to move to a new Screen and to drop back to the previous Screen in a tree (or to quit the application if there is no predecessor).

  • change(cls, cls_new_screen, mode=Screen.STACK, *, args=[], kwargs={})
    Change screen, refreshing the display. Mandatory positional argument: the new screen class name. This must be a class subclassed from Screen. The class will be instantiated and displayed. Optional keyword arguments args, kwargs enable passing positional and keyword arguments to the constructor of the new, user defined, screen. By default the new screen overlays the old. When the new Screen is closed (via back) the old is re-displayed having retained state. If mode=Screen.REPLACE is passed the old screen instance is deleted. The new one retains the parent of the old, so if it is closed that parent is re-displayed with its state retained. This enables arbitrary navigation between screens (directed graph rather than tree structure). See demo screen_replace.
  • back(cls) Restore previous screen. If there is no parent, quits the application.

These are uncommon:

  • shutdown(cls) Clear the screen and shut down the GUI. Normally done by a CloseButton instance.
  • show(cls, force). This causes the screen to be redrawn. If force is False unchanged widgets are not refreshed. If True, all visible widgets are re-drawn. Explicit calls to this should never be needed.

See demos/plot.py for an example of multi-screen design, or screen_change.py for a minimal example demostrating the coding technique.

4.2 Constructor

This takes no arguments.

4.3 Callback methods

These are null functions which may be redefined in user subclasses.

  • on_open(self) Called when a screen is instantiated but prior to display.
  • after_open(self) Called after a screen has been displayed.
  • on_hide(self) Called when a screen ceases to be current.

See demos/plot.py for examples of usage of after_open.

4.4 Method

  • reg_task(self, task, on_change=False) The first arg may be a Task instance or a coroutine. Returns the passed task object.

This is a convenience method which provides for the automatic cancellation of tasks. If a screen runs independent tasks it can opt to register these. If the screen is overlaid by another, tasks registered with on_change True are cancelled. If the screen is closed, all tasks registered to it are cancelled regardless of the state of on_change. On shudown, any tasks registered to the base screen are cancelled.

For finer control, applications can ignore this method and handle cancellation explicitly in code.

4.5 Class variable

  • do_gc = True By default a coroutine is launched to periodically perform garbage collection (GC). On most platforms this reduces latency by doing GC before too much garbage has accumulated. However on platforms with SPIRAM GC can take hundreds of ms, causing unacceptable latency. If do_gc is False the application can perform GC at times when fast response to user actions is not required. If turned off, the GC task cannot be re-started.

4.6 Usage

The Screen.change() classmethod returns immediately. This has implications where the new, top screen sets up data for use by the underlying screen. One approach is for the top screen to populate class variables. These can be acccessed by the bottom screen's after_open method which will run after the top screen has terminated.

If a Screen throws an exception when instantiated, check that its constructor calls super().__init__().

5. Window class

This is a Screen subclass providing for modal windows. As such it has positional and dimension information. Usage consists of writing a user class subclassed from Window. Example code is in demos/screens.py. Code in a window must not attempt to open another Window or Screen. Doing so will raise a ValueError. Modal behaviour means that the only valid screen change is a return to the calling screen.

5.1 Constructor

This takes the following positional args:

  • row
  • col
  • height
  • width

Followed by keyword-only args

  • draw_border=True
  • bgcolor=None Background color, default black.
  • fgcolor=None Foreground color, default white.
  • writer=None See Popups below.

5.2 Class method

  • value(cls, val=None) The val arg can be any Python type. It allows widgets on a Window to store information in a way which can be accessed from the calling screen. This typically occurs after the window has closed and no longer exists as an instance.

Another approach, demonstrated in demos/screens.py, is to pass one or more callbacks to the user window constructor args. These may be called by widgets to send data to the calling screen. Note that widgets on the screen below will not be updated until the window has closed.

5.3 Popup windows

In general Screen and Window instances need at least one active widget. There is a special case of a popup window which typically displays status data, possibly with a progress meter. A popup has no user controls and is closed by user code. A popup is created by passing a Writer (or CWriter) to the constructor and is closed by issuing the close() static method.

6. Widgets

6.1 Label widget

from gui.widgets import Label  # File: label.py

Image

Various styles of Label.

The purpose of a Label instance is to display text at a specific screen location.

Text can be static or dynamic. In the case of dynamic text the background is cleared to ensure that short strings cleanly replace longer ones.

Labels can be displayed with an optional single pixel border.

Colors are handled flexibly. By default the colors used are those of the Writer instance, however they can be changed dynamically; this might be used to warn of overrange or underrange values. The color15.py demo illustrates this.

Constructor args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col
  4. text If a string is passed it is displayed: typically used for static text. If an integer is passed it is interpreted as the maximum text length in pixels; typically obtained from writer.stringlen('-99.99'). Nothing is dsplayed until .value() is called. Intended for dynamic text fields.
  5. invert=False Display in inverted or normal style.
  6. fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  7. bgcolor=BLACK Background color of object. If None the Writer background default is used.
  8. bdcolor=False Color of border. If False no border will be drawn. If None the fgcolor will be used, otherwise a color may be passed. If a color is available, a border line will be drawn around the control.
  9. justify=Label.LEFT Options are Label.RIGHT and Label.CENTRE (note British spelling). Justification can only occur if there is sufficient space in the Label i.e. where an integer is supplied for the text arg.

The constructor displays the string at the required location.

Method:
value Redraws the label. This takes the following args:

  • text=None The text to display. If None displays last value.
  • invert=False If true, show inverse text.
  • fgcolor=None Foreground color: if None the Writer default is used.
  • bgcolor=None Background color, as per foreground.
  • bdcolor=None Border color. As per above except that if False is passed, no border is displayed. This clears a previously drawn border.
    Returns the current text string.
  • justify=None By default justify using the constructor default. Override with Label.LEFT, Label.RIGHT or Label.CENTRE.

If the value method is called with a text string too long for the Label the text will be clipped to fit the width. In this case value() will return the truncated text.

If constructing a label would cause it to extend beyond the screen boundary a warning is printed at the console. The label may appear at an unexpected place. The following is a complete "Hello world" script.

from hardware_setup import ssd  # Create a display instance
from gui.core.ugui import Screen
from gui.core.writer import CWriter
from gui.core.colors import *

from gui.widgets import Label, CloseButton
import gui.fonts.freesans20 as freesans20


class BaseScreen(Screen):

    def __init__(self):
        super().__init__()
        wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False)
        Label(wri, 2, 2, 'Hello world!')
        CloseButton(wri)

Screen.change(BaseScreen)

6.1.1 Grid widget

from gui.widgets import Grid  # Files: grid.py, parse2d.py

Image

This is a rectangular array of Label instances: as such it is a passive widget. Rows are of a fixed height equal to the font height + 4 (i.e. the label height). Column widths are specified in pixels with the column width being the specified width +4 to allow for borders. The dimensions of the widget including borders are thus:
height = no. of rows * (font height + 4)
width = sum(column width + 4)
Cells may be addressed as a 1 or 2-dimensional array.

Constructor args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location of grid on screen.
  3. col
  4. lwidth If an integer N is passed all labels will have width of N pixels. A list or tuple of integers will define the widths of successive columns. If the list has fewer entries than there are columns, the last entry will define the width of those columns. Thus [20, 30] will produce a grid with column 0 being 20 pixels and all subsequent columns being 30.
  5. nrows Number of rows.
  6. ncols Number of columns.
  7. invert=False Display in inverted or normal style.
  8. fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  9. bgcolor=BLACK Background color of cells. If None the Writer background default is used.
  10. bdcolor=None Color of border of the widget and its internal grid. If False no border or grid will be drawn. If None the fgcolor will be used, otherwise a color may be passed.
  11. justify=Label.LEFT Options are Label.RIGHT and Label.CENTRE (note British spelling). Justification can only occur if there is sufficient space in the Label as defined by lwidth.

Method:

  • __getitem__ Returns an iterator enabling Label instances to be accessed.
  • __setitem__ Assign a value to one or more labels. If multiple labels are specified and a single text value is passed, all labels will receive that value. If an iterator is passed, consecutive labels will receive values from the iterator. If the iterator runs out of data, the last value will be repeated.

Addressing:
The Label instances may be addressed as a 1D array as follows

grid[20] = str(42)
grid[20:25] = iter([str(n) for n in range(20, 25)])

or as a 2D array:

grid[2, 5] = "A"  # Row == 2, col == 5
grid[0:7, 3] = "b"  # Populate col 3 of rows 0..6
grid[1:3, 1:3] = (str(n) for n in range(25))  # Produces
# 0 1
# 2 3

Columns are populated from left to right, rows from top to bottom. Unused iterator values are ignored. If an iterator runs out of data the last value is repeated, thus

grid[1:3, 1:3] = (str(n) for n in range(2))  # Produces
# 0 1
# 1 1

Read access:

for label in grid[2, 0:]:
    v = label.value()  # Access text of each label in row 2

Example uses:

colwidth = (20, 30)  # Col 0 width is 20, subsequent columns 30
self.grid = Grid(wri, row, col, colwidth, rows, cols, justify=Label.CENTRE)
self.grid[20] = ""  # Clear cell 20 by setting its value to ""
self.grid[2, 5] = str(42)  # 2D array syntax
grid[1:6, 0] = iter("ABCDE")  # Label row and col headings
grid[0, 1:cols] = (str(x + 1) for x in range(cols))
d = {}  # For indiviual control of cell appearance
d["fgcolor"] = RED
d["text"] = str(99)
self.grid[3, 7] = d  # Specify color as well as text
del d["fgcolor"]  # Revert to default
d["invert"] = True
self.grid[17] = d

See the example calendar.py.

6.2 LED widget

from gui.widgets import LED  # File: led.py

Image

This is a virtual LED whose color may be altered dynamically. An LED may be defined with a color and turned on or off by setting .value to a boolean. For more flexibility the .color method may be use to set it to any color.

Constructor mandatory positional args:

  1. writer The Writer instance (defines font) to use.
  2. row Location on screen.
  3. col

Keyword only args:

  • height=30 Height of LED.
  • fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  • bgcolor=None Background color of object. If None the Writer background default is used.
  • bdcolor=False Color of border. If False no border will be drawn. If a color is provided, a border line will be drawn around the control. shown in the foreground color. If a color is passed, it is used.
  • color=RED Color when illuminated (i.e. if value is True.

Methods:

  1. value arg val=None If True is passed, lights the LED in its current color. False extinguishes it. None has no effect. Returns current value.
  2. color arg c=None Change the LED color to c. If c is None the LED is turned off (rendered in the background color).

Note that __call__ is a synonym for value. An LED instance can be controlled with led(True) or led(False).

6.3 Checkbox widget

from gui.widgets import Checkbox  # File: checkbox.py

Image
This provides for Boolean data entry and display. In the True state the control can show an 'X' or a filled block of any color depending on the fillcolor constructor arg.

Constructor mandatory positional args:

  1. writer The Writer instance (defines font) to use.
  2. row Location on screen.
  3. col

Optional keyword only arguments:

  • height=30 Dimension of the square bounding box. Default 30 pixels.
  • fillcolor=None Fill color of checkbox when True. If None an 'X' will be drawn.
  • fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  • bgcolor=None Background color of object. If None the Writer background default is used.
  • bdcolor=False Color of border. If False no border will be drawn. If a color is provided, a border line will be drawn around the control.
  • callback=dolittle Callback function which will run when the value changes. The default is a null function.
  • args=[] A list/tuple of arguments for above callback.
  • value=False Initial value.
  • active=True By default user input is accepted.

Methods:

  • greyed_out Optional Boolean argument val=None. If None returns the current 'greyed out' status of the control. Otherwise enables or disables it, showing it in its new state.
  • value Optional Boolean argument val. If the provided value does not correspond to the control's current value, updates it; the checkbox is re-drawn and the callback executed. Always returns the control's value.

6.4 Button and CloseButton widgets

from gui.core.colors import *  # Colors and shapes
from gui.widgets import Button  # File: buttons.py

Image

Using an icon font:

Image

In these images Button "a" and the "Forward" button have the focus. Pressing the physical select button will press the virtual Button.

This emulates a pushbutton, with a callback being executed each time the button is pressed. Physically this consists of pressing the select button when the Button instance has focus. Buttons may be any one of three shapes: CIRCLE, RECTANGLE or CLIPPED_RECT.

Constructor mandatory positional args:

  1. writer The Writer instance (defines font) to use.
  2. row Location on screen.
  3. col

Optional keyword only arguments:

  • shape=RECTANGLE Must be CIRCLE, RECTANGLE or CLIPPED_RECT.
  • height=20 Height of button or diameter in CIRCLE case.
  • width=50 Width of button. If text is supplied and width is too low to accommodate the text, it will be increased to enable the text to fit. In CIRCLE case any passed value is ignored.
  • fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  • bgcolor=None Background color of object. If None the Writer background default is used.
  • bdcolor=False Color of border. If False no border will be drawn. If a color is provided, a border line will be drawn around the control.
  • textcolor=None Text color. Defaults to fgcolor.
  • litcolor=None If provided the button will display this color for one second after being pressed.
  • text='' Shown in centre of button. It is possible to show simple icons, for example media playback symbols.
  • callback=dolittle Callback function which runs when button is pressed.
  • args=() A list/tuple of arguments for the above callback.

Method:

  • greyed_out Optional Boolean argument val=None. If None returns the current 'greyed out' status of the control. Otherwise enables or disables it, showing it in its new state.

Class variable:

  • lit_time=1000 Period in ms the litcolor is displayed.

CloseButton

Image
This example has focus, as shown by white border.

This Button subclass is a special case of a Button. Its constructor takes a single arg, being a Writer instance. It produces a red "X" button at the top right hand corner of the current Screen. Operating it causes the screen to close, with the screen below being revealed. On the bottom level screen, a CloseButton will shut down the application.

Constructor mandatory positional arg:

  • writer

Optional keyword only arguments:

  • width=0 By default dimensions are calculated from font size. The button is is square. Optionally width may be specified.
  • callback=dolittle Optional callback, not normally required.
  • args=() Args for above.
  • bgcolor=RED

6.5 ButtonList object

from gui.core.colors import *  # Colors and shapes
from gui.widgets import Button, ButtonList  # File: buttons.py

A ButtonList groups a number of buttons together to implement a button which changes state each time it is pressed. For example it might toggle between a green Start button and a red Stop button. The buttons are defined and added in turn to the ButtonList object. Typically they will be the same size, shape and location but will differ in color and/or text. At any time just one of the buttons will be visible, initially the first to be added to the object.

Buttons in a ButtonList should not have callbacks. The ButtonList has its own user supplied callback which runs each time the object is pressed. However each button can have its own list of args. Callback arguments comprise the currently visible button followed by its arguments.

Constructor argument:

  • callback=dolittle The callback function. Default does nothing.

Methods:

  • add_button Adds a button to the ButtonList. Arguments: as per the Button constructor. Returns the button object.
  • greyed_out Optional Boolean argument val=None. If None returns the current 'greyed out' status of the control. Otherwise enables or disables it, showing it in its new state.
  • value Optional args button=None, new_cb=False. The button arg, if provided, should be a button in the set. If supplied and the button is not active the currency changes to the supplied button, which is displayed. By default the callback of the previous button is run, otherwise the callback of the newly displayed button.

Always returns the active button.

Counter intuitively, running the callback of the previous button is normal behaviour. Consider a ButtonList consisting of ON and OFF buttons. If ON is visible this implies that the machine under control is off. Pressing select causes the ON callback to run, starting the machine. The new button displayed now reads OFF.

Typical usage is as follows:

def callback(button, arg):
    print(arg)

table = [
     {'fgcolor' : GREEN, 'shape' : CLIPPED_RECT, 'text' : 'Start', 'args' : ['Live']},
     {'fgcolor' : RED, 'shape' : CLIPPED_RECT, 'text' : 'Stop', 'args' : ['Die']},
]
bl = ButtonList(callback)
for t in table:  # Buttons overlay each other at same location
    bl.add_button(wri, 10, 10, textcolor = BLACK, **t)

6.6 RadioButtons object

from gui.core.colors import *  # Colors and shapes
from gui.widgets import Button, RadioButtons  # File: buttons.py

Image

This object groups a set of buttons at different locations. When a button is pressed, it becomes highlighted and remains so until another button in the set is pressed. A callback runs each time the current button is changed.

Constructor positional arguments:

  • highlight Color to use for the highlighted button. Mandatory.
  • callback Callback when a new button is pressed. Default does nothing.
  • selected Index of initial button to be highlighted. Default 0.

Methods:

  • add_button Adds a button. Arguments: as per the Button constructor. Returns the Button instance.
  • greyed_out Optional Boolean argument val=None. If None returns the current 'greyed out' status of the control. Otherwise enables or disables it, showing it in its new state.
  • value Optional argument: a button in the set. If supplied, and the button is not currently active, the supplied button receives the focus and its callback is run. Always returns the currently active button.

Typical usage:

def callback(button, arg):
    print(arg)

table = [
    {'text' : '1', 'args' : ['1']},
    {'text' : '2', 'args' : ['2']},
    {'text' : '3', 'args' : ['3']},
    {'text' : '4', 'args' : ['4']},
]
col = 0
rb = RadioButtons(BLUE, callback) # color of selected button
for t in table:
    rb.add_button(wri, 10, col, textcolor = WHITE,
                  fgcolor = LIGHTBLUE, height = 40, **t)
    col += 60 # Horizontal row of buttons

6.7 Listbox widget

from gui.widgets import Listbox  # File: listbox.py

Image

A listbox with the second item highlighted. Pressing the physical select button will cause the callback to run.

A Listbox is an active widget. By default its height is determined by the number of entries in it and the font in use. It may be reduced by specifying dlines in which case scrolling will occur. When the widget has focus the currently selected element may be changed using increase and decrease buttons or by turning the encoder. On pressing select a callback runs.

Constructor mandatory positional args:

  1. writer The Writer instance (defines font) to use.
  2. row Location on screen.
  3. col

Mandatory keyword only argument:

  • elements A list or tuple of strings to display. Must have at least one entry. An alternative format is described below which enables each item in the list to have a separate callback.

Optional keyword only arguments:

  • dlines=None By default the height of the control is determined by the number of elements. If an integer < number of elements is passed the list will show that number of lines; its height will correspond. Scrolling will occur to ensure that the current element is always visible. To indicate when scrolling is possible, one or two vertical bars will appear to the right of the list.
  • width=None Control width in pixels. By default this is calculated to accommodate all elements. If a width is specified, and some elements are too long to fit, they will be clipped. This is a visual effect only and does not affect the value of that element.
  • value=0 Index of currently selected list item. If necessary the list will scroll to ensure the item is visible.
  • fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  • bgcolor=None Background color of object. If None the Writer background default is used.
  • bdcolor=False Color of border. If False no border will be drawn. If a color is provided, a border line will be drawn around the control.
  • fontcolor=None Text color. Defaults to system text color.
  • select_color=DARKBLUE Background color for selected item in list.
  • callback=dolittle Callback function which runs when select is pressed.
  • args=[] A list/tuple of arguments for above callback.
  • also=0 Options are Listbox.ON_MOVE or Listbox.ON_LEAVE. By default the callback runs only when the select button is pressed. The ON_LEAVE value causes it also to run when the focus moves from the control if the currently selected element has changed. The ON_MOVE arg causes the callback to run every time the highlighted element is changed.

Methods:

  • greyed_out Optional Boolean argument val=None. If None returns the current 'greyed out' status of the control. Otherwise enables or disables it, showing it in its new state.
  • value Argument val=None. If a provided argument is a valid index for the list, that entry becomes current and the callback is executed. Always returns the index of the currently active entry.
  • textvalue Argument text=None. If a string argument is provided and is in the control's list, that item becomes current. Normally returns the current string. If a provided arg did not match any list item, the control's state is not changed and None is returned.

The callback's first argument is the listbox instance followed by any args specified to the constructor. The currently selected item may be retrieved by means of the instance's value or textvalue methods.

Alternative approach

By default the Listbox runs a common callback regardless of the item chosen. This can be changed by specifying elements such that each element comprises a 3-list or 3-tuple with the following contents:

  1. String to display.
  2. Callback.
  3. Tuple of args (may be ()).

In this case constructor args callback and args must not be supplied. Args received by the callback functions comprise the Listbox instance followed by any supplied args. The following is a complete example (minus initial import statements).

class BaseScreen(Screen):
    def __init__(self):
        def cb(lb, s):
            print('Callback', s)

        def cb_radon(lb, s):
            print('Radioactive', s)

        super().__init__()
        wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False)
        els = (('Hydrogen', cb, ('H2',)),
               ('Helium', cb, ('He',)),
               ('Neon', cb, ('Ne',)),
               ('Xenon', cb, ('Xe',)),
               ('Radon', cb_radon, ('Ra',)))
        Listbox(wri, 2, 2, elements = els, bdcolor=RED)
        CloseButton(wri)

Screen.change(BaseScreen)

6.8 Dropdown widget

from gui.widgets import Dropdown  # File: dropdown.py

Image

Closed dropdown list.

Image

Open dropdown list. When closed, hidden items below are refreshed.

A dropdown list. The list, when active, is drawn over the control. The height of the control is determined by the height of the font in use. By default the height of the list is determined by the number of entries in it and the font in use. It may be reduced by specifying dlines in which case scrolling will occur. The dropdown should be placed high enough on the screen to ensure that the list can be displayed.

Constructor mandatory positional args:

  1. writer The Writer instance (defines font) to use.
  2. row Location on screen.
  3. col

Mandatory keyword only argument:

  • elements A list or tuple of strings to display. Must have at least one entry. See below for an alternative way to use the Dropdown which enables each item on the dropdown list to have a separate callback.

Optional keyword only arguments:

  • dlines=None By default the height of the dropdown list is determined by the number of elements. If an integer < number of elements is passed the list will show that number of lines; its height will correspond. Scrolling will occur to ensure that the current element is always visible. To indicate when scrolling is possible, one or two vertical bars will appear to the right of the list.
  • width=None Control width in pixels. By default this is calculated to accommodate all elements.
  • value=0 Index of currently selected list item.
  • fgcolor=None Color of foreground (the control itself). If None the Writer foreground default is used.
  • bgcolor=None Background color of object. If None the Writer background default is used.
  • bdcolor=False Color of border. If False no border will be drawn. If a color is provided, a border line will be drawn around the control.
  • fontcolor=None Text color. Defaults to foreground color.
  • select_color=DARKBLUE Background color for selected item in list.
  • callback=dolittle Callback function which runs when a list entry is picked.
  • args=[] A list/tuple of arguments for above callback.

Methods:

  • greyed_out Optional Boolean argument val=None. If None returns the current 'greyed out' status of the control. Otherwise enables or disables it, showing it in its new state.
  • value Argument val=None. If a provided arg is a valid index into the list, that entry becomes current and the callback is executed. Always returns the index of the currently active entry.
  • textvalue Argument text=None. If a string argument is provided and is in the control's list, that item becomes current. Normally returns the current string. If a provided arg did not match any list item, the control's state is not changed and None is returned.

If select is pressed when the Dropdown has focus, the list is displayed. The increase and decrease buttons move the list currency. If select is pressed after changing the currency the callback is triggered, the list is closed and the control will display the newly selected entry. If next or prev are pressed while the list is open, focus will move to the next widget. In this event the list will close and no selection change will be recognised: the control will show the element which was visible at the start and the callback will not run. Moving the focus is a means of cancelling any changes.

The callback's first argument is the dropdown instance followed by any args specified to the constructor. The currently selected item may be retrieved by means of the instance's value or textvalue methods.

Alternative approach

By default the Dropdown runs a single callback regardless of the element chosen. This can be changed by specifying elements such that each element comprises a 3-list or 3-tuple with the following contents:

  1. String to display.
  2. Callback.
  3. Tuple of args (may be ()).

In this case constructor args callback and args must not be supplied. Args received by the callback functions comprise the Dropdown instance followed by any supplied args. The following is a complete example (minus initial import statements):

class BaseScreen(Screen):
    def __init__(self):
        def cb(dd, arg):
            print('Gas', arg)

        def cb_radon(dd, arg):
            print('Radioactive', arg)

        super().__init__()
        wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False)
        els = (('hydrogen', cb, ('H2',)),
               ('helium', cb, ('He',)),
               ('neon', cb, ('Ne',)),
               ('xenon', cb, ('Xe',)),
               ('radon', cb_radon, ('Ra',)))
        Dropdown(wri, 2, 2, elements = els,
                bdcolor = RED, fgcolor=RED, fontcolor = YELLOW)
        CloseButton(wri)


Screen.change(BaseScreen)

6.9 DialogBox class

from gui.widgets import DialogBox  # File: dialog.py

Image

An active dialog box. Auto generated dialogs contain only pushbutton instances, but user created dialogs may contain any widget.

This implements a modal dialog box based on a horizontal row of pushbuttons. Any button press will close the dialog. The caller can determine which button was pressed. The size of the buttons and the width of the dialog box are calculated from the strings assigned to the buttons. This ensures that buttons are evenly spaced and identically sized. Typically used for simple queries such as "yes/no/cancel".

Constructor positional args:

  1. writer The Writer instance (defines font) to use.
  2. row=20 Location on screen.
  3. col=20

Mandatory keyword only arg:

  • elements A list or tuple of 2-tuples. Each defines the text and color of a pushbutton, e.g. (('Yes', RED), ('No', GREEN)).

Optional keyword only args:

  • label=None Text for an optional label displayed in the centre of the dialog box.
  • bgcolor=DARKGREEN Background color of window.
  • buttonwidth=25 Minimum width of buttons. In general button dimensions are calculated from the size of the strings in elements.
  • closebutton=True If set, a close button will be displayed at the top RH corner of the dialog box.
  • callback=dolittle
  • args=[]

Classmethod (inherited from Screen):

  • value(cls, val=None) The val arg can be any Python type.

The DialogBox is a Screen subclass. Pressing any button closes the dialog and sets the Screen value to the text of the button pressed or "Close" in the case of the close button. The outcome can therefore be tested by running Screen.value() or by implementing the callback. The latter receives the DialogBox instance as a first arg, followed by any args supplied to the constructor.

Note that dialog boxes can also be constructed manually, enabling more flexible designs. For example these might have widgets other than pushbuttons. The approach is to write a user subclass of Window. Example code may be found in gui/demos/screens.py.

6.10 Textbox widget

from gui.widgets import Textbox  # File: textbox.py