Einfache GUIs

Dieses Tutorial zeigt, wie JControl-Anwendungen mit einer grafischen Benutzeroberfläche auf Grundlage der Display-Klasse jcontrol.io.Display entwickelt werden können. Das könnte beispielsweise für Anwendungen von Interesse sein, bei denen es auf besonders geringen Speicherplatzverbrauch ankommt - denn aufgrund der universellen Auslegung fertiger GUI-Frameworks (z.B. JControl/Vole) werden diese in Summe immer mehr Ressourcen benötigen als "handgestrickte" Anwendungen. Eine weitere Motivation für die GUI-Programmierung ohne GUI-Framework sind spezielle Funktionen, die in den verfügbaren Bibliotheken nicht zur Verfügung stehen.

Bild 1: Das Beispielprogramm SystemSetup

Die Klasse jcontrol.io.Display bietet viele einfache grafische Grundfunktionen, die z.B. das Setzen einzelner Pixel ermöglichen oder das Zeichnen von Linien, Rechtecken oder auch ganzen Bildern. Einfache grafische Benutzeroberflächen sind damit leicht realisierbar. Das oben gezeigte Beispiel aus dem SystemSetup wurde beispielsweise damit erstellt: Das ganze Menü besteht lediglich aus einem einzelnen Bild (JCIF), das zuvor mit dem Grafik-Editor JControl/PictureEdit erstellt wurde. Wird ein einzelner Menüpunkt (dargestellt durch die Icons) optisch selektiert, so braucht der Ausschnitt für das dazugehörige Icon lediglich noch einmal neu gezeichnet zu werden - nur eben invertiert.

Zwar erscheint die Klasse jcontrol.io.Display im Vergleich mit einem kompletten GUI-Framework vielleicht etwas rudimentär, aber wie man noch sehen wird läßt sich mit ihr ohne allzuviel Aufwand treiben zu müssen eine ansprechende grafische Benutzeroberfläche realisieren. Wie das funktioniert, zeigt das nun folgende Tutorial. Um es aktiv nachvollziehen zu können, ist die Installation der JControl/IDE sowie ein JControl-Gerät mit grafischem Display erforderlich.

Download: http://www.jcontrol.org/examples/UIDemo.zip Dieses Tutorial mit allen Quelltexten und Ressourcen als Projekt für die JControl/IDE downloaden (als ZIP-Archiv)



Das Design

Die in diesem Tutorial vorgestellte grafische Benutzeroberfläche besteht prinzipiell aus zwei Hierarchieebenen: Dem Hauptmenü sowie mehreren einzelnen Untermenüs. Da das Hauptmenü in der Regel nur wenige Menüpunkte enthält, spricht nichts dagegen, diese einzeln und bildschirmfüllend darzustellen. Auf diese Weise steht mehr Fläche für die Gestaltung der Menüpunkte zur Verfügung und die jeweilige Funktion läßt sich leichter auf einen Blick erkennen - was vor allem aus ergonomischer Sicht ein Vorteil ist. Bild 1 zeigt den Entwurf eines Hauptmenüs, das für die Bedienung mit einer Joystick-Tastatur entworfen wurde. Es handelt sich übrigens dabei um die grafische Benutzeroberfläche für ein Gerät zur Analyse der Übertragungseigenschaften von Signalen, die über das ganz normale Stromversorgungsnetz versendet werden (JControl/PLUI, ein sog. Power-Line-Analyzer) - aber das sei nur am Rande bemerkt.

Bild 1: Ein Eintrag im Hauptmenü

Bild 2: Ein Untermenü

Die Pfeile an den Bildschirmrändern sollen dem Benutzer zur besseren Orientierung dienen und zeigen an, in welche Richtung sich die Symbole bei Betätigung der Joystick-Tastatur verschieben werden. Bei der abgebildeten Anwendung ist der Menüpunkt "System Settings" der letzte Eintrag des Hauptmenüs, weshalb kein Pfeil nach unten angezeigt wird. Ein Tastendruck nach oben springt zum vorhergehenden Hauptmenüpunkt und ein Tastendruck auf Auswahl (mittlere Taste) wählt den angezeigten Menüpunkt aus.

In Bild 2 ist ein Untermenü dargestellt. Um mehr als einen Menüpunkt gleichzeitig anzeigen zu können, wählen wir für die einzelnen Untermenüs kleinere Piktogramme als wir es im Hauptmenü getan haben. Das Untermenü in Bild 2 hat sechs Einträge und passt somit genau auf das Display. Für Untermenüs mit noch mehr Einträgen könnte eine Scroll-Funktion implementiert werden, die den Bildschriminhalt nach links weiterschrebt wenn der siebente Menüpunkt angezeigt werden soll.


