peterhinch / micropython-touch

A GUI for touch panel displays.
MIT License
25 stars 5 forks source link

micropython-touch

This is a lightweight, portable, MicroPython GUI library for displays having a touch interface and with drivers subclassed from framebuf. Written in Python it runs under a standard MicroPython firmware build. Input is by touch. The design is intended to support a range of touch controllers. Initially the TSC2007, XPT2046 and FT6206 are supported.

It is larger and more complex than nano-gui owing to its support for input. The GUI enables switching between screens and launching modal windows. Widgets are a substantial superset of nano-gui widgets. It is compatible with all nano-gui display drivers so is portable to a wide range of displays. Support for e-paper is theoretically possible if any exist with touch controllers. The GUI is also portable between hosts.

It was developed from micro-gui with the aim of supporting a variety of touch controllers. Touch drivers share a common API via a common abstract base class, the aim being to simplify the design of touch controller drivers.

Image
Raspberry Pico with an ILI9341 from eBay (XPT2046 touch controller).

Image
Raspberry Pico with Adafruit 3.2" display and TSC2007 touch controller.

Documents

Supported displays.
Setup guide Quick start guide. Calibration and approaches to application development.
Touchpad drivers Details of supported drivers and technical notes.
Commercial links to hardware that produced excellent results:
TSC2007 breakout Interface to displays that bring out analog touch signals, such as
Adafruit 3.2" touchscreen
Waveshare make touch displays where the Raspberry Pico plugs in e.g. Waveshare Pico res touch.

Alternative GUIs for MicroPython

The following are similar GUI repos with differing objectives.

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

Oct 2024: Refresh locking can now be handled by device driver.
Sept 2024: Dropdown and Listbox widgets support dynamically variable lists of elements.
May 2024: Add support for round displays with CST816S touch controller.
April 2024: Touch ABC simplified and bugs fixed. Demos updated to take advantage of larger displays.
March 2024: Port from micro-gui.

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 Widget control Operation of variable controls.
    1.5 Hardware definition How to configure your hardware.
    1.6 Quick start Also a guide to hardware choice, calibration, application development.
    1.7 Files Discussion of the files in the library.
         1.7.1 Demos Simple demos showing coding techniques.
         1.7.2 Test scripts GUI tests, some needing larger displays
    1.8 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.
  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.7.1 Dynamic changes Alter listbox contents at runtime.
    6.8 Dropdown widget Dropdown lists.
         6.8.1 Dynamic changes Alter dropdown contents at runtime.
    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 Convert a Meter to a thermostat type object.
    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 Menu class
    6.18 BitMap widget Draw bitmaps from files.
    6.19 QRMap widget Draw QR codes created by uQR.
    6.20 Pad widget Invisible region sensitive to touch.
  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. Realtime applications Accommodating tasks requiring fast RT performance: refresh control.

Appendix 1 Application design Useful hints.
Appendix 2 Freezing bytecode Optional way to save RAM.
Appendix 3 Cross compiling Another way to save RAM.
Appendix 4 GUI Design notes The reason for continuous refresh.
Appendix 5 Bus sharing Using the SD card on Waveshare boards.

1. Basic concepts

Internally micropython-touch uses asyncio. The interface is callback-based; 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 linked to touch controller
from gui.core.tgui 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:

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.

Contents

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, having 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 respond to touch. Widget objects have dimensions defined by bound variables height and width.

Contents

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:

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

Contents

1.4 Widget control

Some widgets support the entry of floating point values, for example slider controls. These operate as follows. A touch near one end of the control causes the value to increase, touching near the other end causes it to decrease. The rate of change depends on the distance between the touch and the widget centre. This enables rapid change, but also slow and extremely precise adjustment.

Contents

1.5 Hardware definition

A file hardware_setup.py must exist in the GUI root directory. This defines the connections to the display and the display driver. It also defines the touch driver and the pins used for its interface. The doc referenced in the next section describes the creation of a hardware_setup.py in detail. Example files may be found in the setup_examples directory. Further examples (without touch controller definitions) are in this nano-gui directory.

The following is a typical example for a Raspberry Pi Pico driving an ILI9341 display with TSC2007 touch controller:

from machine import Pin, SoftI2C, SPI, freq
import gc
from drivers.ili93xx.ili9341 import ILI9341 as SSD

freq(250_000_000)  # RP2 overclock
# Create and export an SSD instance
prst = Pin(8, Pin.OUT, value=1)
pdc = Pin(9, Pin.OUT, value=0)  # Arbitrary pins
pcs = Pin(10, Pin.OUT, value=1)
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=30_000_000)
gc.collect()  # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, height=240, width=320, usd=True)  # 240x320 default
from gui.core.tgui import Display

# Touch configuration
from touch.tsc2007 import TSC2007
i2c = SoftI2C(scl=Pin(27), sda=Pin(26), freq=100_000)
tpad = TSC2007(i2c)
# The following line of code is the outcome of calibration.
tpad.init(240, 320, 241, 292, 3866, 3887, True, True, False)
display = Display(ssd, tpad)
Contents

1.6 Quick start

Please ensure device firmware is up to date. SETUP.md describes how to configure, calibrate and test a touchscreen so that the demos below may be run. It includes ideas on application development.

Contents

1.7 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:

Touch support is in the touch directory:

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

1.7.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.

The initial ones are minimal and aim to demonstrate a single technique.

1.7.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).

Contents

1.8 Floating Point Widgets

The following widgets provide floating point input:

Former iterations of touch GUIs used dragging. This provided rather coarse adjustment, even when the hardware interfaces were fast. The above controls use a different algorithm where a touch causes the control's value to change at a rate depending on the location of the touch. The closer the touch is to the
centreline of the control, the slower the rate of change. This allows very precise changes to be made by adjusting the location and duration of a touch. "Closer" is on the horizontal axis for horizontal widgets, otherwise vertical.

