Lopaka Basics 06: Screens & Menüs

Klick: der nächste Screen. Klick: und der nächste. Der andere Button öffnet ein Menü, und der Cursor wartet auf der Seite, von der du kamst. Es fühlt sich an wie ein echtes Gadget, und es sind vier Zeichnungen aus Lopaka, zwei Buttons und ein kurzer Sketch.
In dieser Episode findest du heraus, wie jedes Menü innen funktioniert: eine Karte aus Screens und Buttons, die in zwei Variablen passt. Dann zeichnest du die vier Screens in Lopaka, schreibst den Sketch, der die Buttons liest, und führst ihn auf einem simulierten Arduino UNO aus.
Was du bauen wirst: ein kleines Uhr-, Wetter- und Herzfrequenz-Gadget auf einem 128×64-OLED. Der NEXT-Button blättert durch drei Seiten, und OK öffnet ein Menü mit einem Cursor. Was du brauchst: einen Browser und die Simulator-Einrichtung aus Episode 1: ein Arduino UNO, ein SSD1306-OLED und die U8g2-Bibliothek. Zwei Drucktaster sind neu. Ein echtes Board ist optional; die Verdrahtung ist dieselbe wie in Episode 5.
Ein Menü ist eine Karte
Das Gerät ist immer auf einem Screen
Jedes Menü in jedem Gadget funktioniert wie eine Karte. In jedem Moment zeigt das Gerät genau einen Screen. Welcher Screen das ist, ist der Zustand des Geräts.
Jeder Button ist ein Pfeil
Jeder Tastendruck bewegt das Gerät von einem Zustand zum nächsten, wie ein Pfeil auf der Karte. Unser Gadget hat drei Seiten und ein Menü:
- NEXT geht durch die Seiten: Uhr, Wetter, Herzfrequenz und zurück zur Uhr.
- OK springt nach unten ins Menü, mit dem Cursor auf der Seite, von der du kamst.
- Im Menü bewegt NEXT den Cursor, und OK öffnet diese Seite.

Zeichne zuerst die Karte, und der Code schreibt sich fast von selbst.
Zwei Variablen halten die ganze Karte
Die ganze Karte passt in zwei Variablen:
page: welche Seite, 0 für die Uhr, 1 für das Wetter, 2 für die Herzfrequenz;menu: ist das Menü offen, nein oder ja.
Der Menü-Cursor braucht keine eigene Variable: Er steht immer auf page.

Schritt 1: Der erste Screen
Zeichne die Uhrseite
Starte ein neues Projekt wie in Episode 1: + Neues Projekt, Plattform U8g2, Screen-Größe 128×64. Wir nannten es „Pocket Gadget“.
Die erste Seite hat ein Icon und einen Titel oben, eine große Uhr in der Mitte und drei Punkte unten, die zeigen, auf welcher Seite du bist:
- Das Icon. Öffne den Tab Bilder, suche nach „clock“ und klicke das
clock-Icon. Setze zurück in Ebenen X4und Y3. - Der Titel. Füge einen Text
Clockin der Schriftartprofont12bei X24, Y14hinzu. - Die Uhr. Füge einen Text
10:24inprofont29bei X24, Y46hinzu. - Die Punkte. Wähle das Ellipsen-Werkzeug (das Kreis-Icon) und zeichne drei kleine Kreise mit RX und RY
2, alle bei Y56: den ersten bei X54mit eingeschaltetem Gefüllt, dann bei X62und X70mit ausgeschaltetem Gefüllt.
Jedes Zeichen von profont29 ist 16 Pixel breit, also ist „10:24“ 80 Pixel breit, und (128 − 80) / 2 = 24 legt es in die Mitte. Im Code werden die Punkte zu drawFilledEllipse(56, 58, 2, 2), drawEllipse(64, 58, 2, 2) und drawEllipse(72, 58, 2, 2): Der Inspektor zeigt die obere linke Ecke der Box um einen Kreis, U8g2 will seinen Mittelpunkt. Der mittlere Punkt sitzt bei x = 64, der Mitte des Screens.
Gruppiere die Punkte
Klicke im Ebenen-Panel auf ellipse 6, dann Shift-klicke auf ellipse 4, um alle drei Punkte auszuwählen. Drücke Strg + G (Cmd + G auf einem Mac): Die Punkte sind jetzt eine Gruppe und bewegen sich als ein Stück. Doppelklicke auf den Namen der Gruppe und nenne sie dots.
Benenne den Screen um: Der Name wird zur Funktion
Doppelklicke auf den Namen des Screens über der Zeichenfläche, Screen 1, und benenne ihn in Clock um. Dieser Name geht direkt in den Code: Der Screen wird zu einer Funktion namens drawClock.

