To get set up, we will need CircuitPython, a few libraries and the PyPortal Calculator Source Code and font downloaded from Github.
CircuitPython
First, make sure you are running the latest version of Adafruit CircuitPython for your board. Because of the speed-ups that were added, the PyPortal Calculator requires at least CircuitPython 4.1.0.
To download the CircuitPython beta, visit the following link for the PyPortal and download the UF2 for CircuitPython beta. You must be using CircuitPython 4.1.0 or later for PyPortal Calculator to work fast enough!
PyPortal Calculator Source Code
Download all the needed files from GitHub by clicking the Download Project Bundle button below.
# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT """ PyPortal Calculator Demo """ import time from collections import namedtuple import board import displayio from adafruit_display_text.label import Label from adafruit_bitmap_font import bitmap_font from adafruit_display_shapes.rect import Rect from adafruit_button import Button from calculator import Calculator import adafruit_touchscreen Coords = namedtuple("Point", "x y") ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR, board.TOUCH_YD, board.TOUCH_YU, calibration=((5200, 59000), (5800, 57000)), size=(320, 240)) # Settings BUTTON_WIDTH = 60 BUTTON_HEIGHT = 30 BUTTON_MARGIN = 8 MAX_DIGITS = 29 BLACK = 0x0 ORANGE = 0xFF8800 WHITE = 0xFFFFFF GRAY = 0x888888 LABEL_OFFSET = 290 # Make the display context calc_group = displayio.Group() board.DISPLAY.root_group = calc_group # Make a background color fill color_bitmap = displayio.Bitmap(320, 240, 1) color_palette = displayio.Palette(1) color_palette[0] = GRAY bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0) calc_group.append(bg_sprite) # Load the font font = bitmap_font.load_font("/fonts/Arial-12.bdf") buttons = [] # Some button functions def button_grid(row, col): return Coords(BUTTON_MARGIN * (row + 1) + BUTTON_WIDTH * row + 20, BUTTON_MARGIN * (col + 1) + BUTTON_HEIGHT * col + 40) def add_button(row, col, label, width=1, color=WHITE, text_color=BLACK): pos = button_grid(row, col) new_button = Button(x=pos.x, y=pos.y, width=BUTTON_WIDTH * width + BUTTON_MARGIN * (width - 1), height=BUTTON_HEIGHT, label=label, label_font=font, label_color=text_color, fill_color=color, style=Button.ROUNDRECT) buttons.append(new_button) return new_button def find_button(label): result = None for _, btn in enumerate(buttons): if btn.label == label: result = btn return result border = Rect(20, 8, 280, 35, fill=WHITE, outline=BLACK, stroke=2) calc_display = Label(font, text="0", color=BLACK) calc_display.y = 25 clear_button = add_button(0, 0, "AC") add_button(1, 0, "+/-") add_button(2, 0, "%") add_button(3, 0, "/", 1, ORANGE, WHITE) add_button(0, 1, "7") add_button(1, 1, "8") add_button(2, 1, "9") add_button(3, 1, "x", 1, ORANGE, WHITE) add_button(0, 2, "4") add_button(1, 2, "5") add_button(2, 2, "6") add_button(3, 2, "-", 1, ORANGE, WHITE) add_button(0, 3, "1") add_button(1, 3, "2") add_button(2, 3, "3") add_button(3, 3, "+", 1, ORANGE, WHITE) add_button(0, 4, "0", 2) add_button(2, 4, ".") add_button(3, 4, "=", 1, ORANGE, WHITE) # Add the display and buttons to the main calc group calc_group.append(border) calc_group.append(calc_display) for b in buttons: calc_group.append(b) calculator = Calculator(calc_display, clear_button, LABEL_OFFSET) button = "" while True: point = ts.touch_point if point is not None: # Button Down Events for _, b in enumerate(buttons): if b.contains(point) and button == "": b.selected = True button = b.label elif button != "": # Button Up Events last_op = calculator.get_current_operator() op_button = find_button(last_op) # Deselect the last operation when certain buttons are pressed if op_button is not None: if button in ('=', 'AC', 'CE'): op_button.selected = False elif button in ('+', '-', 'x', '/') and button != last_op: op_button.selected = False calculator.add_input(button) b = find_button(button) if b is not None: if button not in ('+', '-', 'x', '/') or button != calculator.get_current_operator(): b.selected = False button = "" time.sleep(0.05)
All of the files should go onto your PyPortal main CIRCUITPY drive in the directories noted.