Contents

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. 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.tgui 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 touch-gui...')
    Screen.change(BaseScreen)

test()

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 Button 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.

Contents

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.

Contents

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 three default colors which are defined by a color_map list. These may be reassigned in user code. The color_map index constants and default colors (defined in colors.py) are:

Index Color Purpose
FG WHITE Window foreground default
BG BLACK Background default including screen clear
GREY_OUT GREY Color to render greyed-out controls
Contents

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. I am not aware of any non-emitting displays (e.g. ePaper) with touch controllers.

Contents

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.tgui 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 tgui. Normal GUI applications only need to import ssd. This reference 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 touch subclass instance used for input.

The constructor takes the following positional args:

  1. objssd The SSD instance. A reference to the display driver.
  2. objtouch=None Touch controller instance. None allows the display to be tested prior to implementing the touch interface.
Contents

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 a bound variable: this facilitates communication between them.

Contents

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).

These are uncommon:

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

Contents

4.2 Constructor

This takes no arguments.

4.3 Callback methods

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

See demos/plot.py, demos/primitives.py for examples of after_open; this method is particularly useful for drawing onto a screen and for displaying images.

4.4 Method

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

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 accessed 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__().

Contents

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:

Followed by keyword-only args

5.2 Class method

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

There is a special case of a popup window with no touch sensitive controls. This typically displays status data, possibly with a progress meter. Such a popup is closed by user code. A popup is created by passing a Writer (or CWriter) to the constructor and is closed by issuing the Window.close() static method.

Contents

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:

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.tgui 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)
Contents

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:

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.

Contents

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 used 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:

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).

Contents

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:

Methods:

Contents

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

This emulates a pushbutton, with a callback being executed each time the button is pressed. Buttons may be any one of three shapes: CIRCLE, RECTANGLE or CLIPPED_RECT. By default the callback is triggered on release of the touch. Triggering on press carries a hazard if a button causes a screen change: this results if the new screen has an active widget at the same location, when that widget would inadvertently be triggered.

Constructor mandatory positional args:

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

Optional keyword only arguments:

Method:

Class variables:

CloseButton

Image

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:

Optional keyword only arguments:

Class variable:

Contents

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:

Methods:

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 the button causes the ON callback to run, starting the machine. The new button displayed now reads OFF. There are situations in which the opposite behaviour is required such as when choosing an option from a list: in this case the callback from the newly visible button might be expected to run.

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)
Contents

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:

Methods:

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
Contents

6.7 Listbox widget

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

Image

A listbox with the second item highlighted. Touching an entry 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. A short vertical line is visible in the top right if scrolling down is possible, likewise in the bottom right if the contents may be scrolled up. A long touch on the top or bottom entry initiates scrolling.

Constructor mandatory positional args:

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

Mandatory keyword only argument:

Optional keyword only arguments:

Methods:

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.7.1 Dynamic changes

The contents of a listbox may be changed at runtime. To achieve this, elements must be defined as a list rather than a tuple. After the application has modified the list, it should call the .update method to refresh the control. The demo script listbox_var.py illustrates this.

Contents

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:

Optional keyword only arguments:

Methods:

When the dropdown is touched the list is displayed. If an entry in the list is touched the callback is triggered, the list is closed and the control displays the newly selected entry. If the list contains more entries than can be shown, scrolling may be used. A short vertical line is visible in the top right if scrolling down is possible, likewise in the bottom right if the contents may be scrolled up. A long touch on the top or bottom entry initiates scrolling.

The callback's first argument is the dropdown instance followed by any args specified to the constructor. The current 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.8.1 Dynamic changes

The contents of a Dropdown may be changed at runtime. To achieve this, elements must be defined as a list rather than a tuple. After the application has modified the list, it should call the .update method to refresh the control. Changes should be made when the dropdown list is not visible; the consequence of .update will immediately be visible only if the currently visible item is deleted. The demo scripts dropdown_var.py and dropdown_var_tuple.py illustrate this.

Contents

6.9 DialogBox class

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

Image

An active dialog box. Auto generated dialogs contain only Button 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:

Optional keyword only args:

Classmethod (inherited from Screen):

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 Buttons. The approach is to write a user subclass of Window. Example code may be found in gui/demos/screens.py.

Contents

6.10 Textbox widget

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

Image

Displays multiple lines of text in a field of fixed dimensions. Text may be clipped to the width of the control or may be word-wrapped. If the number of lines of text exceeds the height available, scrolling will occur. Access to text that has scrolled out of view may be achieved by calling a method. If the widget is instantiated as active, scrolling may be performed by touching near the top or bottom of the control. The rate of scrolling depends on the distance between the touch and the centreline of the control. The widget supports fixed and variable pitch fonts.

Constructor mandatory positional arguments:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col
  4. width Width of the object in pixels.
  5. nlines Number of lines of text to display. The object's height is determined from the height of the font:
    height in pixels = nlines*font_height
    As per all widgets the border is drawn two pixels beyond the control's boundary.

Keyword only arguments:

Methods:

Fast updates:
Rendering text to the screen is relatively slow. To send a large amount of text the fastest way is to perform a single append. Text may contain newline ('\n') characters as required. In that way rendering occurs once only.

append arg ntrim
If text is regularly appended to a Textbox its buffer grows, using RAM. The value of ntrim sets a limit to the number of lines which are retained, with the oldest (topmost) being discarded as required.

Contents

6.11 Meter widget

This passive widget displays a single floating point value on a vertical linear scale. Optionally it can support data dependent callbacks.

from gui.widgets import Meter  # File: meter.py

