Theming
What is a theme?
A theme is a small declarative file (.pyhmt) that holds the appearance defaults of an HMI: semantic colors (accent, text, backgrounds), font sizes and corner radii. Instead of hard-coding colors widget by widget, a project references one theme file; switching the file switches the appearance of every new element and of the scene window — without touching the UI description.
Themes are inert data: loading or applying a theme never executes code.
File format
A theme file is YAML with four optional sections. Indentation and keys follow the same convention as PyHML.
window:
background: [15, 23, 42, 255]
canvas:
rect:
fill: [203, 213, 225]
stroke: [51, 65, 85]
widgets:
Button:
background_color: [37, 99, 235]
text_color: [255, 255, 255]
global:
accent: [37, 99, 235]
font_size: 14
button_radius: 6
| Section | Key | Applies to |
|---|---|---|
window | background | the scene window element (background color) |
canvas | rect, ellipse, line, text, image | defaults for NEW canvas elements of that type |
widgets | any widget class name (Button, Slider, ...) | defaults for NEW widget elements of that class |
global | accent, font_size, button_radius, widget_radius, panel_radius, corner_radius | shared values; radii map onto the border_radius property of elements that expose it |
Value forms: colors as [r, g, b(, a)] lists or #rrggbb strings (normalized to lists on load), booleans as true/1/yes (for known boolean properties) and numbers as plain ints (digit strings are coerced for known numeric properties).
Editor ThemeBar
The UI Scene Editor has a Theme bar in the top toolbar: a name field, three icon buttons and a screen-background swatch.
- Open file (leftmost open icon) — opens a file dialog pre-filled with the editor's theme folder (where
default.pyhmtanddark.pyhmtlive) and filtered to.pyhmtfiles. Picking a file loads it through the same path as the text-box flow, including the status line and error handling for missing or corrupt files. - Load (open icon) — resolves the name in the field (appends
.pyhmtif missing), loads the file and shows the file path in the status line. A missing or corrupt file is reported in the status line; the current theme is kept. - Save (save icon) — writes the current theme to the path in the name field, creating parent directories as needed. Overwriting an existing file is an explicit user action and is confirmed in the status line.
- Screen background (colored swatch, right end) — opens the R/G/B color panel for the scene canvas background. The swatch shows the current background; the value is visible in the live canvas, stored on the scene, exported as
Window.background_colorand persisted when the scene is saved and reopened.
The editor also shows the PyHMI logo in the top-left corner, left of the project name field.
Theme semantics: values apply to new elements created after the theme was loaded — existing scene elements are not mutated. Loading a theme does not create an undo entry (no scene mutation).
When nothing is selected, the properties pane shows a Theme section with the editable global values (accent, font size, radii). Property rows that are driven by the theme carry a (theme) hint.
Python API
from pathlib import Path
from pyhmi.cli.editor.theme import (
Theme, load_theme_file, save_theme_file,
resolve_theme_path, default_template_path,
apply_theme_to_tree,
)
theme = load_theme_file("mytheme.pyhmt") # Theme with 4 sections
accent = theme.accent # [r, g, b(, a)] or None
applied = apply_theme_to_tree(theme, window) # apply to a live widget tree
save_theme_file(theme, Path("out") / "theme.pyhmt")
| Function | Purpose |
|---|---|
Theme.from_dict(raw) | build a theme from loaded data (tolerant of unknown sections/junk) |
theme.get(section, name, prop, fallback) | 3-level lookup with fallback |
theme.element_defaults(etype, kind) | defaults for one element type |
theme.element_overrides(etype, kind) | defaults + global font/radius, section value wins |
theme.merge(other) | new theme with other on top |
load_theme_file(path) | load .pyhmt/YAML (BOM tolerant); non-mapping raises ValueError |
save_theme_file(theme, path) | write YAML, creating parent directories |
resolve_theme_path(name) | name or path; appends .pyhmt to missing names |
default_template_path() | the default.pyhmt shipped with the editor |
apply_theme_to_tree(theme, root) | set matching properties on a live widget tree; returns applied count |
Generated project launchers inject apply_theme_to_tree automatically when the project ships a .pyhmt next to the scene, so the same theme drives the editor preview and the running app.
Bundled templates
default.pyhmt | dark.pyhmt | |
|---|---|---|
| Window background | [247, 248, 250, 255] light | [15, 23, 42, 255] dark slate |
| Accent | [37, 99, 235] | [96, 165, 250] |
| Text | slate-900 | slate-200 |
| Font size | 14 | 14 |
See the examples features/09_theme.py (dark theme applied to a scene) and features/10_theme_editor_export.py (save/load round-trip).