PyHMI Tutorials

Step-by-step guides from beginner to advanced.

Table of Contents

  1. Hello World
  2. Your First PyHML File
  3. Data Binding
  4. Scene Navigation
  5. Custom Widgets
  6. Styling with Templates
  7. Animations
  8. Dialogs
  9. Building a Dashboard App
  10. Deploying to Embedded

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

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

EasingEffect
LINEARConstant speed
EASE_INSlow start, fast end
EASE_OUTFast start, slow end
EASE_IN_OUTSlow both ends
BOUNCEBounce 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