kopia lustrzana https://github.com/peterhinch/micropython-samples
ugui.py now includes all classes. Rotary knob now supported
rodzic
ceda9894b4
commit
84c7a5df4b
|
@ -0,0 +1,150 @@
|
|||
# micropython-gui
|
||||
|
||||
Provides a simple touch driven event based GUI interface for the Pyboard when used with a TFT
|
||||
display. The latter should be based on SSD1963 controller with XPT2046 touch controller. Such
|
||||
displays are available in electronics stores and on eBay. The software is based on drivers for the
|
||||
TFT and touch controller from Robert Hammelrath together with a cooperative scheduler of my own
|
||||
design.
|
||||
|
||||
It is targeted at hardware control and display applications.
|
||||
|
||||
# Pre requisites
|
||||
|
||||
[TFT driver](https://github.com/robert-hh/SSD1963-TFT-Library-for-PyBoard.git)
|
||||
[XPT2046 driver](https://github.com/robert-hh/XPT2046-touch-pad-driver-for-PyBoard.git)
|
||||
[Scheduler](https://github.com/peterhinch/Micropython-scheduler.git)
|
||||
|
||||
Core files:
|
||||
1. TFT_io.py Low level TFT driver *
|
||||
2. touch.py Touch controller driver *
|
||||
3. tft.py TFT driver
|
||||
4. usched.py Scheduler
|
||||
5. delay.py Used with the scheduler for watchdog type delays.
|
||||
6. ugui.py The micro GUI library.
|
||||
|
||||
Optional files:
|
||||
1. font10.py Font used by the test programs.
|
||||
2. font14.py Font used by the test programs.
|
||||
|
||||
Test/demo programs:
|
||||
1. vst.py A test program for vertical linear sliders.
|
||||
2. hst.py Tests horizontal slider controls, meters and LED.
|
||||
3. buttontest.py Pushbuttons and checkboxes.
|
||||
4. knobtest.py Rotary control test.
|
||||
|
||||
It should be noted that by the standards of the Pyboard this is a large library. Attempts to use it
|
||||
in the normal way are likely to provoke memory errors owing to heap fragmentation. It is
|
||||
recommended that the core and optional files are included with the firmware as persistent bytecode.
|
||||
You may also want to include any other fonts you plan to use. The first two core files listed above
|
||||
cannot be included as they use inline assembler. Instructions on how to do this may be found
|
||||
[here](http://forum.micropython.org/viewtopic.php?f=6&t=1776).
|
||||
|
||||
It is also wise to issue ctrl-D to soft reset the Pyboard before importing a module which uses the
|
||||
library. The test programs require a ctrl-D before import.
|
||||
|
||||
Instructions on creating font files may be found in the README for the TFT driver listed above.
|
||||
|
||||
# Concepts
|
||||
|
||||
### Coordinates
|
||||
|
||||
In common with most displays, the top left hand corner of the display is (0, 0) with increasing
|
||||
values of x to the right, and increasing values of y downward. Display objects exist within a
|
||||
rectangular bounding box; in the case of touch sensitive controls this corresponds to the sensitive
|
||||
region. The location of the object is defined as the coordinates of the top left hand corner of the
|
||||
bounding box. Locations are defined as a 2-tuple (x, y).
|
||||
|
||||
### Colours
|
||||
|
||||
These are defined as a 3-tuple (r, g, b) with values of red, green and blue in range 0 to 255. The
|
||||
interface uses the American spelling (color) throughout for consistency with the TFT library.
|
||||
|
||||
### Callbacks
|
||||
|
||||
The interface is event driven. Optional callbacks may be provided which will be executed when a
|
||||
given event occurs. 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 list
|
||||
of items. Note that a list rather than a tuple should be used.
|
||||
|
||||
# Initialisation Code
|
||||
|
||||
# Displays
|
||||
|
||||
These classes provide ways to display data and are not touch sensitive.
|
||||
|
||||
## Class Label
|
||||
|
||||
Displays text in a fixed length field. Constructor mandatory positional arguments:
|
||||
1. ``tft`` The TFT object.
|
||||
2. ``location`` 2-tuple defining position.
|
||||
Keyword only arguments:
|
||||
1. ``font`` Mandatory. Font object to use.
|
||||
2. ``width`` Mandatory. The width of the object in pixels.
|
||||
3. ``border`` Border width in pixels - typically 2. If omitted, no border will be drawn.
|
||||
4. ``fgcolor`` Color of border. Defaults to system color.
|
||||
5. ``bgcolor`` Background color of object. Defaults to system background.
|
||||
6. ``fontcolor`` Text color. Defaults to system text color.
|
||||
7. ``text`` Initial text. Defaults to ''.
|
||||
Method:
|
||||
1. ``show`` Argument: ``text``. Displays the string in the label.
|
||||
|
||||
## Class Dial
|
||||
|
||||
Displays angles in a circular dial. Angles are in radians with zero represented by a vertical
|
||||
pointer. Positive angles appear as clockwise rotation of the pointer. The object can display
|
||||
multiple angles using pointers of differing lengths (e.g. clock face). Constructor mandatory
|
||||
positional arguments:
|
||||
1. ``tft`` The TFT object.
|
||||
2. ``location`` 2-tuple defining position.
|
||||
Keyword only arguments (all
|
||||
1. ``height`` Dimension of the square bounding box. Default 100 pixels.
|
||||
2. ``fgcolor`` Color of border. Defaults to system color.
|
||||
3. ``bgcolor`` Background color of object. Defaults to system background.
|
||||
4. ``border`` Border width in pixels - typically 2. If omitted, no border will be drawn.
|
||||
5. ``pointers`` Tuple of floats in range 0 to 0.9. Defines the length of each pointer as a
|
||||
proportion of the dial diameter. Default (0.9,) i.e. one pointer.
|
||||
6. ``ticks`` Defines the number of graduations around the dial. Default 4.
|
||||
Method:
|
||||
1. ``show`` Displays an angle. Arguments: ``angle`` (mandatory), ``pointer`` the pointer index
|
||||
(default 0).
|
||||
|
||||
## Class LED
|
||||
|
||||
Displays a boolean state. Can display other information by varying the color. Constructor mandatory
|
||||
positional arguments:
|
||||
1. ``tft`` The TFT object.
|
||||
2. ``location`` 2-tuple defining position.
|
||||
Keyword only arguments:
|
||||
1. ``height`` Dimension of the square bounding box. Default 30 pixels.
|
||||
2. ``fgcolor`` Color of border. Defaults to system color.
|
||||
3. ``bgcolor`` Background color of object. Defaults to system background.
|
||||
4. ``border`` Border width in pixels - typically 2. If omitted, no border will be drawn.
|
||||
5. ``color`` The color of the LED.
|
||||
Methods:
|
||||
1. ``off`` No arguments. Turns the LED off.
|
||||
2. ``on`` Optional arguemnt ``color``. Turns the LED on. By default it will use the ``color``
|
||||
specified in the constructor.
|
||||
|
||||
## Class Meter
|
||||
|
||||
This displays a single value in range 0.0 to 1.0 on a vertical linear meter. Constructor mandatory
|
||||
positional arguments:
|
||||
1. ``tft`` The TFT object.
|
||||
2. ``location`` 2-tuple defining position.
|
||||
Keyword only arguments:
|
||||
1. ``height`` Dimension of the bounding box. Default 200 pixels.
|
||||
2. ``width`` Dimension of the bounding box. Default 30 pixels.
|
||||
3. ``font`` Font to use in any legends. Default: ``None`` No legends will be displayed.
|
||||
4. ``legends`` A tuple of strings to display on the centreline of the meter. These should be
|
||||
short to physically fit. They will be displayed equidistantly along the vertical scale, with
|
||||
string 0 at the bottom. Default ``None``: no legends will be shown.
|
||||
5. ``divisions`` Count of graduations on the meter scale. Default 10.
|
||||
6. ``fgcolor`` Color of border. Defaults to system color.
|
||||
7. ``bgcolor`` Background color of object. Defaults to system background.
|
||||
8. ``fontcolor`` Text color. Defaults to system text color.
|
||||
9. ``pointercolor`` Color of meter pointer. Defaults to ``fgcolor``.
|
||||
10. ``value`` Initial value to display. Default 0.
|
||||
Methods:
|
||||
1.``value`` Optional argument ``val``. If set, refreshes the meter display with a new value,
|
||||
otherwise returns its current value.
|
||||
|
1189
tft_gui/TFT_io.py
1189
tft_gui/TFT_io.py
Plik diff jest za duży
Load Diff
|
@ -1,231 +0,0 @@
|
|||
# button.py Pushbutton classes for Pybboard TFT GUI
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
from delay import Delay
|
||||
from ui import get_stringsize, print_centered, Touchable, CIRCLE, RECTANGLE, CLIPPED_RECT
|
||||
|
||||
# Button coordinates relate to bounding box (BB). x, y are of BB top left corner.
|
||||
# likewise width and height refer to BB, regardless of button shape
|
||||
# If font is None button will be rendered without text
|
||||
|
||||
class Button(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, *, font, shape=CIRCLE, height=50, width=50, fill=True,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, litcolor=None, text='', show=True, callback=lambda x, y : None,
|
||||
args=[]):
|
||||
super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, None)
|
||||
self.shape = shape
|
||||
self.radius = height // 2
|
||||
self.fill = fill
|
||||
self.litcolor = litcolor
|
||||
self.text = text
|
||||
self.callback = callback
|
||||
self.callback_args = args
|
||||
self.orig_fgcolor = fgcolor
|
||||
if self.litcolor is not None:
|
||||
self.delay = Delay(objsched, self.shownormal)
|
||||
self.visible = True # ditto
|
||||
self.litcolor = litcolor if self.fgcolor is not None else None
|
||||
self.busy = False
|
||||
if show:
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
x = self.location[0]
|
||||
y = self.location[1]
|
||||
if not self.visible: # erase the button
|
||||
self.set_color(self.bgcolor)
|
||||
tft.fillRectangle(x, y, x + self.width, y + self.height)
|
||||
self.restore_color()
|
||||
return
|
||||
self.set_color() # to foreground
|
||||
if self.shape == CIRCLE: # Button coords are of top left corner of bounding box
|
||||
x += self.radius
|
||||
y += self.radius
|
||||
if self.fill:
|
||||
tft.fillCircle(x, y, self.radius)
|
||||
else:
|
||||
tft.drawCircle(x, y, self.radius)
|
||||
if self.font is not None and len(self.text):
|
||||
print_centered(tft, x, y, self.text, self.fontcolor, self.font)
|
||||
else:
|
||||
x1 = x + self.width
|
||||
y1 = y + self.height
|
||||
if self.shape == RECTANGLE: # rectangle
|
||||
if self.fill:
|
||||
tft.fillRectangle(x, y, x1, y1)
|
||||
else:
|
||||
tft.drawRectangle(x, y, x1, y1)
|
||||
if self.font is not None and len(self.text):
|
||||
print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font)
|
||||
elif self.shape == CLIPPED_RECT: # clipped rectangle
|
||||
if self.fill:
|
||||
tft.fillClippedRectangle(x, y, x1, y1)
|
||||
else:
|
||||
tft.drawClippedRectangle(x, y, x1, y1)
|
||||
if self.font is not None and len(self.text):
|
||||
print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font)
|
||||
self.restore_color()
|
||||
|
||||
def shownormal(self):
|
||||
self.fgcolor = self.orig_fgcolor
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # If touched, process it otherwise do nothing
|
||||
is_touched = False
|
||||
if self.shape == CIRCLE:
|
||||
r = self.radius
|
||||
dx = r - (x - self.location[0])
|
||||
dy = r - (y - self.location[1])
|
||||
if (dx * dx + dy * dy) < (r * r): # Pythagoras is alive!
|
||||
is_touched = True
|
||||
elif self.shape in (RECTANGLE, CLIPPED_RECT): # rectangle
|
||||
x0 = self.location[0]
|
||||
x1 = self.location[0] + self.width
|
||||
y0 = self.location[1]
|
||||
y1 = self.location[1] + self.height
|
||||
if x0 <= x <= x1 and y0 <= y <= y1:
|
||||
is_touched = True
|
||||
if is_touched and self.litcolor is not None:
|
||||
self.fgcolor = self.litcolor
|
||||
self.show()
|
||||
self.delay.trigger(1)
|
||||
if is_touched and not self.busy: # Respond once to a press
|
||||
self.callback(self, self.callback_args) # Callback not a bound method so pass self
|
||||
self.busy = True # Ensure no response to continued press
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
self.busy = False
|
||||
|
||||
class Buttons(object):
|
||||
def __init__(self, user_callback):
|
||||
self.user_callback = user_callback
|
||||
self.lstbuttons = []
|
||||
|
||||
def add_button(self, *args, **kwargs):
|
||||
kwargs['show'] = False
|
||||
self.lstbuttons.append(Button(*args, **kwargs))
|
||||
|
||||
# Group of buttons, typically at same location, where pressing one shows
|
||||
# the next e.g. start/stop toggle or sequential select from short list
|
||||
class Buttonset(Buttons):
|
||||
def __init__(self, user_callback):
|
||||
super().__init__(user_callback)
|
||||
|
||||
def run(self):
|
||||
for idx, button in enumerate(self.lstbuttons):
|
||||
if idx:
|
||||
button.visible = False # Only button zero visible and sensitive
|
||||
button.enabled = False
|
||||
button.callback_args.append(idx)
|
||||
button.callback = self.callback
|
||||
self.lstbuttons[0].show()
|
||||
|
||||
def callback(self, button, args):
|
||||
button_no = args[-1]
|
||||
old = self.lstbuttons[button_no]
|
||||
new = self.lstbuttons[(button_no + 1) % len(self.lstbuttons)]
|
||||
old.enabled = False
|
||||
old.visible = False
|
||||
old.show()
|
||||
new.enabled = True
|
||||
new.visible = True
|
||||
new.busy = True # Don't respond to continued press
|
||||
new.show()
|
||||
self.user_callback(new, args[:-1]) # user gets button with args they specified
|
||||
|
||||
# Group of buttons at different locations, where pressing one shows
|
||||
# only current button highlighted and oes callback from current one
|
||||
class RadioButtons(Buttons):
|
||||
def __init__(self, user_callback, highlight, selected=0):
|
||||
super().__init__(user_callback)
|
||||
self.highlight = highlight
|
||||
self.selected = selected
|
||||
|
||||
def run(self):
|
||||
for idx, button in enumerate(self.lstbuttons):
|
||||
if idx == self.selected: # Initial selection
|
||||
button.fgcolor = self.highlight
|
||||
else:
|
||||
button.fgcolor = button.orig_fgcolor
|
||||
button.show()
|
||||
button.callback = self.callback
|
||||
|
||||
def callback(self, button, args):
|
||||
for but in self.lstbuttons:
|
||||
if but is button:
|
||||
but.fgcolor = self.highlight
|
||||
else:
|
||||
but.fgcolor = but.orig_fgcolor
|
||||
but.show()
|
||||
self.user_callback(button, args) # user gets button with args they specified
|
||||
|
||||
|
||||
class Checkbox(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, *, height=30, fillcolor=None,
|
||||
fgcolor=None, bgcolor=None, callback=lambda x, y : None, args=[], value=False, border=None):
|
||||
super().__init__(objsched, tft, objtouch, location, None, height, height, fgcolor, bgcolor, None, border)
|
||||
self.callback = callback
|
||||
self.callback_args = args
|
||||
self.fillcolor = fillcolor
|
||||
self.busy = False
|
||||
self.value = value
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
x = self.location[0] + bw
|
||||
y = self.location[1] + bw
|
||||
height = self.height - 2 * bw
|
||||
x1 = x + height
|
||||
y1 = y + height
|
||||
if self.fillcolor is None or not self.value:
|
||||
self.set_color(self.bgcolor) # blank
|
||||
tft.fillRectangle(x, y, x1, y1)
|
||||
if self.fillcolor is not None and self.value:
|
||||
self.set_color(self.fillcolor)
|
||||
tft.fillRectangle(x, y, x1, y1)
|
||||
self.set_color()
|
||||
tft.drawRectangle(x, y, x1, y1)
|
||||
if self.fillcolor is None and self.value:
|
||||
tft.drawLine(x, y, x1, y1)
|
||||
tft.drawLine(x, y1, x1, y)
|
||||
|
||||
def touched(self, x, y): # If touched, process it otherwise do nothing
|
||||
is_touched = False
|
||||
x0 = self.location[0]
|
||||
x1 = self.location[0] + self.width
|
||||
y0 = self.location[1]
|
||||
y1 = self.location[1] + self.height
|
||||
if x0 <= x <= x1 and y0 <= y <= y1:
|
||||
is_touched = True
|
||||
if is_touched and not self.busy: # Respond once to a press
|
||||
self.value = not self.value
|
||||
self.callback(self, self.callback_args) # Callback not a bound method so pass self
|
||||
self.busy = True # Ensure no response to continued press
|
||||
self.show()
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
self.busy = False
|
|
@ -26,20 +26,16 @@ from font14 import font14
|
|||
from tft import TFT, LANDSCAPE
|
||||
from usched import Sched
|
||||
from touch import TOUCH
|
||||
from button import Button, Buttonset, RadioButtons, Checkbox
|
||||
from ui import CIRCLE, RECTANGLE, CLIPPED_RECT, WHITE, BLACK, RED, GREEN, BLUE, YELLOW, GREY
|
||||
from displays import Label
|
||||
from ugui import Button, Buttonset, RadioButtons, Checkbox, Label
|
||||
from ugui import CIRCLE, RECTANGLE, CLIPPED_RECT, WHITE, BLACK, RED, GREEN, BLUE, YELLOW, GREY
|
||||
|
||||
def callback(button, args):
|
||||
arg = args[0]
|
||||
label = args[1]
|
||||
def callback(button, arg, label):
|
||||
label.show(arg)
|
||||
if arg == 'Q':
|
||||
button.objsched.stop()
|
||||
|
||||
def cbcb(checkbox, args):
|
||||
label = args[0]
|
||||
if checkbox.value:
|
||||
def cbcb(checkbox, label):
|
||||
if checkbox.value():
|
||||
label.show('True')
|
||||
else:
|
||||
label.show('False')
|
||||
|
@ -85,11 +81,16 @@ labels = { 'width' : 70,
|
|||
|
||||
# USER TEST FUNCTION
|
||||
|
||||
def cbtest(checkbox):
|
||||
while True:
|
||||
yield 3
|
||||
checkbox.value(not checkbox.value())
|
||||
|
||||
def test():
|
||||
print('Testing TFT...')
|
||||
objsched = Sched() # Instantiate the scheduler
|
||||
mytft = TFT("SSD1963", "LB04301", LANDSCAPE)
|
||||
mytouch = TOUCH("XPT2046", objsched)
|
||||
mytouch = TOUCH("XPT2046", objsched, confidence = 50, margin = 50)
|
||||
mytft.backlight(100) # light on
|
||||
lstlbl = []
|
||||
for n in range(3):
|
||||
|
@ -124,14 +125,15 @@ def test():
|
|||
for t in table4:
|
||||
t['args'].append(lstlbl[2])
|
||||
rb.add_button(objsched, mytft, mytouch, (x, 180), font = font14, fontcolor = WHITE,
|
||||
fgcolor = (0, 0, 90), height = 30, **t)
|
||||
x += 40
|
||||
fgcolor = (0, 0, 90), height = 40, **t)
|
||||
x += 60
|
||||
rb.run()
|
||||
|
||||
# Checkbox
|
||||
Checkbox(objsched, mytft, mytouch, (300, 0), callback = cbcb, args = [lstlbl[0]])
|
||||
Checkbox(objsched, mytft, mytouch, (300, 50), fillcolor = RED, callback = cbcb, args = [lstlbl[1]])
|
||||
cb2 = Checkbox(objsched, mytft, mytouch, (300, 50), fillcolor = RED, callback = cbcb, args = [lstlbl[1]])
|
||||
|
||||
objsched.add_thread(cbtest(cb2)) # Toggle every 2 seconds
|
||||
objsched.run() # Run it!
|
||||
|
||||
test()
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
# displays.py Non touch sensitive display elements for Pyboard TFT GUI
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
from ui import print_centered, NoTouch, BLACK, RED
|
||||
import math
|
||||
import TFT_io
|
||||
|
||||
class Label(NoTouch):
|
||||
def __init__(self, tft, location, *, font, border=None, width, fgcolor=None, bgcolor=None, fontcolor=None, text=''):
|
||||
super().__init__(tft, location, font, None, width, fgcolor, bgcolor, fontcolor, border)
|
||||
self.height = self.font.bits_vert
|
||||
self.height += 2 * self.border # Height determined by font and border
|
||||
self.draw_border() # Must explicitly draw because ctor did not have height
|
||||
self.show(text)
|
||||
|
||||
def show(self, text):
|
||||
tft = self.tft
|
||||
bw = self.border
|
||||
if text:
|
||||
x = self.location[0]
|
||||
y = self.location[1]
|
||||
self.set_color(self.bgcolor)
|
||||
tft.fillRectangle(x + bw, y + bw, x + self.width - bw, y + self.height - bw)
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
tft.setTextPos(x + bw, y + bw, clip = self.width - 2 * bw, scroll = False)
|
||||
tft.printString(text)
|
||||
self.restore_color() # restore fg color
|
||||
|
||||
# class displays angles
|
||||
class Dial(NoTouch):
|
||||
def __init__(self, tft, location, *, height=100, fgcolor=None, bgcolor=None, border=None, pointers=(0.9,), ticks=4):
|
||||
NoTouch.__init__(self, tft, location, None, height, height, fgcolor, bgcolor, None, border) # __super__ provoked Python bug
|
||||
border = self.border # border width
|
||||
radius = height / 2 - border
|
||||
self.radius = radius
|
||||
self.xorigin = location[0] + border + radius
|
||||
self.yorigin = location[1] + border + radius
|
||||
self.pointers = tuple(z * self.radius for z in pointers) # Pointer lengths
|
||||
self.angles = [None for _ in pointers]
|
||||
self.set_color() # set fg color
|
||||
ticklen = 0.1 * radius
|
||||
for tick in range(ticks):
|
||||
theta = 2 * tick * math.pi / ticks
|
||||
x_start = int(self.xorigin + radius * math.sin(theta))
|
||||
y_start = int(self.yorigin - radius * math.cos(theta))
|
||||
x_end = int(self.xorigin + (radius - ticklen) * math.sin(theta))
|
||||
y_end = int(self.yorigin - (radius - ticklen) * math.cos(theta))
|
||||
self.tft.drawLine(x_start, y_start, x_end, y_end)
|
||||
tft.drawCircle(self.xorigin, self.yorigin, radius)
|
||||
self.restore_color()
|
||||
|
||||
def show(self, angle, pointer=0):
|
||||
tft = self.tft
|
||||
if self.angles[pointer] is not None:
|
||||
self.set_color(self.bgcolor)
|
||||
self.drawpointer(self.angles[pointer], pointer) # erase old
|
||||
self.set_color()
|
||||
self.drawpointer(angle, pointer) # draw new
|
||||
self.angles[pointer] = angle # update old
|
||||
self.restore_color()
|
||||
|
||||
def drawpointer(self, radians, pointer):
|
||||
length = self.pointers[pointer]
|
||||
x_end = int(self.xorigin + length * math.sin(radians))
|
||||
y_end = int(self.yorigin - length * math.cos(radians))
|
||||
self.tft.drawLine(int(self.xorigin), int(self.yorigin), x_end, y_end)
|
||||
|
||||
class LED(NoTouch):
|
||||
def __init__(self, tft, location, *, border=None, height=30, fgcolor=None, bgcolor=None, color=RED):
|
||||
super().__init__(tft, location, None, height, height, fgcolor, bgcolor, None, border)
|
||||
self.radius = (self.height - 2 * self.border) / 2
|
||||
self.x = location[0] + self.radius + self.border
|
||||
self.y = location[1] + self.radius + self.border
|
||||
self.color = color
|
||||
self.off()
|
||||
|
||||
def _show(self, color): # Light the LED
|
||||
self.set_color(color)
|
||||
self.tft.fillCircle(int(self.x), int(self.y), int(self.radius))
|
||||
self.set_color()
|
||||
self.tft.drawCircle(int(self.x), int(self.y), int(self.radius))
|
||||
self.restore_color()
|
||||
|
||||
def on(self, color=None): # Light in current color
|
||||
if color is not None:
|
||||
self.color = color
|
||||
self._show(self.color)
|
||||
|
||||
def off(self):
|
||||
self._show(BLACK)
|
||||
|
||||
class Meter(NoTouch):
|
||||
def __init__(self, tft, location, *, font=None, height=200, width=30,
|
||||
fgcolor=None, bgcolor=None, pointercolor=None, fontcolor=None,
|
||||
divisions=10, legends=None, value=0):
|
||||
border = 5 if font is None else 1 + font.bits_vert / 2
|
||||
NoTouch.__init__(self, tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border) # __super__ provoked Python bug
|
||||
border = self.border # border width
|
||||
self.ptrbytes = 3 * (self.width + 1) # 3 bytes per pixel
|
||||
self.ptrbuf = bytearray(self.ptrbytes) #???
|
||||
self.x0 = self.location[0]
|
||||
self.x1 = self.location[0] + self.width
|
||||
self.y0 = self.location[1] + border + 2
|
||||
self.y1 = self.location[1] + self.height - border
|
||||
self.divisions = divisions
|
||||
self.legends = legends
|
||||
self.pointercolor = pointercolor if pointercolor is not None else fgcolor
|
||||
self._value = value
|
||||
self._old_value = -1 # invalidate
|
||||
self.ptr_y = -1 # Invalidate old position
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
width = self.width
|
||||
dx = 5
|
||||
self.set_color()
|
||||
x0 = self.x0
|
||||
x1 = self.x1
|
||||
y0 = self.y0
|
||||
y1 = self.y1
|
||||
height = y1 - y0
|
||||
if self.divisions > 0:
|
||||
dy = height / (self.divisions) # Tick marks
|
||||
for tick in range(self.divisions + 1):
|
||||
ypos = int(y0 + dy * tick)
|
||||
tft.drawHLine(x0, ypos, dx)
|
||||
tft.drawHLine(x1 - dx, ypos, dx)
|
||||
|
||||
if self.legends is not None and self.font is not None: # Legends
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
if len(self.legends) <= 1:
|
||||
dy = 0
|
||||
else:
|
||||
dy = height / (len(self.legends) -1)
|
||||
yl = self.y1 # Start at bottom
|
||||
for legend in self.legends:
|
||||
print_centered(tft, int(self.x0 + self.width /2), int(yl), legend, self.fontcolor, self.font)
|
||||
yl -= dy
|
||||
|
||||
y0 = self.ptr_y
|
||||
y1 = y0
|
||||
if self.ptr_y >= 0: # Restore background
|
||||
tft.setXY(x0, y0, x1, y1)
|
||||
TFT_io.tft_write_data_AS(self.ptrbuf, self.ptrbytes)
|
||||
ptrpos = int(self.y1 - self._value * height)
|
||||
y0 = ptrpos
|
||||
y1 = ptrpos
|
||||
tft.setXY(x0, y0, x1, y1) # Read background
|
||||
TFT_io.tft_read_cmd_data_AS(0x2e, self.ptrbuf, self.ptrbytes)
|
||||
self.ptr_y = y0
|
||||
self.set_color(self.pointercolor)
|
||||
tft.drawHLine(x0, y0, width) # Draw pointer
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
self._value = min(max(val, 0.0), 1.0)
|
||||
if self._value != self._old_value:
|
||||
self._old_value = self._value
|
||||
self.show()
|
|
@ -26,44 +26,37 @@ from font10 import font10
|
|||
from tft import TFT, LANDSCAPE
|
||||
from usched import Sched
|
||||
from touch import TOUCH
|
||||
from slider import HorizSlider
|
||||
from button import Button
|
||||
from displays import Dial, Label, LED, Meter
|
||||
from ui import CLIPPED_RECT, GREEN, RED, YELLOW, WHITE, BLUE
|
||||
from ugui import HorizSlider, Button, Dial, Label, LED, Meter, CLIPPED_RECT, GREEN, RED, YELLOW, WHITE, BLUE
|
||||
import pyb
|
||||
|
||||
# CALLBACKS
|
||||
# cb_end occurs when user stops touching the control
|
||||
def callback(slider, args):
|
||||
print('{} returned {}'.format(args[0], slider.value()))
|
||||
def callback(slider, control_name):
|
||||
print('{} returned {}'.format(control_name, slider.value()))
|
||||
|
||||
def to_string(val):
|
||||
return '{:3.1f} ohms'.format(val * 10)
|
||||
|
||||
def master_moved(slider, args):
|
||||
def master_moved(slider, slave1, slave2, label, led):
|
||||
val = slider.value()
|
||||
slave1 = args[0]
|
||||
slave1.value(val)
|
||||
slave2 = args[1]
|
||||
slave2.value(val)
|
||||
label = args[2]
|
||||
label.show(to_string(val))
|
||||
led = args[3]
|
||||
if val > 0.8:
|
||||
led.on()
|
||||
else:
|
||||
led.off()
|
||||
|
||||
# Either slave has had its slider moved (by user or by having value altered)
|
||||
def slave_moved(slider, args):
|
||||
def slave_moved(slider, label):
|
||||
val = slider.value()
|
||||
if val > 0.8:
|
||||
slider.fgcolor = RED
|
||||
else:
|
||||
slider.fgcolor = GREEN
|
||||
label = args[0]
|
||||
label.show(to_string(val))
|
||||
|
||||
def doquit(button, args):
|
||||
def doquit(button):
|
||||
button.objsched.stop()
|
||||
|
||||
# USER TEST FUNCTION
|
||||
|
@ -95,11 +88,11 @@ def testmeter(meter):
|
|||
meter.value(oldvalue)
|
||||
yield 0.05
|
||||
|
||||
def test(duration = 0):
|
||||
def test():
|
||||
print('Test TFT panel...')
|
||||
objsched = Sched() # Instantiate the scheduler
|
||||
mytft = TFT("SSD1963", "LB04301", LANDSCAPE)
|
||||
mytouch = TOUCH("XPT2046", objsched, confidence = 50) #, calibration = (-3886,-0.1287,-3812,-0.132,-3797,-0.07685,-3798,-0.07681))
|
||||
mytouch = TOUCH("XPT2046", objsched, confidence = 50, margin = 50) #, calibration = (-3886,-0.1287,-3812,-0.132,-3797,-0.07685,-3798,-0.07681))
|
||||
mytft.backlight(100) # light on
|
||||
led = LED(mytft, (420, 0), border = 2)
|
||||
meter1 = Meter(mytft, (320, 0), font=font10, legends=('0','5','10'), pointercolor = YELLOW, fgcolor = GREEN)
|
||||
|
@ -112,11 +105,11 @@ def test(duration = 0):
|
|||
lstlbl.append(Label(mytft, (x, 40 + 60 * n), font = font10, **labels))
|
||||
x = 0
|
||||
slave1 = HorizSlider(objsched, mytft, mytouch, (x, 100), font10,
|
||||
fgcolor = GREEN, cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = (lstlbl[1],), **table)
|
||||
fgcolor = GREEN, cbe_args = ['Slave1'], cb_move = slave_moved, cbm_args = [lstlbl[1]], **table)
|
||||
slave2 = HorizSlider(objsched, mytft, mytouch, (x, 160), font10,
|
||||
fgcolor = GREEN, cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = (lstlbl[2],), **table)
|
||||
fgcolor = GREEN, cbe_args = ['Slave2'], cb_move = slave_moved, cbm_args = [lstlbl[2]], **table)
|
||||
master = HorizSlider(objsched, mytft, mytouch, (x, 40), font10,
|
||||
fgcolor = YELLOW, cbe_args = ('Master',), cb_move = master_moved, slidecolor=RED, cbm_args = (slave1, slave2, lstlbl[0], led), value=0.5, **table)
|
||||
fgcolor = YELLOW, cbe_args = ['Master'], cb_move = master_moved, slidecolor=RED, cbm_args = [slave1, slave2, lstlbl[0], led], value=0.5, **table)
|
||||
objsched.add_thread(testmeter(meter1))
|
||||
objsched.add_thread(testmeter(meter2))
|
||||
objsched.run() # Run it!
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
from font10 import font10
|
||||
from tft import TFT, LANDSCAPE
|
||||
from usched import Sched
|
||||
from touch import TOUCH
|
||||
from ugui import Knob, Dial, Label, Button, WHITE, YELLOW, GREEN, RED, CLIPPED_RECT
|
||||
from math import pi
|
||||
|
||||
# CALLBACKS
|
||||
# cb_end occurs when user stops touching the control
|
||||
def callback(knob, control_name):
|
||||
print('{} returned {}'.format(control_name, knob.value()))
|
||||
|
||||
def knob_moved(knob, dial):
|
||||
val = knob.value() # range 0..1
|
||||
dial.show(2 * (val - 0.5) * pi)
|
||||
|
||||
def doquit(button):
|
||||
button.objsched.stop()
|
||||
|
||||
def test():
|
||||
print('Test TFT panel...')
|
||||
objsched = Sched() # Instantiate the scheduler
|
||||
mytft = TFT("SSD1963", "LB04301", LANDSCAPE)
|
||||
mytouch = TOUCH("XPT2046", objsched, confidence = 50, margin = 50)
|
||||
mytft.backlight(100) # light on
|
||||
Button(objsched, mytft, mytouch, (400, 240), font = font10, callback = doquit, fgcolor = RED,
|
||||
height = 30, text = 'Quit', shape = CLIPPED_RECT)
|
||||
dial1 = Dial(mytft, (120, 0), fgcolor = YELLOW, border = 2, pointers = (0.9, 0.7))
|
||||
Knob(objsched, mytft, mytouch, (0, 0), fgcolor = GREEN, bgcolor=(0, 0, 80), color = (168,63,63), border = 2,
|
||||
cb_end = callback, cbe_args = ['Knob1'], cb_move = knob_moved, cbm_args = [dial1]) #, arc = pi * 1.5)
|
||||
Knob(objsched, mytft, mytouch, (0, 120), fgcolor = WHITE, border = 2,
|
||||
cb_end = callback, cbe_args = ['Knob2'], arc = pi * 1.5)
|
||||
objsched.run() # Run it!
|
||||
|
||||
test()
|
|
@ -1,231 +0,0 @@
|
|||
# slider.py Vertical and horizontal slider control classes for Pyboard TFT GUI
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# A slider's text items lie outside its bounding box (area sensitive to touch)
|
||||
|
||||
from ui import Touchable, get_stringsize
|
||||
import TFT_io
|
||||
|
||||
class Slider(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, font, *, height=200, width=30, divisions=10, legends=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, border=None,
|
||||
cb_end=lambda x, y : None, cbe_args=[], cb_move=lambda x, y : None, cbm_args=[], value=0.0):
|
||||
super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border)
|
||||
self.divisions = divisions
|
||||
self.legends = legends
|
||||
self.slidecolor = slidecolor
|
||||
self.cb_end = cb_end
|
||||
self.cbe_args = cbe_args
|
||||
self.cb_move = cb_move
|
||||
self.cbm_args = cbm_args
|
||||
self.was_touched = False
|
||||
slidewidth = int(width / 1.3) & 0xfe # Ensure divisible by 2
|
||||
self.slideheight = 6 # must be divisible by 2
|
||||
# We draw an odd number of pixels:
|
||||
self.slidebytes = (self.slideheight + 1) * (slidewidth + 1) * 3
|
||||
self.slidebuf = bytearray(self.slidebytes)
|
||||
self._old_value = -1 # Invalidate
|
||||
b = self.border
|
||||
self.pot_dimension = self.height - 2 * (b + self.slideheight // 2)
|
||||
width = self.width - 2 * b
|
||||
xcentre = self.location[0] + b + width // 2
|
||||
self.slide_x0 = xcentre - slidewidth // 2
|
||||
self.slide_x1 = xcentre + slidewidth // 2 # slide X coordinates
|
||||
self.slide_y = -1 # Invalidate old position
|
||||
self.value(value)
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
x = self.location[0] + bw
|
||||
y = self.location[1] + bw + self.slideheight // 2 # Allow space above and below slot
|
||||
width = self.width - 2 * bw
|
||||
self.set_color()
|
||||
height = self.pot_dimension # Height of slot
|
||||
dx = width / 3
|
||||
tft.drawRectangle(x + dx, y, x + 2 * dx, y + height)
|
||||
|
||||
if self.divisions > 0:
|
||||
dy = height / (self.divisions) # Tick marks
|
||||
for tick in range(self.divisions + 1):
|
||||
ypos = int(y + dy * tick)
|
||||
tft.drawHLine(x + 1, ypos, dx)
|
||||
tft.drawHLine(x + 1 + 2 * dx, ypos, dx)
|
||||
|
||||
if self.legends is not None: # Legends
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
if len(self.legends) <= 1:
|
||||
dy = 0
|
||||
else:
|
||||
dy = height / (len(self.legends) -1)
|
||||
yl = y + height # Start at bottom
|
||||
fhdelta = self.font.bits_vert / 2
|
||||
for legend in self.legends:
|
||||
tft.setTextPos(x + self.width, int(yl - fhdelta))
|
||||
tft.printString(legend)
|
||||
yl -= dy
|
||||
|
||||
sh = self.slideheight # Handle slider
|
||||
x0 = self.slide_x0
|
||||
y0 = self.slide_y
|
||||
x1 = self.slide_x1
|
||||
y1 = y0 + sh
|
||||
if self.slide_y >= 0: # Restore background
|
||||
tft.setXY(x0, y0, x1, y1)
|
||||
TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes)
|
||||
sliderpos = int(y + height - self._value * height)
|
||||
y0 = sliderpos - sh // 2
|
||||
y1 = sliderpos + sh // 2
|
||||
tft.setXY(x0, y0, x1, y1) # Read background
|
||||
TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes)
|
||||
self.slide_y = y0
|
||||
if self.slidecolor is not None:
|
||||
self.set_color(self.slidecolor)
|
||||
tft.fillRectangle(x0, y0, x1, y1) # Draw slider
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
self._value = min(max(val, 0.0), 1.0)
|
||||
if self._value != self._old_value:
|
||||
self._old_value = self._value
|
||||
self.cb_move(self, self.cbm_args) # Callback not a bound method so pass self
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # If touched in bounding box, process it otherwise do nothing
|
||||
x0 = self.location[0]
|
||||
x1 = self.location[0] + self.width
|
||||
y0 = self.location[1]
|
||||
y1 = self.location[1] + self.height
|
||||
if x0 <= x <= x1 and y0 <= y <= y1:
|
||||
self.was_touched = True
|
||||
self.value((y1 - y) / self.pot_dimension)
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
if self.was_touched:
|
||||
self.cb_end(self, self.cbe_args) # Callback not a bound method so pass self
|
||||
self.was_touched = False
|
||||
|
||||
class HorizSlider(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, font, *, height=30, width=200, divisions=10, legends=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, border=None,
|
||||
cb_end=lambda x, y : None, cbe_args=[], cb_move=lambda x, y : None, cbm_args=[], value=0.0):
|
||||
super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border)
|
||||
self.divisions = divisions
|
||||
self.legends = legends
|
||||
self.slidecolor = slidecolor
|
||||
self.cb_end = cb_end
|
||||
self.cbe_args = cbe_args
|
||||
self.cb_move = cb_move
|
||||
self.cbm_args = cbm_args
|
||||
self.was_touched = False
|
||||
slideheight = int(height / 1.3) & 0xfe # Ensure divisible by 2
|
||||
self.slidewidth = 6 # must be divisible by 2
|
||||
# We draw an odd number of pixels:
|
||||
self.slidebytes = (slideheight + 1) * (self.slidewidth + 1) * 3
|
||||
self.slidebuf = bytearray(self.slidebytes)
|
||||
self._old_value = -1 # Invalidate
|
||||
b = self.border
|
||||
self.pot_dimension = self.width - 2 * (b + self.slidewidth // 2)
|
||||
height = self.height - 2 * b
|
||||
ycentre = self.location[1] + b + height // 2
|
||||
self.slide_y0 = ycentre - slideheight // 2
|
||||
self.slide_y1 = ycentre + slideheight // 2 # slide Y coordinates
|
||||
self.slide_x = -1 # Invalidate old position
|
||||
self.value(value)
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
x = self.location[0] + bw + self.slidewidth // 2 # Allow space left and right slot for slider at extremes
|
||||
y = self.location[1] + bw
|
||||
height = self.height - 2 * bw
|
||||
self.set_color()
|
||||
width = self.pot_dimension # Length of slot
|
||||
dy = height / 3
|
||||
ycentre = y + height // 2
|
||||
tft.drawRectangle(x, y + dy, x + width, y + 2 * dy)
|
||||
|
||||
if self.divisions > 0:
|
||||
dx = width / (self.divisions) # Tick marks
|
||||
for tick in range(self.divisions + 1):
|
||||
xpos = int(x + dx * tick)
|
||||
tft.drawVLine(xpos, y + 1, dy) # TODO Why is +1 fiddle required here?
|
||||
tft.drawVLine(xpos, y + 1 + 2 * dy, dy) # and here
|
||||
|
||||
if self.legends is not None: # Legends
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
if len(self.legends) <= 1:
|
||||
dx = 0
|
||||
else:
|
||||
dx = width / (len(self.legends) -1)
|
||||
xl = x
|
||||
for legend in self.legends:
|
||||
offset = get_stringsize(legend, self.font)[0] / 2
|
||||
tft.setTextPos(int(xl - offset), y - self.font.bits_vert) # Arbitrary left shift should be char width /2
|
||||
tft.printString(legend)
|
||||
xl += dx
|
||||
|
||||
sw = self.slidewidth # Handle slider
|
||||
x0 = self.slide_x
|
||||
y0 = self.slide_y0
|
||||
x1 = x0 + sw
|
||||
y1 = self.slide_y1
|
||||
if self.slide_x >= 0: # Restore background
|
||||
tft.setXY(x0, y0, x1, y1)
|
||||
TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes)
|
||||
sliderpos = int(x + self._value * width)
|
||||
x0 = sliderpos - sw // 2
|
||||
x1 = sliderpos + sw // 2
|
||||
tft.setXY(x0, y0, x1, y1) # Read background
|
||||
TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes)
|
||||
self.slide_x = x0
|
||||
if self.slidecolor is not None:
|
||||
self.set_color(self.slidecolor)
|
||||
tft.fillRectangle(x0, y0, x1, y1) # Draw slider
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
self._value = min(max(val, 0.0), 1.0)
|
||||
if self._value != self._old_value:
|
||||
self._old_value = self._value
|
||||
self.cb_move(self, self.cbm_args) # Callback not a bound method so pass self
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # If touched in bounding box, process it otherwise do nothing
|
||||
x0 = self.location[0]
|
||||
x1 = self.location[0] + self.width
|
||||
y0 = self.location[1]
|
||||
y1 = self.location[1] + self.height
|
||||
if x0 <= x <= x1 and y0 <= y <= y1:
|
||||
self.was_touched = True
|
||||
self.value((x - x0) / self.pot_dimension)
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
if self.was_touched:
|
||||
self.cb_end(self, self.cbe_args) # Callback not a bound method so pass self
|
||||
self.was_touched = False
|
374
tft_gui/tft.py
374
tft_gui/tft.py
|
@ -21,23 +21,25 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Class supporting TFT LC-displays with a parallel Interface
|
||||
# First example: Controller SSD1963
|
||||
# It uses X1..X8 for data and Y3, Y9, Y10, Y11 and Y12 for control signals.
|
||||
# The minimal connection just for writes is X1..X8 for data, Y9 for /Reset. Y11 for /WR and Y12 for /RS
|
||||
# Then LED and /CS must be hard tied to Vcc and GND, and /RD is not used.
|
||||
#
|
||||
# Some parts of the software are a port of code provided by Rinky-Dink Electronics, Henning Karlsen,
|
||||
# with the following copyright notice:
|
||||
# Some parts of the software are a port of code provided by Rinky-Dink Electronics, Henning Karlsen,
|
||||
# with the following copyright notice:
|
||||
#
|
||||
## Copyright (C)2015 Rinky-Dink Electronics, Henning Karlsen. All right reserved
|
||||
## This library is free software; you can redistribute it and/or
|
||||
## modify it under the terms of the CC BY-NC-SA 3.0 license.
|
||||
## Please see the included documents for further information.
|
||||
## This library is free software; you can redistribute it and/or
|
||||
## modify it under the terms of the CC BY-NC-SA 3.0 license.
|
||||
## Please see the included documents for further information.
|
||||
#
|
||||
# Class supporting TFT LC-displays with a parallel Interface
|
||||
# First example: Controller SSD1963 with a 4.3" or 7" display
|
||||
#
|
||||
# The minimal connection is:
|
||||
# X1..X8 for data, Y9 for /Reset, Y10 for /RD, Y11 for /WR and Y12 for /RS
|
||||
# Then LED must be hard tied to Vcc and /CS to GND.
|
||||
#
|
||||
|
||||
import pyb, stm
|
||||
from uctypes import addressof
|
||||
from TFT_io import TFT_io
|
||||
import TFT_io
|
||||
|
||||
# define constants
|
||||
#
|
||||
|
@ -73,7 +75,7 @@ class TFT:
|
|||
self.v_flip = v_flip # flip vertical
|
||||
self.h_flip = h_flip # flip horizontal
|
||||
self.c_flip = 0 # flip blue/red
|
||||
self.rc_flip = 0 # flip row/column (does not seem to work)
|
||||
self.rc_flip = 0 # flip row/column
|
||||
|
||||
self.setColor((255, 255, 255)) # set FG color to white as can be.
|
||||
self.setBGColor((0, 0, 0)) # set BG to black
|
||||
|
@ -90,9 +92,13 @@ class TFT:
|
|||
# this may have to be moved to the controller specific section
|
||||
if orientation == PORTRAIT:
|
||||
self.setXY = TFT_io.setXY_P
|
||||
self.drawPixel = TFT_io.drawPixel_P
|
||||
else:
|
||||
self.setXY = TFT_io.setXY_L
|
||||
# ----------
|
||||
self.drawPixel = TFT_io.drawPixel_L
|
||||
self.swapbytes = TFT_io.swapbytes
|
||||
self.swapcolors = TFT_io.swapcolors
|
||||
# ----------
|
||||
for pin_name in ["X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8",
|
||||
"Y10", "Y11", "Y12"]:
|
||||
pin = pyb.Pin(pin_name, pyb.Pin.OUT_PP) # set as output
|
||||
|
@ -251,13 +257,12 @@ class TFT:
|
|||
#
|
||||
# Set character printing defaults
|
||||
#
|
||||
self.setTextPos(0,0)
|
||||
self.setScrollArea(0, self.disp_y_size + 1, 0)
|
||||
self.text_font = None
|
||||
self.setTextStyle(None, None, 0, None, 0)
|
||||
self.setTextStyle(self.color, self.BGcolor, 0, None, 0)
|
||||
#
|
||||
# Init done. clear Screen and switch BG LED on
|
||||
#
|
||||
self.text_x = self.text_y = self.text_yabs = 0
|
||||
self.clrSCR() # clear the display
|
||||
# self.backlight(100) ## switch BG LED on
|
||||
#
|
||||
|
@ -311,6 +316,9 @@ class TFT:
|
|||
def setBGColor(self, bgcolor):
|
||||
self.BGcolor = bgcolor
|
||||
self.BGcolorvect = bytearray(self.BGcolor) # prepare byte array
|
||||
self.BMPcolortable = bytearray([self.BGcolorvect[2], # create colortable
|
||||
self.BGcolorvect[1], self.BGcolorvect[0],0,
|
||||
self.colorvect[2], self.colorvect[1], self.colorvect[0],0])
|
||||
#
|
||||
# get the color used for the draw commands
|
||||
#
|
||||
|
@ -322,20 +330,25 @@ class TFT:
|
|||
def getBGColor(self):
|
||||
return self.BGcolor
|
||||
#
|
||||
# Draw a single pixel at location x, y
|
||||
# Draw a single pixel at location x, y with color
|
||||
# Rather slow at 40µs/Pixel
|
||||
#
|
||||
def drawPixel(self, x, y):
|
||||
def drawPixel_py(self, x, y, color):
|
||||
self.setXY(x, y, x, y)
|
||||
TFT_io.displaySCR_AS(self.colorvect, 1) #
|
||||
TFT_io.displaySCR_AS(color, 1) #
|
||||
#
|
||||
# clear screen, set it to BG color.
|
||||
#
|
||||
def clrSCR(self):
|
||||
def clrSCR(self, color = None):
|
||||
if color is None:
|
||||
colorvect = self.BGcolorvect
|
||||
else:
|
||||
colorvect = bytearray(color)
|
||||
self.clrXY()
|
||||
TFT_io.fillSCR_AS(self.BGcolorvect, (self.disp_x_size + 1) * (self.disp_y_size + 1))
|
||||
self.text_x = self.text_y = self.scroll_start = 0
|
||||
TFT_io.fillSCR_AS(colorvect, (self.disp_x_size + 1) * (self.disp_y_size + 1))
|
||||
self.setScrollArea(0, self.disp_y_size + 1, 0)
|
||||
self.setScrollStart(0)
|
||||
self.setTextPos(0,0)
|
||||
#
|
||||
# reset the address range to fullscreen
|
||||
#
|
||||
|
@ -348,19 +361,23 @@ class TFT:
|
|||
# Draw a line from x1, y1 to x2, y2 with the color set by setColor()
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def drawLine(self, x1, y1, x2, y2):
|
||||
def drawLine(self, x1, y1, x2, y2, color = None):
|
||||
if y1 == y2:
|
||||
self.drawHLine(x1, y1, x2 - x1 + 1)
|
||||
self.drawHLine(x1, y1, x2 - x1 + 1, color)
|
||||
elif x1 == x2:
|
||||
self.drawVLine(x1, y1, y2 - y1 + 1)
|
||||
self.drawVLine(x1, y1, y2 - y1 + 1, color)
|
||||
else:
|
||||
if color is None:
|
||||
colorvect = self.colorvect
|
||||
else:
|
||||
colorvect = bytearray(color)
|
||||
dx, xstep = (x2 - x1, 1) if x2 > x1 else (x1 - x2, -1)
|
||||
dy, ystep = (y2 - y1, 1) if y2 > y1 else (y1 - y2, -1)
|
||||
col, row = x1, y1
|
||||
if dx < dy:
|
||||
t = - (dy >> 1)
|
||||
while True:
|
||||
self.drawPixel(col, row)
|
||||
self.drawPixel(col, row, colorvect)
|
||||
if row == y2:
|
||||
return
|
||||
row += ystep
|
||||
|
@ -371,7 +388,7 @@ class TFT:
|
|||
else:
|
||||
t = - (dx >> 1)
|
||||
while True:
|
||||
self.drawPixel(col, row)
|
||||
self.drawPixel(col, row, colorvect)
|
||||
if col == x2:
|
||||
return
|
||||
col += xstep
|
||||
|
@ -383,44 +400,52 @@ class TFT:
|
|||
# Draw a horizontal line with 1 Pixel width, from x,y to x + l - 1, y
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def drawHLine(self, x, y, l): # draw horiontal Line
|
||||
def drawHLine(self, x, y, l, color = None): # draw horiontal Line
|
||||
if color is None:
|
||||
colorvect = self.colorvect
|
||||
else:
|
||||
colorvect = bytearray(color)
|
||||
if l < 0: # negative length, swap parameters
|
||||
l = -l
|
||||
x -= l
|
||||
self.setXY(x, y, x + l - 1, y) # set display window
|
||||
TFT_io.fillSCR_AS(self.colorvect, l)
|
||||
TFT_io.fillSCR_AS(colorvect, l)
|
||||
#
|
||||
# Draw a vertical line with 1 Pixel width, from x,y to x, y + l - 1
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def drawVLine(self, x, y, l): # draw horiontal Line
|
||||
def drawVLine(self, x, y, l, color = None): # draw horiontal Line
|
||||
if color is None:
|
||||
colorvect = self.colorvect
|
||||
else:
|
||||
colorvect = bytearray(color)
|
||||
if l < 0: # negative length, swap parameters
|
||||
l = -l
|
||||
y -= l
|
||||
self.setXY(x, y, x, y + l - 1) # set display window
|
||||
TFT_io.fillSCR_AS(self.colorvect, l)
|
||||
TFT_io.fillSCR_AS(colorvect, l)
|
||||
#
|
||||
# Draw rectangle from x1, y1, to x2, y2
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def drawRectangle(self, x1, y1, x2, y2):
|
||||
def drawRectangle(self, x1, y1, x2, y2, color = None):
|
||||
if x1 > x2:
|
||||
t = x1; x1 = x2; x2 = t
|
||||
x1, x2 = x2, x1
|
||||
if y1 > y2:
|
||||
t = y1; y1 = y2; y2 = t
|
||||
self.drawHLine(x1, y1, x2 - x1 + 1)
|
||||
self.drawHLine(x1, y2, x2 - x1 + 1)
|
||||
self.drawVLine(x1, y1, y2 - y1 + 1)
|
||||
self.drawVLine(x2, y1, y2 - y1 + 1)
|
||||
y1, y2 = y2, y1
|
||||
self.drawHLine(x1, y1, x2 - x1 + 1, color)
|
||||
self.drawHLine(x1, y2, x2 - x1 + 1, color)
|
||||
self.drawVLine(x1, y1, y2 - y1 + 1, color)
|
||||
self.drawVLine(x2, y1, y2 - y1 + 1, color)
|
||||
#
|
||||
# Fill rectangle
|
||||
# Almost straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def fillRectangle(self, x1, y1, x2, y2, color = None):
|
||||
def fillRectangle(self, x1, y1, x2, y2, color=None):
|
||||
if x1 > x2:
|
||||
t = x1; x1 = x2; x2 = t
|
||||
x1, x2 = x2, x1
|
||||
if y1 > y2:
|
||||
t = y1; y1 = y2; y2 = t
|
||||
y1, y2 = y2, y1
|
||||
self.setXY(x1, y1, x2, y2) # set display window
|
||||
if color:
|
||||
TFT_io.fillSCR_AS(bytearray(color), (x2 - x1 + 1) * (y2 - y1 + 1))
|
||||
|
@ -431,29 +456,33 @@ class TFT:
|
|||
# Draw smooth rectangle from x1, y1, to x2, y2
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def drawClippedRectangle(self, x1, y1, x2, y2):
|
||||
def drawClippedRectangle(self, x1, y1, x2, y2, color = None):
|
||||
if x1 > x2:
|
||||
t = x1; x1 = x2; x2 = t
|
||||
x1, x2 = x2, x1
|
||||
if y1 > y2:
|
||||
t = y1; y1 = y2; y2 = t
|
||||
y1, y2 = y2, y1
|
||||
if (x2-x1) > 4 and (y2-y1) > 4:
|
||||
self.drawPixel(x1 + 2,y1 + 1)
|
||||
self.drawPixel(x1 + 1,y1 + 2)
|
||||
self.drawPixel(x2 - 2,y1 + 1)
|
||||
self.drawPixel(x2 - 1,y1 + 2)
|
||||
self.drawPixel(x1 + 2,y2 - 1)
|
||||
self.drawPixel(x1 + 1,y2 - 2)
|
||||
self.drawPixel(x2 - 2,y2 - 1)
|
||||
self.drawPixel(x2 - 1,y2 - 2)
|
||||
self.drawHLine(x1 + 3, y1, x2 - x1 - 5)
|
||||
self.drawHLine(x1 + 3, y2, x2 - x1 - 5)
|
||||
self.drawVLine(x1, y1 + 3, y2 - y1 - 5)
|
||||
self.drawVLine(x2, y1 + 3, y2 - y1 - 5)
|
||||
if color is None:
|
||||
colorvect = self.colorvect
|
||||
else:
|
||||
colorvect = bytearray(color)
|
||||
self.drawPixel(x1 + 2,y1 + 1, colorvect)
|
||||
self.drawPixel(x1 + 1,y1 + 2, colorvect)
|
||||
self.drawPixel(x2 - 2,y1 + 1, colorvect)
|
||||
self.drawPixel(x2 - 1,y1 + 2, colorvect)
|
||||
self.drawPixel(x1 + 2,y2 - 1, colorvect)
|
||||
self.drawPixel(x1 + 1,y2 - 2, colorvect)
|
||||
self.drawPixel(x2 - 2,y2 - 1, colorvect)
|
||||
self.drawPixel(x2 - 1,y2 - 2, colorvect)
|
||||
self.drawHLine(x1 + 3, y1, x2 - x1 - 5, colorvect)
|
||||
self.drawHLine(x1 + 3, y2, x2 - x1 - 5, colorvect)
|
||||
self.drawVLine(x1, y1 + 3, y2 - y1 - 5, colorvect)
|
||||
self.drawVLine(x2, y1 + 3, y2 - y1 - 5, colorvect)
|
||||
#
|
||||
# Fill smooth rectangle from x1, y1, to x2, y2
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def fillClippedRectangle(self, x1, y1, x2, y2):
|
||||
def fillClippedRectangle(self, x1, y1, x2, y2, color = None):
|
||||
if x1 > x2:
|
||||
t = x1; x1 = x2; x2 = t
|
||||
if y1 > y2:
|
||||
|
@ -461,22 +490,27 @@ class TFT:
|
|||
if (x2-x1) > 4 and (y2-y1) > 4:
|
||||
for i in range(((y2 - y1) // 2) + 1):
|
||||
if i == 0:
|
||||
self.drawHLine(x1 + 3, y1 + i, x2 - x1 - 5)
|
||||
self.drawHLine(x1 + 3, y2 - i, x2 - x1 - 5)
|
||||
self.drawHLine(x1 + 3, y1 + i, x2 - x1 - 5, color)
|
||||
self.drawHLine(x1 + 3, y2 - i, x2 - x1 - 5, color)
|
||||
elif i == 1:
|
||||
self.drawHLine(x1 + 2, y1 + i, x2 - x1 - 3)
|
||||
self.drawHLine(x1 + 2, y2 - i, x2 - x1 - 3)
|
||||
self.drawHLine(x1 + 2, y1 + i, x2 - x1 - 3, color)
|
||||
self.drawHLine(x1 + 2, y2 - i, x2 - x1 - 3, color)
|
||||
elif i == 2:
|
||||
self.drawHLine(x1 + 1, y1 + i, x2 - x1 - 1)
|
||||
self.drawHLine(x1 + 1, y2 - i, x2 - x1 - 1)
|
||||
self.drawHLine(x1 + 1, y1 + i, x2 - x1 - 1, color)
|
||||
self.drawHLine(x1 + 1, y2 - i, x2 - x1 - 1, color)
|
||||
else:
|
||||
self.drawHLine(x1, y1 + i, x2 - x1 + 1)
|
||||
self.drawHLine(x1, y2 - i, x2 - x1 + 1)
|
||||
self.drawHLine(x1, y1 + i, x2 - x1 + 1, color)
|
||||
self.drawHLine(x1, y2 - i, x2 - x1 + 1, color)
|
||||
#
|
||||
# draw a circle at x, y with radius
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
#
|
||||
def drawCircle(self, x, y, radius):
|
||||
def drawCircle(self, x, y, radius, color = None):
|
||||
|
||||
if color is None:
|
||||
colorvect = self.colorvect
|
||||
else:
|
||||
colorvect = bytearray(color)
|
||||
|
||||
f = 1 - radius
|
||||
ddF_x = 1
|
||||
|
@ -484,10 +518,10 @@ class TFT:
|
|||
x1 = 0
|
||||
y1 = radius
|
||||
|
||||
self.drawPixel(x, y + radius)
|
||||
self.drawPixel(x, y - radius)
|
||||
self.drawPixel(x + radius, y)
|
||||
self.drawPixel(x - radius, y)
|
||||
self.drawPixel(x, y + radius, colorvect)
|
||||
self.drawPixel(x, y - radius, colorvect)
|
||||
self.drawPixel(x + radius, y, colorvect)
|
||||
self.drawPixel(x - radius, y, colorvect)
|
||||
|
||||
while x1 < y1:
|
||||
if f >= 0:
|
||||
|
@ -497,47 +531,67 @@ class TFT:
|
|||
x1 += 1
|
||||
ddF_x += 2
|
||||
f += ddF_x
|
||||
self.drawPixel(x + x1, y + y1)
|
||||
self.drawPixel(x - x1, y + y1)
|
||||
self.drawPixel(x + x1, y - y1)
|
||||
self.drawPixel(x - x1, y - y1)
|
||||
self.drawPixel(x + y1, y + x1)
|
||||
self.drawPixel(x - y1, y + x1)
|
||||
self.drawPixel(x + y1, y - x1)
|
||||
self.drawPixel(x - y1, y - x1)
|
||||
self.drawPixel(x + x1, y + y1, colorvect)
|
||||
self.drawPixel(x - x1, y + y1, colorvect)
|
||||
self.drawPixel(x + x1, y - y1, colorvect)
|
||||
self.drawPixel(x - x1, y - y1, colorvect)
|
||||
self.drawPixel(x + y1, y + x1, colorvect)
|
||||
self.drawPixel(x - y1, y + x1, colorvect)
|
||||
self.drawPixel(x + y1, y - x1, colorvect)
|
||||
self.drawPixel(x - y1, y - x1, colorvect)
|
||||
#
|
||||
# fill a circle at x, y with radius
|
||||
# Straight port from the UTFT Library at Rinky-Dink Electronics
|
||||
# Instead of caluclating x = sqrt(r*r - y*y), it searches the x
|
||||
# Instead of calculating x = sqrt(r*r - y*y), it searches the x
|
||||
# for r*r = x*x + x*x
|
||||
#
|
||||
def fillCircle(self, x, y, radius):
|
||||
def fillCircle(self, x, y, radius, color = None):
|
||||
r_square = radius * radius * 4
|
||||
for y1 in range (-(radius * 2), 1):
|
||||
y_square = y1 * y1
|
||||
for x1 in range (-(radius * 2), 1):
|
||||
if x1*x1+y_square <= r_square:
|
||||
x1i = x1//2
|
||||
y1i = y1//2
|
||||
self.drawHLine(x + x1i, y + y1i, 2 * (-x1i))
|
||||
self.drawHLine(x + x1i, y - y1i, 2 * (-x1i))
|
||||
x1i = x1 // 2
|
||||
y1i = y1 // 2
|
||||
self.drawHLine(x + x1i, y + y1i, 2 * (-x1i), color)
|
||||
self.drawHLine(x + x1i, y - y1i, 2 * (-x1i), color)
|
||||
break;
|
||||
#
|
||||
# Draw a bitmap at x,y with size sx, sy
|
||||
# mode determines the type of expected data
|
||||
# mode = 0: The data must contain 3 bytes/pixel red/green/blue
|
||||
# mode = 1: The data must contain 2 packed bytes/pixel blue/green/red in 565 format
|
||||
# mode = 2: The data contains 1 bit per pixel, mapped to fg/bg color
|
||||
# mode = 1: The data contains 1 bit per pixel, mapped to fg/bg color
|
||||
# unless a colortable is provided
|
||||
# mode = 2: The data contains 2 bit per pixel; a colortable with 4 entries must be provided
|
||||
# mode = 4: The data contains 4 bit per pixel;
|
||||
# a colortable with 16 entries must be provided
|
||||
# mode = 8: The data contains 8 bit per pixel;
|
||||
# a colortable with 256 entries must be provided
|
||||
# mode = 16: The data must contain 2 packed bytes/pixel red/green/blue in 565 format
|
||||
# mode = 24: The data must contain 3 bytes/pixel red/green/blue
|
||||
#
|
||||
def drawBitmap(self, x, y, sx, sy, data, mode = 0):
|
||||
def drawBitmap(self, x, y, sx, sy, data, mode = 24, colortable = None):
|
||||
self.setXY(x, y, x + sx - 1, y + sy - 1)
|
||||
if mode == 0:
|
||||
if mode == 24:
|
||||
TFT_io.displaySCR_AS(data, sx * sy)
|
||||
elif mode == 1:
|
||||
elif mode == 16:
|
||||
TFT_io.displaySCR565_AS(data, sx * sy)
|
||||
elif mode == 1:
|
||||
if colortable is None:
|
||||
colortable = self.BMPcolortable # create colortable
|
||||
TFT_io.displaySCR_bmp(data, sx*sy, 1, colortable)
|
||||
elif mode == 2:
|
||||
control = bytearray(self.BGcolorvect + self.colorvect + chr(self.transparency))
|
||||
TFT_io.displaySCR_bitmap(data, sx*sy, control, 0)
|
||||
if colortable is None:
|
||||
return
|
||||
TFT_io.displaySCR_bmp(data, sx*sy, 2, colortable)
|
||||
elif mode == 4:
|
||||
if colortable is None:
|
||||
return
|
||||
TFT_io.displaySCR_bmp(data, sx*sy, 4, colortable)
|
||||
elif mode == 8:
|
||||
if colortable is None:
|
||||
return
|
||||
TFT_io.displaySCR_bmp(data, sx*sy, 8, colortable)
|
||||
|
||||
#
|
||||
# set scroll area to the region between the first and last line
|
||||
#
|
||||
|
@ -546,22 +600,51 @@ class TFT:
|
|||
[(tfa >> 8) & 0xff, tfa & 0xff,
|
||||
(vsa >> 8) & 0xff, vsa & 0xff,
|
||||
(bfa >> 8) & 0xff, bfa & 0xff]), 6)
|
||||
self.scroll_fta = tfa
|
||||
self.scroll_tfa = tfa
|
||||
self.scroll_vsa = vsa
|
||||
self.scroll_bfa = bfa
|
||||
self.setScrollStart(self.scroll_tfa)
|
||||
x, y = self.getTextPos()
|
||||
self.setTextPos(x, y) # realign pointers
|
||||
#
|
||||
# get scroll area of the region between the first and last line
|
||||
#
|
||||
def getScrollArea(self):
|
||||
return self.scroll_tfa, self.scroll_vsa, self.scroll_bfa
|
||||
#
|
||||
# set the line which is displayed first
|
||||
#
|
||||
def setScrollStart(self, lline):
|
||||
TFT_io.tft_cmd_data_AS(0x37, bytearray([(lline >> 8) & 0xff, lline & 0xff]), 2)
|
||||
self.scroll_start = lline # store the logical first line
|
||||
TFT_io.tft_cmd_data_AS(0x37, bytearray([(lline >> 8) & 0xff, lline & 0xff]), 2)
|
||||
#
|
||||
# get the line which is displayed first
|
||||
#
|
||||
def getScrollStart(self):
|
||||
return self.scroll_start # get the logical first line
|
||||
|
||||
#
|
||||
# Scroll vsa up/down by a number of pixels
|
||||
#
|
||||
def scroll(self, pixels):
|
||||
line = ((self.scroll_start - self.scroll_tfa + pixels) % self.scroll_vsa
|
||||
+ self.scroll_tfa)
|
||||
self.setScrollStart(line) # set the new line
|
||||
#
|
||||
# Set text position
|
||||
#
|
||||
def setTextPos(self, x, y, clip = False, scroll = True):
|
||||
self.text_width, self.text_height = self.getScreensize()
|
||||
self.text_width, self.text_height = self.getScreensize() ## height possibly wrong
|
||||
self.text_x = x
|
||||
self.text_y = y
|
||||
if self.scroll_tfa <= y < (self.scroll_tfa + self.scroll_vsa): # in scroll area ? check later for < or <=
|
||||
# correct position relative to scroll start
|
||||
self.text_y = (y + self.scroll_start - self.scroll_tfa)
|
||||
if self.text_y >= (self.scroll_tfa + self.scroll_vsa):
|
||||
self.text_y -= self.scroll_vsa
|
||||
else: # absolute
|
||||
self.text_y = y
|
||||
self.text_yabs = y
|
||||
# Hint: self.text_yabs = self.text_y - self.scroll_start) % self.scroll_vsa + self.scroll_tfa)
|
||||
if clip and (self.text_x + clip) < self.text_width:
|
||||
self.text_width = self.text_x + clip
|
||||
self.text_scroll = scroll
|
||||
|
@ -569,62 +652,84 @@ class TFT:
|
|||
# Get text position
|
||||
#
|
||||
def getTextPos(self):
|
||||
return (self.text_x, self.text_y)
|
||||
return (self.text_x, self.text_yabs)
|
||||
#
|
||||
# Set Text Style
|
||||
#
|
||||
def setTextStyle(self, fgcolor = None, bgcolor = None, transparency = None, font = None, gap = None):
|
||||
if font != None:
|
||||
def setTextStyle(self, fgcolor=None, bgcolor=None, transparency=None, font=None, gap=None):
|
||||
if font is not None:
|
||||
self.text_font = font
|
||||
if font:
|
||||
self.text_rows, self.text_cols, nchar, first = font.get_properties() #
|
||||
if transparency != None:
|
||||
if transparency is not None:
|
||||
self.transparency = transparency
|
||||
if gap != None:
|
||||
if gap is not None:
|
||||
self.text_gap = gap
|
||||
self.text_color = bytearray(0)
|
||||
if bgcolor != None:
|
||||
self.text_color += bytearray(bgcolor)
|
||||
else:
|
||||
self.text_color += self.BGcolorvect
|
||||
if fgcolor != None:
|
||||
self.text_color += bytearray(fgcolor)
|
||||
else:
|
||||
self.text_color += self.colorvect
|
||||
if transparency != None:
|
||||
if bgcolor is not None:
|
||||
self.text_bgcolor = bgcolor
|
||||
if fgcolor is not None:
|
||||
self.text_fgcolor = fgcolor
|
||||
if transparency is not None:
|
||||
self.transparency = transparency
|
||||
self.text_color += bytearray([self.transparency])
|
||||
if gap != None:
|
||||
self.text_color = (bytearray(self.text_bgcolor)
|
||||
+ bytearray(self.text_fgcolor)
|
||||
+ bytearray([self.transparency]))
|
||||
if gap is not None:
|
||||
self.text_gap = gap
|
||||
#
|
||||
# Get Text Style: return (color, bgcolor, font, transpareny, gap)
|
||||
#
|
||||
def getTextStyle(self):
|
||||
return (self.text_color[3:6], self.text_color[0:3],
|
||||
self.transparency, self.text_font, self.text_gap)
|
||||
|
||||
#
|
||||
# Check, if a new line is to be opened
|
||||
# if yes, advance, including scrolling, and clear line, if flags is set
|
||||
# Obsolete?
|
||||
#
|
||||
def printNewline(self):
|
||||
self.text_y += self.text_rows
|
||||
if (self.text_y + self.text_rows) > self.scroll_vsa: # does the line fit?
|
||||
self.text_y = 0
|
||||
newline = self.text_rows
|
||||
self.setScrollStart(newline)
|
||||
elif self.scroll_start > 0: # Scrolling has started
|
||||
newline = (self.scroll_start + self.text_rows) % self.scroll_vsa
|
||||
self.setScrollStart(newline)
|
||||
def printNewline(self, clear = False):
|
||||
if (self.text_yabs + self.text_rows) >= (self.scroll_tfa + self.scroll_vsa): # does the line fit?
|
||||
self.scroll(self.text_rows) # no. scroll
|
||||
else: # Yes, just advance pointers
|
||||
self.text_yabs += self.text_rows
|
||||
self.setTextPos(self.text_x, self.text_yabs)
|
||||
if clear:
|
||||
self.printClrLine(2) # clear actual line
|
||||
#
|
||||
# Carriage Return
|
||||
#
|
||||
def printCR(self): # clear to end of line
|
||||
self.text_x = 0
|
||||
#
|
||||
# clear to end-of-line
|
||||
# clear line modes
|
||||
#
|
||||
def printClrEOL(self): # clear to end of line
|
||||
self.setXY(self.text_x, self.text_y,
|
||||
self.text_width - self.text_x - 1, self.text_y + self.text_rows - 1) # set display window
|
||||
TFT_io.fillSCR_AS(self.text_color, self.text_width * self.text_rows)
|
||||
def printClrLine(self, mode = 0): # clear to end of line/bol/line
|
||||
if mode == 0:
|
||||
self.setXY(self.text_x, self.text_y,
|
||||
self.text_width - 1, self.text_y + self.text_rows - 1) # set display window
|
||||
TFT_io.fillSCR_AS(self.text_color, (self.text_width - self.text_x + 1) * self.text_rows)
|
||||
elif mode == 1 and self.text_x > 0:
|
||||
self.setXY(0, self.text_y,
|
||||
self.text_x - 1, self.text_y + self.text_rows - 1) # set display window
|
||||
TFT_io.fillSCR_AS(self.text_color, (self.text_x - 1) * self.text_rows)
|
||||
elif mode == 2:
|
||||
self.setXY(0, self.text_y,
|
||||
self.text_width - 1, self.text_y + self.text_rows - 1) # set display window
|
||||
TFT_io.fillSCR_AS(self.text_color, self.text_width * self.text_rows)
|
||||
#
|
||||
# clear sreen modes
|
||||
#
|
||||
def printClrSCR(self): # clear Area set by setScrollArea
|
||||
self.setXY(0, self.scroll_tfa,
|
||||
self.text_width - 1, self.scroll_tfa + self.scroll_vsa) # set display window
|
||||
TFT_io.fillSCR_AS(self.text_color, self.text_width * self.scroll_vsa)
|
||||
self.setScrollStart(self.scroll_tfa)
|
||||
self.setTextPos(0, self.scroll_tfa)
|
||||
#
|
||||
# Print string s, returning the length of the printed string in pixels
|
||||
#
|
||||
def printString(self, s, bg_buf = None):
|
||||
def printString(self, s, bg_buf=None):
|
||||
len = 0
|
||||
for c in s:
|
||||
cols = self.printChar(c, bg_buf)
|
||||
|
@ -635,7 +740,7 @@ class TFT:
|
|||
#
|
||||
# Print string c using the given char bitmap at location x, y, returning the width of the printed char in pixels
|
||||
#
|
||||
def printChar(self, c, bg_buf = None):
|
||||
def printChar(self, c, bg_buf=None):
|
||||
# get the charactes pixel bitmap and dimensions
|
||||
if self.text_font:
|
||||
fontptr, rows, cols = self.text_font.get_ch(ord(c))
|
||||
|
@ -646,11 +751,10 @@ class TFT:
|
|||
if self.text_x + cols > self.text_width: # does the char fit on the screen?
|
||||
if self.text_scroll:
|
||||
self.printCR() # No, then CR
|
||||
self.printNewline() # NL: advance to the next line
|
||||
self.printClrEOL() # clear to end of line
|
||||
self.printNewline(True) # NL: advance to the next line
|
||||
else:
|
||||
return 0
|
||||
# set data arrays & XY-Range
|
||||
# Retrieve Background data if transparency is required
|
||||
if self.transparency: # in case of transpareny, the frame buffer content is needed
|
||||
if not bg_buf: # buffer allocation needed?
|
||||
bg_buf = bytearray(pix_count * 3) # sigh...
|
||||
|
@ -658,9 +762,9 @@ class TFT:
|
|||
TFT_io.tft_read_cmd_data_AS(0x2e, bg_buf, pix_count * 3) # read background data
|
||||
else:
|
||||
bg_buf = 0 # dummy assignment, since None is not accepted
|
||||
# print char
|
||||
# Set XY range & print char
|
||||
self.setXY(self.text_x, self.text_y, self.text_x + cols - 1, self.text_y + rows - 1) # set area
|
||||
TFT_io.displaySCR_bitmap(fontptr, pix_count, self.text_color, bg_buf) # display char!
|
||||
TFT_io.displaySCR_charbitmap(fontptr, pix_count, self.text_color, bg_buf) # display char!
|
||||
#advance pointer
|
||||
self.text_x += (cols + self.text_gap)
|
||||
return cols + self.text_gap
|
||||
|
|
|
@ -0,0 +1,734 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import TFT_io
|
||||
import math
|
||||
from delay import Delay
|
||||
|
||||
TWOPI = 2 * math.pi
|
||||
|
||||
CIRCLE = 1
|
||||
RECTANGLE = 2
|
||||
CLIPPED_RECT = 3
|
||||
WHITE = (255, 255, 255)
|
||||
BLACK = (0, 0, 0)
|
||||
RED = (255, 0, 0)
|
||||
GREEN = (0, 255, 0)
|
||||
BLUE = (0, 0, 255)
|
||||
YELLOW = (255, 255, 0)
|
||||
GREY = (100, 100, 100)
|
||||
|
||||
def get_stringsize(s, font):
|
||||
hor = 0
|
||||
for c in s:
|
||||
_, vert, cols = font.get_ch(ord(c))
|
||||
hor += cols
|
||||
return hor, vert
|
||||
|
||||
def print_centered(tft, x, y, s, color, font):
|
||||
length, height = get_stringsize(s, font)
|
||||
tft.setTextStyle(color, None, 2, font)
|
||||
tft.setTextPos(x - length // 2, y - height // 2)
|
||||
tft.printString(s)
|
||||
|
||||
# Base class for all displayable objects
|
||||
class NoTouch(object):
|
||||
old_color = None
|
||||
def __init__(self, tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border):
|
||||
self.tft = tft
|
||||
self.location = location
|
||||
self.font = font
|
||||
self.height = height
|
||||
self.width = width
|
||||
self.fill = bgcolor is not None
|
||||
self.fgcolor = fgcolor if fgcolor is not None else tft.getColor()
|
||||
self.bgcolor = bgcolor if bgcolor is not None else tft.getBGColor()
|
||||
self.fontcolor = fontcolor if fontcolor is not None else tft.getColor()
|
||||
self.hasborder = border is not None
|
||||
self.border = 0 if border is None else border
|
||||
if NoTouch.old_color is None:
|
||||
NoTouch.old_color = tft.getColor()
|
||||
if height is not None and width is not None: # beware special cases where height and width not yet known
|
||||
self.draw_border()
|
||||
|
||||
def draw_border(self): # Draw background and bounding box if required
|
||||
tft = self.tft
|
||||
fgcolor = tft.getColor()
|
||||
x = self.location[0]
|
||||
y = self.location[1]
|
||||
if self.fill:
|
||||
tft.setColor(self.bgcolor)
|
||||
tft.fillRectangle(x, y, x + self.width, y + self.height)
|
||||
bw = 0 # border width
|
||||
if self.hasborder: # Draw a bounding box
|
||||
bw = self.border
|
||||
tft.setColor(self.fgcolor)
|
||||
tft.drawRectangle(x, y, x + self.width, y + self.height)
|
||||
tft.setColor(fgcolor)
|
||||
return bw # Actual width (may be 0)
|
||||
|
||||
def restore_color(self): # Restore to system default
|
||||
self.tft.setColor(NoTouch.old_color)
|
||||
|
||||
# Base class for touch-enabled classes.
|
||||
class Touchable(NoTouch):
|
||||
touchlist = []
|
||||
objtouch = None
|
||||
|
||||
@classmethod
|
||||
def touchtest(cls): # Singleton thread tests all touchable instances
|
||||
mytouch = cls.objtouch
|
||||
while True:
|
||||
yield
|
||||
if mytouch.ready:
|
||||
x, y = mytouch.get_touch_async()
|
||||
for obj in cls.touchlist:
|
||||
if obj.enabled:
|
||||
obj.trytouch(x, y)
|
||||
elif not mytouch.touched:
|
||||
for obj in cls.touchlist:
|
||||
if obj.was_touched:
|
||||
obj.was_touched = False # Call untouched once only
|
||||
obj.busy = False
|
||||
obj.untouched()
|
||||
|
||||
def __init__(self, objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border, can_drag):
|
||||
super().__init__(tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border)
|
||||
Touchable.touchlist.append(self)
|
||||
self.can_drag = can_drag
|
||||
self.busy = False
|
||||
self.enabled = True # Available to user/subclass
|
||||
self.was_touched = False
|
||||
self.objsched = objsched
|
||||
if Touchable.objtouch is None: # Initialising class and thread
|
||||
Touchable.objtouch = objtouch
|
||||
objsched.add_thread(self.touchtest()) # One thread only
|
||||
|
||||
def trytouch(self, x, y): # If touched in bounding box, process it otherwise do nothing
|
||||
x0 = self.location[0]
|
||||
x1 = self.location[0] + self.width
|
||||
y0 = self.location[1]
|
||||
y1 = self.location[1] + self.height
|
||||
if x0 <= x <= x1 and y0 <= y <= y1:
|
||||
self.was_touched = True
|
||||
if not self.busy or self.can_drag:
|
||||
self.touched(x, y) # Called repeatedly for draggable objects
|
||||
self.busy = True # otherwise once only
|
||||
|
||||
def untouched(self): # Default if not defined in subclass
|
||||
pass
|
||||
|
||||
# *********** DISPLAYS: NON-TOUCH CLASSES FOR DATA DISPLAY ***********
|
||||
|
||||
class Label(NoTouch):
|
||||
def __init__(self, tft, location, *, font, border=None, width, fgcolor=None, bgcolor=None, fontcolor=None, text=''):
|
||||
super().__init__(tft, location, font, None, width, fgcolor, bgcolor, fontcolor, border)
|
||||
self.height = self.font.bits_vert
|
||||
self.height += 2 * self.border # Height determined by font and border
|
||||
self.draw_border() # Must explicitly draw because ctor did not have height
|
||||
self.show(text)
|
||||
|
||||
def show(self, text):
|
||||
tft = self.tft
|
||||
bw = self.border
|
||||
if text:
|
||||
x = self.location[0]
|
||||
y = self.location[1]
|
||||
tft.fillRectangle(x + bw, y + bw, x + self.width - bw, y + self.height - bw, self.bgcolor)
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
tft.setTextPos(x + bw, y + bw, clip = self.width - 2 * bw, scroll = False)
|
||||
tft.printString(text)
|
||||
self.restore_color() # restore fg color
|
||||
|
||||
# class displays angles. Angle 0 is vertical, +ve increments are clockwise.
|
||||
class Dial(NoTouch):
|
||||
def __init__(self, tft, location, *, height=100, fgcolor=None, bgcolor=None, border=None, pointers=(0.9,), ticks=4):
|
||||
NoTouch.__init__(self, tft, location, None, height, height, fgcolor, bgcolor, None, border) # __super__ provoked Python bug
|
||||
border = self.border # border width
|
||||
radius = height / 2 - border
|
||||
self.radius = radius
|
||||
self.xorigin = location[0] + border + radius
|
||||
self.yorigin = location[1] + border + radius
|
||||
self.pointers = tuple(z * self.radius for z in pointers) # Pointer lengths
|
||||
self.angles = [None for _ in pointers]
|
||||
ticklen = 0.1 * radius
|
||||
for tick in range(ticks):
|
||||
theta = 2 * tick * math.pi / ticks
|
||||
x_start = int(self.xorigin + radius * math.sin(theta))
|
||||
y_start = int(self.yorigin - radius * math.cos(theta))
|
||||
x_end = int(self.xorigin + (radius - ticklen) * math.sin(theta))
|
||||
y_end = int(self.yorigin - (radius - ticklen) * math.cos(theta))
|
||||
self.tft.drawLine(x_start, y_start, x_end, y_end, self.fgcolor)
|
||||
tft.drawCircle(self.xorigin, self.yorigin, radius, self.fgcolor)
|
||||
self.restore_color()
|
||||
|
||||
def show(self, angle, pointer=0):
|
||||
tft = self.tft
|
||||
if self.angles[pointer] is not None:
|
||||
self.drawpointer(self.angles[pointer], pointer, self.bgcolor) # erase old
|
||||
self.drawpointer(angle, pointer, self.fgcolor) # draw new
|
||||
self.angles[pointer] = angle # update old
|
||||
self.restore_color()
|
||||
|
||||
def drawpointer(self, radians, pointer, color):
|
||||
length = self.pointers[pointer]
|
||||
x_end = int(self.xorigin + length * math.sin(radians))
|
||||
y_end = int(self.yorigin - length * math.cos(radians))
|
||||
self.tft.drawLine(int(self.xorigin), int(self.yorigin), x_end, y_end, color)
|
||||
|
||||
class LED(NoTouch):
|
||||
def __init__(self, tft, location, *, border=None, height=30, fgcolor=None, bgcolor=None, color=RED):
|
||||
super().__init__(tft, location, None, height, height, fgcolor, bgcolor, None, border)
|
||||
self.radius = (self.height - 2 * self.border) / 2
|
||||
self.x = location[0] + self.radius + self.border
|
||||
self.y = location[1] + self.radius + self.border
|
||||
self.color = color
|
||||
self.off()
|
||||
|
||||
def _show(self, color): # Light the LED
|
||||
self.tft.fillCircle(int(self.x), int(self.y), int(self.radius), color)
|
||||
self.tft.drawCircle(int(self.x), int(self.y), int(self.radius), self.fgcolor)
|
||||
self.restore_color()
|
||||
|
||||
def on(self, color=None): # Light in current color
|
||||
if color is not None:
|
||||
self.color = color
|
||||
self._show(self.color)
|
||||
|
||||
def off(self):
|
||||
self._show(BLACK)
|
||||
|
||||
class Meter(NoTouch):
|
||||
def __init__(self, tft, location, *, font=None, height=200, width=30,
|
||||
fgcolor=None, bgcolor=None, pointercolor=None, fontcolor=None,
|
||||
divisions=10, legends=None, value=0):
|
||||
border = 5 if font is None else 1 + font.bits_vert / 2
|
||||
NoTouch.__init__(self, tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border) # __super__ provoked Python bug
|
||||
border = self.border # border width
|
||||
self.ptrbytes = 3 * (self.width + 1) # 3 bytes per pixel
|
||||
self.ptrbuf = bytearray(self.ptrbytes) #???
|
||||
self.x0 = self.location[0]
|
||||
self.x1 = self.location[0] + self.width
|
||||
self.y0 = self.location[1] + border + 2
|
||||
self.y1 = self.location[1] + self.height - border
|
||||
self.divisions = divisions
|
||||
self.legends = legends
|
||||
self.pointercolor = pointercolor if pointercolor is not None else fgcolor
|
||||
self._value = value
|
||||
self._old_value = -1 # invalidate
|
||||
self.ptr_y = -1 # Invalidate old position
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
width = self.width
|
||||
dx = 5
|
||||
x0 = self.x0
|
||||
x1 = self.x1
|
||||
y0 = self.y0
|
||||
y1 = self.y1
|
||||
height = y1 - y0
|
||||
if self.divisions > 0:
|
||||
dy = height / (self.divisions) # Tick marks
|
||||
for tick in range(self.divisions + 1):
|
||||
ypos = int(y0 + dy * tick)
|
||||
tft.drawHLine(x0, ypos, dx, self.fgcolor)
|
||||
tft.drawHLine(x1 - dx, ypos, dx, self.fgcolor)
|
||||
|
||||
if self.legends is not None and self.font is not None: # Legends
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
if len(self.legends) <= 1:
|
||||
dy = 0
|
||||
else:
|
||||
dy = height / (len(self.legends) -1)
|
||||
yl = self.y1 # Start at bottom
|
||||
for legend in self.legends:
|
||||
print_centered(tft, int(self.x0 + self.width /2), int(yl), legend, self.fontcolor, self.font)
|
||||
yl -= dy
|
||||
|
||||
y0 = self.ptr_y
|
||||
y1 = y0
|
||||
if self.ptr_y >= 0: # Restore background
|
||||
tft.setXY(x0, y0, x1, y1)
|
||||
TFT_io.tft_write_data_AS(self.ptrbuf, self.ptrbytes)
|
||||
ptrpos = int(self.y1 - self._value * height)
|
||||
y0 = ptrpos
|
||||
y1 = ptrpos
|
||||
tft.setXY(x0, y0, x1, y1) # Read background
|
||||
TFT_io.tft_read_cmd_data_AS(0x2e, self.ptrbuf, self.ptrbytes)
|
||||
self.ptr_y = y0
|
||||
tft.drawHLine(x0, y0, width, self.pointercolor) # Draw pointer
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
self._value = min(max(val, 0.0), 1.0)
|
||||
if self._value != self._old_value:
|
||||
self._old_value = self._value
|
||||
self.show()
|
||||
|
||||
# *********** PUSHBUTTON AND CHECKBOX CLASSES ***********
|
||||
|
||||
# Button coordinates relate to bounding box (BB). x, y are of BB top left corner.
|
||||
# likewise width and height refer to BB, regardless of button shape
|
||||
# If font is None button will be rendered without text
|
||||
|
||||
class Button(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, *, font, shape=CIRCLE, height=50, width=50, fill=True,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, litcolor=None, text='', show=True, callback=lambda *_ : None,
|
||||
args=[]):
|
||||
super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, None, False)
|
||||
self.shape = shape
|
||||
self.radius = height // 2
|
||||
self.fill = fill
|
||||
self.litcolor = litcolor
|
||||
self.text = text
|
||||
self.callback = callback
|
||||
self.callback_args = args
|
||||
self.orig_fgcolor = fgcolor
|
||||
if self.litcolor is not None:
|
||||
self.delay = Delay(objsched, self.shownormal)
|
||||
self.visible = True # ditto
|
||||
self.litcolor = litcolor if self.fgcolor is not None else None
|
||||
if show:
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
x = self.location[0]
|
||||
y = self.location[1]
|
||||
if not self.visible: # erase the button
|
||||
tft.fillRectangle(x, y, x + self.width, y + self.height, self.bgcolor)
|
||||
self.restore_color()
|
||||
return
|
||||
if self.shape == CIRCLE: # Button coords are of top left corner of bounding box
|
||||
x += self.radius
|
||||
y += self.radius
|
||||
if self.fill:
|
||||
tft.fillCircle(x, y, self.radius, self.fgcolor)
|
||||
else:
|
||||
tft.drawCircle(x, y, self.radius, self.fgcolor)
|
||||
if self.font is not None and len(self.text):
|
||||
print_centered(tft, x, y, self.text, self.fontcolor, self.font)
|
||||
else:
|
||||
x1 = x + self.width
|
||||
y1 = y + self.height
|
||||
if self.shape == RECTANGLE: # rectangle
|
||||
if self.fill:
|
||||
tft.fillRectangle(x, y, x1, y1, self.fgcolor)
|
||||
else:
|
||||
tft.drawRectangle(x, y, x1, y1, self.fgcolor)
|
||||
if self.font is not None and len(self.text):
|
||||
print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font)
|
||||
elif self.shape == CLIPPED_RECT: # clipped rectangle
|
||||
if self.fill:
|
||||
tft.fillClippedRectangle(x, y, x1, y1, self.fgcolor)
|
||||
else:
|
||||
tft.drawClippedRectangle(x, y, x1, y1, self.fgcolor)
|
||||
if self.font is not None and len(self.text):
|
||||
print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font)
|
||||
self.restore_color()
|
||||
|
||||
def shownormal(self):
|
||||
self.fgcolor = self.orig_fgcolor
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # Process touch
|
||||
if self.litcolor is not None:
|
||||
self.fgcolor = self.litcolor
|
||||
self.show()
|
||||
self.delay.trigger(1)
|
||||
self.callback(self, *self.callback_args) # Callback not a bound method so pass self
|
||||
|
||||
class Buttons(object):
|
||||
def __init__(self, user_callback):
|
||||
self.user_callback = user_callback
|
||||
self.lstbuttons = []
|
||||
|
||||
def add_button(self, *args, **kwargs):
|
||||
kwargs['show'] = False
|
||||
self.lstbuttons.append(Button(*args, **kwargs))
|
||||
|
||||
# Group of buttons, typically at same location, where pressing one shows
|
||||
# the next e.g. start/stop toggle or sequential select from short list
|
||||
class Buttonset(Buttons):
|
||||
def __init__(self, user_callback):
|
||||
super().__init__(user_callback)
|
||||
|
||||
def run(self):
|
||||
for idx, button in enumerate(self.lstbuttons):
|
||||
if idx:
|
||||
button.visible = False # Only button zero visible and sensitive
|
||||
button.enabled = False
|
||||
button.callback_args.append(idx)
|
||||
button.callback = self.callback
|
||||
self.lstbuttons[0].show()
|
||||
|
||||
def callback(self, button, *args):
|
||||
button_no = args[-1]
|
||||
old = self.lstbuttons[button_no]
|
||||
new = self.lstbuttons[(button_no + 1) % len(self.lstbuttons)]
|
||||
old.enabled = False
|
||||
old.visible = False
|
||||
old.show()
|
||||
new.enabled = True
|
||||
new.visible = True
|
||||
new.busy = True # Don't respond to continued press
|
||||
new.show()
|
||||
self.user_callback(new, *args[:-1]) # user gets button with args they specified
|
||||
|
||||
# Group of buttons at different locations, where pressing one shows
|
||||
# only current button highlighted and oes callback from current one
|
||||
class RadioButtons(Buttons):
|
||||
def __init__(self, user_callback, highlight, selected=0):
|
||||
super().__init__(user_callback)
|
||||
self.highlight = highlight
|
||||
self.selected = selected
|
||||
|
||||
def run(self):
|
||||
for idx, button in enumerate(self.lstbuttons):
|
||||
if idx == self.selected: # Initial selection
|
||||
button.fgcolor = self.highlight
|
||||
else:
|
||||
button.fgcolor = button.orig_fgcolor
|
||||
button.show()
|
||||
button.callback = self.callback
|
||||
|
||||
def callback(self, button, *args):
|
||||
for but in self.lstbuttons:
|
||||
if but is button:
|
||||
but.fgcolor = self.highlight
|
||||
else:
|
||||
but.fgcolor = but.orig_fgcolor
|
||||
but.show()
|
||||
self.user_callback(button, *args) # user gets button with args they specified
|
||||
|
||||
|
||||
class Checkbox(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, *, height=30, fillcolor=None,
|
||||
fgcolor=None, bgcolor=None, callback=lambda x, y : None, args=[], value=False, border=None):
|
||||
super().__init__(objsched, tft, objtouch, location, None, height, height, fgcolor, bgcolor, None, border, False)
|
||||
self.callback = callback
|
||||
self.callback_args = args
|
||||
self.fillcolor = fillcolor
|
||||
if value is None:
|
||||
self._value = False # special case: don't execute callback on initialisation
|
||||
self.show()
|
||||
else:
|
||||
self._value = not value
|
||||
self.value(value)
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
x = self.location[0] + bw
|
||||
y = self.location[1] + bw
|
||||
height = self.height - 2 * bw
|
||||
x1 = x + height
|
||||
y1 = y + height
|
||||
if self._value:
|
||||
if self.fillcolor is not None:
|
||||
tft.fillRectangle(x, y, x1, y1, self.fillcolor)
|
||||
else:
|
||||
tft.fillRectangle(x, y, x1, y1, self.bgcolor)
|
||||
tft.drawRectangle(x, y, x1, y1, self.fgcolor)
|
||||
if self.fillcolor is None and self._value:
|
||||
tft.drawLine(x, y, x1, y1, self.fgcolor)
|
||||
tft.drawLine(x, y1, x1, y, self.fgcolor)
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
val = bool(val)
|
||||
if val != self._value:
|
||||
self._value = val
|
||||
self.callback(self, *self.callback_args) # Callback not a bound method so pass self
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # Was touched
|
||||
self.value(not self._value) # Upddate and refresh
|
||||
|
||||
# *********** SLIDER CLASSES ***********
|
||||
# A slider's text items lie outside its bounding box (area sensitive to touch)
|
||||
|
||||
class Slider(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, font, *, height=200, width=30, divisions=10, legends=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, border=None,
|
||||
cb_end=lambda *_ : None, cbe_args=[], cb_move=lambda *_ : None, cbm_args=[], value=0.0):
|
||||
super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border, True)
|
||||
self.divisions = divisions
|
||||
self.legends = legends
|
||||
self.slidecolor = slidecolor
|
||||
self.cb_end = cb_end
|
||||
self.cbe_args = cbe_args
|
||||
self.cb_move = cb_move
|
||||
self.cbm_args = cbm_args
|
||||
slidewidth = int(width / 1.3) & 0xfe # Ensure divisible by 2
|
||||
self.slideheight = 6 # must be divisible by 2
|
||||
# We draw an odd number of pixels:
|
||||
self.slidebytes = (self.slideheight + 1) * (slidewidth + 1) * 3
|
||||
self.slidebuf = bytearray(self.slidebytes)
|
||||
self._old_value = -1 # Invalidate
|
||||
b = self.border
|
||||
self.pot_dimension = self.height - 2 * (b + self.slideheight // 2)
|
||||
width = self.width - 2 * b
|
||||
xcentre = self.location[0] + b + width // 2
|
||||
self.slide_x0 = xcentre - slidewidth // 2
|
||||
self.slide_x1 = xcentre + slidewidth // 2 # slide X coordinates
|
||||
self.slide_y = -1 # Invalidate old position
|
||||
self.value(value)
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
x = self.location[0] + bw
|
||||
y = self.location[1] + bw + self.slideheight // 2 # Allow space above and below slot
|
||||
width = self.width - 2 * bw
|
||||
height = self.pot_dimension # Height of slot
|
||||
dx = width / 3
|
||||
tft.drawRectangle(x + dx, y, x + 2 * dx, y + height, self.fgcolor)
|
||||
|
||||
if self.divisions > 0:
|
||||
dy = height / (self.divisions) # Tick marks
|
||||
for tick in range(self.divisions + 1):
|
||||
ypos = int(y + dy * tick)
|
||||
tft.drawHLine(x + 1, ypos, dx, self.fgcolor)
|
||||
tft.drawHLine(x + 1 + 2 * dx, ypos, dx, self.fgcolor)
|
||||
|
||||
if self.legends is not None: # Legends
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
if len(self.legends) <= 1:
|
||||
dy = 0
|
||||
else:
|
||||
dy = height / (len(self.legends) -1)
|
||||
yl = y + height # Start at bottom
|
||||
fhdelta = self.font.bits_vert / 2
|
||||
for legend in self.legends:
|
||||
tft.setTextPos(x + self.width, int(yl - fhdelta))
|
||||
tft.printString(legend)
|
||||
yl -= dy
|
||||
|
||||
sh = self.slideheight # Handle slider
|
||||
x0 = self.slide_x0
|
||||
y0 = self.slide_y
|
||||
x1 = self.slide_x1
|
||||
y1 = y0 + sh
|
||||
if self.slide_y >= 0: # Restore background
|
||||
tft.setXY(x0, y0, x1, y1)
|
||||
TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes)
|
||||
sliderpos = int(y + height - self._value * height)
|
||||
y0 = sliderpos - sh // 2
|
||||
y1 = sliderpos + sh // 2
|
||||
tft.setXY(x0, y0, x1, y1) # Read background
|
||||
TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes)
|
||||
self.slide_y = y0
|
||||
color = self.slidecolor if self.slidecolor is not None else self.fgcolor
|
||||
tft.fillRectangle(x0, y0, x1, y1, color) # Draw slider
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
self._value = min(max(val, 0.0), 1.0)
|
||||
if self._value != self._old_value:
|
||||
self._old_value = self._value
|
||||
self.cb_move(self, *self.cbm_args) # Callback not a bound method so pass self
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # Touched in bounding box. A drag will call repeatedly.
|
||||
self.value((self.location[1] + self.height - y) / self.pot_dimension)
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
self.cb_end(self, *self.cbe_args) # Callback not a bound method so pass self
|
||||
|
||||
class HorizSlider(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, font, *, height=30, width=200, divisions=10, legends=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, border=None,
|
||||
cb_end=lambda *_ : None, cbe_args=[], cb_move=lambda *_ : None, cbm_args=[], value=0.0):
|
||||
super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border, True)
|
||||
self.divisions = divisions
|
||||
self.legends = legends
|
||||
self.slidecolor = slidecolor
|
||||
self.cb_end = cb_end
|
||||
self.cbe_args = cbe_args
|
||||
self.cb_move = cb_move
|
||||
self.cbm_args = cbm_args
|
||||
slideheight = int(height / 1.3) & 0xfe # Ensure divisible by 2
|
||||
self.slidewidth = 6 # must be divisible by 2
|
||||
# We draw an odd number of pixels:
|
||||
self.slidebytes = (slideheight + 1) * (self.slidewidth + 1) * 3
|
||||
self.slidebuf = bytearray(self.slidebytes)
|
||||
self._old_value = -1 # Invalidate
|
||||
b = self.border
|
||||
self.pot_dimension = self.width - 2 * (b + self.slidewidth // 2)
|
||||
height = self.height - 2 * b
|
||||
ycentre = self.location[1] + b + height // 2
|
||||
self.slide_y0 = ycentre - slideheight // 2
|
||||
self.slide_y1 = ycentre + slideheight // 2 # slide Y coordinates
|
||||
self.slide_x = -1 # Invalidate old position
|
||||
self.value(value)
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
bw = self.draw_border() # and background if required. Result is width of border
|
||||
x = self.location[0] + bw + self.slidewidth // 2 # Allow space left and right slot for slider at extremes
|
||||
y = self.location[1] + bw
|
||||
height = self.height - 2 * bw
|
||||
width = self.pot_dimension # Length of slot
|
||||
dy = height / 3
|
||||
ycentre = y + height // 2
|
||||
tft.drawRectangle(x, y + dy, x + width, y + 2 * dy, self.fgcolor)
|
||||
|
||||
if self.divisions > 0:
|
||||
dx = width / (self.divisions) # Tick marks
|
||||
for tick in range(self.divisions + 1):
|
||||
xpos = int(x + dx * tick)
|
||||
tft.drawVLine(xpos, y + 1, dy, self.fgcolor) # TODO Why is +1 fiddle required here?
|
||||
tft.drawVLine(xpos, y + 1 + 2 * dy, dy, self.fgcolor) # and here
|
||||
|
||||
if self.legends is not None: # Legends
|
||||
tft.setTextStyle(self.fontcolor, None, 2, self.font)
|
||||
if len(self.legends) <= 1:
|
||||
dx = 0
|
||||
else:
|
||||
dx = width / (len(self.legends) -1)
|
||||
xl = x
|
||||
for legend in self.legends:
|
||||
offset = get_stringsize(legend, self.font)[0] / 2
|
||||
tft.setTextPos(int(xl - offset), y - self.font.bits_vert) # Arbitrary left shift should be char width /2
|
||||
tft.printString(legend)
|
||||
xl += dx
|
||||
|
||||
sw = self.slidewidth # Handle slider
|
||||
x0 = self.slide_x
|
||||
y0 = self.slide_y0
|
||||
x1 = x0 + sw
|
||||
y1 = self.slide_y1
|
||||
if self.slide_x >= 0: # Restore background
|
||||
tft.setXY(x0, y0, x1, y1)
|
||||
TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes)
|
||||
sliderpos = int(x + self._value * width)
|
||||
x0 = sliderpos - sw // 2
|
||||
x1 = sliderpos + sw // 2
|
||||
tft.setXY(x0, y0, x1, y1) # Read background
|
||||
TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes)
|
||||
self.slide_x = x0
|
||||
color = self.slidecolor if self.slidecolor is not None else self.fgcolor
|
||||
tft.fillRectangle(x0, y0, x1, y1, color) # Draw slider
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
self._value = min(max(val, 0.0), 1.0)
|
||||
if self._value != self._old_value:
|
||||
self._old_value = self._value
|
||||
self.cb_move(self, *self.cbm_args) # Callback not a bound method so pass self
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # Touched in bounding box. A drag will call repeatedly.
|
||||
self.value((x - self.location[0]) / self.pot_dimension)
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
self.cb_end(self, *self.cbe_args) # Callback not a bound method so pass self
|
||||
|
||||
# *********** CONTROL KNOB CLASS ***********
|
||||
|
||||
class Knob(Touchable):
|
||||
def __init__(self, objsched, tft, objtouch, location, *, height=100, arc=TWOPI, ticks=9, value=0.0,
|
||||
fgcolor=None, bgcolor=None, color=None, border=None,
|
||||
cb_end=lambda *_ : None, cbe_args=[], cb_move=lambda *_ : None, cbm_args=[]):
|
||||
Touchable.__init__(self, objsched, tft, objtouch, location, None, height, height, fgcolor, bgcolor, None, border, True)
|
||||
border = self.border # Geometry: border width
|
||||
radius = height / 2 - border
|
||||
arc = min(max(arc, 0), TWOPI)
|
||||
self.arc = arc # Usable angle of control
|
||||
self.radius = radius
|
||||
self.xorigin = location[0] + border + radius
|
||||
self.yorigin = location[1] + border + radius
|
||||
ticklen = 0.1 * radius
|
||||
self.pointerlen = radius - ticklen - 5
|
||||
ticks = max(ticks, 2) # start and end of travel
|
||||
|
||||
self.cb_end = cb_end # Callbacks
|
||||
self.cbe_args = cbe_args
|
||||
self.cb_move = cb_move
|
||||
self.cbm_args = cbm_args
|
||||
|
||||
self._old_value = None # data: invalidate
|
||||
self._value = -1
|
||||
|
||||
self.color = color
|
||||
for tick in range(ticks):
|
||||
theta = (tick / (ticks - 1)) * arc - arc / 2
|
||||
x_start = int(self.xorigin + radius * math.sin(theta))
|
||||
y_start = int(self.yorigin - radius * math.cos(theta))
|
||||
x_end = int(self.xorigin + (radius - ticklen) * math.sin(theta))
|
||||
y_end = int(self.yorigin - (radius - ticklen) * math.cos(theta))
|
||||
self.tft.drawLine(x_start, y_start, x_end, y_end, self.fgcolor)
|
||||
if color is not None:
|
||||
tft.fillCircle(self.xorigin, self.yorigin, radius - ticklen, color)
|
||||
tft.drawCircle(self.xorigin, self.yorigin, radius - ticklen, self.fgcolor)
|
||||
tft.drawCircle(self.xorigin, self.yorigin, radius - ticklen - 3, self.fgcolor)
|
||||
self.value(value) # Cause the object to be displayed and callback to be triggered
|
||||
self.restore_color()
|
||||
|
||||
def show(self):
|
||||
tft = self.tft
|
||||
if self._old_value is not None:
|
||||
color = self.bgcolor if self.color is None else self.color
|
||||
self.drawpointer(self._old_value, color) # erase old
|
||||
self.drawpointer(self._value, self.fgcolor) # draw new
|
||||
self._old_value = self._value # update old
|
||||
self.restore_color()
|
||||
|
||||
def value(self, val=None):
|
||||
if val is None:
|
||||
return self._value
|
||||
val = min(max(val, 0.0), 1.0)
|
||||
if val != self._value:
|
||||
self._value = val # Update value for callback
|
||||
self.cb_move(self, *self.cbm_args) # Callback not a bound method so pass self
|
||||
self.show()
|
||||
|
||||
def touched(self, x, y): # Touched in bounding box. A drag will call repeatedly.
|
||||
dy = self.yorigin - y
|
||||
dx = x - self.xorigin
|
||||
if (dx**2 + dy**2) / self.radius**2 < 0.5:
|
||||
return # vector too short
|
||||
alpha = math.atan2(dx, dy) # axes swapped: orientate relative to vertical
|
||||
arc = self.arc
|
||||
alpha = min(max(alpha, -arc / 2), arc / 2) + arc / 2
|
||||
self.value(alpha /arc)
|
||||
|
||||
def untouched(self): # User has released touchpad or touched elsewhere
|
||||
self.cb_end(self, *self.cbe_args) # Callback not a bound method so pass self
|
||||
|
||||
def drawpointer(self, value, color):
|
||||
arc = self.arc
|
||||
length = self.pointerlen
|
||||
angle = value * arc - arc / 2
|
||||
x_end = int(self.xorigin + length * math.sin(angle))
|
||||
y_end = int(self.yorigin - length * math.cos(angle))
|
||||
self.tft.drawLine(int(self.xorigin), int(self.yorigin), x_end, y_end, color)
|
118
tft_gui/ui.py
118
tft_gui/ui.py
|
@ -1,118 +0,0 @@
|
|||
# ui.py Constants, base classes and utilities for Pybboard TFT GUI
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
CIRCLE = 1
|
||||
RECTANGLE = 2
|
||||
CLIPPED_RECT = 3
|
||||
WHITE = (255, 255, 255)
|
||||
BLACK = (0, 0, 0)
|
||||
RED = (255, 0, 0)
|
||||
GREEN = (0, 255, 0)
|
||||
BLUE = (0, 0, 255)
|
||||
YELLOW = (255, 255, 0)
|
||||
GREY = (100, 100, 100)
|
||||
|
||||
def get_stringsize(s, font):
|
||||
hor = 0
|
||||
for c in s:
|
||||
_, vert, cols = font.get_ch(ord(c))
|
||||
hor += cols
|
||||
return hor, vert
|
||||
|
||||
def print_centered(tft, x, y, s, color, font):
|
||||
length, height = get_stringsize(s, font)
|
||||
tft.setTextStyle(color, None, 2, font)
|
||||
tft.setTextPos(x - length // 2, y - height // 2)
|
||||
tft.printString(s)
|
||||
|
||||
# Base class for all displayable objects
|
||||
class NoTouch(object):
|
||||
old_color = None
|
||||
def __init__(self, tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border):
|
||||
self.tft = tft
|
||||
self.location = location
|
||||
self.font = font
|
||||
self.height = height
|
||||
self.width = width
|
||||
self.fill = bgcolor is not None
|
||||
self.fgcolor = fgcolor if fgcolor is not None else tft.getColor()
|
||||
self.bgcolor = bgcolor if bgcolor is not None else tft.getBGColor()
|
||||
self.fontcolor = fontcolor if fontcolor is not None else tft.getColor()
|
||||
self.hasborder = border is not None
|
||||
self.border = 0 if border is None else border
|
||||
if NoTouch.old_color is None:
|
||||
NoTouch.old_color = tft.getColor()
|
||||
if height is not None and width is not None: # beware special cases where height and width not yet known
|
||||
self.draw_border()
|
||||
|
||||
def draw_border(self): # Draw background and bounding box if required
|
||||
tft = self.tft
|
||||
fgcolor = tft.getColor()
|
||||
x = self.location[0]
|
||||
y = self.location[1]
|
||||
if self.fill:
|
||||
tft.setColor(self.bgcolor)
|
||||
tft.fillRectangle(x, y, x + self.width, y + self.height)
|
||||
bw = 0 # border width
|
||||
if self.hasborder: # Draw a bounding box
|
||||
bw = self.border
|
||||
tft.setColor(self.fgcolor)
|
||||
tft.drawRectangle(x, y, x + self.width, y + self.height)
|
||||
tft.setColor(fgcolor)
|
||||
return bw # Actual width (may be 0)
|
||||
|
||||
def set_color(self, color=None):
|
||||
new = self.fgcolor if color is None else color
|
||||
self.tft.setColor(new)
|
||||
|
||||
def restore_color(self): # Restore to system default
|
||||
self.tft.setColor(NoTouch.old_color)
|
||||
|
||||
# Base class for touch-enabled classes.
|
||||
class Touchable(NoTouch):
|
||||
touchlist = []
|
||||
objtouch = None
|
||||
|
||||
@classmethod
|
||||
def touchtest(cls): # Singleton thread tests all touchable instances
|
||||
mytouch = cls.objtouch
|
||||
while True:
|
||||
yield
|
||||
if mytouch.ready:
|
||||
x, y = mytouch.get_touch_async()
|
||||
for obj in cls.touchlist:
|
||||
if obj.enabled:
|
||||
obj.touched(x, y)
|
||||
elif not mytouch.touched:
|
||||
for obj in cls.touchlist:
|
||||
obj.untouched()
|
||||
|
||||
def __init__(self, objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border):
|
||||
super().__init__(tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border)
|
||||
Touchable.touchlist.append(self)
|
||||
self.enabled = True # Available to user/subclass
|
||||
self.objsched = objsched
|
||||
if Touchable.objtouch is None: # Initialising class and thread
|
||||
Touchable.objtouch = objtouch
|
||||
objsched.add_thread(self.touchtest()) # One thread only
|
|
@ -1,4 +1,4 @@
|
|||
# slidetest.py Demo/test program for vertical slider class for Pyboard TFT GUI
|
||||
# vst.py Demo/test program for vertical slider class for Pyboard TFT GUI
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
|
@ -26,38 +26,29 @@ from font10 import font10
|
|||
from tft import TFT, LANDSCAPE
|
||||
from usched import Sched
|
||||
from touch import TOUCH
|
||||
from slider import Slider
|
||||
from button import Button
|
||||
from displays import Dial, Label
|
||||
from ui import CLIPPED_RECT, WHITE, BLACK, RED, GREEN, BLUE, YELLOW, GREY
|
||||
from ugui import Slider, Button, Dial, Label, CLIPPED_RECT, WHITE, BLACK, RED, GREEN, BLUE, YELLOW, GREY
|
||||
from math import pi
|
||||
|
||||
# CALLBACKS
|
||||
# cb_end occurs when user stops touching the control
|
||||
def callback(slider, args):
|
||||
print('{} returned {}'.format(args[0], slider.value()))
|
||||
def callback(slider, device):
|
||||
print('{} returned {}'.format(device, slider.value()))
|
||||
|
||||
def to_string(val):
|
||||
return '{:3.1f} ohms'.format(val * 10)
|
||||
|
||||
def master_moved(slider, args):
|
||||
def master_moved(slider, slave1, slave2, label):
|
||||
val = slider.value()
|
||||
slave1 = args[0]
|
||||
slave1.value(val)
|
||||
slave2 = args[1]
|
||||
slave2.value(val)
|
||||
label = args[2]
|
||||
label.show(to_string(val))
|
||||
|
||||
# Either slave has had its slider moved (by user or by having value altered)
|
||||
def slave_moved(slider, args):
|
||||
def slave_moved(slider, label):
|
||||
val = slider.value()
|
||||
dial = args[0]
|
||||
dial.delta = val
|
||||
label = args[1]
|
||||
label.show(to_string(val))
|
||||
|
||||
def doquit(button, args):
|
||||
def doquit(button):
|
||||
button.objsched.stop()
|
||||
|
||||
# THREADS
|
||||
|
@ -88,11 +79,11 @@ table = {'fontcolor' : WHITE,
|
|||
}
|
||||
# 'border' : 2,
|
||||
|
||||
def test(duration = 0):
|
||||
def test():
|
||||
print('Test TFT panel...')
|
||||
objsched = Sched() # Instantiate the scheduler
|
||||
mytft = TFT("SSD1963", "LB04301", LANDSCAPE)
|
||||
mytouch = TOUCH("XPT2046", objsched, confidence=50)
|
||||
mytouch = TOUCH("XPT2046", objsched, confidence=50, margin = 50)
|
||||
mytft.backlight(100) # light on
|
||||
Button(objsched, mytft, mytouch, (400, 240), font = font10, callback = doquit, fgcolor = RED,
|
||||
height = 30, text = 'Quit', shape = CLIPPED_RECT)
|
||||
|
@ -103,11 +94,11 @@ def test(duration = 0):
|
|||
lstlbl.append(Label(mytft, (80 * n, 240), font = font10, **labels))
|
||||
y = 5
|
||||
slave1 = Slider(objsched, mytft, mytouch, (80, y), font10,
|
||||
fgcolor = (0, 255, 0), cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = (dial1, lstlbl[1]), **table)
|
||||
fgcolor = GREEN, cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = [lstlbl[1]], **table)
|
||||
slave2 = Slider(objsched, mytft, mytouch, (160, y), font10,
|
||||
fgcolor = (0, 255, 0), cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = (dial2, lstlbl[2]), **table)
|
||||
fgcolor = GREEN, cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = [lstlbl[2]], **table)
|
||||
master = Slider(objsched, mytft, mytouch, (0, y), font10,
|
||||
fgcolor = (255, 255, 0), cbe_args = ('Master',), cb_move = master_moved, cbm_args = (slave1, slave2, lstlbl[0]), value=0.5, **table)
|
||||
fgcolor = YELLOW, cbe_args = ('Master',), cb_move = master_moved, cbm_args = (slave1, slave2, lstlbl[0]), value=0.5, **table)
|
||||
objsched.add_thread(mainthread(slave1, dial1))
|
||||
objsched.add_thread(mainthread(slave2, dial2))
|
||||
objsched.run() # Run it!
|
Ładowanie…
Reference in New Issue