Vorbereitungen

Bevor wir mit der Implementierung der einzelnen Menüs beginnen, müssen wir zunächst die Menüstruktur festlegen und entsprechende Piktogramme entwerfen. Für den Entwurf der Piktogramme bietet sich das Werkzeug JControl/PictureEdit an, das in die JControl/IDE integriert ist und im Tutorial Verwenden von ROM-Ressourcen: Bilder und Schriftarten bereits vorgestellt wurde.

Für unser Beispiel legen wir nun die folgende Menüstruktur fest:

Mit dem Werkzeug "JControl/PictureEdit" zeichnen wir nun vier Bilder. Diese können wir in einem Projekt über das Kontext-Menü (rechte Maustaste -> neues Bild erstellen) anlegen. Wer sich die Mühe ersparen möchte, kann die hier gezeigten Dateien auch direkt downloaden:

1: syssettings.jcif

2: syssettingschooser.jcif

3: examples.jcif

4: exampleschooser.jcif

Um die Anwendung abzurunden, soll noch ein "Splashscreen" nach dem Einschalten angezeigt werden (Datei splashscreen.jcif.) Außerdem gibt die Bilddatei background.jcif den Haupt- und Untermenüs einen angemessenen grafischen Rahmen. Die Bilddateien up.jcif, down.jcif, left.jcif, right.jcif sowie big_right.jcif stellen die Richtungen dar, in die der Joystick bewegt werden kann.

5: splashscreen.jcif

6: background.jcif

7: big_right.jcif

8: up.jcif

9: left.jcif

10: right.jcif

11: down.jcif


UIDemo: Die Hauptklasse

Um die Anwendung übersichtlich und leicht erweiterbar zu halten, wird sie in vier einzelne Klassen aufgeteilt: Die Hauptklasse UIDemo (für die Initialisierung), die innere Klasse UIDemo$MainMenu (sorgt für das Hauptmenü), sowie die beiden inneren Klassen UIDemo$ExamplesMenu und UIDemo$SystemMenu (für die Untermenüs). Übrigens werden bei der Programmiersprache Java innere Klasse immer durch ein "$"-Zeichen im Namen gekennzeichnet.

In diesem Abschnitt wollen wir nun die Hauptklasse unserer Anwendung implementieren. Betrachten Sie dazu das nachfolgende Listing der Klasse UIDemo. Es enthält zum einen den Konstruktor der Klasse und zum anderen eine Methode init(), die einmal bei der Instantiierung eines Objektes aufgerufen wird. Dabei werden die Objekte für die Ansteuerung des LC-Displays (jcontrol.io.Display), der Tastatur (jcontrol.io.Keyboard) sowie des Buzzers (jcontrol.io.Buzzer) angelegt. Außerdem wird hier ein Begrüßungsbildschirm splashscreen.jcif für die Dauer einer Sekunde angezeigt.

Die eigentliche "Logik" für die menügesteuerte Benutzerführung ist im Konstruktor implementiert: Eine Endlosschleife (eingeleitet durch die Befehlssequenz for (;;)) ruft fortwährend die Methode choose aus der Klasse UIDemo$MainMenu auf, um danach anhand des Rückgabewertes zu entscheiden, in welches Untermenü verzweigt werden soll (switch-Anweisung). Die Methode choose arbeitet übrigens blockierend, was bedeutet, daß ihr Aufruf die Programmausführung solange anhält, bis ein genz bestimmtes Ereignis eingetreten ist: Ein Tastendruck.