Here is the CircuitPython calculator class in its entirety. If you click on Download Project Bundle button, it will download all files used in this tutorial also.
# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT """ CircuitPython library to handle the input and calculations * Author(s): Melissa LeBlanc-Williams """ # pylint: disable=eval-used def calculate(number_one, operator, number_two): result = eval(number_one + operator + number_two) if int(result) == result: result = int(result) return str(result) class Calculator: def __init__(self, calc_display, clear_button, label_offset): self._error = False self._calc_display = calc_display self._clear_button = clear_button self._label_offset = label_offset self._accumulator = "0" self._operator = None self._equal_pressed = False self._operand = None self._all_clear() def get_current_operator(self): operator = self._operator if operator == "*": operator = "x" return operator def _all_clear(self): self._accumulator = "0" self._operator = None self._equal_pressed = False self._clear_entry() def _clear_entry(self): self._operand = None self._error = False self._set_button_ce(False) self._set_text("0") def _set_button_ce(self, entry_only): self._clear_button.selected = False if entry_only: self._clear_button.label = "CE" else: self._clear_button.label = "AC" def _set_text(self, text): self._calc_display.text = text _, _, screen_w, _ = self._calc_display.bounding_box self._calc_display.x = self._label_offset - screen_w def _get_text(self): return self._calc_display.text def _handle_number(self, input_key): display_text = self._get_text() if self._operand is None and self._operator is not None: display_text = "" elif self._operand is not None and self._operator is not None and self._equal_pressed: self._accumulator = self._operand self._operator = None self._operand = None display_text = "" elif display_text == "0": display_text = "" display_text += input_key self._set_text(display_text) if self._operator is not None: self._operand = display_text self._set_button_ce(True) self._equal_pressed = False def _handle_operator(self, input_key): if input_key == "x": input_key = "*" if self._equal_pressed: self._operand = None if self._operator is None: self._operator = input_key else: # Perform current calculation before changing input_keys if self._operand is not None: self._accumulator = calculate(self._accumulator, self._operator, self._operand) self._set_text(self._accumulator) self._operand = None self._operator = input_key self._accumulator = self._get_text() self._equal_pressed = False def _handle_equal(self): if self._operator is not None: if self._operand is None: self._operand = self._get_text() self._accumulator = calculate(self._accumulator, self._operator, self._operand) self._set_text(self._accumulator) self._equal_pressed = True def _update_operand(self): if self._operand is not None: self._operand = self._get_text() def add_input(self, input_key): try: if self._error: self._clear_entry() elif input_key == "AC": self._all_clear() elif input_key == "CE": self._clear_entry() elif self._operator is None and input_key == "0": pass elif len(input_key) == 1 and 48 <= ord(input_key) <= 57: self._handle_number(input_key) elif input_key in ('+', '-', '/', 'x'): self._handle_operator(input_key) elif input_key == ".": if not input_key in self._get_text(): self._set_text(self._get_text() + input_key) self._set_button_ce(True) self._equal_pressed = False elif input_key == "+/-": self._set_text(calculate(self._get_text(), "*", "-1")) self._update_operand() elif input_key == "%": self._set_text(calculate(self._get_text(), "/", "100")) self._update_operand() elif input_key == "=": self._handle_equal() except (ZeroDivisionError, RuntimeError): self._all_clear() self._error = True self._set_text("Error")
After copying everything over, your PyPortal should display a Calculator on it. In the next few sections, we'll go over the User Interface Elements and examine the code more closely.
Required CircuitPython Libraries
All the needed libraries should be available in the Project Bundle.
Here are the libraries used:
- adafruit_bitmap_font
- adafruit_display_shapes
- adafruit_display_text
- adafruit_button
- adafruit_touchscreen
Before continuing make sure your board's lib folder or root filesystem have the adafruit_bitmap_font, adafruit_display_shapes, adafruit_display_text, adafruit_button, and adafruit_touchscreen files and folders copied over.
Page last edited January 21, 2025
Text editor powered by tinymce.