Java-Applet 'CWUhr'

Christian Wirthensohn, christian@wirthensohn.de
14.01.2003

Praktikum Rechnernetze 2 (Info)
WS 2002/03, Prof. Dr. Passing, Fachhochschule Worms




Dieses Java-Applet stellt eine quasi fotorealistische Armbanduhr mit digitaler und analoger Anzeige dar. Als Vorlage diente hier die Funk-Solaruhr "Mega Solar" der Firma Jung*ans (siehe Originalbild, Quelle: Online-Katalog Versandhaus Quelle).

Wie bei einer "echten" Uhr springt der Minutenzeiger nicht im Minutentakt und der Stundenzeiger immer auf die volle Stunde, sondern wird mit Fortschreiten des Sekunden- bzw. Minutenzeigers kontinuierlich mitgezogen. Es bleiben allerdings die üblichen Einschränkungen der gerasterten Darstellung.

Im Gegensatz zur originalen Uhr wird auf der Digitalanzeige nicht Wochentag und der Tag des Monats, sondern Stunde und Minute angezeigt.

Das Java-Applet basiert ausschließlich auf den bei Java mitgelieferten Funktionen der AWT-Bibliothek (Abstract Windowing Toolkit, siehe Java 2 Platform API) und greift sonst auf keine weiteren Klassen zu. Es ist somit praktisch auf allen Rechnern und allen Browsern lauffähig.

Da Java die Möglichkeit bietet, wiederkehrende Aufgaben - wie es die "Animation" einer Uhr ist - ressourcenschonend als Thread auszuführen, verwendet auch das vorliegende Applet diese Technik.

Ein Thread ist ein Teil des Programms, der eigenständig abläuft, während sich das eigentliche Programm um andere Aufgaben kümmert. Hier dient der Thread dem zyklischen Neuaufbau das Applets, wodurch Zeiger und Digitalanzeige aktualisiert werden.


Aufbau

Die Darstellung der Uhr setzt sich im Wesentlichen aus drei Komponenten zusammen.

Die eigentliche Uhr, die von digitale Ziffern und Zeigern bereinigt wurde, wird als erstes Puzzleteil sozusagen als Hintergrundbild geladen (siehe das bereinigte Bild).

Es folgt darauf die Darstellung der digitalen Ziffern an der entsprechenden Stelle im Bild. Die digitalen Ziffern wurden als einzelne Grafikdateien abgelegt: . Das Applet lädt diese Grafiken bei der Initialisierung in ein entsprechendes Array, aus dem die Grafiken dann herausgeholt und an der entsprechenden Stelle auf das Hintergrundbild "aufgebracht" werden.

Fehlen nur noch die Zeiger. Diese werden mit Hilfe der in der AWT-Klasse enthaltenen Zeichenfunktionen (hier Polygon) vom Applet zur Laufzeit gezeichnet und liegen im Gegensatz zur Digitalanzeige nicht als fertige Grafik vor.


Quellcode

Der Quellcode kann auch im Ganzen eingesehen und heruntergeladen werden

Das Applet bildet die Klasse CWUhr, das von der Java-eigenen Klasse Applet abgeleitet ist, wodurch dann praktisch automatisch ein im Browser lauffähiges Applet entsteht.

