kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
Add grid widget, calendar demo.
rodzic
69e173be59
commit
e02888f43d
95
README.md
95
README.md
|
@ -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.
|
||||
|
|
|
@ -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()
|
|
@ -25,6 +25,7 @@ _attrs = {
|
|||
"Textbox": "textbox",
|
||||
"BitMap": "bitmap",
|
||||
"QRMap": "qrcode",
|
||||
"Grid": "grid",
|
||||
}
|
||||
|
||||
# Lazy loader, effectively does:
|
||||
|
|
|
@ -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)
|
Ładowanie…
Reference in New Issue