Image
The two styles of meter, both showing a value of 0.65. This passive widget provides a vertical linear meter display of values scaled between 0.0 and 1.0. In these examples each meter simply displays a data value.

Image
This example has two data sensitive regions, a control region with hysteresis and an alarm region. Callbacks can run in response to specific changes in the Meter's value emulating data-dependent behaviour including alarms and controls (like thermostats) having hysteresis.

The class supports one or more Region instances. Visually these appear as colored bands on the scale. If the meter's value enters, leaves or crosses one of these bands a callback is triggered. This receives an arg indicating the nature of the change which caused the trigger. For example an alarm might be triggered when the value, initially below the region, enters it or crosses it. The alarm might be cleared on exit or if crossed from above. Hysteresis as used in thermostats is simple to implement. Examples of these techniques may be found in gui.demos.tstat.py.

Regions may be modified, added or removed programmatically.

Constructor mandatory positional args:

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

Keyword only args:

Methods:

  1. value Args: n=None, color=None.

    • n should be a float in range 0 to 1.0. Causes the meter to be updated. Out of range values are constrained. If None is passed the meter is not updated.
    • color Updates the color of the bar or line if a value is also passed. None causes no change.

    Returns the current value.

  2. text Updates the label if present (otherwise throws a ValueError). 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.
  3. del_region Arg: a Region instance. Deletes the region. No callback will run.

Legends

Depending on the font in use for legends additional space may be required above and below the Meter to display the top and bottom legends.

Example of use of Regions

# Instantiate Meter
ts = Meter(wri, row, sl.mcol + 5, ptcolor=YELLOW, height=100, width=15,
           style=Meter.BAR, legends=('0.0', '0.5', '1.0'))
# Instantiate two Regions and associate with the Meter instance.
reg = Region(ts, 0.4, 0.55, MAGENTA, ts_cb)
al = Region(ts, 0.9, 1.0, RED, al_cb)

The callback ts_cb will run in response to data values between 0.4 and 0.55: if the value enters that range having been outside it, if it leaves the range, or if successive values are either side of the range. The al_cb callback behaves similarly for data values between 0.9 and 1.0.

Contents

6.11.1 Region class

from gui.widgets import Region  # File: region.py

Instantiating a Region associates it with a supporting widget (currently only a Meter). Constructor positional args are as follows:

Method:

Class variables (constants).

These define the reasons why a callback occurred. A change in the Tstat value or an adjustment of the Region values can trigger a callback. The value might change such that it enters or exits the region. Alternatively it might change from being below the region to above it: this is described as a transit. The following cover all possible options.

The following, taken from gui.demos.tstat.py is an example of a thermostat callback with hysteresis:

    def ts_cb(self, reg, reason):
        # Turn on if T drops below low threshold when it had been above high threshold. Or
        # in the case of a low going drop so fast it never registered as being within bounds
        if reason == reg.EX_WA_IB or reason == reg.T_IB:
            self.led.value(True)
        elif reason == reg.EX_WB_IA or reason == reg.T_IA:
            self.led.value(False)

Values for these constants enable them to be combined with the bitwise or operator if you prefer that coding style:

if reason & (reg.EX_WA_IB | reg.T_IB):  # Leaving region heading down

On instantiation of a Region callbacks do not run. The desirability of this is application dependent. If the user Screen is provided with an after_open method, this can be used to assign a value to the Tstat to cause region callbacks to run as appropriate.

Contents

6.12 Slider and HorizSlider widgets

from gui.widgets import Slider, HorizSlider  # File: sliders.py

Image

Different styles of slider.

These emulate linear potentiometers in order to display or control floating point values. A description of the user interface in the active case may be found in Floating Point Widgets.

Vertical Slider and horizontal HorizSlider variants are available. These are constructed and used similarly. The short forms (v) or (h) are used below to identify these variants.

Constructor mandatory positional args:

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

Optional keyword only arguments:

Methods:

If instantiated as active, the floating point widget behaves as per section 1.12.

Callback

The callback receives an initial arg being the widget instance followed by any user supplied args. The callback can be a bound method, typically of a Screen subclass. The callback runs when the widget is instantiated and whenever the value changes. This enables dynamic color change. See gui/demos/active.py.

Legends

Depending on the font in use for legends additional space may be required around sliders to display all legends.

Contents

6.13 Scale widget

from gui.widgets import Scale  # File: scale.py

Image

This displays floating point data having a wide dynamic range, and optionally provides for user input of such values. It is modelled on old radios where a large scale scrolls past a small window having a fixed pointer. This enables a scale with (say) 200 graduations (ticks) to readily be visible on a small display, with sufficient resolution to enable the user to interpolate between ticks.

The Scale may be active or passive. A description of the user interface in the active case may be found in Floating Point Widgets.

The scale handles floats in range -1.0 <= V <= 1.0, however data values may be scaled to match any given range.

Legends for the scale are created dynamically as it scrolls past the window. The user may control this by means of a callback. Example code may be found in nano-gui which has a Scale whose value range is 88.0 to 108.0. A callback ensures that the display legends match the user variable. A further callback can enable the scale's color to change over its length or in response to other circumstances.

Constructor mandatory positional args:

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

Optional keyword only arguments:

Methods:

For example code see gui/demos/active.py.

Callback

The callback receives an initial arg being the widget instance followed by any user supplied args. The callback can be a bound method, typically of a Screen subclass. The callback runs when the widget is instantiated and whenever the value changes. This enables dynamic color change.

Callback legendcb

The display window contains 20 ticks comprising two divisions; by default a division covers a range of 0.1. A division has a legend at the start and end whose text is defined by the legendcb callback. If no user callback is supplied, legends will be of the form 0.3, 0.4 etc. User code may override these to cope with cases where a user variable is mapped onto the control's range. The callback takes a single float arg which is the value of the tick (in range -1.0 <= v <= 1.0). It must return a text string. An example from ths nano-gui demo shows FM radio frequencies:

def legendcb(f):
    return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88))

The above arithmetic aims to show the logic. It can (obviously) be simplified.

Callback tickcb

This callback enables the tick color to be changed dynamically. For example a scale might change from green to orange, then to red as it nears the extremes. The callback takes two args, being the value of the tick (in range -1.0 <= v <= 1.0) and the default color. It must return a color. This example is taken from the scale.py demo:

def tickcb(f, c):
    if f > 0.8:
        return RED
    if f < -0.8:
        return BLUE
    return c

Increasing the ticks value

This increases the precision of the display.

It does this by lengthening the scale while keeping the window the same size, with 20 ticks displayed. If the scale becomes 10x longer, the value diference between consecutive large ticks and legends is divided by 10. This means that the tickcb callback must return a string having an additional significant digit. If this is not done, consecutive legends will have the same value.

Precision

For performance reasons the control stores values as integers. This means that if you set value and subsequently retrieve it, there may be some loss of precision. Each visible division on the control represents 10 integer units.

Contents

6.14 ScaleLog widget

from gui.widgets import ScaleLog  # File: scale_log.py

Image

This displays floating point values with extremely wide dynamic range and optionally enables their input. The dynamic range is handled by means of a base 10 logarithmic scale. In other respects the concept is that of the Scale class.

The control is modelled on old radios where a large scale scrolls past a small window having a fixed pointer. The use of a logarithmic scale enables the value to span a range of multiple orders of magnitude.

The Scale may be active or passive. A description of the user interface in the active case may be found in Floating Point Widgets. Owing to the logarithmic nature of the widget, the changes discussed in that reference are multiplicative rather than additive. Thus a long touch will multiply the widget's value by a progressively larger factor, enabling many decades to be traversed quickly.

Legends for the scale are created dynamically as it scrolls past the window, with one legend for each decade. The user may control this by means of a callback, for example to display units, e.g. 10MHz. A further callback enables the scale's color to change over its length or in response to other circumstances.

The scale displays floats in range 1.0 <= V <= 10**decades where decades is a constructor arg. The user may readily scale these. For example a control with a range of 1-10,000 controls a user value from 1e-6 to 1e-2 while displaying ticks labelled 1μs, 10μs, 100μs, 1ms and 10ms.

Constructor mandatory positional args:

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

Keyword only arguments (all optional):

Methods:

For example code see gui/demos/active.py.

Callback

The callback receives an initial arg being the widget instance followed by any user supplied args. The callback can be a bound method, typically of a Screen subclass. The callback runs when the widget is instantiated and whenever the value changes. This enables dynamic color change.

Callback legendcb

The start of each decade is marked by a long "tick" with a user-definable text label. By default it will display a number corresponding to the value at that tick (of form 10**n where n is an integer), but this can be overridden to display values such as "10MHz". The following is a simple example from the scale_ctrl_test demo:

def legendcb(f):
    if f < 999:
        return '{:<1.0f}'.format(f)
    return '{:<1.0f}K'.format(f/1000)

Callback tickcb

This callback enables the tick color to be changed dynamically. For example a scale might change from green to orange, then to red as it nears the extremes. The callback takes two args, being the value of the tick (of form 10**n where n is an integer) and the default color. It must return a color. This example is taken from the scale_ctrl_test demo:

def tickcb(f, c):
    if f > 30000:
        return RED
    if f < 10:
        return BLUE
    return c
Contents

6.15 Dial widget

from gui.widgets import Dial, Pointer  # File: dial.py

Image Image

A Dial is a passive widget. It presents a circular display capable of displaying an arbitrary number of vectors; each vector is represented by a Pointer instance. The format of the display may be chosen to resemble an analog clock or a compass. In the CLOCK case a pointer resembles a clock's hand extending from the centre towards the periphery. In the COMPASS case pointers are chevrons extending equally either side of the circle centre.

In both cases the length, angle and color of each Pointer may be changed dynamically. A Dial can include an optional Label at the bottom which may be used to display any required text.

In use, a Dial is instantiated. Then one or more Pointer objects are instantiated and assigned to it. The Pointer.value method enables the Dial to be updated affecting the length, angle and color of the Pointer. Pointer values are complex numbers.

Dial class

Constructor mandatory positional args:

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

Keyword only args:

Method:

  1. text Updates the label if present (otherwise throws a ValueError). 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.

When a Pointer is instantiated it is assigned to the Dial by the Pointer constructor.

Pointer class

Constructor arg:

  1. dial The Dial instance on which it is to be dsplayed.

Methods:

  1. value Args:
    • v=None The value is a complex number. A magnitude exceeding unity is reduced (preserving phase) to constrain the Pointer within the unit circle.
    • color=None By default the pointer is rendered in the foreground color of the parent Dial. Otherwise the passed color is used.
      Returns the current value.

Typical usage:

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

from gui.widgets import Dial, Pointer, CloseButton
import gui.fonts.freesans20 as freesans20

async def run(dial):
    hrs = Pointer(dial)
    mins = Pointer(dial)
    hrs.value(0 + 0.7j, RED)
    mins.value(0 + 0.9j, YELLOW)
    dm = cmath.exp(-1j * cmath.pi / 30)  # Rotate by 1 minute
    dh = cmath.exp(-1j * cmath.pi / 1800)  # Rotate hours by 1 minute
    # Twiddle the hands: see vtest.py for an actual clock
    while True:
        await asyncio.sleep_ms(200)
        mins.value(mins.value() * dm, RED)
        hrs.value(hrs.value() * dh, YELLOW)