Das ist der Code des Clock-Screens:
// [BEGIN lopaka generated]
static const unsigned char image_clock_bits[] U8X8_PROGMEM = {0xe0,0x03,0x18,0x0c,0x94,0x14,0x82,0x20,0x86,0x30,0x81,0x40,0x81,0x40,0x87,0x70,0x01,0x41,0x01,0x42,0x06,0x34,0x02,0x20,0x94,0x14,0x98,0x0c,0xe0,0x03,0x00,0x00};
void drawClock(void) {
u8g2.setFontMode(1);
u8g2.setBitmapMode(1);
// clock
u8g2.drawXBMP(4, 3, 15, 16, image_clock_bits);
// string 2
u8g2.setFont(u8g2_font_profont12_tr);
u8g2.drawStr(24, 14, "Clock");
// string 3
u8g2.setFont(u8g2_font_profont29_tr);
u8g2.drawStr(24, 46, "10:24");
// ellipse 4
u8g2.drawFilledEllipse(56, 58, 2, 2);
// ellipse 5
u8g2.drawEllipse(64, 58, 2, 2);
// ellipse 6
u8g2.drawEllipse(72, 58, 2, 2);
}
// [END lopaka generated]Die Gruppe erscheint nicht im Code. Sie hilft dir nur, die Punkte gemeinsam im Editor zu bewegen.
Schritt 2: Dupliziere die Seiten
Die anderen Seiten teilen sich dasselbe Layout, also fange nicht von vorne an. Wähle den Clock-Screen aus, indem du seinen Namen klickst, und klicke im Inspektor auf Duplizieren. Eine Kopie namens Clock copy erscheint rechts. Benenne sie in Weather um und ändere dann, was sich unterscheidet:
- Das Icon. Wähle die Ebene
clockaus und drücke Löschen. Suche in Bilder nach „sun“, fügeweather_sunhinzu und setze X4, Y3. - Die Texte. Wähle
string 2aus und tippeWeather. Wählestring 3aus, tippe23Cund setze X auf40: Drei Zeichen mit 16 Pixeln machen 48, und (128 − 48) / 2 = 40. - Die Punkte. Öffne die Gruppe
dots, schalte Gefüllt fürellipse 4aus und fürellipse 5ein.

Dupliziere noch einmal für die Herzfrequenz: Dupliziere Weather, benenne die Kopie in Heart um, ersetze die Sonne durch das heart-Icon bei X 4, Y 3, tippe Heart und 72 bpm mit X 16 und fülle den dritten Punkt statt des zweiten. Jede Kopie bekommt ihre eigene Funktion: drawWeather() und drawHeart().
Schritt 3: Das Menü
Drei Zeilen
Das Menü ist ein neuer Screen. Klicke im Ebenen-Panel auf Screen hinzufügen und benenne den neuen Screen in Menu um. Füge drei Zeilen hinzu, jede mit einem Icon und einer Beschriftung:
| Zeile | Icon bei X 6 | Beschriftung in profont12 bei X 26 |
|---|---|---|
| Clock | clock, Y 3 | Clock, Y 15 |
| Weather | weather_sun, Y 24 | Weather, Y 36 |
| Heart | heart, Y 45 | Heart, Y 57 |
Ein neuer Text startet in der letzten Schriftart, die du genutzt hast, also prüfe, dass jede Beschriftung in profont12 ist. Die Zeilen sind 21 Pixel auseinander: Jede Zahl in der zweiten Zeile ist die erste plus 21, und die dritte Zeile addiert wieder 21.
Eine Leiste im XOR-Modus
Jetzt der Cursor: eine gefüllte Leiste über der ersten Zeile.
- Wähle das Rechteck-Werkzeug, ziehe ein Rechteck und setze X
2, Y1, W124, H21. - Schalte Gefüllt und Invertiert (XOR) ein.
Die Leiste ist 21 Pixel hoch, die Höhe einer Zeile. Im XOR-Modus kippt sie die Pixel darunter, wie in Episode 2: Das Icon und der Text unter der Leiste werden schwarz. Die Leiste ist die oberste Ebene, also wird sie zuletzt gezeichnet, über der Zeile, die sie kippt.
Eine Variable für die Position der Leiste
Der Sketch muss die Leiste von Zeile zu Zeile bewegen. Doppelklicke auf den Ebenennamen des Rechtecks und benenne ihn in bar um. Klicke dann den kleinen Button Variable hinzufügen am rechten Rand des Y-Feldes im Inspektor.

