Add grid widget, calendar demo.

encoder_driver
peterhinch 2023-02-16 10:30:28 +00:00
rodzic 69e173be59
commit e02888f43d
4 zmienionych plików z 331 dodań i 6 usunięć

Wyświetl plik

@ -82,7 +82,7 @@ development so check for updates.
1.3 [Fonts](./README.md#13-fonts)
1.4 [Navigation](./README.md#14-navigation) How the GUI navigates between widgets.
1.5 [Hardware definition](./README.md#15-hardware-definition) How to configure your hardware.
1.6 [Quick hardware check](./README.md#16-quick-hardware-check) Testing the hardware config.
1.6 [Quick hardware check](./README.md#16-quick-hardware-check) Testing the hardware config. Please do this first.
1.7 [Installation](./README.md#17-installation) Installing the library.
1.8 [Performance and hardware notes](./README.md#18-performance-and-hardware-notes)
1.9 [Firmware and dependencies](./README.md#19-firmware-and-dependencies)
@ -112,6 +112,7 @@ development so check for updates.
5.3 [Popup windows](./README.md#53-popup-windows)
6. [Widgets](./README.md#6-widgets) Displayable objects.
6.1 [Label widget](./README.md#61-label-widget) Single line text display.
     6.1.1 [Grid widget](./README.md#611-grid-widget) A spreadsheet-like array of labels.
6.2 [LED widget](./README.md#62-led-widget) Display Boolean values.
6.3 [Checkbox widget](./README.md#63-checkbox-widget) Enter Boolean values.
6.4 [Button and CloseButton widgets](./README.md#64-button-and-closebutton-widgets) Pushbutton emulation.
@ -147,7 +148,8 @@ development so check for updates.
8. [ESP32 touch pads](./README.md#8-esp32-touch-pads) Replacing buttons with touch pads.
9. [Realtime applications](./README.md#9-realtime-applications) Accommodating tasks requiring fast RT performance.
[Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, encoder interface, use of graphics primitives
[Appendix 2 Freezing bytecode](./README.md#appendix-2-freezing-bytecode) Optional way to save RAM.
[Appendix 2 Freezing bytecode](./README.md#appendix-2-freezing-bytecode) Optional way to save RAM.
[Appendix 3 Cross compiling](./README.md#appendix-3-cross-compiling) Another way to save RAM.
# 1. Basic concepts
@ -432,6 +434,10 @@ 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](./README.md#appendix-3-cross-compiling).
#### Speed
The consequence of inadequate speed is that brief button presses can be missed.
@ -575,6 +581,7 @@ Some of these require larger screens. Required sizes are specified as
* `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).
###### [Contents](./README.md#0-contents)
@ -1012,8 +1019,8 @@ Constructor args:
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=None` Background color of object. If `None` the `Writer` background
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.
@ -1065,6 +1072,65 @@ Screen.change(BaseScreen)
###### [Contents](./README.md#0-contents)
### 6.1.1 Grid widget
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 columen 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(colun width + 4)
Cells may be addressed as a 1-dimensional list or by a `[row, col]` 2-list or
2-tuple.
Constructor args:
writer, row, col, lwidth, nrows, ncols, invert=False, fgcolor=None, bgcolor=BLACK, bdcolor=None, justify=0
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 object. 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` i.e. where an integer is supplied for the `text` arg.
Method:
* `__getitem__` This enables an individual `Label`'s `value` method to be
retrieved using index notation. The args detailed above enable inividual cells
to be updated.
Example uses:
```python
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) # Note syntax
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](./README.md#0-contents)
## 6.2 LED widget
```python
@ -3021,8 +3087,8 @@ have the following bound variables, which should be considered read-only:
* `height` As specified. Does not include border.
* `width` Ditto.
* `mrow` Maximum absolute row occupied by the widget.
* `mcol` Maximum absolute col occupied by the widget.
* `mrow` Maximum absolute row occupied by the widget (including border).
* `mcol` Maximum absolute col occupied by the widget (including border).
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 that
@ -3121,3 +3187,20 @@ 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](https://github.com/micropython/micropython/blob/master/mpy-cross/README.md).
Change to the directory `gui/core` and issue:
```bash
$ /path/to/micropython/mpy-cross/mpy-cross ugui.py
```
This creates a file `ugui.mpy`. It is necessary to move, delete or rename
`ugui.py` as MicroPython loads a `.py` file in preference to `.mpy`.
If "incorrect mpy version" errors occur, the cross compiler should be
recompiled.

Wyświetl plik

@ -0,0 +1,171 @@
# calendar.py Test Grid class.
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
# As written requires a 240*320 pixel display, but could easily be reduced
# with smaller fonts.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
from gui.widgets import Grid, CloseButton, Label, Button
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.font10 as font
import gui.fonts.font14 as font1
from gui.core.colors import *
from time import mktime, localtime
SECS_PER_DAY = const(86400)
class Date:
days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')
months = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December')
def __init__(self):
self.now()
def now(self):
lt = list(localtime())
lt[3] = 6 # Disambiguate midnight
self.cur = mktime(lt) // SECS_PER_DAY
self.update()
def update(self, lt=None):
if lt is not None:
lt[3] = 6
self.cur = mktime(lt) // SECS_PER_DAY
lt = localtime(self.cur * SECS_PER_DAY)
self.year = lt[0]
self.month = lt[1]
self.mday = lt[2]
self.wday = lt[6]
ml = self.mlen(self.month)
self.month_length = ml
self.wday1 = (self.wday - self.mday + 1) % 7 # Weekday of 1st of month
# Commented out code provides support for UK DST calculation
#wdayld = (self.wday1 + ml -1) % 7 # Weekday of last day of month
#self.mday_sun = ml - (wdayld + 1) % 7 # Day of month of last Sunday
def add_days(self, n):
self.cur += n
self.update()
def add_months(self, n): # Crude algorithm for small n
for _ in range(abs(n)):
self.cur += self.month_length if n > 0 else -self.mlen(self.month - 1)
self.update()
def add_years(self, n):
lt = list(localtime(self.cur * SECS_PER_DAY))
lt[0] += n
self.update(lt)
def mlen(self, month):
days = (31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1]
year = self.year
return days if days else (28 if year % 4 else (29 if year % 100 else 28))
class BaseScreen(Screen):
def __init__(self):
super().__init__()
self.date = Date()
wri = CWriter(ssd, font, GREEN, BLACK, False)
wri1 = CWriter(ssd, font1, WHITE, BLACK, False)
col = 2
row = 2
rows = 6
cols = 7
colwidth = 35
self.lbl = Label(wri, row, col, text = (colwidth + 4) * cols, justify=Label.CENTRE)
row = self.lbl.mrow
self.grid = Grid(wri, row, col, colwidth, rows, cols, justify=Label.CENTRE)
for n, day in enumerate(Date.days):
self.grid[[0, n]] = day[:3]
row = self.grid.mrow + 4
ht = 30
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="y-", callback=self.adjust, args=(self.date.add_years, -1))
col = b.mcol + 2
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="y+", callback=self.adjust, args=(self.date.add_years, 1))
col = b.mcol + 5
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="m-", callback=self.adjust, args=(self.date.add_months, -1))
col = b.mcol + 2
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="m+", callback=self.adjust, args=(self.date.add_months, 1))
col = b.mcol + 5
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="w-", callback=self.adjust, args=(self.date.add_days, -7))
col = b.mcol + 2
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="w+", callback=self.adjust, args=(self.date.add_days, 7))
col = b.mcol + 5
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="d-", callback=self.adjust, args=(self.date.add_days, -1))
col = b.mcol + 2
b = Button(wri, row, col, height=ht, shape=CIRCLE, text="d+", callback=self.adjust, args=(self.date.add_days, 1))
col = b.mcol + 5
b = Button(wri, row, col, height=ht, shape=CIRCLE, fgcolor=BLUE, text="H", callback=self.adjust, args=(self.date.now,))
#row = b.mrow + 10
col = 2
row = ssd.height - (wri1.height + 2)
self.lblnow = Label(wri1, row, col, text =ssd.width - 4, fgcolor=WHITE)
self.update()
CloseButton(wri) # Quit the application
def adjust(self, _, f, n=0):
f(n) if n else f()
self.update()
def update(self):
def cell():
#d.clear()
if cur.year == today.year and cur.month == today.month and mday == today.mday: # Today
d["fgcolor"] = RED
elif mday == cur.mday: # Currency
d["fgcolor"] = YELLOW
else:
d["fgcolor"] = GREEN
d["text"] = str(mday)
self.grid[idx] = d
today = Date()
cur = self.date # Currency
self.lbl.value(f"{Date.months[cur.month - 1]} {cur.year}")
d = {} # Args for Label.value
wday = 0
mday = 1
seek = True
for idx in range(7, self.grid.ncells):
self.grid[idx] = ""
for idx in range(7, self.grid.ncells):
if seek: # Find column for 1st of month
if wday < cur.wday1:
self.grid[idx] = ""
wday += 1
else:
seek = False
if not seek:
if mday <= cur.month_length:
cell()
mday += 1
else:
self.grid[idx] = ""
idx = 7
while mday <= cur.month_length:
cell()
idx += 1
mday += 1
day = Date.days[today.wday]
month = Date.months[today.month - 1]
self.lblnow.value(f"{day} {today.mday} {month} {today.year}")
def test():
print('Calendar demo.')
Screen.change(BaseScreen) # A class is passed here, not an instance.
test()

Wyświetl plik

@ -25,6 +25,7 @@ _attrs = {
"Textbox": "textbox",
"BitMap": "bitmap",
"QRMap": "qrcode",
"Grid": "grid",
}
# Lazy loader, effectively does:

Wyświetl plik

@ -0,0 +1,70 @@
# grid.py micro-gui widget providing the Grid class: a 2d array of Label instances.
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from gui.core.ugui import Widget, display
from gui.core.writer import Writer
from gui.core.colors import *
from gui.widgets import Label
# lwidth may be integer Label width in pixels or a tuple/list of widths
class Grid(Widget):
def __init__(self, writer, row, col, lwidth, nrows, ncols, invert=False, fgcolor=None, bgcolor=BLACK, bdcolor=None, justify=0):
self.nrows = nrows
self.ncols = ncols
self.ncells = nrows * ncols
self.cheight = writer.height + 4 # Cell height including borders
# Build column width list. Column width is Label width + 4.
if isinstance(lwidth, int):
self.cwidth = [lwidth + 4] * ncols
else: # Ensure len(.cwidth) == ncols
self.cwidth = [w + 4 for w in lwidth][:ncols]
self.cwidth.extend([lwidth[-1] + 4] * (ncols - len(lwidth)))
width = sum(self.cwidth) - 4 # Dimensions of widget interior
height = nrows * self.cheight - 4
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor)
self.cells = []
r = row
c = col
for _ in range(self.nrows):
for cw in self.cwidth:
self.cells.append(Label(writer, r, c, cw - 4, invert, fgcolor, bgcolor, False, justify)) # No border
c += cw
r += self.cheight
c = col
def _idx(self, n):
if isinstance(n, tuple) or isinstance(n, list):
if n[0] >= self.nrows:
raise ValueError("Grid row index too large")
if n[1] >= self.ncols:
raise ValueError("Grid col index too large")
idx = n[1] + n[0] * self.ncols
else:
idx = n
if idx >= self.ncells:
raise ValueError("Grid cell index too large")
return idx
def __getitem__(self, n): # Return the Label instance
return self.cells[self._idx(n)]
# allow grid[[r, c]] = "foo" or kwargs for Label:
# grid[[r, c]] = {"text": str(n), "fgcolor" : RED}
def __setitem__(self, n, x):
v = self.cells[self._idx(n)].value
_ = v(**x) if isinstance(x, dict) else v(x)
def show(self):
super().show() # Draw border
if self.has_border: # Draw grid
color = self.bdcolor
x = self.col - 2 # Border top left corner
y = self.row - 2
dy = self.cheight
for row in range(1, self.nrows):
display.hline(x, y + row * dy, self.width + 4, color)
for cw in self.cwidth[:-1]:
x += cw
display.vline(x, y, self.height + 4, color)