class BaseScreen(Screen):

    def __init__(self):
        super().__init__()
        wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False)
        dial = Dial(wri, 5, 5, ticks = 12, bdcolor=None)
        self.reg_task(run(dial))
        CloseButton(wri)

Screen.change(BaseScreen)
Contents

6.16 Knob widget

from gui.widgets import Knob  # File: knob.py

Image

Rightmost example has no border and 270° travel. Others have 360°.

This emulates a rotary control capable of being rotated through a predefined arc in order to display or set a floating point variable. A Knob may be active or passive. A description of the user interface in the active case may be found in Floating Point Widgets.

Constructor mandatory positional args:

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

Optional keyword only arguments:

Methods:

Callback

The callback receives an initial arg being the widget instance followed by any user supplied args. The callback can be a bound method, typically of a Screen subclass. The callback runs when the widget is instantiated and whenever the value changes. This enables dynamic color change.

Contents

6.17 Menu class

from gui.widgets import Menu  # File: menu.py

Image

The Menu class enables the creation of single or multiple level menus. The top level of the menu comprises a row of Button instances at the top of the physical screen. Each button can either call a callback or instantiate a dropdown menu comprising the second menu level.

Each item on a dropdown menu can invoke either a callback or a lower level menu.

Constructor mandatory positional arg:

  1. writer The Writer instance (defines font) to use.

Keyword only args:

Each element in the elements tuple is a tuple defining a menu item. This can take two forms, each of which has the text for the menu item as the first value:

  1. (text, cb, (args,)) The element triggers callback cb with positional args defined by the supplied tuple (which may be ()). The callback receives an initial arg being the Listbox instance which corresponds to the parent dropdown menu.
  2. (text, (elements,)) This element triggers a submenu with a recursive instance of elements.

The following (from gui/demos/menui.py) is complete apart from initial import statements. It illustrates a 3-level menu.

class BaseScreen(Screen):

    def __init__(self):
        def cb(button, n):
            print('Help callback', n)

        def cb_sm(lb, n):
            print('Submenu callback', lb.value(), lb.textvalue(), n)

        super().__init__()
        metals2 = (('Gold', cb_sm, (10,)),
                   ('Silver', cb_sm, (11,)),
                   ('Iron', cb_sm, (12,)),
                   ('Zinc', cb_sm, (13,)),
                   ('Copper', cb_sm, (14,)))  # Level 3

        gases = (('Helium', cb_sm, (0,)),
                 ('Neon', cb_sm, (1,)),
                 ('Argon', cb_sm, (2,)),
                 ('Krypton', cb_sm, (3,)),
                 ('Xenon', cb_sm, (4,)),
                 ('Radon', cb_sm, (5,)))  # Level 2

        metals = (('Lithium', cb_sm, (6,)),
                  ('Sodium', cb_sm, (7,)),
                  ('Potassium', cb_sm, (8,)),
                  ('Rubidium', cb_sm, (9,)),
                  ('More', metals2))  # Level 2

        mnu = (('Gas', gases),
               ('Metal', metals),
               ('Help', cb, (2,)))  # Top level 1

        wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
        Menu(wri, bgcolor=BLUE, textcolor=WHITE, args = mnu)
        CloseButton(wri)

Screen.change(BaseScreen)

The code

        mnu = (('Gas', gases),
               ('Metal',metals),
               ('Help', cb, (2,)))

defines the top level, with the first two entries invoking submenus and the third running a callback cb with 2 as an arg.

This produces a second level menu with one entry ('More') invoking a third level (metals2):

        metals = (('Lithium', cb_sm, (6,)),
                  ('Sodium', cb_sm, (7,)),
                  ('Potassium', cb_sm, (8,)),
                  ('Rubidium', cb_sm, (9,)),
                  ('More', metals2))

The other entries all run cb_sm with a different arg. They could each run a different callback if the application required it.

Contents

6.18 BitMap Widget

from gui.widgets import BitMap  # File: bitmap.py

Image

This renders a monochrome bitmap stored in a file to a rectangular region. The bitmap file format is C source code generated by the Linux bitmap editor. The bitmap may be rendered in any color. Data and colors can be changed at run time. The widget is intended for larger bitmaps and is designed to minimise RAM usage at cost of performance. For fast updates of smaller bitmaps consider using an icon font.

Constructor mandatory positional args:

  1. writer A Writer instance.
  2. row Location on screen.
  3. col
  4. height Image height in pixels. Dimensions must exactly match the image file.
  5. width Image width in pixels.

Keyword only args:

Methods:__

Because of the use of file storage when an update occurs there will be a brief "dead time" when the GUI is unresponsive. This is not noticeable if the image is displayed when a screen initialises, or if it changes in response to a user action. Use in animations is questionable.

See gui/demos/bitmap.py for a usage example. For this demo four files must be copied from gui/fonts/bitmaps/ to the root directory of the device.

Contents

6.19 QRMap Widget

from gui.widgets import QRMap  # File: qrcode.py

Image

This renders QR codes generated using the uQR application. Images may be scaled to render them at larger sizes. Please see the notes below on performance and RAM usage.

Constructor positional args:

  1. writer A Writer instance.
  2. row Location on screen.
  3. col
  4. version=4 Defines the size of the image: see below.
  5. scale=1

Keyword only args:

Methods:__

Static Method:__

Note on image sizes. The size of a QR code bitmap depends on the version and scale parameters according to this formula:
edge_length_in_pixels = (4 * version + 17) * scale
To this must be added a mandatory 4 pixel border around every edge. So the height and width occupied on screen is:
dimension = (4 * version + 25) * scale

Performance
The uQR get_matrix() method blocks: in my testing for about 750ms. A QRMap buffers the scaled matrix and renders it using bit blitting. Blocking by QRMap methods is minimal; refreshing a screen with the same contents is fast.