Die Y-Position der Leiste ist jetzt eine Variable, benannt nach der Ebene:
// [BEGIN lopaka generated]
int bar_y = 1;
void drawMenu(void) {
u8g2.setFontMode(1);
u8g2.setBitmapMode(1);
// clock
u8g2.drawXBMP(6, 3, 15, 16, image_clock_bits);
// weather_sun
u8g2.drawXBMP(6, 24, 15, 16, image_weather_sun_bits);
// heart
u8g2.drawXBMP(6, 45, 15, 16, image_heart_bits);
// string 22
u8g2.setFont(u8g2_font_profont12_tr);
u8g2.drawStr(26, 15, "Clock");
// string 23
u8g2.drawStr(26, 36, "Weather");
// string 24
u8g2.drawStr(26, 57, "Heart");
// bar
u8g2.setDrawColor(2);
u8g2.drawBox(2, bar_y, 124, 21);
}
// [END lopaka generated]Jetzt kann der Code den Cursor in jede Zeile bewegen: bar_y ist 1 für die erste Zeile, 22 für die zweite und 43 für die dritte. setDrawColor(2) ist der XOR-Modus von U8g2. Die Bild-Arrays über drawMenu sind hier ausgelassen; mehr dazu in Schritt 5.
Schritt 4: Ordne die Screens wie die Karte an
Alle vier Screens leben auf einer großen Zeichenfläche, also ordne sie wie die Karte an: Ziehe den Menu-Screen an seinem Namen und lege ihn unter Weather. Du kannst die Position auch in den Inspektor tippen: Menu bei X 144, Y 100. Klicke rechts auf der Zeichenfläche auf Alle einpassen, um alles auf einmal zu sehen.

Jeder Screen bekommt seinen eigenen Tab im Code-Panel und seine eigene Zeichenfunktion: drawClock(), drawWeather(), drawHeart() und drawMenu(). Wo ein Screen auf der Zeichenfläche sitzt, ändert seinen Code nicht.
Schritt 5: Die Buttons und der Sketch
Ein Button zieht den Pin auf Masse
Jetzt die Buttons. Jeder verbindet einen Pin des Boards mit Masse: NEXT mit Pin 2, OK mit Pin 3. Mit dem Pin auf INPUT_PULLUP gesetzt hält ein Widerstand im Chip ihn HIGH, und er liest LOW, während der Button gedrückt ist.

Fange den Druck, nicht das Halten
Der offensichtliche Code sagt: Wenn der Button unten ist, gehe zur nächsten Seite.
if (digitalRead(NEXT) == LOW) page++;Aber loop() läuft wieder und wieder, viele Male während eines Drucks, und die Seiten wirbeln wie ein Spielautomat.

Echte Buttons prellen auch: Für etwa eine Millisekunde flattern die Kontakte, und ein Druck kann wie mehrere aussehen. Also frage nicht, ob der Button unten ist. Fange den Moment, in dem er nach unten geht, und ignoriere ihn für die nächsten zwanzig Millisekunden.

Das tut die pressed()-Funktion im Sketch:
// true only at the moment the button goes down; ignore bounce for 20 ms
bool pressed(int pin) {
static bool was[4];
static unsigned long t[4];
bool down = digitalRead(pin) == LOW;
bool press = down && !was[pin] && millis() - t[pin] > 20;
if (down != was[pin]) t[pin] = millis();
was[pin] = down;
return press;
}was merkt sich, ob der Button in der vorherigen Schleife unten war, und t merkt sich, wann er sich zuletzt änderte. Ein Druck zählt nur, wenn der Button jetzt unten ist, vorher oben war und seit mehr als 20 ms ruhig ist. Beide Arrays sind nach Pinnummer indiziert und haben vier Slots, genug für Pins 2 und 3. Wenn du einen Button an Pin 4 oder höher verdrahtest, mache sie größer.
Der ganze Sketch
Die vier Zeichenfunktionen aus Lopaka kommen dorthin, wo der Kommentar sagt; alles andere im Sketch ist die Menülogik:
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// Paste the code of the Clock, Weather, Heart and Menu tabs here
const int NEXT = 2, OK = 3;
int page = 0; // 0 clock, 1 weather, 2 heart
bool menu = false;
bool changed = true;
// true only at the moment the button goes down; ignore bounce for 20 ms
bool pressed(int pin) {
static bool was[4];
static unsigned long t[4];
bool down = digitalRead(pin) == LOW;
bool press = down && !was[pin] && millis() - t[pin] > 20;
if (down != was[pin]) t[pin] = millis();
was[pin] = down;
return press;
}
void setup() {
pinMode(NEXT, INPUT_PULLUP);
pinMode(OK, INPUT_PULLUP);
u8g2.begin();
}
void loop() {
if (pressed(NEXT)) { page = (page + 1) % 3; changed = true; }
if (pressed(OK)) { menu = !menu; changed = true; }
if (!changed) return;
changed = false;
u8g2.clearBuffer();
u8g2.setDrawColor(1);
if (menu) {
bar_y = 1 + page * 21;
drawMenu();
} else {
switch (page) {
case 0: drawClock(); break;
case 1: drawWeather(); break;
case 2: drawHeart(); break;
}
}
u8g2.sendBuffer();
}Die zwei Variablen aus der Karte sind oben: page und menu. In loop():
- NEXT bewegt zur nächsten Seite.
% 3ist der Rest nach dem Teilen durch 3, also kommt nach Seite 2 wieder Seite 0: von der Herzfrequenz zurück zur Uhr. Im Menü bewegt dieselbe Zeile den Cursor. - OK öffnet oder schließt das Menü. Das Menü zeigt die aktuelle
page, also wartet der Cursor auf der Seite, von der du kamst, und OK im Menü öffnet die Seite unter dem Cursor. - Dann zeichnen wir, aber nur, wenn sich etwas geändert hat.
changedstartet alstrue, also wird die erste Seite beim Start einmal gezeichnet. - Im Menü kommt die Position der Leiste von der Seitenzahl:
1 + page * 21ergibt 1, 22 oder 43, die drei Zeilen aus Schritt 3. - Sonst wählt ein
switch, welche Seite gezeichnet wird.

