PyHMI Tutorials
Step-by-step guides from beginner to advanced.
Table of Contents
Tutorial 1: Hello World
Create a minimal PyHMI application with a single label.
Setup
# Install PyHMI
bash install.sh
source .venv/bin/activate
Code
from pyhmi import (
Engine, Sdl2Display, Label, Window,
FlexLayout, EventType, Event,
)
# Create display and engine
display = Sdl2Display(width=320, height=240, title="Hello PyHMI")
engine = Engine(display)
# Create window
window = Window()
window.width = 320
window.height = 240
window.background_color = (30, 30, 30, 255)
# Set up flex layout
layout = FlexLayout(direction="column", main_alignment="center", cross_alignment="center")
engine.set_layout(layout)
# Create label
label = Label()
label.text = "Hello, World!"
label.font_size = 20
label.text_color = (255, 255, 255)
# Add to window
window.add_child(label)
# Set as root
engine.set_root(window)
engine.tree.compute_layout()
# Run
engine.run()
Run
python hello.py
You should see a dark window with "Hello, World!" centered.
Tutorial 2: Your First PyHML File
PyHML is PyHMI's declarative UI language, inspired by Qt QML.
Create dashboard.pyhml
Window:
width: 320
height: 240
background_color: [30, 30, 30, 255]
layout: Column
spacing: 10
Label:
text: "Temperature"
font_size: 18
text_color: [200, 200, 200]
Slider:
id: tempSlider
min_value: 0
max_value: 100
value: 22
width: 280
height: 20
Label:
text: "{{ tempSlider.value }} degrees C"
font_size: 24
text_color: [0, 120, 215]
Load it
from pyhmi import Engine, Sdl2Display, PyHMLParser, FlexLayout
display = Sdl2Display(width=320, height=240)
engine = Engine(display)
engine.set_layout(FlexLayout(direction="column", main_alignment="center", spacing=10))
parser = PyHMLParser(engine)
root = parser.parse_file("dashboard.pyhml")
engine.set_root(root)
engine.run()
Key Concepts
- Hierarchy: Indentation defines parent-child relationships
- Properties:
key: valuesyntax (YAML) - IDs:
id: namefor cross-references - Binding:
{{ expression }}for data binding - Layout:
layout: Columnorlayout: Row
Tutorial 3: Data Binding
Data binding connects widget properties to data sources automatically.
One-Way Binding
from pyhmi import Engine, Sdl2Display, Label, Window, Observable, FlexLayout
display = Sdl2Display(width=320, height=240)
engine = Engine(display)
window = Window()
window.width = 320
window.height = 240
# Create observable
temperature = Observable(22, name="temp")
engine.binding_engine.define("temp", temperature)
# Create label with binding
label = Label()
label.id = "tempLabel"
label.text = "22 C"
label.font_size = 24
engine.binding_engine.bind(label, "text", "{{ temp }} C")
engine.binding_engine.apply_binding(label, "text", "{{ temp }} C")
# Set up auto-update
def update_label(new_value):
engine.binding_engine.apply_binding(label, "text", "{{ temp }} C")
temperature.subscribe(update_label)
window.add_child(label)
engine.set_root(window)
engine.tree.compute_layout()
# Simulate temperature change
import time
for t in range(22, 31):
temperature.value = t
time.sleep(0.5)
engine.run()
Two-Way Binding with Slider
from pyhmi import Engine, Sdl2Display, Slider, Label, Window, FlexLayout
display = Sdl2Display(width=320, height=240)
engine = Engine(display)
window = Window()
window.width = 320
window.height = 240
layout = FlexLayout(direction="column", main_alignment="center", spacing=15)
engine.set_layout(layout)
slider = Slider()
slider.id = "volume"
slider.min_value = 0
slider.max_value = 100
slider.value = 50
slider.width = 280
slider.height = 20
label = Label()
label.id = "volLabel"
label.text = "50%"
label.font_size = 20
# Bind label to slider
engine.binding_engine.define("volume", slider)
engine.binding_engine.bind(label, "text", "{{ volume.value }}%")
engine.binding_engine.apply_binding(label, "text", "{{ volume.value }}%")
# Update label on slider change
def on_change(event):
engine.binding_engine.apply_binding(label, "text", "{{ volume.value }}%")
slider.on_signal("ValueChanged", on_change)
window.add_child(slider)
window.add_child(label)
engine.set_root(window)
engine.tree.compute_layout()
engine.run()
Tutorial 4: Scene Navigation
Multi-screen applications with scene management.
Basic Navigation
from pyhmi import (
Engine, Sdl2Display, Window, Label, Button,
FlexLayout, SceneManager, EventType, Event,
)
display = Sdl2Display(width=320, height=240)
engine = Engine(display)
manager = SceneManager(engine)
# Scene 1: Home
home = Window()
home.width = 320
home.height = 240
home.background_color = (30, 30, 30, 255)
home_label = Label()
home_label.text = "Home Screen"
home_label.font_size = 20
go_btn = Button()
go_btn.text = "Go to Settings"
go_btn.width = 160
go_btn.height = 32
def on_go(event):
manager.push("settings", segue={"from": "home"})
go_btn.on_signal("Clicked", on_go)
home.add_child(home_label)
home.add_child(go_btn)
# Scene 2: Settings
settings = Window()
settings.width = 320
settings.height = 240
settings.background_color = (40, 40, 60, 255)
settings_label = Label()
settings_label.text = "Settings"
settings_label.font_size = 20
back_btn = Button()
back_btn.text = "Back"
back_btn.width = 120
back_btn.height = 32
def on_back(event):
manager.pop()
back_btn.on_signal("Clicked", on_back)
settings.add_child(settings_label)
settings.add_child(back_btn)
# Set layout
layout = FlexLayout(direction="column", main_alignment="center", spacing=20)
engine.set_layout(layout)
# Load home scene
manager.load(home)
engine.tree.compute_layout()
engine.run()
With Transitions
# Fade transition
manager.push(settings, transition={"type": "fade", "duration": 0.5})
# Slide transition
manager.push(settings, transition={"type": "slide", "direction": "right", "duration": 0.3})
Data Segue
# Pass data forward
manager.push("profile", segue={"username": "admin", "level": 5})
# Receive in scene
profile_scene.on_enter = lambda segue: print(f"User: {segue['username']}")
# Pass data backward
manager.pop(segue={"settings_updated": True})
Tutorial 5: Custom Widgets
Create reusable, domain-specific widgets.
Basic Custom Widget
from pyhmi import CustomElement, register_element
@register_element("TempGauge")
class TempGauge(CustomElement):
value: float = 0.0
unit: str = "C"
min_value: float = -40.0
max_value: float = 120.0
def render(self, painter, x, y):
# Background
painter.draw_rect(x, y, self.width, self.height, (50, 50, 50, 255))
# Value text
text = f"{self.value:.1f}\u00b0{self.unit}"
painter.draw_text(
x + self.width // 2,
y + self.height // 2,
text, self.font_size, self.text_color, "center"
)
# Progress bar
ratio = (self.value - self.min_value) / (self.max_value - self.min_value)
bar_width = int(self.width * 0.8 * ratio)
painter.draw_rect(
x + self.width // 4, y + self.height - 10,
bar_width, 6, (0, 120, 215, 255), fill=True
)
Use in PyHML
Window:
width: 320
height: 240
TempGauge:
value: 22.5
unit: "C"
width: 280
height: 60
font_size: 24
Nested Custom Elements
@register_element("SensorCard")
class SensorCard(CustomElement):
sensor_name: str = "Temperature"
value: float = 0.0
def render(self, painter, x, y):
# Card background
painter.draw_rect(x, y, self.width, self.height, (60, 60, 60, 255))
# Render children (includes TempGauge)
for child in self._children:
child.render(painter, x + child.x, y + child.y)
SensorCard:
sensor_name: "Kitchen"
value: 22.5
width: 300
height: 100
TempGauge:
value: 22.5
width: 280
height: 60
Tutorial 6: Styling with Templates
Apply consistent styling across your application.
Define Templates
from pyhmi import StyleTemplate, Engine
dark_theme = StyleTemplate("dark")
dark_theme.set(
background_color=(30, 30, 30, 255),
text_color=(255, 255, 255),
font_size=14,
border_color=(80, 80, 80),
)
light_theme = StyleTemplate("light")
light_theme.set(
background_color=(240, 240, 240, 255),
text_color=(30, 30, 30),
font_size=14,
border_color=(180, 180, 180),
)
engine.register_template(dark_theme)
engine.register_template(light_theme)
engine.set_active_template("dark")
Switch Themes at Runtime
def on_theme_toggle(event):
current = "dark" if engine._active_template == "light" else "light"
engine.set_active_template(current)
Widget-Specific Overrides
Templates apply cascading: widget > class > theme. Explicit widget properties override templates.
button = Button()
button.text = "Important"
button.background_color = (255, 0, 0, 255) # Overrides theme
Tutorial 7: Animations
Smooth transitions and dynamic effects.
Property Animation
from pyhmi import Animation, Easing, Button, Window
btn = Button()
btn.text = "Slide Me"
btn.x = 0
btn.width = 120
btn.height = 40
# Animate x position
anim = Animation(
target=btn,
property_name="x",
from_value=0,
to_value=200,
duration=1.0,
easing=Easing.EASE_OUT,
)
engine.animation_engine.animate(anim)
Easing Curves
| Easing | Effect |
|---|---|
LINEAR | Constant speed |
EASE_IN | Slow start, fast end |
EASE_OUT | Fast start, slow end |
EASE_IN_OUT | Slow both ends |
BOUNCE | Bounce at end |
Keyframe Animation
from pyhmi import Animation, Easing, Keyframe
anim = Animation(
target=label,
property_name="opacity",
duration=2.0,
keyframes=[
Keyframe(time=0.0, value=0.0),
Keyframe(time=0.5, value=1.0),
Keyframe(time=1.0, value=0.0),
],
)
Tutorial 8: Dialogs
Modal dialogs for user interaction.
Message Dialog
from pyhmi import DialogManager
dlg = DialogManager(engine)
dlg.show_message("Info", "Operation completed successfully!")
Confirmation Dialog
def on_confirm(result):
if result.confirmed:
delete_item()
dlg.show_confirmation("Delete", "Are you sure?", callback=on_confirm)
Input Dialog
def on_input(result):
if result.confirmed:
username = result.data.get("text", "")
print(f"Username: {username}")
dlg.show_input("Login", "Enter username:", placeholder="admin", callback=on_input)
Tutorial 9: Building a Dashboard App
Complete multi-screen dashboard application.
from pyhmi import (
Engine, Sdl2Display, Window, Label, Button, Slider, CheckBox,
FlexLayout, SceneManager, DialogManager, StyleTemplate,
LineChart, Gauge, Event,
)
# Setup
display = Sdl2Display(width=480, height=320, title="Dashboard")
engine = Engine(display)
manager = SceneManager(engine)
dlg = DialogManager(engine)
# Theme
dark = StyleTemplate("dark")
dark.set(background_color=(20, 20, 30, 255), text_color=(255, 255, 255), font_size=14)
engine.register_template(dark)
engine.set_active_template("dark")
# Home scene
home = Window()
home.width = 480
home.height = 320
home.background_color = (20, 20, 30, 255)
chart = LineChart()
chart.width = 400
chart.height = 200
chart.data = [10, 25, 30, 20, 45, 35, 50]
gauge = Gauge()
gauge.width = 120
gauge.height = 120
gauge.value = 75
settings_btn = Button()
settings_btn.text = "Settings"
settings_btn.width = 100
settings_btn.height = 32
def go_settings(event):
manager.push(settings_scene, segue={"from": "home"})
settings_btn.on_signal("Clicked", go_settings)
# Layout and add children
layout = FlexLayout(direction="column", main_alignment="center", spacing=15)
engine.set_layout(layout)
home.add_child(chart)
home.add_child(gauge)
home.add_child(settings_btn)
# Settings scene
settings_scene = Window()
settings_scene.width = 480
settings_scene.height = 320
settings_scene.background_color = (30, 30, 50, 255)
alert_btn = Button()
alert_btn.text = "Show Alert"
alert_btn.width = 120
alert_btn.height = 32
def show_alert(event):
dlg.show_alert("Warning", "This is a test alert!")
alert_btn.on_signal("Clicked", show_alert)
back_btn = Button()
back_btn.text = "Back"
back_btn.width = 100
back_btn.height = 32
def go_back(event):
manager.pop()
back_btn.on_signal("Clicked", go_back)
settings_scene.add_child(alert_btn)
settings_scene.add_child(back_btn)
# Start
manager.load(home)
engine.tree.compute_layout()
engine.run()
Tutorial 10: Deploying to Embedded
Raspberry Pi (Framebuffer)
from pyhmi import Engine, FramebufferDisplay, PyHMLParser
display = FramebufferDisplay(device="/dev/fb0")
engine = Engine(display)
parser = PyHMLParser(engine)
root = parser.parse_file("dashboard.pyhml")
engine.set_root(root)
engine.run()
Raspberry Pi Pico (SPI LCD)
import displayio
import busio
import board
from pyhmi import Engine, SpiLcdDisplay, PyHMLParser
# Initialize display (CircuitPython)
spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI)
display = displayio.FourWire(spi, command=board.D9, chip_select=board.D10)
lcd = SpiLcdDisplay(driver=display, color_format="rgb565")
engine = Engine(lcd)
parser = PyHMLParser(engine)
root = parser.parse_file("dashboard.pyhml")
engine.set_root(root)
engine.run()
Memory Optimization for MCU
# Use bitmap fonts instead of TrueType
# Limit widget count
# Use partial updates
display.partial_update = True