The uQR library is large, and compiling it uses a substantial amount of RAM. If memory errors are encountered try cross-compiling or the use of frozen byte code.

See gui/demos/qrcode.py for a usage example. The demo expects uQR.py to be located in the root directory of the target.

Contents

6.20 Pad widget

This rectangular active widget is invisible. It can be used to enable passive widgets or objects drawn with display primitives to respond to touch.

Constructor mandatory positional arguments:

  1. writer A Writer instance.
  2. row Location on screen.
  3. col

Optional keyword only arguments:

Method:

Class variable:

The demo primitives.py illustrates this widget.

Contents

7. Graph Plotting

from gui.widgets.graph import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence

Image Image

Image Image

Image Realtime time sequence simulation.

For example code see gui/demos/plot.py.

7.1 Concepts

Data for Cartesian graphs constitutes a sequence of x, y pairs, for polar graphs it is a sequence of complex z values. The module supports three common cases:

  1. The dataset to be plotted is complete at the outset.
  2. Arbitrary data arrives gradually and needs to be plotted as it arrives.
  3. One or more y values arrive gradually. The X axis represents time. This is a simplifying case of 2.

7.1.1 Graph classes

A user program first instantiates a graph object (PolarGraph or CartesianGraph). This creates an empty graph image upon which one or more curves may be plotted. Graphs are passive widgets so do not respond to touch.

7.1.2 Curve classes

The user program then instantiates one or more curves (Curve or PolarCurve) as appropriate to the graph. Curves may be assigned colors to distinguish them.

A curve is plotted by means of a user defined populate generator. This assigns points to the curve in the order in which they are to be plotted. The curve will be displayed on the graph as a sequence of straight line segments between successive points.

Where it is required to plot realtime data as it arrives, this is achieved via calls to the curve's point method. If a prior point exists it causes a line to be drawn connecting the point to the last one drawn.

7.1.3 Coordinates

PolarGraph and CartesianGraph objects are subclassed from Widget and are positioned accordingly by row and col with a 2-pixel outside border. The coordinate system within a graph conforms to normal mathematical conventions.

Scaling is provided on Cartesian curves enabling user defined ranges for x and y values. Points lying outside of the defined range will produce lines which are clipped at the graph boundary.

Points on polar curves are defined as Python complex types and should lie within the unit circle. Points which are out of range may be plotted beyond the unit circle but will be clipped to the rectangular graph boundary.

Contents

7.2 Graph classes

7.2.1 Class CartesianGraph

Constructor.
Mandatory positional arguments:

  1. writer A CWriter instance.
  2. row Position of the graph in screen coordinates.
  3. col

Keyword only arguments (all optional):

Method:

7.2.2 Class PolarGraph

Constructor.
Mandatory positional arguments:

  1. writer A CWriter instance.
  2. row Position of the graph in screen coordinates.
  3. col

Keyword only arguments (all optional):

Method:

Contents

7.3 Curve classes

7.3.1 Class Curve

The Cartesian curve constructor takes the following positional arguments:

Mandatory arguments:

  1. graph The CartesianGraph instance.
  2. color If None is passed, the graph foreground color is used.

Optional arguments:

  1. populate=None A generator to populate the curve. See below.
  2. origin=(0,0) 2-tuple containing x and y values for the origin. Provides for an optional shift of the data's origin.
  3. excursion=(1,1) 2-tuple containing scaling values for x and y.

Methods:

The populate generator may take zero or more positional arguments. It should repeatedly yield x, y values before returning. Where a curve is discontinuous None, None may be yielded: this causes the line to stop. It is resumed when the next valid x, y pair is yielded.

If populate is not provided the curve may be plotted by successive calls to the point method. This may be of use where data points are acquired in real time, and realtime plotting is required. See class RTRect in gui/demos/plot.py.

Scaling

By default, with symmetrical axes, x and y values are assumed to lie between -1 and +1.

To plot x values from 1000 to 4000 we would set the origin x value to 1000 and the excursion x value to 3000. The excursion values scale the plotted values to fit the corresponding axis.

7.3.2 Class PolarCurve

The constructor takes the following positional arguments:

Mandatory arguments:

  1. graph The PolarGraph instance.
  2. color

Optional arguments:

  1. populate=None A generator to populate the curve. See below.

Methods:

The populate generator may take zero or more positional arguments. It should yield a complex z value for each point before returning. Where a curve is discontinuous a value of None may be yielded: this causes plotting to stop. It is resumed when the next valid z point is yielded.

If populate is not provided the curve may be plotted by successive calls to the point method. This may be of use where data points are acquired in real time, and realtime plotting is required. See class RTPolar in gui/demos/plot.py.

Scaling

Complex points should lie within the unit circle to be drawn within the grid.

Contents

7.4 Class TSequence

A common task is the acquisition and plotting of real time data against time, such as hourly temperature and air pressure readings. This class facilitates this. Time is on the x-axis with the most recent data on the right. Older points are plotted to the left until they reach the left hand edge when they are discarded. This is akin to old fashioned pen plotters where the pen was at the rightmost edge (corresponding to time now) with old values scrolling to the left with the time axis in the conventional direction.

The user instantiates a graph with the X origin at the right hand side and then instantiates one or more TSequence objects. As each set of data arrives it is appended to its TSequence using the add method. See the example below.

The constructor takes the following args:

Mandatory arguments:

  1. graph The CartesianGraph instance.
  2. color
  3. size Integer. The number of time samples to be plotted. See below.

Optional arguments:

  1. yorigin=0 These args provide scaling of Y axis values as per the Curve class. 5 yexc=1

Method:

  1. add Arg v the value to be plotted. This should lie between -1 and +1 unless scaling is applied.