Im Simulator braucht das Senden eines vollen Frames an dieses Display etwa 44 Millisekunden; wir maßen sendBuffer() mit micros(). Bei jeder Schleife neu zu zeichnen, würde die meiste Zeit damit verbringen, dasselbe Bild erneut zu senden. Mit changed zeichnet der Screen nur nach einem Druck neu, und loop() prüft weiter die Buttons.
drawMenu() lässt den XOR-Modus an, also wechselt der Sketch mit setDrawColor(1) vor jedem Frame zurück zum normalen Zeichnen.
Kopiere jeden Tab in den Sketch
Kopieren nimmt den Code des offenen Tabs. Öffne den Tab Clock, klicke auf Kopieren und füge den Code in den Sketch ein, dann mache dasselbe für Weather und Heart.
Bevor du den Tab Menu kopierst, schalte Bilder deklarieren in den Schaltern neben dem Code aus. Das Menü nutzt dieselben drei Icons wie die Seiten, und die Seiten-Tabs haben ihre Arrays bereits deklariert. Der Schalter gilt für das ganze Projekt, also schalte ihn danach wieder ein.
Für einen UNO kompiliert, nutzt der Sketch 14.588 Bytes Flash (45 %) und 1.619 Bytes RAM (79 %). Wie in Episode 1 ist der größte Teil dieses RAM der 1.024-Byte-Frame-Puffer.
Führe ihn im Simulator aus
Öffne das Simulatorprojekt aus Episode 1: ein Arduino UNO und ein SSD1306-OLED, verdrahtet mit GND, 5V, A5 (SCL) und A4 (SDA), mit der U8g2-Bibliothek. Füge zwei Drucktaster hinzu und verdrahte sie:
- NEXT: ein Bein zu Pin 2, das andere zu GND;
- OK: ein Bein zu Pin 3, das andere zu GND.
Füge den Sketch mit den vier Tabs an Ort und Stelle ein und drücke Play. Weiter, weiter, weiter: Die Seiten gehen rund und zurück zur Uhr. OK öffnet das Menü, genau auf der aktuellen Seite. NEXT bewegt den Cursor zum Wetter, und OK öffnet es. Ein Druck, ein Schritt.

Verdrahte die Buttons auf einem echten Board auf dieselbe Weise. Ein gängiger taktiler Button hat vier Beine, paarweise verbunden; wenn ein Button ständig oder nie gedrückt zu sein scheint, nutze zwei Beine an gegenüberliegenden Ecken.
Zusammenfassung
- Jeder Screen deines Gadgets ist ein Screen in Lopaka: Jeder wird zu seiner eigenen Zeichenfunktion, und Duplizieren behält das Layout.
- Zeichne zuerst die Karte der Zustände: Zwei Variablen, die Seite und ob das Menü offen ist, können das ganze Menü betreiben.
- Fange den Druck, nicht das Halten: Reagiere auf den Moment, in dem ein Button nach unten geht, und ignoriere ihn danach 20 ms lang.
Nächste Episode: Animationen. Die Screens beginnen sich zu bewegen: von einem GIF zu einem winzigen OLED. Lies Episode 7 oder schaue sie auf YouTube.
Bereit, es zu versuchen? Öffne Lopaka und zeichne die Screens deines eigenen Gadgets.