1    import java.io.IOException;
2    
3    import jcontrol.io.Buzzer;
4    import jcontrol.io.Display;
5    import jcontrol.io.Keyboard;
6    import jcontrol.io.Resource;
7    import jcontrol.lang.ThreadExt;
8    
9    /**
10     * UIDemo shows how to create a graphical user interface
11     * with JControl.
12     *
13     * <p>(C) DOMOLOGIC Home Automation GmbH 2003-2004</p>
14     */
15    public class UIDemo {
16      static Keyboard keys;
17      static Display lcd;
18      static Buzzer buzzer;
19    
20      /**
21       * Inits the UIDemo user interface. Calls UIDemo$MainMenu
22       * to draw the main menu and calls the submenu classes
23       * on user's command.
24       */
25      public UIDemo() {
26        int select = 0;
27    
28        // init
29        init();
30    
31        UIDemo$MainMenu mm = new UIDemo$MainMenu();
32    
33        // mainloop
34        for (;;) {
35          mm.draw();
36          select = mm.choose(select);
37          switch (select) {
38            case 0:
39              new UIDemo$ExamplesMenu();
40              break;
41            case 1:
42              new UIDemo$SystemMenu();
43              break;
44            default: break;
45          }
46        }
47      }
48    
49    
50      /**
51       * Inits class variables, shows splashscreen
52       */
53      private void init() {
54        keys =   new Keyboard()// get keyboard
55        lcd =    new Display();   // get display
56        buzzer = new Buzzer();    // get buzzer
57    
58        // show splashscreen
59        try {
60          lcd.drawImage(new Resource("splashscreen.jcif"), 0,0);
61        } catch (java.io.IOException e) {}
62    
63        // wait a while
64        try {
65          ThreadExt.sleep(1000);
66        } catch (InterruptedException e) {}
67    
68        lcd.clearDisplay();
69      }
70    
71    
72      /**
73       * Main method. Program execution starts here.
74       */
75      public static void main(String[] args) {
76        new UIDemo();
77      }
Listing 1: Auszug aus UIDemo.java

Der obenstehende Quelltext ruft die Methoden draw() und choose(int) der Klasse UIDemo$MainMenu auf. Diese wird im folgenden Abschnitt implementiert.


UIDemo: Die interne Klasse 'MainMenu'

Im letzten Abschnitt haben wir gesehen, dass die Klasse UIDemo die Methoden draw() und choose(int) aus der Klasse UIDemo$MainMenu aufruft, die nun implementiert werden soll.

Die Aufgabe von draw() ist, das Hintergrundbild zu zeichnen. Eigentlich ist diese Aufgabe so trivial, dass sie gar nicht in eine eigene Methode gekapselt werden müsste. Allerdings wäre es denkbar, die Methode in einem späteren Entwicklungsstadium der Software zu erweitern.

Die Methode choose(int) besteht aus einer Schleife, in der das Piktogramm des jeweils angezeigten Hauptmenüs gezeichnet und dann auf eine Tastatureingabe gewartet wird. Durch Drücken eines Tasters wird die Schleife verlassen (Befehl break mainloop;) und die Nummer des ausgewählten Menüpunktes zurückgegeben. Das folgende Listing zeigt die genaue Implementierung der beiden Methoden:

1    import java.io.IOException;
2    
3    import jcontrol.io.Buzzer;
4    import jcontrol.io.Display;
5    import jcontrol.io.Keyboard;
6    import jcontrol.io.Resource;
7    import jcontrol.lang.ThreadExt;
8    
9    /**
10     * UIDemo shows how to create a graphical user interface
11     * with JControl.
12     *
13     * <p>(C) DOMOLOGIC Home Automation GmbH 2003-2004</p>
14     */
15    public class UIDemo {
16      static Keyboard keys;
17      static Display lcd;
18      static Buzzer buzzer;
19    
20      /**
21       * Inits the UIDemo user interface. Calls UIDemo$MainMenu
22       * to draw the main menu and calls the submenu classes
23       * on user's command.
24       */
25      public UIDemo() {
26        int select = 0;
27    
28        // init
29        init();
30    
31        UIDemo$MainMenu mm = new UIDemo$MainMenu();
32    
33        // mainloop
34        for (;;) {
35          mm.draw();
36          select = mm.choose(select);
37          switch (select) {
38            case 0:
39              new UIDemo$ExamplesMenu();
40              break;
41            case 1:
42              new UIDemo$SystemMenu();
43              break;
44            default: break;
45          }
46        }
47      }
48    
49    
50      /**
51       * Inits class variables, shows splashscreen
52       */
53      private void init() {
54        keys =   new Keyboard()// get keyboard
55        lcd =    new Display();   // get display
56        buzzer = new Buzzer();    // get buzzer
57    
58        // show splashscreen
59        try {
60          lcd.drawImage(new Resource("splashscreen.jcif"), 0,0);
61        } catch (java.io.IOException e) {}
62    
63        // wait a while
64        try {
65          ThreadExt.sleep(1000);
66        } catch (InterruptedException e) {}
67    
68        lcd.clearDisplay();
69      }
70    
71    
72      /**
73       * Main method. Program execution starts here.
74       */
75      public static void main(String[] args) {
76        new UIDemo();
77      }
Listing 2: Die interne Klasse MainMenu aus UIDemo.java

Damit wäre das Hauptmenu fertig. Was noch bleibt ist die Umsetzung der beiden Untermenüs (SystemMenu und ExamplesMenu), womit wir uns in den folgenden beiden Abschnitten beschäftigen wollen.


UIDemo: Die interne Klasse 'SystemMenu'

Dieser Abschnitt befasst sich mit der Implementierung des ersten Untermenüs "SystemMenu". Dazu wird im Wesentlichen das Bild des jeweiligen Untermenüs auf das LCD gezeichnet. Der vom Benutzer ausgewählte Untermenüpunkt soll durch eine Invertierung kenntlich gemacht werden. Der folgende Quelltext zeigt die Implementierung dieser Funktionalität anhand der Klasse UIDemo$SystemMenu. An den markierten Stellen können Befehle eingefügt werden, die nach Auswahl eines Menüpunkts ausgeführt werden sollen. Unser Beispiel zeigt den ausgewählten Menüpunkt für 1 Sekunde als String auf dem Display an.

260    /**
261     * Subclass that draws the system settings submenu.
262     */
263    class UIDemo$SystemMenu {
264      public UIDemo$SystemMenu() {
265        int select = 0;
266    
267        loop:for (;;) {
268    
269          UIDemo.lcd.clearRect(0,10,90,7);
270          UIDemo.lcd.clearRect(1,18,126,45);
271          UIDemo.lcd.drawString("System Settings", 0, 10);
272    
273          select = choose(select);
274          switch (select) {
275            case 0: // insert code for menu item 1 here
276            case 1: // insert code for menu item 2 here
277            case 2: // insert code for menu item 3 here
278            case 3: // insert code for menu item 4 here
279            case 4: // insert code for menu item 5 here
280              UIDemo.lcd.clearRect(1,18,126,45);
281              UIDemo.lcd.drawString("Selected menu item: ".concat(
282                                     String.valueOf(select+1)), 10,20);
283              try {
284                ThreadExt.sleep(1000);
285              } catch (InterruptedException e) {}
286            case 5: return;
287            default: break;
288          }
289        }
290      }
291    
292      /**
293       * Draws system settings menu and highlights user's choice.
294       * @param select preselection
295       * @return int selected menu item
296       */
297      int choose(int select) {
298        int oldselect = -1;
299        final int images = 5;
300        Display lcd = UIDemo.lcd;
301    
302        // draw menu
303        try {
304          lcd.drawImage(new Resource("syssettingschooser.jcif"),
305                        14, 23);
306        } catch (java.io.IOException e) {}
307        // left arrow
308        try {
309          lcd.drawImage(new Resource("left.jcif"), 1, 31);
310        } catch (IOException e) {}
311    
312        mainloop:for (;;) {
313          if (select!=oldselect) {
314            // set selection
315            lcd.setDrawMode(Display.XOR);
316            if (oldselect!=-1)
317              lcd.fillRect(14+oldselect*20, 23, 20, 22);
318            lcd.fillRect(14+select*20, 23, 20, 22);
319            oldselect = select;
320            lcd.setDrawMode(Display.NORMAL);
321          }
322    
323          switch (UIDemo.keys.read()) {
324            case 'u':
325            case 'U':
326            case 'L':
327              if (select == 0) {
328                select = images;
329                break mainloop;
330              }
331              else select--;
332              break;
333            case 'd':
334            case 'D':
335            case 'R':
336              if (select<images-1) select++;
337              break;
338            case 'S':
339              break mainloop;
340          }
341        }
342        return select;
343      }
344    }
Listing 3: Die interne Klasse SystemMenu aus UIDemo.java

Für die Vollständigkeit dieser Demo-Applikation fehlt nun lediglich noch das Untermenu ExamplesMenu, welches im nächsten Abschnitt implementiert wird.


UIDemo: Die interne Klasse 'ExamplesMenu'

Die Implementierung des "Examples"-Untermenüs wäre eigentlich die gleiche wie bei den "System Settings", wenn nicht das "Examples"-Untermenü mehr Einträge hätte als gleichzeitig auf das Display passen. Abhilfe schafft der Befehl drawImage() der Systemklasse jcontrol.io.Display, der durch Angabe zusätzlicher Parameter ermöglicht, einen beliebigen Ausschnitt einer Bilddatei anzuzeigen. So können wir das Bild mit den Menüpunkten auf dem Bildschirm horizontal hin- und herschieben, während der Benutzer durch die Einträge scrollt. Dieses Vorgehen ist übrigens wesentlich effizienter als jeden Menüpunkt in einer eigenen Bilddatei abzulegen und das Menü "manuell" -- Eintrag für Eintrag -- auf das Display zu zeichnen.

Der folgende Quelltext zeigt die Implementierung der Klasse UIDemo$ExamplesMenu, die zum Großteil der Implementierung von UIDemo$SystemMenu entspricht. Der zusätzliche Code für die Scroll-Funktion ist farblich hervorgehoben.

152    /**
153     * Subclass that draws the examples submenu.
154     */
155    class UIDemo$ExamplesMenu {
156      public UIDemo$ExamplesMenu() {
157        int select = 0;
158    
159        loop:for (;;) {
160    
161          UIDemo.lcd.clearRect(0,10,90,7);
162          UIDemo.lcd.clearRect(1,18,126,45);
163          UIDemo.lcd.drawString("Examples", 0, 10);
164    
165          select = choose(select);
166          switch (select) {
167            case 0: // insert code for menu item 1 here
168            case 1: // insert code for menu item 2 here
169            case 2: // insert code for menu item 3 here
170            case 3: // insert code for menu item 4 here
171            case 4: // insert code for menu item 5 here
172            case 5: // insert code for menu item 6 here
173            case 6: // insert code for menu item 7 here
174            case 7: // insert code for menu item 8 here
175              UIDemo.lcd.clearRect(1,18,126,45);
176              UIDemo.lcd.drawString("Selected menu item: ".concat(
177                                     String.valueOf(select+1)),
178                                    10,20);
179              try {
180                ThreadExt.sleep(1000);
181              } catch (InterruptedException e) {}
182            case 8: return;
183            default: break;
184          }
185        }
186      }
187    
188    
189      /**
190       * Trace user choices in network menu
191       */
192      int choose(int select) {
193        int shift = 0, oldselect = -1, oldshift = -1;
194        final int images = 8;
195        Display lcd = UIDemo.lcd;
196    
197        mainloop:for (;;) {
198          if (select-shift>5) shift = select-5;
199          if (select-shift<0) shift = select;
200          if (shift!=oldshift) {
201            // the icons to choose
202            try {
203              lcd.drawImage(new Resource("exampleschooser.jcif"),
204                            4, 23, 120, 40, 20*shift,0);
205            } catch (java.io.IOException e) {}
206            if (oldshift==2) {
207              // delete obsolete pixels
208              lcd.clearRect(1,44,4,19);
209              lcd.clearRect(123,44,4,19);
210            }
211            // left arrow
212            try {
213              lcd.drawImage(new Resource("left.jcif"), 1, 31);
214            } catch (IOException e) {}
215            if (shift<(images-6)) {
216              // right arrow
217              try {
218                lcd.drawImage(new Resource("right.jcif"), 124, 31);
219              } catch (IOException e) {}
220            } else {
221              // no arrow
222              lcd.clearRect(124,30,3,9);
223            }
224          }
225          if (select!=oldselect) {
226            // set selection
227            lcd.setDrawMode(Display.XOR);
228            if (oldselect!=-1 && oldshift==shift)
229              lcd.fillRect(4+(oldselect-shift)*20, 23, 20, 22);
230            lcd.fillRect(4+(select-shift)*20, 23, 20, 22);
231            oldselect = select;
232            lcd.setDrawMode(Display.NORMAL);
233            oldshift = shift;
234          }
235    
236          switch (UIDemo.keys.read()) {
237            case 'u':
238            case 'U':
239            case 'L':
240              if (select == 0) {
241                select = images;
242                break mainloop;
243              }
244              else select--;
245              break;
246            case 'd':
247            case 'D':
248            case 'R':
249              if (select<images-1) select++;
250              break;
251            case 'S':
252              break mainloop;
253          }
254        }
255        return select;
256      }
257    }
Listing 4: Die interne Klasse ExamplesMenu aus UIDemo.java

Nun sind alle Klassen und Methoden die für dieses Beispiel benötigt werden vorhanden. Der nächste Abschnitt beschreibt, wie das fertige UIDemo getestet wird.


Testen der UIDemo

Zum Testen der UIDemo: Laden Sie die Datei UIDemo.zip herunter und entpacken Sie das ZIP-Archiv in ein leeres Verzeichnis. Es enthält alle in diesem Tutorial vorgestellten Dateien, sowie eine JControl/IDE-Projektdatei . Öffnen Sie die Projektdatei mit der JControl/IDE und klicken Sie auf den Simulations-Button. Daraufhin öffnet sich der JControl-Simulator mit der Darstellung des ausgewählten JControl-Moduls und des grafischen Benutzermenüs (ähnlich wie in Bild 3).

Bild 3: Die UIDemo im JControl/IDE-Simulator



© 2000-2006 DOMOLOGIC Home Automation GmbH. All Rights Reserved.