Note that there is little point in setting the size argument to a value greater than the number of X-axis pixels on the graph. It will work but RAM and execution time will be wasted: the constructor instantiates an array of floats of this size.

Each time a data set arrives the graph should be cleared and a data value is added to each TSequence instance. The following (slightly simplified) is taken from gui/demos/plot.py and simulates the slow arrival of sinusoidal values.

class TSeq(Screen):
    def __init__(self):
        super().__init__()
        self.g = CartesianGraph(wri, 2, 2, xorigin = 10, fgcolor=GREEN,
                                gridcolor=LIGHTGREEN, bdcolor=False)

    def after_open(self):  # After graph has been drawn
        self.reg_task(self.run(self.g), True)  # Cancel on screen change

    async def run(self, g):
        await asyncio.sleep_ms(0)
        tsy = TSequence(g, YELLOW, 50)
        tsr = TSequence(g, RED, 50)
        t = 0
        while True:
            g.show()  # Redraw the empty graph
            tsy.add(0.9*math.sin(t/10))
            tsr.add(0.4*math.cos(t/10))  # Plot the new curves
            await asyncio.sleep_ms(400)
            t += 1
Contents

8. Realtime applications

These notes assume an application based on asyncio that needs to handle events occurring in real time. There are two ways in which the GUI might affect real time performance:

The GUI uses asyncio internally and runs a number of tasks. Most of these are simple and undemanding, the one exception being refresh. This has to copy the contents of the frame buffer to the hardware, and runs continuously. The way this works depends on the display type. On small displays with relatively few pixels it is a blocking, synchronous method. On bigger screens such a method would block for many tens of ms causing latency which would affect the responsiveness of the user interface. The drivers for such screens have an asynchronous do_refresh method: this divides the refresh into a small number of segments, each of which blocks for a short period, preserving responsiveness.

In the great majority of applications this works well. For demanding cases a user-accessible Lock is provided to enable refresh to be paused. This is Screen.rfsh_lock. Further, the behaviour of this Lock can be modified. By default the refresh task will hold the Lock for the entire duration of a refresh. Alternatively the Lock can be held for the duration of the update of one segment. In testing on a Pico with ILI9341 the Lock duration was reduced from 95ms to 11.3ms. If an application has a task which needs to be scheduled at a high rate, this corresponds to an increase from 10Hz to 88Hz.

If an application acquires the lock, accesses to the touch controller will also be paused. In systems with a shared SPI bus this guarantees that the application has exclusive access to the bus. While the lock is held the application may use the bus as required.

The mechanism for controlling lock behaviour is a method of the ssd instance:

The following (pseudocode, simplified) illustrates this mechanism:

class Screen:
    rfsh_lock = Lock()  # Refresh pauses until lock is acquired

    @classmethod
    async def auto_refresh(cls):
        while True:
            if display_supports_segmented_refresh and short_lock_is_enabled:
                # At intervals yield and release the lock
                await ssd.do_refresh(split, cls.rfsh_lock)
            else:  # Lock for the entire refresh
                await asyncio.sleep_ms(0)  # Let user code respond to event
                async with cls.rfsh_lock:
                    if display_supports_segmented_refresh:
                        # Yield at intervals (retaining lock)
                        await ssd.do_refresh(split)  # Segmented refresh
                    else:
                        ssd.show()  # Blocking synchronous refresh on small screen.

User code can wait on the lock and, once acquired, run asynchronous code which cannot be interrupted by a refresh. This is normally done with an asynchronous context manager:

async with Screen.rfsh_lock:
    # do something that can't be interrupted with a refresh

The demo refresh_lock.py illustrates this mechanism, allowing refresh to be started and stopped. The demo also allows the short_lock method to be tested, with a display of the scheduling rate of a minimal locked task. In a practical application this rate is dependant on various factors. A number of debugging aids exist to assist in measuring and optimising this. See this doc.

The micro-gui audio demo provides an example, where the play_song task gives priority to maintaining the audio buffer. It does this by holding the lock for several iterations of buffer filling before releasing the lock to allow a single refresh.

See Appendix 4 GUI Design notes for the reason for continuous refresh.

Contents

Appendix 1 Application design

Screen layout

Widgets are positioned using absolute row and col coordinates. These may optionally be calculated using the metrics of other widgets. This facilitates relative positioning which can make layouts easier to modify. Such layouts can also automatically adapt to changes of fonts. To simplify this, all widgets have the following bound variables, which should be considered read-only:

A further aid to metrics is the Writer method .stringlen(s). This takes a string as its arg and returns its length in pixels when rendered using the font of that Writer instance.

The mrow and mcol values enable other widgets to be positioned relative to the one previously instantiated. In the cases of sliders, Dial and Meter widgets these take account of space occupied by legends or labels.

The aclock.py and linked_sliders.py demos provide simple examples of this approach.

Use of graphics primitives

See demo primitives.py.

These notes are for those wishing to draw directly to the Screen instance. This is done by providing the user Screen class with an after_open() method which is written to issue the display driver calls.

The following code instantiates two classes:

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

The ssd object is an instance of the object defined in the display driver. It is a requirement that this is a subclass of framebuf.FrameBuffer. Hence ssd supports all the graphics primitives provided by FrameBuffer. These may be used to draw on the Screen.

The display object has methods with the same names and args as those of ssd. These support greying out. So you can write (for example)

display.rect(10, 10, 50, 50, RED)

To render in the correct colors it is wise ensure that greying out is disabled prior to calling display methods. This is done with

display.usegrey(False)

There is little point in issuing display.rect as it confers no advantage over ssd.rect. However the Display class adds methods not currently available in framebuf. These are listed below.

Hopefully these are self explanatory. The Display methods use the framebuf convention of x, y coordinates rather than the row, col system used by the GUI interface.

The primitives.py demo provides a simple example.

Callbacks