Beim Start das Applets wird dieses über die eigene init-Funktion initialisiert:

           public void init()
           {
               MediaTracker tracker = new MediaTracker(this);
               int Count;
               
               UhrImage = getImage(getCodeBase(), "uhr.jpg");
               tracker.addImage(UhrImage, 0);
               try
               {
                   tracker.waitForID(0);
               }
               catch (Exception e) { }

Es wird dabei das Hintergrundbild aus der Datei uhr.jpg in eine Image-Variable geladen. Der MediaTracker mit der Funktion addImage sorgt dafür, dass das Bild auch tatsächlich geladen wird. Außerdem wird noch durch das waitForID so lange gewartet, bis das Bild auch komplett im Speicher vorhanden ist.

Würde man nicht so vorgehen, könnte es sein, dass das Applet wunderbar abläuft, aber kein Bild zu erkennen ist.

Um eine flickerfreie Darstellung zu erreichen, wird eine Technik namens Double Buffering eingesetzt, bei der das Bild zuerst nur im Speicher, also im Puffer aufgebaut und anschließend in einem Schub auf dem Bildschirm dargestellt wird:

              Buffer = createImage(UhrImage.getWidth(this),
                                   UhrImage.getHeight(this));
              GraphicsBuffer = Buffer.getGraphics();

Abschließend werden dann auch noch die einzelnen Grafiken der Ziffern für die Digitalanzeige in ein Array geladen:

              for (Count=0; Count<10; Count++)
              {
                  UhrImageDigits[Count] = getImage(getCodeBase(),
                                                   Count + ".jpg");
                  tracker.addImage(UhrImageDigits[Count], Count+1);
                  try
                  {
                      tracker.waitForID(Count);
                  }
                  catch (Exception e) { }
              }

Beim Start des Applets wird automatisch die Methode start ausgeführt, die hier genutzt wird, um die bereits angesprochene Funktionalität des Threads anzuwerfen:

          public void start()
          {
              if (UhrThread == null)
              {
                  UhrThread = new Thread(this);
                  UhrThread.start();
              }
          }

Entsprechend wird bei Beendigung des Applets, d. h. beim Schließen des Browsers, bzw. wenn eine andere Seite im Fenster geladen wird, die Methode stop angesprungen, in der der laufende Thread abgebrochen wird:

          public void stop()
          {
              if (UhrThread != null)
              {
                  UhrThread.stop();
                  UhrThread = null;
              }
          }

Während nun der Thread abläuft, wird zyklisch die Methode run gestartet. Diese kann nun genutzt werden, um über einen Aufruf der Methode paint (das übernimmt das Kommando repaint) die Darstellung der Uhr zu aktualisieren, also neu aufzubauen:

          public void run()
          {
              while (UhrThread != null)
              {
                  try
                  {
                      UhrThread.sleep(100);
                  }
                  catch (Exception e) { }
                  
                  repaint();
              }
          }

Natürlich macht es keinen Sinn, wenn die Uhr ein paar Millionen mal pro Sekunde neu aufgebaut wird, wo doch eine Uhr üblicherweise nur einmal pro Sekunde Ihren Zustand ändert. Daher wird mit Hilfe von UhrThread.sleep der gesamte Thread eine Weile schlafen gelegt, wodurch er dann praktisch keine Prozessorleistung mehr benötigt.

Soweit die Pflicht, es folgt die Kür. Das Event paint, zyklisch von Thread angestossen, "zeichnet" die Uhr. Dazu muss natürlich zuerst die Systemzeit abgefragt werden. Sicherheitshalber wird das Ganze auch nur dann ausgeführt, wenn sich die Systemzeit, d. h. die Sekunden, geändert haben...

          public void paint(Graphics g)
          {
              Date UhrDatum = new Date();
              
              int UhrSekunden = UhrDatum.getSeconds();
              
              if (UhrSekunden != UhrSekundenAlt)
              {
                  int UhrMinuten = UhrDatum.getMinutes();
                  int UhrStunden = UhrDatum.getHours();

...zwischendrin werden dann noch die einzelnen Ziffern für die Digitalanzeige berechnet und schon mal das Hintergrundbild in den Puffer geworfen...

                  int Digit1 = UhrStunden / 10;
                  int Digit2 = UhrStunden % 10;
                  int Digit3 = UhrMinuten / 10;
                  int Digit4 = UhrMinuten % 10;
                  
                  g.drawImage(Buffer, 0, 0, this);
                  
                  GraphicsBuffer.drawImage(UhrImage, 0, 0, this);
                  GraphicsBuffer.setColor(Color.black);

...bevor es dann interessant wird und mit Hilfe von drawImage die einzelnen Grafiken der Digitalanzeige an ihren vorgesehenen Platz auf der Grafik positioniert werden. Dazu werden die passenden Grafiken aus ihrem Array herausgefischt:

                  GraphicsBuffer.drawImage(UhrImageDigits[Digit1],
                                           103, 222, this);
                  GraphicsBuffer.drawImage(UhrImageDigits[Digit2],
                                           114, 222, this);
                  GraphicsBuffer.drawImage(UhrImageDigits[Digit3],
                                           127, 222, this);
                  GraphicsBuffer.drawImage(UhrImageDigits[Digit4],
                                           138, 222, this);

Als kleine Zugabe dürfen noch zwei schwarze Punkte zwischen Stunden und Minuten der Digitalanzeige im Sekundentakt blinken. Genau genommen werden diese zwei Punkte nur jede zweite Sekunde auf die Grafik "gemalt":

                  UhrSekundenTakt = 1 - UhrSekundenTakt;
                  if (UhrSekundenTakt == 1)
                  {
                      GraphicsBuffer.fillRect(125, 227, 2, 2);
                      GraphicsBuffer.fillRect(125, 232, 2, 2);
                  }

Etwas kniffliger wird dann allerdings die Darstellung der analogen Zeiger. Da es drei Zeiger sind, darf diese Aufgabe für die einzelnen Zeiger eine eigene Funktion übernehmen, der an dieser Stelle als Informationen die gewünschte Länge und Breite der Zeiger, sowie der Winkel, um den der Zeiger um den Mittelpunkt der Uhr rotiert werden soll, übergeben werden.

            	  Linie(50, 3,
                        (UhrStunden * 30) + (UhrMinuten / 2),
                        Color.gray, 0);
                  Linie(80, 3,
                        (UhrMinuten * 6) + (UhrSekunden / 10),
                        Color.gray, 0);
                  Linie(80, 1, UhrSekunden * 6, Color.gray, 0);

Die Funktion Linie ist so konstruiert, dass sie einen Zeiger in gewünschter Breite und Höhe zeichnet, der aber einfach nur senkrecht nach oben stehen würde...

          void Linie(int laenge, int halbebreite, int winkel, Color farbe, int offset)
          {
              Polygon Zeiger = new Polygon();
              
              PolygonPunkt (Zeiger, 0-halbebreite, 15-offset, winkel);
              PolygonPunkt (Zeiger, halbebreite, 15-offset, winkel);
              PolygonPunkt (Zeiger, halbebreite, 0-laenge, winkel);
              PolygonPunkt (Zeiger, 0, 0-laenge-halbebreite, winkel);
              PolygonPunkt (Zeiger, 0-halbebreite, 0-laenge, winkel);
              
              GraphicsBuffer.setColor(farbe);
              GraphicsBuffer.fillPolygon(Zeiger);

...und sich selbst nochmals rekursiv aufruft, um eine etwas verkleinerte, cyan-farbene Version dieses Zeigers über den gerade gezeichneten Zeiger "drüber malt", wodurch der zweifarbige Zeiger entsteht...

              if (halbebreite > 2)
              {
                  // Sich selbst nochmal aufrufen, um den Cyan-farbenen Teil
                  // des Zeigers zu zeichnen.
                  
                  Linie (laenge - 3, halbebreite - 2, winkel, Color.cyan, 3);
              }
          }

...aber irgendwie ist es reizlos, wenn alle drei Zeiger nur senkrecht nach oben stehen (außer natürlich exakt um 12 Uhr). Deshalb übernimmt die Funktion PolygonPunkt die Verschiebung der einzelnen Punkte des Polygons, so dass eine Rotation des Zeigers um seinen Mittelpunkt entsteht:

          void PolygonPunkt(Polygon Zeiger, int x, int y, int winkel)
          {
              double rad = (winkel * 2 * Math.PI) / 360;
              double cos = Math.cos(rad);
              double sin = Math.sin(rad);
              
              Zeiger.addPoint ((int)(126+x*cos-y*sin), (int)(181+y*cos+x*sin));
          }

Pferdefuß

Diese Version des Applets hat einen kleinen Haken, der jedoch vernachlässigbar sein dürfte:

Das Applet wird generell nur sekündlich neu gezeichnet, was dazu führen kann, dass beim hineinscrollen des Applet in den sichtbaren Bereich des Browserfensters das Applet für maximal eine Sekunde - teilweise oder ganz - nur grau dargestellt wird.

Dies zu ändern würde jedoch eine deutlich Mehrbelastung der Systemressourcen zur Folge haben.


Einbau in eine HTML-Seite

Um das Applet sinnvoll in einem Browserfenster anzeigen zu können, muss eine HTML-Seite gebastelt werden, die dieses Applet entsprechend aufruft. Im BODY-Teil dieser HTML-Seite muss folgendes Code-Schnippsel enthalten sein:

	<applet code=CWUhr.class width=250 height=400>
      	</applet>

Die Angaben von Höhe und Breite muss natürlich den Maßen der Hintergrundgrafik und somit den tatsächlichen Abmessungen des Applets entsprechen.


Verweise