Callback functions should execute quickly, otherwise screen refresh will not occur until the callback is complete. Where a time consuming task is to be triggered by a callback an asyncio task should be launched. In the following sample an LED widget is to be cycled through various colors in response to a callback.

def callback(self, button, val):
    self.reg_task(self.flash_led(), on_change=True)

async def flash_led(self):  # Will be cancelled if the screen ceases to be current
    self.led.color(RED)
    self.led.value(True)  # Turn on LED
    await asyncio.sleep_ms(500)
    self.led.color(YELLOW)
    await asyncio.sleep_ms(500)
    self.led.color(GREEN)
    await asyncio.sleep_ms(500)
    self.led.value(False)  # Turn it off. Task is complete.

The callback() executes fast, with flash_led() running as a background task. The use of reg_task is because flash_led() is a method of the Screen object accessing bound objects. The method ensures that the task is cancelled if the user closes or overlays the current screen. For more information on asyncio, see the official docs and tutorial.

Contents

Appendix 2 Freezing bytecode

This achieves a major saving of RAM. The correct way to do this is via a manifest file. The first step is to clone MicroPython and prove that you can build and deploy firmware to the chosen platform. Build instructions vary between ports and can be found in the MicroPython source tree in ports/<port>/README.md.

The following is an example of how the entire GUI with fonts, demos and all widgets can be frozen on RP2.

Build script:

cd /mnt/qnap2/data/Projects/MicroPython/micropython/ports/rp2
MANIFEST='/mnt/qnap2/Scripts/manifests/rp2_manifest.py'

make submodules
make clean
if make -j 8 BOARD=PICO FROZEN_MANIFEST=$MANIFEST
then
    echo Firmware is in build-PICO/firmware.uf2
else
    echo Build failure
fi
cd -

Manifest file contents (first line ensures that the default files are frozen):

include("$(MPY_DIR)/ports/rp2/boards/manifest.py")
freeze('/mnt/qnap2/Scripts/modules/rp2_modules')

The directory /mnt/qnap2/Scripts/modules/rp2_modules contains only a symlink to the gui directory of the micropython-touch source tree. The freezing process follows symlinks and respects directory structures.

It is usually best to keep hardware_setup.py unfrozen for ease of making changes. I also keep the display driver and boolpalette.py in the filesystem as I have experienced problems freezing display drivers - but feel free to experiment.

Appendix 3 Cross compiling

This addresses the case where a memory error occurs on import. There are better savings with frozen bytecode, but cross compiling the main program module saves the compiler from having to compile a large module on the target hardware. The cross compiler is documented here.

Change to the directory gui/core and issue:

$ /path/to/micropython/mpy-cross/mpy-cross tgui.py

This creates a file tgui.mpy. It is necessary to move, delete or rename tgui.py as MicroPython loads a .py file in preference to .mpy.

If "incorrect mpy version" errors occur, the cross compiler should be recompiled.

Contents

Appendix 4 GUI Design notes

A user (Toni Röyhy) raised the question of why refresh operates as a continuous background task, even when nothing has changed on screen. The concern was that it may result in needless power consumption. The following reasons apply:

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

from gui.widgets import Label, Button, CloseButton, LED
from gui.core.writer import CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *
import asyncio

async def stop_rfsh():
    await Screen.rfsh_lock.acquire()

def cby(_):
    asyncio.create_task(stop_rfsh())

def cbn(_):
    Screen.rfsh_lock.release()  # Allow refresh

class BaseScreen(Screen):
    def __init__(self):

        super().__init__()
        wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
        col = 2
        row = 2
        Label(wri, row, col, "Refresh test")
        self.led = LED(wri, row, 80)
        row = 50
        Button(wri, row, col, text="Stop", callback=cby)
        col += 60
        Button(wri, row, col, text="Start", callback=cbn)
        self.reg_task(self.flash())
        CloseButton(wri)  # Quit

    async def flash(self):  # Proof of stopped refresh
        while True:
            self.led.value(not self.led.value())
            await asyncio.sleep_ms(300)

def test():
    print("Refresh test.")
    Screen.change(BaseScreen)

test()
Contents

Appendix 5 Bus sharing

Boards from Waveshare use the same SPI bus to access the display controller, the touch controller, and an optional SD card. If an SD card is fitted, it is possible to mount this in boot.py: doing this enables the filesystem on the SD card to be managed at the Bash prompt using mpremote. There is a "gotcha" here. For this to work reliably, the CS\ pins of the display controller and the touch controller must be set high, otherwise bus contention on the miso line can occur. The following is an example of a boot.py for the 2.8" Pico Res touch.

from machine import SPI, Pin
from sdcard import SDCard
import os
BAUDRATE = 3_000_000  # Much higher rates seem OK, but may depend on card.
# Initialise all CS\ pins
cst = Pin(16, Pin.OUT, value=1)  # Touch XPT2046
csd = Pin(9, Pin.OUT, value=1)  # Display ST7789
css = Pin(22, Pin.OUT, value=1)  # SD card
spi = SPI(1, BAUDRATE, sck=Pin(10), mosi=Pin(11), miso=Pin(12))
sd = SDCard(spi, css, BAUDRATE)
vfs = os.VfsFat(sd)
os.mount(vfs, "/sd")

An application which is to access the SD card must ensure that the GUI is prevented from accessing the SPI bus for the duration of SD card access. This may be done with an asynchronous context manager. When the context manager terminates, refresh and touch sensitivity will re-start.

async def read_data():
    async with Screen.rfsh_lock:
        # set up the SPI bus baudrate for the SD card
        # read the data
    await asyncio.sleep_ms(0)  # Allow refresh and touch to proceed
    # Do anything else you need

See section 8 for further background. Tested by @bianc104 in iss 15

Contents