Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
gp Kapitel 14 Grafikprogrammierung mit dem AWT
  gp 14.1 Das abstrakte Window-Toolkit
    gp 14.1.1 Java Foundation Classes
  gp 14.2 Fenster unter grafischen Oberflächen
    gp 14.2.1 Fenster öffnen
    gp 14.2.2 Größe und Position des Fensters verändern
    gp 14.2.3 Fenster und Dialog-Dekoration
  gp 14.3 Das Toolkit
    gp 14.3.1 Einen Hinweis beepen
  gp 14.4 Grundlegendes zum Zeichnen
    gp 14.4.1 Die paint()-Methode
    gp 14.4.2 Auffordern zum Neuzeichnen mit repaint()
  gp 14.5 Punkte, Linien und Rechtecke aller Art
    gp 14.5.1 Linien
    gp 14.5.2 Rechtecke
  gp 14.6 Alles was rund ist
  gp 14.7 Polygone und Polylines
    gp 14.7.1 Die Polygon-Klasse
    gp 14.7.2 N-Ecke zeichnen
    gp 14.7.3 Vollschlanke Linien zeichnen
  gp 14.8 Zeichenketten schreiben
    gp 14.8.1 Einen neuen Zeichensatz bestimmen
    gp 14.8.2 Zeichensätze des Systems ermitteln
    gp 14.8.3 Die Klasse FontMetrics
    gp 14.8.4 True Type Fonts
  gp 14.9 Clipping-Operationen
  gp 14.10 Farben
    gp 14.10.1 Zufällige Farbblöcke zeichnen
    gp 14.10.2 Farbanteile zurückgeben
    gp 14.10.3 Vordefinierte Farben
    gp 14.10.4 Farben aus Hexadezimalzahlen erzeugen
    gp 14.10.5 Einen helleren oder dunkleren Farbton wählen
    gp 14.10.6 Farbmodelle HSB und RGB
    gp 14.10.7 Die Farben des Systems
  gp 14.11 Bilder anzeigen und Grafiken verwalten
    gp 14.11.1 Eine Grafik zeichnen
    gp 14.11.2 Grafiken zentrieren
    gp 14.11.3 Laden von Bildern mit dem MediaTracker beobachten
    gp 14.11.4 Kein Flackern durch Double-Buffering
    gp 14.11.5 Bilder skalieren
  gp 14.12 Programmicon setzen
    gp 14.12.1 VolatileImage
  gp 14.13 Grafiken speichern
    gp 14.13.1 Bilder im GIF-Format speichern
    gp 14.13.2 Gif speichern mit dem ACME-Paket
    gp 14.13.3 JPEG-Dateien mit dem Sun-Paket schreiben
    gp 14.13.4 Java Image Management Interface (JIMI)
  gp 14.14 Von Produzenten, Konsumenten und Beobachtern
    gp 14.14.1 Producer und Consumer für Bilder
    gp 14.14.2 Beispiel für die Übermittlung von Daten
    gp 14.14.3 Bilder selbst erstellen
    gp 14.14.4 Die Bildinformationen wieder auslesen
  gp 14.15 Filter
    gp 14.15.1 Grundlegende Eigenschaft von Filtern
    gp 14.15.2 Konkrete Filterklassen
    gp 14.15.3 Mit CropImageFilter Teile ausschneiden
    gp 14.15.4 Transparenz
  gp 14.16 Alles wird bunt mit Farbmodellen
    gp 14.16.1 Die abstrakte Klasse ColorModel
    gp 14.16.2 Farbwerte im Pixel mit der Klasse DirectColorModel
    gp 14.16.3 Die Klasse IndexColorModel
  gp 14.17 Drucken
    gp 14.17.1 Drucken mit dem einfachen Ansatz
    gp 14.17.2 Ein PrintJob
    gp 14.17.3 Drucken der Inhalte
    gp 14.17.4 Komponenten drucken
    gp 14.17.5 Den Drucker am Parallelport ansprechen
  gp 14.18 Java 2D API
    gp 14.18.1 Grafische Objekte zeichnen
    gp 14.18.2 Geometrische Objekte durch Shape gekennzeichnet
    gp 14.18.3 Eigenschaften geometrischer Objekte
    gp 14.18.4 Transformationen mit einem AffineTransform-Objekt
  gp 14.19 Graphic Layers Framework
  gp 14.20 Grafikverarbeitung ohne grafische Oberfläche
    gp 14.20.1 Xvfb-Server
    gp 14.20.2 Pure Java AWT Toolkit (PJA)

Kapitel 14 Grafikprogrammierung mit dem AWT

Die meiste Gefahr geht nicht von den Erfahrungen aus,
die man machen muss,
sondern von denen, die man nicht machen darf.
– Hellmut Walters


Galileo Computing

14.1 Das abstrakte Window-Toolkit  downtop

Eine Programmiersprache, die zum Ziel hat, plattformunabhängige Softwareentwicklung zu unterstützen, muss auch eine Bibliothek anbieten, damit sich grafische Oberflächen gestalten lassen. Die Bibliothek muss im Wesentlichen drei Dinge abdecken:

gp  Sie muss grafische Primitivoperationen wie Linien und Polygone zeichnen, Farben und Zeichensätze zuweisen können
gp  Sie muss grafische Interaktionskomponenten, die so genannten Widgets (Window Elements), wie Fenster, Schaltflächen, Textfelder, Menüs und Container unterstützen
gp  Ein Modell zur Behandlung der Ereignisse definieren

Da Java-Programme portabel sein müssen, entsteht schnell das Problem, dass eine tolle Komponente von einer bestimmten Plattform nicht unterstützt wird und ein anderes Feature wiederum von einer anderen nicht. Die Bibliothek kann daher nur das aufnehmen, was auf jeden Fall von jeder grafischen Oberfläche unterstützt wird. Das ist leider nicht viel. Java definiert dazu das Abstract Window Toolkit (AWT), das konkrete Plattformen wie Windows, MacOS oder Unix implementiert. So wird jede Komponente in Java auf eine Komponente der Plattform abgebildet. Daher sehen portierte Anwendungen auf jedem Rechner so aus, wie der Rest der Programme. Da das AWT jedoch so einfach gehalten ist, dass eine professionelle Oberfläche nur mit Mühe zu erstellen ist, sind für die Abkürzung »AWT« noch ein paar hämische Deutungen im Umlauf: Awful Window Toolkit, Awkward Window Toolkit oder Annoying Window Toolkit.


Galileo Computing

14.1.1 Java Foundation Classes  downtop

Nach der Freigabe des ersten AWTs wurde klar, dass das Leistungsangebot keineswegs für die Zukunft reicht. Die Entwickler haben sich drangemacht und interessante Erweitungen geschaffen, die unter dem Namen Java Foundation Classes (JFC) in Java eingeführt wurden. Die Klassen bestehen im Wesentlichen aus:

gp  »Swing« GUI-Komponenten
Unter den Swing-Set-Komponenten fallen ganz neue grafische Elemente. Diese sind, anders als die plattformabhängigen Peer-Komponenten des herkömmlichen AWTs, komplett in Java implementiert. Ohne diese Abhängigkeiten kann beispielsweise das Aussehen (engl. look and feel, kurz LOF oder L&F) geändert werden. Der Name »Swing« war ein Projektname dieser Komponenten. Obwohl sie nun Teil der JFC sind, bleibt der Name bestehen.
gp  Pluggable Look and Feel
Dies gibt uns die Möglichkeit, das Aussehen der Komponenten zur Laufzeit, also ohne das Programm neu zu starten, zu ändern. Alle Komponenten des Swing-Sets besitzen diese Fähigkeit automatisch.
gp  Accessibility: Überstützung für Leute mit Behinderungen
Mit dieser API lassen sich mit neuen Interaktionstechniken auf die JFC und AWT Komponenten zugreifen. Zu diesen Techniken zählen unter anderem Lesegeräte für Blinde, Lupe für den Bildschirm und auch Spracherkennung.
gp  Java 2D API
Die 2D Funktionsbibliothek ist eine neue Technik, die über eine Objektbeschreibung – ähnlich wie Postscript – Objekte bildet und diese auf dem Bildschirm darstellt. Zu den Fähigkeiten der Bibliothek zählt, komplexe Objekte durch Pfade zu bilden, und darauf Bewegungs- und Verschiebeoperationen anzuwenden.
gp  Drag and Drop
Daten können mittels Drag and Drop leicht von einer Applikation zur anderen übertragen werden. Dabei profitieren Java-Programme auch davon, Daten von nicht Java-Programmen zu nutzen.

Galileo Computing

14.2 Fenster unter grafischen Oberflächen  downtop

Der Anfang aller Programme unter einer grafischen Benutzeroberfläche ist das Fenster (engl. Frame). Wir müssen uns daher erst mit den Fenstern beschäftigen, bevor wir auf den Inhalt näher eingehen können. Das Fenster dient auch als Grundlage von Dialogen, speziellen Fenstern, die entweder modal oder nicht modal arbeiten können. Ein modaler Dialog möchte erst bedient werden, bis es mit dem Gesamtsystem weitergehen kann.


Galileo Computing

14.2.1 Fenster öffnen  downtop

Damit wir unter Java ein Fenster öffnen können, müssen wir zunächst einmal das awt-Paket mit einbinden. Dann können wir eine Klasse Frame und deren Methoden nutzen. Das Listing ist sehr kurz.

Listing 14.1   HelloFrame.java
import java.awt.Frame;
public class HelloFrame
{
public static void main( String args[] )
{
Frame f =
new Frame( "Das Fenster zur Welt" );
f.setSize( 300, 200 );
f.setVisible( true );
}
}

Das Fenster kann nicht mit X in der Titelleiste geschlossen werden.

Mehr zur Klasse Frame

Neben dem Standardkonstruktor existiert ein weiterer, bei dem wir den Namen in der Titelleiste bestimmen können, wie im Beispiel geschehen.

class java.awt.Frame
extends Window implements MenuContainer

gp  Frame()
Erzeugt ein neues Frame-Objekt, welches am Anfang unsichtbar ist.
gp  Frame( String )
Erzeugt ein neues Frame-Objekt, mit einem Fenster-Titel, welches am Anfang unsichtbar ist.
gp  void setTitle( String )
Setzt den Titel des Fensters außerhalb des Konstruktors.
Abbildung 14.1   Das erste Fenster
Abbildung

Nach der Konstruktion ist das Fenster vorbereitet, aber noch nicht sichtbar. Es wird erst sichtbar, wenn wir die setVisible(true) aufrufen. Alternativ funktioniert auch show(). Da sich die Frame-Klasse direkt von Window ableitet – ein Frame ist ein Window mit Titelleiste –, besitzt Frame keine eigene setVisible()-Funktion.

class java.awt.Window
extends Container

gp  void setVisible( true )
Zeigt das Fenster an. Liegt es im Hintergrund, so wird es wieder in den Vordergrund geholt. Früher wurde die Methode show() verwendet.
gp  boolean isShowing()
true
, wenn sich das Fenster auf dem Bildschirm befindet.
gp  void toBack()
Das Fenster wird als Letztes in die Fensterreihenfolge eingereiht. Ein anderes Fenster wird somit sichtbar.
gp  void toFront()
Platziert das Fenster als Erstes in der Darstellung aller Fenster auf dem Schirm.

Hauptprogramm von Frame ableiten

Wir können unsere neue Klasse auch direkt von Frame ableiten. Dann ist es uns gestattet, auf die Funktionen der Klasse Frame direkt zuzugreifen, zum Beispiel auf setSize(). Im Hauptprogramm erzeugen wir über den Konstruktor dann das Fenster. Der Konstruktor ruft über die super()-Funktion den Konstruktor von Frame auf (da wir Frame ja einfach beerben). In den nachfolgenden Programmen werden wir immer diese Methode verwenden.

Listing 14.2   SinWin.java
import java.awt.Frame;

public class SinWin extends Frame
{
public SinWin( int x, int y )
{
super("Hallo");
setSize( x, y );
}

public static void main( String args[] )
{
SinWin win1 = new SinWin( 100, 200 );
win1.show();

SinWin win2 = new SinWin( 300, 300 );
win2.show();
}
}

Nachdem im Konstruktor das Fenster erzeugt wurde, ändern wir die Größe. Im Hauptprogramm erzeugen wir zwei Fenster win1 und win2, die beide Exemplare der eigenen Klasse sind. Die show()-Methode ist natürlich an ein Objekt gebunden.

Gern gemachter Fehler

Der eine oder andere wird vielleicht die Idee bekommen, folgendes Programmsegment auszuprobieren – auf den ersten Blick liegt es nahe:

public class SoNicht extends Frame
{
public static void main( String args[] )
{
super("Hallo");
setSize( 300, 300 );
show();
}
}

Dies ist nicht erlaubt, und der Fehler ergibt sich aus der Mischung von statischen Methoden und Objektmethoden. Das erste Problem liegt bei super(); es darf nur in Konstruktoren aufgerufen werden, aber nicht in ganz normalen Funktionen wie main(). (Wir haben sicherlich noch im Hinterkopf, dass super() nur in der ersten Zeile eines Konstruktors stehen darf.) Das nächste Problem sind die Funktionen setSize() und show() der Klasse. main() ist statisch, das heißt, alle aufgerufenen Funktionen müssen statisch sein oder sich auf erzeugte Objekte beziehen. setSize() bzw. show() sind aber keine statische Methoden der Klasse jawa.awt.Component, bzw. java.awt.Window, sondern werden dynamisch gebunden.


Galileo Computing

14.2.2 Größe und Position des Fensters verändern  downtop

Mit setSize() können wir die Ausmaße des Fensters verändern.

class java.awt.Component
implements ImageObserver, MenuContainer, Serializable

gp  void setSize( int Breite, int Höhe )
Verändert die Größe einer Komponente. Ehemals size().
Beispiel Die Position des Fensters kann mit setLocation() geändert werden. Wer das Fenster zum Beispiel in der Mitte des Bildschirms positionieren möchte, muss nur vorher die Größe des Bildschirms erfragen. Dies funktioniert mit getScreenSize(), einer Funktion des Toolkits.
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();

setLocation ( (d.width – getSize().width ) / 2,
(d.height- getSize().height) / 2 );

class java.awt.Component
implements ImageObserver, MenuContainer, Serializable

gp  void setLocation( int x, int y )
Setzt die Komponenten an die Position x, y. Ehemals move().
Beispiel Auch das Vergrößern eines Fensters, sodass es die maximale Ausdehnung annimmt, ist mit einer Funktion möglich. Betrachten wir die folgenden Zeilen, die hinter eine setVisible(true)-Methode zum Beispiel im Konstruktor gesetzt werden:
setLocation( 0, 0 );
resize( Toolkit.getDefaultToolkit().getScreenSize() );

Soll der Frame nicht in der Größe veränderbar sein, so setzen wir einfach

setResizable(false).
Frame frame = new Frame( "Du kriegst mich nicht klein." 
);
frame.
setResizable( false );


Galileo Computing

14.2.3 Fenster und Dialog-Dekoration  downtop

Für bestimmte Anwendungen ist es günstig, bei Fenstern und Dialogen die Standard-Dialogelemente auszuschalten, etwa dann, wenn der Benutzer das Fenster nicht verkleinern soll. Seit dem SDK 1.4 haben die Entwickler den Klassen Frame und Dialog eine Methode setUndecorated() geschenkt, mit der sich die Titelleiste, das Systemmenü und der Rahmen abschalten lassen. Da Swing auf den AWT-Komponenten basiert, gilt das auch für Swing-Fenster und Dialoge.

class java.awt.Frame 
extends Window implements MenuContainer
class java.awt.
Dialog extends Window

gp  void setUndecorated( boolean undecorated )

Setzt/Löscht die Dekoration.

gp  boolean isUndecorated()

Erfragt die Dekoration.


Galileo Computing

14.3 Das Toolkit  downtop

Die abstrakte Klasse Toolkit definiert system- und bildschirmabhängige Implementierungen. Wir haben dort zum Beispiel die Ausmaße des Bildschirms erfragt. Doch das Toolkit leistet noch mehr.


Galileo Computing

14.3.1 Einen Hinweis beepen  downtop

Ein Systemhinweis wird oft von einem Sound-Effekt unterstrichen, damit der Anwender die Meldung zusätzlich akustisch wahrnimmt. Zunächst ist denkbar, den ASCII-Code 7 auszugeben. Da die Konsole allerdings nicht alle ASCII-Zeichen verarbeiten muss und somit auf die 7 keine Antwort zu geben hat, ist diese Lösung nicht in allen Fällen von Erfolg gekrönt. Zudem mischt sich hier die Konsolenausgabe mit Operationen auf den Benutzungsschnittstellen; beide sollten entkoppelt werden. Was geschieht zum Beispiel, wenn die Ausgabe auf ein anderes Display umgeleitet wird?

Eine Methode des Toolkits hilft hier: beep(). Die Anwendung ist einfach.

Listing 14.3   ToolkitBeep.java
import java.awt.*;

public class ToolkitBeep
{
public static void main( String args[] )
{
Toolkit.getDefaultToolkit().beep();
System.exit(0);
}
}

Zunächst müssen wir mit getDefaultToolkit() das für die Umgebung aktuelle Toolkit besorgen – die Methode ist abstrakt und wird von einer gültigen Benutzeroberfläche implementiert.

abstract class java.awt.Toolkit

gp  static Toolkit getDefaultToolkit()
Liefert das aktuelle Toolkit zurück.
gp  abstract void beep()
Gibt ein Beep aus.

Eine ganz andere Möglichkeit ist es, Sound-Dateien abzuspielen. So lassen sich die verschiedensten Beeps zusammenstellen.


Galileo Computing

14.4 Grundlegendes zum Zeichnen  downtop

Nachdem wir ein Fenster öffnen können, wollen wir etwas in den Fensterinhalt schreiben. In den nächsten Abschnitten beschäftigen wir uns intensiver mit den Zeichenmöglichkeiten.


Galileo Computing

14.4.1 Die paint()-Methode  downtop

Als einleitendes Beispiel soll uns genügen, einen Text zu platzieren. Dazu implementieren wir die Funktion paint() der Frame-Klasse. Die Component-Klasse definiert update() abstrakt. Indem wir sie implementieren, wird der gewünschte Inhalt immer dann gezeichnet, wenn das Fenster neu aufgebaut wird, oder wir von außen repaint() oder update() aufrufen.

Listing 14.4   Biene.java
import java.awt.*;
import java.awt.event.*;

public class Biene extends Frame
{
public Biene() {
{ setSize( 500, 100 );

addWindowListener( new WindowAdapter() {
public void windowClosing ( WindowEvent e) {
System.exit(0); }
});

}

public void paint( Graphics g )
{
g.drawString( "\"Maja, wo bist du?\" (Mittermeier)", 100, 60 );
}

public static void main( String args[] )
{
new Biene().show();
}
}
Abbildung 14.2   Ein Fenster mit gezeichnetem Inhalt
Abbildung

Ein spezieller Wert wird in der paint()-Methode übergeben – der Grafikkontext, ein Objekt vom Typ Graphics. Graphic besitzt verschiedene Methoden zum Zeichnen (zum Beispiel Linie, Kreis, Oval, Rechteck, String, Grafik). Über paint() kommen wir an dieses Graphics und können auf diese Weise auf der grafischen Oberfläche zeichnen. Dies funktioniert auch dann, wenn die Zeichenfläche nicht direkt sichtbar ist.

Bei jeder Zeichenoperation muss der Grafikkontext angeben werden, denn dieses Objekt führt Buch über mehrere Dinge:

gp  Die Komponente, auf der zu zeichnen ist (hier erst einmal das rohe Fenster).
gp  Koordinaten des Bildbereichs und des Clipping-Bereichs. Die Zeichenoperationen außerhalb des Clipping-Bereichs werden nicht angezeigt. Daher wird ein Clipping-Bereich auch Beschnitt-Bereich genannt.
gp  Der aktuelle Clip-Bereich und Font, die aktuelle Farbe.
gp  Die Pixeloperation (XOR oder Paint).
gp  Die Funktion, mit der die Farbe verknüpft wird.

Wir können nur in der paint()-Methode auf das Graphics-Objekt zugreifen. Diese wiederum wird immer dann aufgerufen, wenn die Komponente neu gezeichnet werden muss. Dies nutzen wir dafür, um einen Text zu schreiben.

Leicht ist zu entnehmen, dass drawString(String text, int x, int y) einen Text in den Zeichenbereich des Grafikkontexts schreibt. Im Folgenden werden wir noch weitere Funktionen kennen lernen.


Galileo Computing

14.4.2 Auffordern zum Neuzeichnen mit repaint()  downtop

Die Methode repaint() kann von außen aufgerufen werden, um ein Neuzeichnen zu erzwingen.

Beispiel Bei einer Animation wird zu bestimmen Zeiten der Bildschirminhalt neu aufgebaut.

Das Neuzeichen wird mit repaint() initiiert, anschließend kann der Programmcode in der update()- beziehungsweise paint()-Methode den Bildschirm neu füllen. Damit die Animation flüssig wirkt, kann ein endlos laufender Thread zu festen Zeiten immer das repaint() auslösen. Das sieht dann wie folgt aus, wenn run() zu einem Thread gehört:

public void run() {
while (true) {
try {
component.repaint();
sleep( time );
} catch ( Exception e ) {}
}
}

time ist eine Konstante, die die Zeit festsetzt, in der die Komponente component einen Aufruf zum Neuzeichnen bekommt. Da wir bisher ohne Komponenten gearbeitet haben, reicht ein repaint() auf dem Frame oder Applet.



Galileo Computing

14.5 Punkte, Linien und Rechtecke aller Art  downtop

Die grafischen Objekte werden in einem Koordinaten-System platziert, welches seine Ursprungskoordinaten – also (0,0) – links oben definiert. Die Angabe ist absolut zum Fensterrahmen. Wählen wir die Koordinate auf der y-Achse klein, so kann es vorkommen, das wir nichts mehr sehen, denn das Objekt wandert in die Bildschirmleiste.

Gelegentlich mischt sich die Umgangssprache mit der Sprache der Mathematik und Computergrafik, sodass wir noch einmal die wichtigsten Begriffe aufzählen:

Punkte

Ein Punkt ist in Abhängigkeit der Dimensionen durch zwei oder mehrere Koordinaten gekennzeichnet. Da er, so kennen wir ihn aus der Mathematik, keine Ausdehnung hat, dürfen wir ihn eigentlich gar nicht sehen. In Java gibt es keine Funktion, mit der Punkte gezeichnet werden. Diese können nur durch einen Linienbefehl erzeugt werden.

Pixel

Das Wort Pixel ist eine Abkürzung für »Picture Element«. Ein Pixel beschreibt einen physikalischen Punkt auf dem Bildschirm und ist daher nicht zu verwechseln mit einem Punkt (obwohl umgangssprachlich keine feste Trennung existiert). Pixel besitzen, wie Punkte, Koordinaten. Wird ein grafisches Objekt gezeichnet, so werden die entsprechenden Punkte auf dem Bildschirm gesetzt. Die Anzahl der Pixel auf dem Monitor ist beschränkt, unter einer Auflösung von 1024 * 768 »Punkten« sind dies also 786.432 Pixel, die einzeln zu setzen sind. Einen Pixel zu setzen, heißt aber nichts anderes, als ihm eine andere Farbe zu geben.


Galileo Computing

14.5.1 Linien  downtop

Auch bei Linien müssen wir uns von der Vorstellung trennen, die uns die analytische Geometrie vermittelt. Denn dort ist eine Linie als kürzeste Verbindung zwischen zwei Punkten definiert – so sagt es Euklid. Da sie eindimensional sind, besitzen sie eine Länge aus unendlich vielen Punkten, aber keine Breite. Auf dem Bildschirm besteht eine Linie nur aus endlich vielen Punkten, und wenn eine Linie gezeichnet wird, dann werden Pixel gesetzt, die nahe an der wirklichen Linie sind. Die Punkte müssen passend in ein Raster gesetzt werden und so passiert es, dass die Linie in Stücke zerbrochen wird. Dieses Problem gibt es bei allen grafischen Operationen, da von Fließkommawerten eine Abbildung auf Ganzzahlen, in unserem Fall, absolute Koordinaten des Bildschirms, gemacht werden müssen. Eine bessere Darstellung der Linien und Kurven ist durch antialiasing zu erreichen. Dies ist eine Art Weichzeichnung mit nicht nur einer Farbe, sondern mit Abstufungen, sodass die Qualität auf dem Bildschirm wesentlich besser ist. Auch bei Zeichensätzen ist dadurch eine gute Verbesserung der Lesbarkeit auf dem Bildschirm zu erzielen. Die Fähigkeit zum Weichzeichnen fehlt jedoch dem alten AWT und ist erst bei der neuen JFC 2D-API implementiert.

abstract class java.awt.Graphics

gp  void drawLine( int x1, int y1, int x2, int y2 )
Zeichnet eine Linie zwischen den Koordinaten (x1,y1) und (x2,y2) in der Vordergrundfarbe.
Beispiel Setze einen Punkt an die Stelle (x,y).
g.drawLine( x, y, x, y );


Galileo Computing

14.5.2 Rechtecke  downtop

Im Folgenden wollen wir nur die paint()-Methode mit etwas Leben füllen. Zunächst ein Blick auf die Funktionen, die uns Rechtecke zeichnen lässt. Die Rückgabewerte sind immer void. Es ist nicht so, dass die Funktionen mitteilen, ob ein tatsächlicher Zeichenbereich gefüllt werden konnte. Liegen die Koordinaten und das zu zeichnende Objekt nicht im Sichtfenster, so passiert einfach gar nichts. Liegen die Koordinaten des zu zeichnenden Objekts nicht im Sichtbereich des Fensters, so passiert nichts. Die Zeichenfunktion ist nicht in der Lage, dies in irgendeiner Form dem Aufrufer mitzuteilen.

abstract class java.awt.Graphics

gp  drawRect( int x, int y, int width, int height )
Zeichnet ein Rechteck in der Vordergrundfarbe. Das Rechteck ist (width + 1) Pixel breit und (height + 1) Pixel hoch.
gp  fillRect( int x, int y, int width, int height )
Zeichnet ein gefülltes Rechteck in der Vordergrundfarbe. Das Rechteck ist (width + 1) Pixel breit und (height + 1) Pixel hoch.
gp  drawRoundRect( int x, y, int width, height, int arcWidth, arcHeight )
Zeichnet ein abgerundetes Rechteck in der Vordergrundfarbe. Das Rechteck ist (width + 1) Pixel breit und (height + 1) Pixel hoch. arcWidth gibt den horizontalen und arcHeight den vertikalen Durchmesser der Kreisbögen der Ränder an.
gp  fillRoundRect( int x, y, int width, height, int arcWidth, arcHeight )
Wie drawRoundRect(), nur gefüllt.
gp  draw3DRect( int x, int y, int width, int height, boolean raised)
Zeichnet ein dreidimensional angedeutetes Rechteck in der Vordergrundfarbe. Der Parameter raised gibt an, ob das Rechteck erhöht oder vertieft wirken soll. Die Farben für den Effekt werden aus der Vordergrundfarben gewonnen.
gp  fill3DRect( int x, int y, int width, int height, boolean raised)
Wie draw3Drect(), nur gefüllt.

Eine konkrete Methodenimplementierung

Die Implementierung einiger Routinen können wir uns im Paket java.awt.Graphics anschauen. So finden wir dort beispielsweise drawRect():

public void drawRect(int x, int 
y, int width, int height)
{
if ((width < 0) || (height < 0)) {
return;
}

if (height == 0 || width == 0) {
drawLine(x, y, x + width, y + height);
} else {
drawLine(x, y, x + width – 1, y);
drawLine(x + width, y, x + width, y + height – 1);
drawLine(x + width, y + height, x + 1, y + height);
drawLine(x, y + height, x, y + 1);
}
}

Neben den anderen beiden Funktionen draw3DRect() und fill3DRect() sind dies aber die einzigen ausprogrammierten Routinen. Die restlichen Methoden werden von der konkreten Graphics-Klasse der darunter liegenden Plattform implementiert.


Galileo Computing

14.6 Alles was rund ist  downtop

Die Graphics-Klasse stellt vier Methoden zum Zeichnen von Ovalen und Kreisbögen bereit. Gefüllte und nicht gefüllte Ellipsen sind immer in einem Rechteck eingepasst.

abstract class java.awt.Graphics

gp  drawOval( int x, int y, int width, int height )
Zeichnet ein Oval in der Vordergrundfarbe, welches die Ausmaße eines Rechtecks hat. Das Oval hat eine Größe von (width + 1) Pixeln in der Breite und (height + 1) Pixel in der Höhe.
gp  fillOval( int x, int y, int width, int height )
Wie drawOval(), nur gefüllt.
gp  void drawArc( int x, int y, int width, int height, int startAngle, int arcAngle )
Zeichnet einen Kreisbogen. Null Grad liegt in der 3 Uhr Position. Bei einem Aufruf mit den Winkel-Parametern 0, 270 wird ein Kreisbogen gezeichnet, bei dem 90 Grad im unteren rechten Bereich nicht gezeichnet sind.
gp  void fillArc( int x, int y, int width, int height, int startAngle, int arcAngle )
Wie drawArc(), nur gefüllt.

Eine Kreis- und Ellipsen-Klasse

Bei der Methode drawOval() müssen wir immer daran denken, dass die Ellipse oder im Spezialfall der Kreis, in ein Rechteck mit Startkoordinaten und mit Breite und Höhe gezeichnet werden. Dies ist nicht immer die natürliche Vorstellung einer Ellipse bzw. eines Kreises. Daher packen wir das ganze in eine Klasse Ellipse und geben ihr eine draw()-Methode.

Listing 14.5   Ellipse.java
import java.awt.*;

public class Ellipse
{
public Ellipse( int x, int y, int r )
{
this.x = x; this.y = y; this.rx = r; this.ry = r;

// oder OOP-schöner this( x, y, r, r );
}

public Ellipse( int x, int y, int rx, int ry )
{
this.x = x; this.y = y; this.rx = rx; this.ry = ry;
}
public void draw( Graphics g )
{
g.
drawOval( x-rx, y-ry, rx+rx, ry+ry );
}

private int x, y, rx, ry;
}

Galileo Computing

14.7 Polygone und Polylines  downtop

Eine Polyline besteht aus einer Menge von Linen, die einen Linienzug beschreiben. Dieser Linienzug muss nicht geschlossen sein. Ist er allerdings geschlossen, so sprechen wir von einem Polygon. In Java gibt es verschiedene Möglichkeiten, Polygone und Polylines zu zeichnen. Zunächst einmal über ein Koordinatenfeld.

abstract class java.awt.Graphics

gp  void drawPolyline( int xPoints[], int yPoints[], int nPoints )
Zeichnet einen Linenzug durch die gegebenen Koordinaten in der Vordergrundfarbe. Die Figur ist nicht automatisch geschlossen, wenn nicht die Start- und Endkoordinaten gleich sind. Mit nPoint kontrollieren wir die Anzahl der gezeichneten Linien.
gp  void drawPolygon( int xPoints[], int yPoints[], int nPoints )
Zeichnet wie drawPolyline() einen Linienzug, schließt diesen aber immer gleich, indem die erste Koordinate mit der Koordinate nPoints verbunden wird.
gp  void fillPolygon( int xPoints[], int yPoints[], int nPoints )
Füllt das Polygon aus. Da eine Polyline offen ist, kann sich nicht gefüllt werden. Somit gibt es die Funktion fillPolyline() nicht.

Galileo Computing

14.7.1 Die Polygon-Klasse  downtop

Neben der Möglichkeit, die Linenzüge durch Koordinatenfelder zu beschreiben, gibt es in Java die Polygon-Klasse Polygon. Sie ist eine Erweiterung des Interfaces Shape. Sie ist aber minimal, lediglich die Methode getBounds() wird implementiert. Ein Polygon-Objekt verwaltet eigenständig seine Koordinaten und von außen können wir Elemente hinzunehmen. Mit der mächtigen Methode contains() können wir herausfinden, ob ein Punkt in der von dem Polygon ausgezeichneten Fläche liegt. Doch zunächst müssen wir ein Polygon-Objekt erzeugen. Dazu dienen zwei Konstruktoren:

class java.awt.Polygon
implements Shape, Serializable

gp  Polygon()
Erzeugt ein Polygon-Objekt ohne Koordinaten.
gp  Polygon( int xpoints[], int ypoints[], int npoints )
Erzeugt ein Polygon mit den angegebenen Koordinaten.

Nun können wir Punkte hinzufügen und Anfragen an das Polygon-Objekt stellen:

gp  Rectangle getBounds()
Gibt die Bounding-Box der Figur zurück. Diese beschreibt ein Rechteck, das das Objekt gerade umschließt. Ein Rectangle-Objekt besitzt die Variablen height (Höhe des Rechtecks), width (Breite des Rechtecks), x (x-Koordinate) und y (y-Koordinate des Rechtecks). Mit verschiedenen Funktionen lassen sich Rechtecke zusammenfassen und schneiden.
gp  void addPoint( int x, int y )
Die Koordinate (x,y) wird hinzugefügt. Die Grenzen (engl. Boundings) werden automatisch aktualisiert.
gp  boolean contains( int x, int y )
Liefert true, wenn der Punkt (x,y) im Polygon liegt. Es wird ein Gerade-/Ungerade-Algorithmus verwendet, um dies herauszufinden.
gp  boolean contains( Point p )
Liefert true, wenn der Punkt p im Polygon liegt. Ein Point-Objekt besitzt die Attribute x und y für die Koordinaten.
Abbildung

Das erzeugte Polygon können wir mit speziellen Methoden, natürlich aus Graphics, zeichnen.

abstract class java.awt.Graphics

gp  void drawPolygon( Polygon )
Zeichnet das Polygon in der Vordergrundfarbe.
gp  void fillPolygon( Polygon )
Zeichnet ein gefülltes Polygon.
Hinweis Die contains()-Funktion beim Polygon arbeitet korrekt für Punkte innerhalb der eingeschlossenen Fläche. Bei Abfrage von Punkten, die den Eckpunkten entsprechen, kommen immer sehr willkürliche Werte heraus, genauso bei der Anfrage, ob die Punkte auf der Linie zum Innenraum gehören oder nicht.


Galileo Computing

14.7.2 N-Ecke zeichnen  downtop

Bisher gibt es im Graphics-Paket keine Funktion, um regelmäßige n-Ecke zu zeichnen. So eine Funktion ist aber leicht und schnell programmiert. Wir teilen dazu einfach einen Kreis in n Teile auf, und berechnen die x- und y-Koordinate der Punkte auf dem Kreis. Diese Punkte fügen wir einem Polygon-Objekt mittels der addPoint()-Methode hinzu. Eine private Funktion drawNeck() übernimmt diese Polygon-Erstellung. Der letzte Parameter der Funktion ist ein Wahrheitswert, der bestimmt, ob das n-Eck gefüllt werden soll oder nicht. Nun kann mit zwei öffentlichen Funktionen ein nicht gefülltes bzw. gefülltes n-Eck gezeichnet werden.

Abbildung

Listing 14.6   nEck.java
import java.awt.*;
import java.awt.event.*;

public class nEck extends Frame
{
public nEck() {
setSize( 200, 200 );
addWindowListener( new WindowAdapter() {
public void windowClosing ( WindowEvent e ) {
System.exit(0); }
});
}

private void drawNeck( Graphics g, int x, int y, int r,
int n, boolean filled )
{

Polygon p = new Polygon();

for ( int i = 0; i < n; i++ )
p.addPoint( (int) ( x + r*Math.cos( i*2*Math.PI/n ) ),
(int) ( y + r*Math.sin( i*2*Math.PI/n ) ) );

if ( filled == true )
g.fillPolygon( p );
else
g.drawPolygon( p );
}

/**
* Draws a n-Eck polygon with the given parameter
*/

public void drawNeck( Graphics g, int x, int y, int r, int n )
{
drawNeck( g, x, y, r, n, false );
}

/**
* Draws a filled n-Eck polygon with the given parameter
*/

public void fillNeck( Graphics g, int x, int y, int r, int n )
{
drawNeck( g, x, y, r, n, true );
}

public void paint( Graphics g )
{
fillNeck( g, 100, 100, 50, 6 );

drawNeck( g, 100, 100, 60, 6 );
}

public static void main( String args[] )
{
nEck poly = new nEck();
poly.show();
}
}

Galileo Computing

14.7.3 Vollschlanke Linien zeichnen  downtop

In Zeichenprogrammen und grafischen Präsentationen besteht häufig die Notwendigkeit, die sonst nur dünnen Standardlinien etwas aufzupusten. Es sind also dickere Linien erwünscht und dies führt zu vielfältigen Problemen, die spontan nicht ersichtlich sind. Zunächst die Frage nach der Zeichentechnik. Die erste Möglichkeit ist, mehrere Linien übereinander zu zeichnen. Dieser Ansatz ist auf den ersten Blick der einfachste, doch ein zweiter Blick auf die Grafik zeigt, dass einige Löcher entstehen; die Linien sind nicht genau übereinander. Dies liegt an den Rechenfehlern der Linienfunktion. Diese Lösung scheidet somit aus, und wir entscheiden und für einen Linienzug, der gefüllt wird. Dies ist der einzige Ausweg. Nur, diese Lösung ist nicht besonders schnell. Denn erst muss der Linienzug gezeichnet werden und anschließend folgt eine kostspielige Füllfunktion. Doch Probleme mit Löchern gibt es nicht. Etwaige Schwierigkeiten, wie etwa ein 2 Pixel hoher Polygonzug, in dem eigentlich kein Platz mehr ist, muss auf die Leistungsfähigkeit der Füll-Methode verlagert werden.

Das zweite Problem betrifft das Ende der Linien. Sollen diese abgerundet, spitz wie ein Pfeil oder wie eine Rampe aussehen? Oder soll die Linie, die dann entsteht, einfach wie ein gedrehtes Rechteck aussehen? Ein Blick in die Grafikbibliotheken von Windows oder X11 zeigt, dass hier viele Arten existieren. Unsere folgende Funktion ist aber sehr einfach gebaut. Sie rundet nicht ab, sondern zeichnet das gedrehte Rechteck. Eine dritte Unsicherheit liegt in der Definition der Endpunkte. Ist eine Linie 10 Pixel breit, so muss sichergestellt werden, wo denn der Startpunkt liegt. Liegt er in der Mitte oder, wenn etwa die Ränder mit einer Spitze gezeichnet sind, an diesen Punkten. Da unsere Methode sehr einfach ist, kümmern wir uns nicht darum und lassen die Endpunkte mittig liegen.

public static void
drawThickLine( int x, int y, int x2, int y2, int thickness, Graphics g )
{
int b = Math.round( thickness /2), deltax, deltay;

double angle;

//if (y2==y) alpha = 0; else
angle = Math.atan( (double)((y2-y)/(x2-x)) );

deltay = (int)Math.round( (Math.cos(angle)*b) );
deltax = (int)Math.round( (Math.sin(angle)*b) );

Polygon p = new Polygon();

p.addPoint( x-deltax, y+deltay );
p.addPoint( x+deltax, y-deltay );
p.addPoint( x2+deltax, y2-deltay );
p.addPoint( x2-deltax, y2+deltay );

g.fillPolygon( p );
}

Aus der Beschreibung am Anfang geht hervor, dass das Zeichnen von dicken Linien mit den gewünschten Zusätzen wie Ränder keine triviale Aufgabe ist. Schön ist, dass sich unter der Java 2 Plattform die Java 2D API um diese Aufgabe kümmert.


Galileo Computing

14.8 Zeichenketten schreiben  downtop

Die Methode, mit der Zeichen in verschiedenen Zeichensätzen (engl. Fonts) auf die Zeichenfläche gebracht werden, heißt drawString(). Diese Funktion besitzt drei Parameter: Zu schreibende Zeichenkette, x-Koordinate und y-Koordinate. drawString() zeichnet im aktuell eingestellten Zeichensatz und die Grundlinie (engl. Baseline) befindet sich auf der übergebenden y-Position.

abstract class java.awt.Graphics

gp  void drawString( String, int x, int y )
Schreibt einen String in der aktuellen Farbe und dem aktuellen Zeichensatz. Die x- und y-Werte bestimmen die Startpunkte der Grundlinie.
gp  void drawChars(char data[], int offset, int length, int x, int y )
Schreibt die Zeichenkette und bezieht die Daten aus einem Char-Feld.
gp  void drawBytes(byte data[], int offset, int length, int x, int y )
Schreibt die Zeichenkette und bezieht die Daten aus einem Byte-Feld.

Galileo Computing

14.8.1 Einen neuen Zeichensatz bestimmen  downtop

Die Funktion drawString() verwendet immer den aktuellen Zeichensatz. Um diesen zu ändern, benutzen wir die Funktion setFont(). Der Übergabeparameter ist ein Font-Objekt, welches wir erst erzeugen müssen. Der Konstruktor von Font ist durch verschiedene Parameter definiert:

class java.awt.Font
implements Serializable

gp  Font( String Name, int Stil, int Größe )
Erzeugt ein Font-Objekt.
gp  Name
Die Namen des Zeichensatzes können von System zu System unterschiedlich sein. Unter WinNT, MacOs, Linux, Solaris und IRIX sind jedenfalls die Zeichensätze Monospaced (früher Courier), Dialog, SansSerif (früher Helvetica) und Serif (früher TimesRoman) erlaubt, unter MacOS kommt noch der Zeichensatz Geneva hinzu. Vor Java 1.1 gab es noch den Zeichensatz Symbol (bzw. ZapfDingbats), der aber durch die Unicode-Zeichen abgedeckt wird.
gp  Stil
Das Font-Objekt definiert drei Konstanten, um die Schriftart fett und kursiv darzustellen. Die symbolischen Werte sind: Font.ITALIC, Font.BOLD und für einen nicht ausgezeichneten Schriftsatz Font.PLAIN. Die Attribute können mit dem binären Oder oder dem arithmetischem Plus verbunden werden, ein fetter und kursiver Zeichensatz ist so durch Font.BOLD|Font.ITALIC (bzw. durch Font.BOLD+Font.ITALIC) zu erreichen.
gp  Größe
Eine Angabe in Punkten, wie groß die Schrift sein soll. Ein Punkt entspricht etwa 1/72 Zoll (etwa 0,376 mm).

Siehe hierzu die nächste Abbildung auf Seite 663.

Beispiel Font-Objekt erzeugen.
new Font( "Serif", Font.PLAIN, 
14 )
Häufig wird dieser sofort in setFont() genutzt, so wie
setFont( new Font( "Serif", Font.BOLD, 
20 ) );

Abbildung


Galileo Computing

14.8.2 Zeichensätze des Systems ermitteln  downtop

Die Umsetzung der Namen auf den verschiedenen Rechnerplattformen übernimmt Java, so heißt »Helvetica« unter Windows »Arial« (aber mit denselben Laufweiten). Der Grund dafür liegt bei den Herstellern der Zeichensätze. Denn diese sind nicht frei und der Name »Helvetica« ist von Adobe geschützt. Doch auch unter X11 heißt Helvetica nicht Helvetica. Da die verschiedenen Zeichensatz-Hersteller den Namen »Helvetica« aber kaufen können, ist der Original-Zeichensatz unter X11 Adobe-Helvetica. Die Firma Adobe war so gnädig und hat die Zeichensätze als Type-1 Schriftarten beigelegt. Type-1 Schriftarten sind unter X11 relativ neu, denn erst als von IBM der Server-Zusatz programmiert wurde, konnten Type-1 Schriften benutzt werden. Vorher wurden die Anwender immer mit kleinen Klötzen abgefertigt, wenn die Schriftgröße einmal zu hoch gewählt wurde. Leider ist dies bei einigen Zeichensätzen immer noch der Fall. Selbst Star-Office unter X11 hat damit zu kämpfen. Und wir auch, falls wir einen Zeichensatz verlangen, der nur als Bitmap in den Standardgrößen gerastert ist.

Um herauszufinden, welche Zeichensätze auf einem System installiert sind, kann die getFontList()-Methode der Klasse Toolkit bemüht werden.

abstract class java.awt.Toolkit

gp  String[] getFontList()
Gibt die Namen der verfügbaren Zeichensätze zurück.
gp  FontMetrics getFontMetrics( Font )
Gibt die Font-Metriken des Bildschirm-Zeichensatzes zurück.

Folgendes Codesegment zeigt die Implementierung einer Schleife, die alle Zeichensatznamen ausgibt. Wir müssen kein Fenster geöffnet haben, um die Zeichensätze abzurufen.

Listing 14.7   ListFont.java
import java.awt.*;

class ListFont
{
public static void main( String args[] )
{
// herkömmlich

String fonts[] =
Toolkit.getDefaultToolkit().getFontList();

for ( int i = 0; i < fonts.length; i++ )
System.out.println( fonts[i] );

System.out.println();

// Seit 1.2

String all_fonts[] =
GraphicsEnvironment.getLocalGraphicsEnvironment().
getAvailableFontFamilyNames();

for ( int i = 0; i < all_fonts.length; i++ )
System.out.println( all_fonts[i] );

}
}

Ein neuer Weg

Neben der getFontList()-Methode des Toolkit ist seit Java 1.2 eine weitere Methode hinzugekommen: getAvailableFontFamilyNames(). Sie ist auf einem GraphicsEnvironment definiert und dies ist die Ausprägung einer grafischen Oberfläche oder eines Druckers. Da jedes Font-Objekt die toString()-Methode passend implementiert, sehen wir den Namen der Zeichensätze. So folgt nach dem Aufruf des Programms (jedenfalls bei mir) die Ausgabe für den ersten Teil:

Dialog
SansSerif
Serif
Monospace
Helvetica
TimesRoman
Courier
DialogInput
ZapfDingbats

Die Funktion getToolkit() gehört zur Klasse Frame – sie erbt die Methode von Component –, sodass wir nicht zwingend die Funktion Toolkit.getDefaultToolkit() von der statischen Klasse Toolkit verwenden müssen.

String fonts[] = getToolkit().getFontList();
abstract class java.awt.Component
implements ImageObserver, MenuContainer, Serializable

gp  Toolkit getToolkit()
Gibt das Toolkit des Fensters zurück.
abstract class java.awt.GraphicsEnvironment

gp  abstract Font[] getAllFonts()
Liefert ein Feld mit allen verfügbaren Font-Objekten im aktuellen GraphicsEnvironment, in einer Größe von einem Punkt.
gp  abstract String[] getAvailableFontFamilyNames()
Liefert ein Feld mit allen verfügbaren Zeichensatzfamilien im aktuellen GraphicsEnvironment.
gp  abstract String[] getAvailableFontFamilyNames( Locale l )
Liefert ein Feld mit allen verfügbaren Zeichensatzfamilien im aktuellen GraphicsEnvironment, die zu einer Sprache l gehören.

Der aktuell verwendete Zeichensatz

Ist im Programm lediglich der aktuell verwendete Zeichensatz nötig, können wir getFont() von der Graphics-Klasse verwenden.

abstract class java.awt.Graphics

gp  Font getFont()
Liefert den aktuellen Zeichensatz.

Galileo Computing

14.8.3 Die Klasse FontMetrics  downtop

Jedes Font-Objekt beinhaltet lediglich Information über Schriftsatzfamilie, Schriftsatznamen, Größe und Stil. Sie bietet keinen Zugriff auf die Abmessungen des Zeichensatzes. Um diese Daten aufzuspüren, erzeugen wir ein FontMetrics-Objekt. Es verwaltet metrische Informationen, die mit einer Schriftart verbunden sind. Dazu gehören Ober- und Unterlänge, Schrifthöhe und Zeilenabstand.

Um das FontMetric-Objekt des aktuellen Grafikkontexts zu nutzen, findet sich eine Methode getFont(). Diese Methode gehört zur Graphics-Klasse und ist nicht mit der getFont()-Methode aus FontMetrics zu verwechseln. Diese verhält sich zwar gleich, ist aber einer anderen Klasse zugeordnet.

In der paint()-Methode kann also mittels

FontMetrics fm = getFontMetrics( 
getFont() );

auf die Metriken des aktuellen Zeichensatzes zugegriffen werden.

abstract class java.awt.Graphics

gp  FontMetrics getFontMetrics()
Liefert die Font-Metriken zum aktuellen Zeichensatz.
gp  FontMetrics getFontMetrics( Font f )
Liefert die Font-Metriken für den Zeichensatz f.

Die Klasse FontMetrics bietet die folgenden Methoden an, wobei sich alle Angaben auf das jeweilige Zeichensatzobjekt beziehen. Beziehen sich die Rückgabeparameter auf die Zeichengröße, so ist die Angabe immer in Pixeln.

abstract class java.awt.FontMetrics
implements Serializable

gp  int bytesWidth( byte[], int, int )
int charsWidth( char[], int, int )
Gibt die Breite aller Zeichen des Felds zurück.
gp  int charWidth( int ), int charWidth( char )
Liefert die Breite zu einem Zeichen.
gp  int getAscent()
Gibt den Abstand von der Grundlinie zur oberen Grenze (Oberlänge) zurück.
gp  int getDescent()
Gibt den Abstand von der Grundlinie zur unteren Grenze (Unterlänge) zurück.
gp  int getFont()
Liefert aktuellen Zeichensatz.
gp  int getHeight()
Gibt die Schrifthöhe einer Textzeile in Pixel zurück. Sie berechnet sich aus Zeilendurchschuss + Oberlänge + Unterlänge.
gp  int getLeading()
Gibt Zwischenraum zweier Zeilen zurück.
gp  int getMaxAdvance()
Liefert die Breite des breitesten Zeichens.
Abbildung

gp  int getMaxAscent()
Liefert das Maximum aller Oberlängen in Pixeln. Einige Zeichen können sich oberhalb der Oberlänge bewegen.
gp  int getMaxDescent()
Liefert das Maximum aller Unterlängen in Pixeln. Vor Java 1.1: getMaxDecent(). Hier gilt gleiches wie bei getMaxAscent().
gp  int[] getWidths()
Liefert in einem Ganzzahlfeld die Breiten der Zeichen zurück. Das Feld ist 256 Elemente groß.
gp  int stringWidth( String )
Gibt die Breite der Zeichenkette zurück, wenn diese gezeichnet würde.

Einen String unterstreichen

Wir wollen nun stringWidth() benutzen, um unterstrichenen Text darzustellen. Dafür gibt es keine Standardfunktion. Also schreiben wir uns einfach eine Methode, die die Koordinaten sowie den String übergeben bekommt. Die Methode drawUnderlinedString() schreibt mit drawString() die Zeichenkette. drawLine() bekommt die Breite der Linie durch die Breite der Zeichenkette. Die Linie ist zwei Punkte unter der Baseline. Natürlich achtet so eine kleine Funktion nicht auf das Aussparen von Buchstaben, die unter der Baseline liegen. Die Buchstaben »y« oder »q« sind dann unten gnadenlos durchgestrichen.

drawUnderlinedString( Graphics 
g, int x, int y, String s )
{
g.drawString( s,10,10 );
g.drawLine( x , y+2 ,
x+getFontMetrics(getFont()).stringWidth(s) , y+2 );
}

Galileo Computing

14.8.4 True Type Fonts  downtop

Grafische Oberflächen stellen wie Drucker Zeichensätze selbstverständlich dar. Doch der Weg von der Datei bis zur Darstellung ist lang und führt unweigerlich über die Firma Adobe, die erstmalig die standardisierte Zeichendefinition PostScript öffentlich machte. Genauer gesagt, definiert PostScript noch etwas mehr, doch das soll uns hier nicht interessieren. Die erste kommerzielle Zeichensatzrevolution begann dann 1985, als der Drucker LaserWriter von Apple das Adobe-Format PostScript rastern konnte. Die Definition eines Zeichensatzes lag bis dahin nur in Bitmaps vor, doch die Postscript Zeichensätze wie auch die TrueType-Zeichensätze, um die es später gehen soll, lagen als Punktbeschreibung vor. Die Rasterung übersetzte diese Punkte in eine Bitmap, die dann entweder auf dem Bildschirm oder dem Drucker ausgegeben wurde. Durch die Punktbeschreibung waren also nicht mehr größenabhängige Beschreibungen vorhanden, sondern die Zeichen (auch Glyphs genannt) wurden durch Linien und Kurven in kubischen Bézier-Kurven beschrieben.

Die Visualisierung der Zeichensätze machte Microsoft und Apple Sorgen, da Adobe mehrere Defintionen der PostScript Zeichensätze pflegte, darunter Type 1 (PS-1) und Type 3 (PS-3). Type 1 nutzen so genannte Hints (Hinweis), um auch bei unterschiedlichen Größen und grafischen Oberflächen optimale Darstellungen zuzulassen. Diese Definition war jedoch geheim. Zeichensätze vom Typ 3 sahen zwar auf dem Papier gut aus, nicht aber auf dem Bildschirm mit niedriger Auflösung – hier fehlten die Informationen aus den Hints. Microsoft und Apple wollten nun ihre Zeichensatzausgabe nicht Adobe überlassen (die natürlich einen Type 1 Rasterer im Programm hatten), sondern sie definierten ihre eigene Font-Technologie, die nicht mehr auf Bézier-Kurven, sondern auf quadratischen B-Splines basierte. Apple machte dabei den Anfang mit Royal, welches später in TrueType (TT) umgetauft wurde. Dies war sechs Jahre nach den PostScript Fonts. Der einzige Hersteller, der dennoch bei PostScript Type 1 Zeichensätzen geblieben ist, ist IBM mit dem Betriebssystem OS/2. Daneben nutze auch NeXtStep diese Zeichensatzdefinitionen, doch das System hallte nicht lange nach.

Nachdem Apple den Anfang mit TT gemacht hatte und es 1991 in MacOS integrierte, übernahm auch Microsoft (die sich bis dahin an einem wenig lauffähigen PostScript-Clone TrueImage versuchten) die Technologie für Windows 3.1. Adobe erkannte früh die Konsequenz dieser Allianz und öffnete die Spezifikation für PostScript Type 1 Zeichensätze im März 1990. In der Mitte des Jahres lieferte Adobe zusätzlich den Adobe Type Manager (ATM) aus, der Type 1 (aber keine Type 3) PostScript Zeichensätze für den Bildschirm und für nicht PostScript fähige Drucker darstellte. Heutzutage existieren beide Definitionen immer noch parallel und für Drucker ist die Frage, welche nun besser ist, nicht zu beantworten. Moderne Drucker haben auch ein eigenes TrueType-Raster im ROM eingebaut. In Zukunft wird die Unterscheidung wohl auch unwichtiger werden, da Microsoft die »offene« OpenType Spezifikation (auch »TrueType Open Version 2« genannt) nach vorne bringt. Der Zeichensatz PS-1 oder TrueType wird dabei in einer OpenType Datei gekapselt und dem Rasterer übergeben und berechnet. Dabei übernimmt die PS-1 Rasterung Adobe, die eine Zusammenarbeit mit Microsoft unterstützt, und die TT Rasterung Microsoft. In Zukunft möchten Microsoft und Adobe Zeichensätze im OpenType unterstützen und nach vorne bringen.

TTF in Java nutzen

Die vordefinierten Standard-Zeichensätze (Dialog, DialogInput, Monospaced, Serif, SansSerif, Symbol) sind leider etwas wenig. Doch die Font-Klasse bietet die statische Methode createFont() an, die aus einem Eingabestrom eines TrueType-Zeichensatzes das entsprechende Font-Objekt erstellt.

Font f = Font.createFont( Font.TRUETYPE_FONT,
new FileInputStream("f.ttf") );

Der erste Parameter ist die fest vorgeschriebene Konstante Font.TRUETYPE_FONT, andere Parameter sind nicht definiert und führen zu einer IllegalArgumentException("font format not recognized"). Der zweite Parameter ist ein Eingabestrom aus der Binärdatei mit den Zeichensatzinformationen. Die Daten werden ausgelesen und zu einem Font-Objekt verarbeitet. Da die Daten intern über einen gepufferten Datenstrom in eine temporäre Datei geschrieben werden, ist eine eigene Pufferung über einen BufferedInputStream nur zusätzlicher Overhead.

Waren die Beschreibungsinformationen in der Datei ungültig, so erzeugt die Font-Klasse eine FontFormatException("Unable to create font – bad font data"). Dateifehler fallen hier nicht darunter und werden extra über eine IOException angezeigt. Der Datenstrom wird anschließend nicht wieder geschlossen.

An dieser Stelle verwundert es vielleicht, dass von der Arbeitsweise her die Methode createFont() mit der des Konstruktors ähnlich sein müsste, aber der Parameterliste die Attribute fehlen. Das liegt daran, dass die Methode automatisch einen Zeichensatz der Größe 1 im Stil Font.PLAIN erzeugt. Um einen größeren Zeichensatz zu erzeugen, müssen wir ein zweites Font-Objekt anlegen. Dies geschieht am einfachsten mit der Methode deriveFont().

font = f.deriveFont( 20f );

Der Parameter ist allerdings ein float und kein double – wer weiß, was die Entwickler sich dabei gedacht haben ...

class java.awt.Font
implements Serializable

gp  static Font createFont( int fontFormat, InputStream fontStream )
throws FontFormatException, IOException
Liefert ein neues Zeichensatzobjekt in der Größe von einem Punkt und keinem besonderen Stil.

Galileo Computing

14.9 Clipping-Operationen  downtop

Alle primitiven Zeichenoperationen wirken sich auf den gesamten Bildschirm aus und sind nicht auf bestimmte Bereiche eingeschränkt. Wenn wir Letzteres erreichen wollen, setzen wir einen so genannten Clipping-Bereich, außerhalb dessen nicht mehr gezeichnet wird. Leider war in der Vergangenheit die Implementierung dieses Clippings immer etwas anfällig gegenüber Fehlern, sodass eine falsche Zeichnung durchaus vorkommen kann. Wer da auf Nummer sicher gehen will, sollte ein Offscreen-Bild anlegen, Operationen auf diesem Image machen und dann das Bild zeichnen. Doch bleiben wir beim herkömmlichen Clipping. Dies ist eine Eigenschaft des aktuellen Graphic-Objekts. Mit der Methode clipRect(int x, int y, int width, int height) lässt sich dieser Bereich einschränken. Dann erfolgen alle Operationen in diesem Bereich. Das folgende Programm erzeugt zwei Clipping-Bereiche und füllt einen sehr großen Bereich aus, der aber nicht sichtbar ist:

Listing 14.8   ClipDemo.java
import java.awt.*;

public class ClipDemo extends Frame
{
public void paint( Graphics g )
{
Graphics gcopy = g.create();

// Clipping auf

g.clipRect( 100, 100, 100, 100 );
g.setColor( Color.orange );
g.fillRect( 0, 0, 500, 500 );
g.setColor( Color.black );
g.drawOval( 150, 100, 100, 100 );

// Zweiter Clipping Bereich

g.clipRect( 250, 250, 50, 50 );
g.setColor( Color.blue );
g.fillRect( 0, 0, 5000, 5000 );

// Die ursprüngliche Größe zurücksetzen

gcopy.setColor( Color.yellow );
gcopy.fillRect( 50, 50, 20, 50 );

gcopy.dispose();
}

public static void main( String args[] )
{
ClipDemo cd = new ClipDemo();
cd.setSize( 400, 400 );
cd.show();
}
}
Abbildung 14.3   Clipping-Bereiche
Abbildung

Den alten Zustand für Graphics wieder herstellen

Für die Zeichenoperationen im Clipping-Bereich gibt es noch eine alternative Implementierung. Diese verzichtet auf die Kopie des Grafikkontexts mittels create() am Anfang und setzt am Schluss vor die Stelle von gcopy ein getGraphics(), mit dem sich der alte Kontext wieder herstellen lässt. Dann können wir wieder mit g.drawXXX() arbeiten und gcopy ist überflüssig. Mit dem Original können wir dann das Clipping zurücksetzen und wieder ohne Zuschnitt arbeiten. Wenn wir nach dem Clipping das Orignal nicht mehr benötigen, können wir selbstverständlich auf die Kopie verzichten.

Alternative Formen

Durch setClip() können alternativ zu den rechteckigen Formen auch beliebige Shape-Objekte die Clipping-Form vorgeben. Nachfolgende paint()-Methode benutzt als Beschnitt ein Dreieck:

Listing 14.9   ClipDemo.java
import java.awt.*;

public class ClipDemo extends Frame
{
public void paint( Graphics g )
{
Rectangle r = g.getClipBounds();

System.out.println( r );

Polygon p = new Polygon(
new int[]{200,100,300},
new int[]{100,300,300}, 3
);

g.setClip( p );

g.setColor( Color.orange );
g.fillRect( 0, 0, 500, 500 );
}

public static void main( String args[] )
{
ClipDemo cd = new ClipDemo();
cd.setSize( 400, 400 );
cd.show();
}
}

Bei alten Implementierungen funktioniert dies nicht. Auf der Konsole erscheint dann eine Fehlermeldung der folgenden Art:

java.lang.IllegalArgumentException:\
setClip(Shape) only supports Rectangle objects

Verdeckte Bereiche und schnelles Bildschirmerneuern

Clipping-Bereiche sind nicht nur zum Einschränken der Primitiv-Operationen sinnvoll. Bei Bereichsüberdeckungen in Fenster liefern sie wertvolle Informationen über den neu zu zeichnenden Bereich. Bei einer guten Applikation wird nur der Teil wirklich neu gezeichnet, der auch überdeckt wurde. So lässt sich Rechenzeit sparen.

Beispiel Informationen über Clipping-Bereiche
public void paint( Graphics g )
{
Rectangle r = g.getClipBounds();

System.out.println( r );
}

Das Programm erzeugt etwa

java.awt.Rectangle[x=4,y=23,width=392,height=373]
java.awt.Rectangle[x=104,y=87,width=292,height=309]
java.awt.Rectangle[x=104,y=87,width=286,height=211]
java.awt.Rectangle[x=104,y=87,width=243,height=196]
java.awt.Rectangle[x=104,y=87,width=221,height=219]
java.awt.Rectangle[x=101,y=89,width=221,height=219]
...

Hieraus lassen sich verschiedene Fensteroperationen ableiten. Ich habe ein fremdes Fenster über das Java-Fenster geschoben und dann das fremde Fenster verkleinert. Die Rectangle-Informationen geben Aufschluss über die Größe der neu zu zeichnenden Bereiche. Haben wir schon daran gedacht, die Information in einem Image-Objekt abzulegen, lässt sich wunderbar drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) nutzen. Hier müssen wir die Werte aus dem Rectangle auslesen und in drawImage() übertragen. getClipBounds() liefert ein Rectangle-Objekt, dessen Werte für drawImage() nötig sind. Da jedoch auch beliebige Formen möglich sind, liefert getClip() ein Shape-Objekt. getClipRect() ist die veraltete Methode zu getClipBounds(), sonst aber identisch. Die Methode getClipBounds(Rectangle) – eine der wenigen nicht abstrakten Methoden in Graphics – legt die Informationen im übergebenen Rectangle-Objekt ab, welches auch zurückgeliefert wird. Sie ruft nur getClipBounds() auf und überträgt die vier Attribute in das Rechteck.


Galileo Computing

14.10 Farben  downtop

Der Einsatz von Farben in Java-Programmen ist Dank der Color-Klasse einfach. Die Klasse stellt eine Vielzahl von Routinen zur Verfügung, mit denen Color-Objekte erzeugt und manipuliert werden können.

class java.awt.Color
implements Paint, Serializable

gp  Color( float r, float g, float b )
Erzeugt ein Color-Objekt mit den Grundfarben Rot, Grün und Blau. Die Werte müssen im Bereich 0.0 bis 1.0 liegen.
gp  Color( int r, int g, int b )
Erzeugt ein Color-Objekt mit den Grundfarben Rot, Grün und Blau. Die Werte müssen im Bereich 0 bis 255 liegen.
gp  Color( int rgb )
Erzeugt ein Color-Objekt aus dem rgb-Wert, der die Farben Rot, Grün und Blau kodiert. Der Rotanteil befindet sich unter den Bits 16 bis 23, der Grünanteil in 8 bis 15 und der Blauanteil in 0 bis 7. Da ein Integer immer 32 Bit breit ist, ist jede Farbe durch ein Byte (8 Bit) repräsentiert.

Eine private Funktion testColorValueRange() der Color-Klasse überprüft, ob die Werte tatsächlich zwischen 0.0 und 1.0 (erster Fall) oder zwischen 0 und 255 (zweiter Fall) liegen. Wenn nicht, wird eine IllegalArgumentException ausgelöst. Im dritten Fall werden von der Ganzzahl nur die Farbinformationen aus den 24 Bit genommen. Sonstige Werte werden einfach nicht betrachtet und mit einem Alpha-Wert gleich 255 überschrieben. So zeigt es auch der Einzeiler aus dem Quelltext an:

public Color( int rgb ) {
value = 0xff000000 | rgb;
}
abstract class java.awt.Graphics

gp  void setColor( Color )
Setzt die aktuelle Farbe, die dann von den Zeichenfunktionen umgesetzt werden.
gp  Color getColor()
Liefert die aktuelle Farbe.
gp  void setXORMode( Color )
Setzt die Pixel-Operation auf XOR. Abwechselnde Punkte werden in der aktuellen Farbe und der mit dieser Funktion gesetzten XOR-Farbe gesetzt.
Hinweis Die menschliche Farbwahrnehmung: Wir Menschen unterscheiden Farben nach drei Eigenschaften: Farbton, Helligkeit und Sättigung. Der Mensch kann etwa zweihundert Farbtöne unterscheiden. Diese werden durch die Wellenlänge des Lichtes bestimmt. Die Lichtintensität und Empfindlichkeit unserer Rezeptoren lässt uns etwa fünfhundert Helligkeitsstufen unterscheiden. Bei der Sättigung handelt es sich um eine Mischung mit weißem Licht. Hier erkennten wir etwa zwanzig Stufen. Damit kann unser visuelles System etwa zwei Millionen (200 x 500 x 20) Farbnuancen unterscheiden.


Galileo Computing

14.10.1 Zufällige Farbblöcke zeichnen  downtop

Um einmal die Möglichkeiten der Farbgestaltung zu beobachten, betrachten wir die Ausgabe eines Programms, welches Rechtecke mit wahllosen Farben anzeigt:

Listing 14.10   ColorBox.java
import java.awt.*;
import java.awt.event.*;

public class ColorBox extends Frame
{
public ColorBox()
{
super( "Neoplastizismus" );
setSize( 300, 300 );
addWindowListener(new WindowAdapter() {
public void windowClosing ( WindowEvent e) {
System.exit(0);
} });
}

final private int random()
{
return (int)(Math.random() * 256 );
}

public void paint( Graphics gr )
{
for ( int y = 20; y < getSize().height – 25; y += 30 )
for ( int x = 40; x < getSize().width – 25; x += 30 )
{
int r = random(), g = random(), b = random();

gr.setColor( new Color(r,g,b) );
gr.fillRect( y, x, 25, 25);
gr.setColor( Color.black );
gr.drawRect( y-1, x-1, 25, 25 );
}
}

public static void main( String args[] )
{
new ColorBox().show();
}
}
Abbildung 14.4   Programmierter Neoplastizismus
Abbildung

Das Fenster der Applikation hat eine gewisse Größe, die wir mit size() in der Höhe und Breite abfragen. Anschließend erzeugen wir Blöcke, die mit einer zufälligen Farbe gefüllt sind. fillRect() übernimmt diese Aufgabe. Da die gefüllten Rechtecke immer in der Vordergrundfarbe gezeichnet werden, setzen wir den Zeichenstift durch die Funktion setColor(), die natürlich Objektmethode von Graphics ist. Entsprechend gibt es eine korrespondierende Funktion getColor(), die die aktuelle Vordergrundfarbe als Color-Objekt zurückgibt. Diese Funktion darf nicht mit den Funktionen getColor(String) beziehungsweise getColor(String, Color) aus der Color-Klasse verwechselt werden.


Galileo Computing

14.10.2 Farbanteile zurückgeben  downtop

Mitunter müssen wir den umgekehrten Weg gehen und von einem gegebenen Color-Objekt wieder die Rot-, Grün- oder Blau-Anteile extrahieren. Dies ist einfach und die Funktionsbibliothek bietet Entsprechendes an:

class java.awt.Color
implements Paint, Serializable

gp  int getRed(), int getGreen(), int getBlue()'
Liefert Rot-, Grün- und Blau-Anteile des Farb-Objekts.
gp  int getRGB()
Gibt die RGB-Farbe als Ganzzahl kodiert zurück.

Galileo Computing

14.10.3 Vordefinierte Farben  downtop

Wenn wir Farben benutzen wollen, dann sind schon viele Werte vordefiniert (wie im vorausgehenden Beispiel die Farbe Rot). Weitere sind: black, blue, cyan, darkGray, gray, green, lightGray, magenta, orange, pink, white und yellow. In der Klasse jawa.awt.Color sind dazu viele Zeilen der Form

/**
* The color white.
*/
public final static Color white = new Color(255, 255, 255);

platziert. Nachfolgend zeigt die Tabelle die Wertbelegung für die Farbtupel:

Tabelle 14.1   Farbanteile für die vordefinierten Standardfarben
Farbname Rot Grün Blau
white 255 255 255
black 0 0 0
lightGray 192 192 192
darkGray 128 128 128
red 255 0 0
green 0 255 0
blue 0 0 255
yellow 255 255 0
Purple 255 0 255


Galileo Computing

14.10.4 Farben aus Hexadezimalzahlen erzeugen  downtop

Um eine Farbbeschreibung im hexadezimalen Format in einzelne Farbkomponenten der Color-Klasse zu zerlegen, also zum Beispiel von FFFFFF nach (255,255,255), gibt es zwei einfache und elegante Wege. Der erste führt über die Wrapper-Klasse Integer. Die folgende Zeile erzeugt aus dem String colorHexString ein Color-Objekt:

Color color = new Color( Integer.parseInt(colorHexString, 
16) );

Ein anderer Weg ist noch eleganter, da die Color-Klasse eine einfache Routine bereitstellt:

Color color = Color.decode( "#" 
+ colorHexString );

decode(String) verlangt eine 24-Bit Integer-Zahl als String kodiert. Durch das Hash-Symbol und dem Plus erzeugen wir ein String-Objekt, welches als Hexadezimalzahl bewertet wird.

class java.awt.Color
implements Paint, Serializable

gp  Color decode( String ) throws NumberFormatException
Liefert die Farbe vom übergebenen String. Die Zeichenkette ist als 24-Bit Integer kodiert.

Nun wertet decode() den String aus, indem wiederum die decode()-Funktion der Integer-Klasse aufgerufen wird. Aus diesem Rückgabewert wird dann wiederum das Color-Objekt aufgebaut.

public static Color decode( String 
nm )
throws NumberFormatException
{
Integer intval = Integer.decode(nm);
int i = intval.intValue();
return new Color((i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF);
}

Wir sehen, dass bei falschen Werten eine NumberFormatException ausgelöst wird. Diese Exception kommt von der decode()-Funktion der Integer-Klasse. Die Implementierung verrät uns die Arbeitsweise, und zeigt uns auf, dass wir auch aus Okalziffern ein Color-Objekt erzeugen könnten, oder aber aus einem String, der nicht mit dem Hash-Zeichen, sondern mit dem gewohnten Präfix 0x beginnt.

public static Integer decode(String 
nm)
throws NumberFormatException
{
if (nm.startsWith("0x")) {
return Integer.valueOf(nm.substring(2), 16);
}
if (nm.startsWith("#")) {
return Integer.valueOf(nm.substring(1), 16);
}
if (nm.startsWith("0") && nm.length() > 1) {
return Integer.valueOf(nm.substring(1), 8);
}
return Integer.valueOf(nm);
}

Galileo Computing

14.10.5 Einen helleren oder dunkleren Farbton wählen  downtop

Zwei besondere Funktionen sind brighter() und darker(). Sie liefern ein Farb-Objekt zurück, das jeweils eine Farb-Nuance heller bzw. dunkler ist.

Beispiel Die Implementierung von draw3DRect() zeigt den Einsatz der Funktionen.
public void draw3DRect(int x, int 
y, int width, int height, boolean raised)
{
Color c = getColor();
Color brighter = c.brighter();
Color darker = c.darker();

setColor(raised ? brighter : darker);
drawLine(x, y, x, y + height);
drawLine(x + 1, y, x + width – 1, y);
setColor(raised ? darker : brighter);
drawLine(x + 1, y + height, x + width, y + height);
drawLine(x + width, y, x + width, y + height – 1);
setColor(c);
}

Wie viele andere Funktionen aus der Color-Klasse sind die Routinen sichtbar implementiert, also nicht nativ:

public Color brighter() {
return new Color(Math.min((int)(getRed() *(1/FACTOR)), 255),
Math.min((int)(getGreen()*(1/FACTOR)), 255),
Math.min((int)(getBlue() *(1/FACTOR)), 255));
}

public Color darker() {
return new Color(Math.max((int)(getRed() *FACTOR), 0),
Math.max((int)(getGreen()*FACTOR), 0),
Math.max((int)(getBlue() *FACTOR), 0));
}

FACTOR ist eine Konstante, die durch

private static final double FACTOR 
= 0.7;

festgelegt ist. Sie lässt sich also nicht ändern.

class java.awt.Color
implements Paint, Serializable

gp  Color brighter()
Gibt einen helleren Farbton zurück.
gp  Color darker()
Gibt einen dunkleren Farbton zurück.

Farbveränderung mit Nullanteilen

Bei den Farbwerten müssen wir nun die Zusammensetzung aus Rot, Grün und Blau bedenken. Ein voller Wert ist mit 255 belegt. Die Berechnung kann diesen Wert noch modifizieren. Doch ist ein Eintrag mit 0 belegt, so erkennen wir aus der Berechung, dass der Wert bei Null bleiben wird. Daher sollten wir bedenken, was bei reinen Farben wie etwa Rot bei einem brighter() passiert. Ein reiner Rotton kann sich zwar in der Helligkeit ändern, aber ein Color.red.brighter() liefert immer noch eine Color.red.

System.out.println( Color.red.brighter() 
);
// ergibt java.awt.Color[r=255,g=0,b=0]

System.out.println( Color.red.darker() );
// ergibt java.awt.Color[r=178,g=0,b=0]

Es ist also nicht so, dass bei brighter() die Farben näher an Weiß herankommen und bei darker() an Schwarz.

Um also echte Helligkeitsveränderungen zu bekommen, müssen wie die Farben vorher umrechnen. Hierzu bieten sich andere Farbräume an, wie beispielsweise der HSB-Raum, in dem wir Komponenten für die Helligkeit haben. RGBtoHSB() gibt ein Feld mit den Werten für Hue, Saturation und Brightness für ein Tripel von Rot-, Grün- und Blau-Werten zurück. Nach einer Veränderung der Helligkeit können wir diesen Farbraum wieder mit HSBtoRGB() zurückkonvertieren.


Galileo Computing

14.10.6 Farbmodelle HSB und RGB  downtop

Zwei Farbmodelle sind in der Computergrafik geläufig. Das RGB-Modell, wo die Farben durch einen Rot-, Grün-, und Blau-Anteil definiert werden, oder das HSB-Modell, das die Farben durch Grundton (Hue), Farbsättigung (Saturation) und Helligkeit (Brightness) definiert. Die Farbmodelle können die gleichen Farben beschreiben und umgerechnet werden.

class java.awt.Color
implements Paint, Serializable

gp  static int HSBtoRGB( float hue, float saturation, float brightness )
Aus einem HSB-kodierten Farbwert wird ein RBG-Farbwert gemacht.
gp  static float[] RGBtoHSB( int r, int g, int b, float hsbvals[] )
Verlangt ein Array hsbvals zur Aufnahme von HSB, in dem die Werte gespeichert werden sollen. Das Array kann null sein und wird somit angelegt. Das Feld wird zurückgegeben.
gp  static Color getHSBColor( float h, float s, float b )
Die Funktion kann genutzt werden, um Color-Objekte aus einem HSB-Modell zu erzeugen.

Die Implementierung von getHSBColor() ist ein Witz:

public static Color getHSBColor(float 
h, float s, float b) {
return new Color(HSBtoRGB(h, s, b));
}

Galileo Computing

14.10.7 Die Farben des Systems  downtop

Bei eigenen Java-Programmen ist es wichtig, dass diese sich so perfekt wie möglich in die Reihe der anderen Host-Programme einreihen, ohne großartig aufzufallen. Dazu muss ein Fenster die globalen Einstellungen wie den Zeichensatz und die Farben kennen. Für die Systemfarben gibt es die Klasse SystemColor, welche alle Farben einer grafischen Oberfläche auf symbolische Konstanten abbildet. Besonders praktisch ist dies bei Änderungen von Farben während der Laufzeit. Über diese Klasse können immer die aktuellen Werte eingeholt werden, denn ändert sich beispielsweise die Hintergrundfarbe der Laufleisten, so ändert sich damit auch der RGB-Wert. Die Systemfarben sind Konstanten von SystemColor und werden mit der Funktion getRGB() in eine Ganzzahl umgewandelt.

Die Klasse definiert folgende statische finale Variablen:

class java.awt.SystemColor
implements Serializable

Tabelle 14.2   Konstanten der Systemfarben
SystemColor Welche Farbe anspricht
desktop Farbe des Desktop-Hintergrunds
activeCaption Hintergrundfarben für Text im Fensterrahmen
activeCaptionText Farbe für Text im Fensterrahmen
activeCaptionBorder Rahmenfarbe für Text im Fensterrahmen
inactiveCaption Hintergrundfarbe für inaktiven Text im Fensterrahmen
inactiveCaptionText Farbe für inaktiven Text im Fensterrahmen
inactiveCaptionBorder Rahmenfarbe für inaktiven Text im Fensterrahmen
window Hintergrundfarbe der Fenster
windowBorder Rahmenfarbe der Fenster
windowText Textfarbe für Fenster
menu Hintergrundfarbe für Menüs
menuText Textfarbe für Menüs
text Hintergrundfarbe für Textkomponenten
textText Textfarbe für Textkomponenten
textHighlight Hintergrundfarbe für hervorgehobenen Text
textHighlightText Farbe des Texts, wenn dieser hervorgehoben ist
textInactiveText Farbe für inaktiven Text
control Hintergrundfarbe für Kontroll-Objekte
controlText Textfarbe für Kontroll-Objekte
controlHighlight Normale Farbe, mit der Kontroll-Objekte hervorgehoben werden
controlLtHighlight Hellere Farbe, mit der Kontroll-Objekte hervorgehoben
werden
controlShadow Normale Hintergrundfarbe für Kontroll-Objekte
controlDkShadow Dunklerer Schatten für Kontroll-Objekte
scrollbar Hintergrundfarbe der Schieberegler
info Hintergrundfarbe der Hilfe
infoText Textfarbe der Hilfe

Um die System-Farbe in eine brauchbare Variable zu konvertieren, gibt es die getRGB()-Funktion. So erzeugen wir mit

new Color( (SystemColor.window).getRGB() 
)

einfach ein Color-Objekt in der Farbe des Fensters.

final class java.awt.SystemColor
implements Serializable

gp  int getRGB()
Liefert den RGB-Wert der Systemfarbe als Ganzzahl kodiert.

Zuordung der Farben unter Windows

Werden die Farben vom System nicht zugewiesen, so werden vordefinierte Werte gesetzt. Folgende Einteilung wird unter Windows vorgenommen und beibehalten, wenn dies nicht vom System überschrieben wird:

Tabelle 14.3   Systemfarben
Farbe Initialisierte Farbe
desktop new Color(0,92,92)
activeCaption new Color(0,0,128)
activeCaptionText Color.white
activeCaptionBorder Color.lightGray
inactiveCaption Color.gray
inactiveCaptionText Color.lightGray
inactiveCaptionBorder Color.lightGray
window Color.white
windowBorder Color.black
windowText Color.black
menu Color.lightGray
menuText Color.black
text Color.lightGray
textText Color.black
textHighlight new Color(0,0,128)
textHighlightText Color.white
textInactiveText Color.gray
control Color.lightGray
controlText Color.black
controlHighlight Color.white
controlLtHighlight new Color(224,224,224)
controlShadow Color.gray
controlDkShadow Color.black
scrollbar new Color(224,224,224)
info new Color(224,224,0)
infoText Color.black

Um zu sehen, welche Farben auf dem laufenden System aktiv sind, formulieren wir ein Programm, das eine kleine Textzeile in der jeweiligen Farbe angibt. Da wir auf die internen Daten nicht zugreifen können, müssen wir ein Farbfeld mit SystemColor-Objekten aufbauen.

Abbildung 14.5   Die System-Farben unter einer Windows-Konfiguration
Abbildung

Listing 14.11   SystemColors.java
import java.awt.*;
import java.awt.event.*;

class SystemColors extends Frame
{
private String systemColorString[] = {
"desktop","activeCaption","activeCaptionText",
"activeCaptionBorder", "inactiveCaption",
"inactiveCaptionText", "inactiveCaptionBorder",
"window", "windowText", "menu", "menuText",
"text", "textText", "textHighlight",
"textHighlightText","textInactiveText",
"control", "controlText", "controlHighlight",
"controlLtHighlight", "controlShadow",
"controlDkShadow", "scrollbar",
"info","infoText"
};

private SystemColor systemColor[] = {
SystemColor.desktop,
SystemColor.activeCaption,
SystemColor.activeCaptionText,
SystemColor.activeCaptionBorder,
SystemColor.inactiveCaption,
SystemColor.inactiveCaptionText,
SystemColor.inactiveCaptionBorder,
SystemColor.window,
SystemColor.windowText,
SystemColor.menu,
SystemColor.menuText,
SystemColor.text,
SystemColor.textText,
SystemColor.textHighlight,
SystemColor.textHighlightText,
SystemColor.textInactiveText,
SystemColor.control,
SystemColor.controlText,
SystemColor.controlHighlight,
SystemColor.controlLtHighlight,
SystemColor.controlShadow,
SystemColor.controlDkShadow,
SystemColor.scrollbar,
SystemColor.info,
SystemColor.infoText
};

public SystemColors() {
setSize( 200, 400 );
addWindowListener(new WindowAdapter() {
public void windowClosing( WindowEvent e ) {
System.exit(0);
}
});
}

public void paint( Graphics g )
{
g.setFont( new Font( "Dialog", Font.BOLD, 12 ) );
for ( int i=0; i < systemColorString.length; i++ ) {
g.setColor( new Color( systemColor[i].getRGB() ) );
g.drawString( systemColorString[i], 20, 40+(i*13) );
}
}

public static void main( String args[] ) {
SystemColors c = new SystemColors();
c.show();
}
}

Galileo Computing

14.11 Bilder anzeigen und Grafiken verwalten  downtop

Bilder sind neben dem Text das wichtigste visuelle Gestaltungsmerkmal. In Java können Grafiken an verschiedenen Stellen eingebunden werden. So zum Beispiel als Grafiken in Zeichengebieten (Canvas) oder als Icons in Buttons, die angeklickt werden und ihre Form ändern. Über Java können GIF-Bilder und JPEG-Bilder geladen werden.

Hinweis GIF und JPEG: Das GIF-Format (Graphics Interchange Format) ist ein komprimierendes Verfahren, das 1987 von CompuServe-Betreibern zum Austausch von Bildern entwickelt wurde. GIF-Bilder können bis zu 1600 x 1600 Punkte umfassen. Die Komprimierung nach einem veränderten LZW -Packverfahren nimmt keinen Einfluss auf die Bildqualität (sie ist verlustfrei). Jedes GIF-Bild kann aus maximal 256 Farben bestehen – bei einer Palette aus 16,7 Millionen Farben. Nach dem Standard von 1989 können mehrere GIF-Bilder in einer Datei gespeichert werden. JPEG-Bilder sind dagegen in der Regel verlustbehaftet und das Komprimierverfahren speichert die Bilder mit einer 24-Bit Farbpalette. Der Komprimierungsfaktor kann prozentual eingestellt werden.

Jede Grafik wird als Exemplar der Klasse Image erzeugt. Um aber ein Grafik-Objekt erst einmal zu bekommen, gibt es zwei grundlegende Verfahren: Laden eines Bilds von einem Applet und Laden eines Bilds aus einer Applikation. In beiden Fällen wird getImage() verwendet, eine Methode, die mehrfach überladen ist, um uns verschiedene Möglichkeiten an die Hand zu geben, Image-Objekte zu erzeugen.

Bilder in Applikationen

Grafiken in einer Applikation werden über die Klasse Toolkit eingebunden. Der Konstruktor kann eine URL beinhalten oder eine Pfadangabe zu der Grafikdatei.

abstract class java.awt.Toolkit

gp  Image getImage( String )
Das Bild wird durch eine Pfadangabe überliefert.
gp  Image getImage( URL )
Das Bild wird durch die URL angegeben.
Beispiel Bilder in Applikationen anfordern
Image pic = Toolkit.getToolkit().getImage( 
"hanswurst.gif" );

Ein Image-Objekt wird erzeugt und das Objekt mit der Datei hanswurst.gif in Verbindung gebracht. Die Formulierung lässt »Laden der Datei« nicht zu, denn die Grafik wird erst aus der Datei bzw. dem Netz geladen, wenn der erste Zeichenaufruf stattfindet. Somit schützt uns die Bibliothek vor unvorhersehbaren Ladevorgängen für Bilder, die später oder gar nicht genutzt werden.

Hinweis Da die getImage()-Funktion für URLs und Strings definiert ist, ist vor folgendem Konstrukt natürlich nur zu warnen:
getImage( "http://hostname/grafik" 
);
Gewiss führt dies zu einem gnadenlosen Fehler, denn eine Datei mit dem Namen http://hostname/grafik gibt es nicht! Korrekt heißt es:
getImage( new URL("http://hostname/grafik") 
);

Bilder in Applets

Die Applet-Klasse kennt ebenso zwei getImage()-Methoden, die wiederum die entsprechenden Methoden aus der Klasse AppletContext aufrufen.

interface java.applet.AppletContext

gp  Image getImage( URL )
Das Bild wird durch die URL angegeben.

Müssen wir in einem Applet die Grafik relativ zu einem Bezugspunkt angeben, der jedoch fehlt, so hilft uns die Funktion getCodeBase() weiter, die uns die relative Adresse des Applets übergibt. (Mit getDocumentBase() bekommen wir die URL des HTML-Dokumentes, unter der das Applet eingebunden ist.)

Bilder aus dem Cache nehmen

Eine Webcam erzeugt kontinuierlich neue Bilder. Sollen diese in einem Applet präsentiert werden, so ergibt sich das Problem, dass ein erneuter Aufruf von getImage() lediglich das alte Bild liefert. Dies liegt an der Verwaltung der Image-Objekte, denn sie werden in einem Cache gehalten. Für sie gibt es keinen GC, der die Entscheidung fällt: »Das Bild ist alt«. Hier hilft die Methode flush() der Image-Klasse weiter. Sie löscht das Bild aus der internen Liste. Eine erneute Aufforderung zum Laden bringt also das gewünschte Ergebnis.

abstract class java.awt.Image

gp  abstract void flush()
Gibt die für das Image belegten Ressourcen frei.
Abbildung

Hinweis Speicher sparen: Image Objekte werden nicht automatisch freigegeben. flush() entsorgt diese Bilder und macht wieder Speicher frei und den Rechner wieder schneller.


Galileo Computing

14.11.1 Eine Grafik zeichnen  downtop

Eine Grafik wird durch die Funktion drawImage() gezeichnet. Wie erwähnt, wird sie, falls noch nicht vorhanden, vom Netz- oder Dateisystem geladen. Das folgende Programmlisting zeigt eine einfache Applikation mit einer Menüleiste, die über ein Dateiauswahldialog eine Grafik lädt. Die Größe des Fensters wird auf die Größe der Grafik gesetzt.

Abbildung 14.6   Ein einfacher Bildbetrachter mit Dateiauswahldialog
Abbildung

Listing 14.12   ImageViewer.java
import java.awt.*;
import java.awt.event.*;

public class ImageViewer extends Frame implements ActionListener
{
public ImageViewer()
{
setTitle( "Bildbetrachter" );

// Konstruiere die Menüzeile

MenuBar mbar = new MenuBar();

Menu menu = new Menu( "Datei" );
MenuItem menuitem =
new MenuItem( "Öffnen",new MenuShortcut((int)'Ö') );
menuitem.addActionListener( this );
menu.add( menuitem );

mbar.add( menu );
setMenuBar( mbar );


// Das Fenster mit X schließen

frame = this;
addWindowListener( new WindowAdapter() {
public void windowClosing ( WindowEvent e ) {
System.exit(0);
}
} );

setSize( 600, 400 );
}

public void paint( Graphics g )
{
if ( image != null )
{
g.drawImage( image, 0, 0, this );
setSize( image.getWidth(this), image.getHeight(this) );
}
}

public void actionPerformed( ActionEvent e )
{
FileDialog d = new FileDialog( frame, "Öffne Grafikdatei",
FileDialog.LOAD );
d.setFile( "*.jpg;*.gif" );
d.show();

String file = d.getDirectory() + d.getFile();

image = Toolkit.getDefaultToolkit().getImage( file );

if ( image != null )
repaint();
}

public static void main( String args[] )
{
new ImageViewer().show();
}

private Image image;
private Frame frame;
}

Galileo Computing

14.11.2 Grafiken zentrieren  downtop

Eine Funktion zum Zentrieren einer Grafik braucht neben der Grafik als Image und dem Graphics noch die Komponente, auf der die Grafik gezeichnet wird. Über die getSize()-Funktion des Component-Objekts kommen wir an die Breite und Höhe der Zeichenfläche. Wir holen uns die Hintergrundfarbe und füllen die Zeichenfläche, anschließend positionieren wir das Bild in der Mitte, indem wir die Breite und Höhe des Bildes von der Breite und Höhe der Zeichenfläche subtrahieren und anschließend durch zwei teilen.

public static void
centerImage( Graphics g, Component component, Image image )
{
g.setColor( component.getBackground() );
Dimension d = component.size();
g.fillRect( 0, 0, d.width, d.height );
g.drawImage( image,
( d.width – image.getWidth( null ) ) / 2,
( d.height – image.getHeight( null ) ) / 2,
null );
}

Galileo Computing

14.11.3 Laden von Bildern mit dem MediaTracker beobachten  downtop

Das Laden von Bildern mittels getImage() wird dann vom System angeregt, wenn das Bild zum ersten Mal benötigt wird. Diese Technik ist zwar ganz schön und entzerrt den Netzwerktransfer, ist aber für einige grafische Einsätze ungeeignet. Nehmen wir zum Beispiel eine Animation, dann können wir nicht erwarten, erst dann die Animation im vollen Ablauf zu sehen, wenn wir nacheinander alle Bilder im Aufbauprozess gesehen haben. Daher ist zu Wünschen, die Bilder erst einmal alle laden zu können, bevor sie angezeigt werden. Die Klasse MediaTracker ist eine Hilfsklasse, mit der wir den Ladeprozess von Media-Objekten, bisher nur Bilder, beobachten können. Um den Überwachungsprozess zu starten, werden die Media-Objekte dem MediaTracker zur Beobachtung übergeben. Neben diesem besitzt die Klasse gegenüber der herkömmlichen Methode noch weitere Vorteile:

gp  Bilder können in Gruppen organisiert werden
gp  Bilder könenn synchron oder asynchron geladen werden
gp  Die Bilder-Gruppen können unabhängig geladen werden

Ein MediaTracker-Objekt erzeugen

Um ein MediaTracker-Objekt zu erzeugen, rufen wir seinen Konstruktor mit einem einzigen Parameter vom Typ Component auf:

MediaTracker tracker = new MediaTracker( 
this );

Wenn wir Applet oder Frame erweitern, kann dies – so wie im Beispiel – der this-Zeiger sein. Dies zeigt aber schon die Einschränkung der Klasse auf das Laden von Bildern, denn was hat eine Musik schon mit einer Komponente zu tun?

Bilder beobachten

Nachdem ein MediaTracker-Objekt erzeugt ist, fügt die addImage(Image)-Methode ein Bild in eine Warteliste ein. Eine weitere überladene Methode addImage(Image, Gruppe ID) erlaubt die Angabe einer Gruppe. Dieser Identifier entspricht gleichzeitig einer Priorität, in der die Bilder geholt werden. Gehören also Bilder zur gleichen Gruppe ist die Priorität immer dieselbe. Bilder mit einer niedrigeren Gruppennummer werden mit einer niedrigeren Priorität geholt als Bilder mit einer höheren ID. Eine dritte Methode von addImage() erlaubt die Angabe einer Skalierungsgröße. Nach dieser wird das geladene Bild skaliert und eingefügt. Schauen wir uns einmal eine typische Programmsequenz an, die ein Hintergrundbild sowie einige animierte Bilder dem Medien-Überwacher überreicht:

Image bg     = getImage( "background.gif" 
),
anim[] = new Image[MAX_ANIM];

MediaTracker tracker =
new MediaTracker( this );
tracker.addImage( bg, 0 );

for ( int i = 0; i < MAX_ANIM; i++ ) {
anim[i] = getImage( getDocumentBase(), " anim"+i+".gif" );
tracker.addImage( anim[i], 1 );
}

Das Hintergrundbild wird dem MediaTracker-Objekt hinzugefügt. Die ID, also die Gruppe, ist 0. Das Bildarray anim[] wird genauso gefüllt und überwacht. Die ID des Feldes ist 1. Also gehören alle Bilder dieser Animation zu einer weiteren Gruppe.

Um den Ladeprozess anzustoßen, benutzen wir eine der Methoden waitForAll() oder waitForID(). Die waitForID()-Methode wird benutzt, um Bilder mit einer bestimmten Gruppe zu laden. Die Gruppennummer muss natürlich dieselbe vergebene Nummer sein, die bei der addImage()-Methode verwendet wurde. Beide Methoden arbeiten synchron, bleiben also so lange in der Methode, bis alle Bilder geladen wurden oder ein Fehler bzw. eine Unterbrechung auftrat. Da dies also das ganze restliche Programm blockieren würde, werden diese Ladeoperationen gerne in Threads gesetzt. Wie diese Methoden in einem Thread verwendet werden, zeigt das folgende Programmsegment. Der Block ist idealerweise in einer run()-Methode platziert oder, bei einem Applet, in der init()-Methode.

try {
tracker.
waitForID( 0 );
tracker.
waitForID( 1 );
}
catch ( InterruptedException e ) { return; }

Die waitForID()-Methode wirft einen Fehler, falls sie beim Ladevorgang unterbrochen wurde. Daher müssen wir unsere Operationen in einen try- und catch-Block setzen.

Während das Bild geladen wird, können wir seinen Ladezustand mit den Methoden checkID() überprüfen. checkID() bekommt als ersten Parameter eine Gruppe zugeordnet und überprüft dann, ob die Bilder, die mit der Gruppe verbunden sind, geladen wurden. Wenn ja, gibt die Methode true zurück, auch dann wenn der Prozess fehlerhaft oder abgebrochen wurde. Ist der Ladeprozess noch nicht gestartet, dann veranlasst checkID(Gruppe) dies nicht. Um dieses Verhalten zu steuern, regt die überladene Funktion checkID(Gruppe, true) das Laden an. Beide geben false zurück, falls der Ladeprozess noch nicht beendet ist.

Eine weitere Überprüfungsfunktion ist checkAll(). Diese arbeitet wie checkID(), nur, dass sie auf alle Bilder in allen Gruppen achtet und nicht auf die ID angewiesen ist. Wie checkID() gibt es checkAll() ebenfalls in zwei Varianten. Die zweite startet den Ladeprozess, falls die Bilder noch nicht geladen wurden.

Die MediaTracker-Klasse verfügt über vier Konstanten, die verschiedene Flags vertreten, um den Status des Objekts zu erfragen. Einige der Methoden geben diese Konstanten ebenso zurück.

Tabelle 14.4   Flags der Klasse MediaTracker
Konstante Bedeutung
LOADING Ein Medien-Objekt wird gerade geladen.
ABORTED Das Laden eines Objekts wurde unterbrochen.
ERRORED Ein Fehler trat während des Ladens auf.
COMPLETE Das Medien-Objekt wurde erfolgreich geladen.

Mit statusID() verbunden, welches ja den Zustand des Ladens überwacht, können wir leicht die Fälle herausfinden, wo das Bild erfolgreich bzw. nicht erfolgreich geladen werden konnte. Dazu verknüpfen wir einfach durch Und die Konstante mit dem Rückgabewert von statusAll() oder statusID():

if ( (tracker.statusAll() & 
MediaTracker.ERRORED) != 0 ) {
// Fehler!

Wie wir sehen, können wir durch solche Zeilen leicht herausfinden, ob bestimmte Bilder schon geladen sind. MediaTracker.COMPLETE sagt uns »ja«, und wenn ein Fehler auftritt, dann ist der Rückgabewert MediaTracker.ERRORED. Wir wollen diese Flags nun verwenden, um in einer paint()-Methode das Vorhandensein von Bildern zu überprüfen, und wenn möglich, diese dann anzuzeigen. Erinnern wir uns daran, dass in der Gruppe 0 ein Hintergrundbild lag und in Gruppe 1 die zu animierenden Bilder. Wenn ein Fehler auftritt, zeichnen wir ein rotes Rechteck auf die Zeichenfläche und signalisieren damit, dass etwas nicht funktionierte.

public void paint( Graphics g )
{
if ( tracker.statusID(0, true) == MediaTracker.ERRORED )
{
g.setColor( Color.red );
g.fillRect( 0, 0, size().width, size().height );
return;
}
g.drawImage( bg, 0, 0, this );
if ( tracker.statusID(1) & MediaTracker.COMPLETE) ) {
g.drawImage( anim[counter%MAX_ANIM], 50, 50, this );
}
}
class java.awt.MediaTracker
implements Serializable

gp  static final int ABORTED
Flag, welches anzeigt, dass das Medium nicht geladen werden konnte. Rückgabewert von statusAll() oder statusID().
gp  static final int ERRORED
Während des Ladens gab es Fehler. Rückgabewert von statusAll() und statusID().
gp  static final int COMPLETE
Medium konnte geladen werden. Rückgabewert von statusAll() und statusID().
gp  MediaTracker( Component )
Erzeugt einen MediaTracker auf einer Komponente, auf der das Bild möglicherweise angezeigt wird.
gp  void addImage( Image image, int id )
Fügt ein Bild nicht skaliert der Ladeliste hinzu. Ruft addImage(image, id, -1, -1) auf.
gp  void addImage( Image image, int id, int w, int h )
Fügt ein skaliertes Bild der Ladeliste hinzu. Soll ein Bild in einer Richtung nicht skaliert werden, ist -1 einzutragen.
gp  public boolean checkAll()
Überprüft, ob alle vom MediaTracker überwachten Medien geladen worden sind. Falls der Ladeprozess noch nicht angestoßen wurde, wird dieser auch nicht initiiert.
gp  boolean checkAll( boolean load )
Überprüft, ob alle vom MediaTracker überwachten Medien geladen worden sind. Falls der Ladeprozess noch nicht angestoßen wurde, wird dieser dazu angeregt.
gp  boolean isErrorAny()
true
, wenn eines der überwachten Bilder einen Fehler beim Laden verursachte.
gp  Object[] getErrorsAny()
Liefert eine Liste aller Objekte, die einen Fehler aufweisen. null, wenn alle korrekt geladen wurden.
gp  void waitForAll() throws InterruptedException
Das Laden aller vom MediaTracker überwachten Bilder wird angestoßen, und es wird so lange gewartet, bis alles geladen wurde, oder ein Fehler beim Laden oder Skalieren auftritt.
gp  boolean waitForAll( long ms ) throws InterruptedException
Startet den Ladeprozess. Die Funktion kehrt erst dann zurück, wenn alle Bilder geladen wurden oder die Zeit überschritten wurde. true, wenn alle korrekt geladen wurden.
gp  int statusAll( boolean load )
Liefert einen mit Oder verknüpften Wert der Flags LOADING, ABORTED, ERRORED und COMPLETE. Der Ladeprozess wird bei load auf true gestartet.
gp  boolean checkID( int id )
Überprüft, ob alle Bilder, die mit der ID id verbunden sind, geladen wurden. Der Ladeprozess wird mit dieser Methode nicht angestoßen. Liefert true, wenn alle Bilder geladen sind, oder ein Fehler auftrat.
gp  boolean checkID( int id, boolean load )
Wie checkID( int id ), nur, dass die Bilder geladen werden, die bisher noch nicht geladen wurden.
gp  boolean isErrorID( int id )
Liefert der Fehler-Status von allen Bildern mit der ID id. true, wenn eines der Bilder beim Laden einen Fehler aufweist.
gp  Object[] getErrorsID( int id )
Liefert eine Liste aller Medien, die einen Fehler aufweisen.
gp  void waitForID( int id ) throws InterruptedException
Startet den Ladeprozess für die gegebene ID. Die Methode wartet solange, bis alle Bilder geladen sind. Bei einem Fehler oder Abbruch wird angenommen, dass aller Bilder ordentlich geladen wurden.
gp  boolean waitForID( int id, long ms ) throws InterruptedException
Wie waitForID(), nur stoppt der Ladeprozess nach einer festen Anzahl von Millisekunden.
gp  int statusID( int id, boolean load )
Liefert einen mit Oder verknüpften Wert der Flags LOADING, ABORTED, ERRORED und COMPLETE. Ein noch nicht geladenes Bild hat den Status 0. Ist der Parameter load gleich true, dann werden die Bilder geladen, die bisher nocht nicht geladen wurden.
gp  void removeImage( Image image )
Entfernt ein Bild von der Liste der Medien-Elemente. Dabei werden alle Objekte, die sich nur in der Skalierung unterscheiden, entfernt.
gp  public void removeImage( Image image, int id )
Entfernt das Bild mit der ID id von der Liste der Medien-Elemente. Dabei werden auch die Objekte entfernt, bei denen sich die Bilder nur in der Skalierung unterscheiden.
gp  public void removeImage( Image image, int id, int width, int height )
Entfernt ein Bild mit den vorgegebenen Ausmaßen und der ID id von der Liste der Medien-Elemente. Doppelte Elemente werden ebenso gelöscht.

Galileo Computing

14.11.4 Kein Flackern durch Double-Buffering  downtop

Zeichnen wir komplexe Grafiken, dann fällt beim Ablauf des Programms deutlich auf, dass der Zeichenvorgang durch Flackern gestört ist. Dieses Flackern tritt in zwei Fällen auf:

gp  Wenn wir Bildschirminhalte verschieben und Teile verdeckt werden, muss über die update()- und paint()-Methode der verdeckte Bildausschnitt neu aufgebaut werden.
gp  In der paint()-Methode kommen oft rechenintensive Zeichenoperationen vor und das Bild muss mittels der Grafikoperationen neu aufgebaut werden. Zeichnen wir ein Dreieck, so müssen wir drei Linien zeichnen. Aber während die Linien gezeichnet werden, fährt der Rasterstrahl mehrmals über den Schirm, und bei jedem Rasterdurchlauf sehen wir ein neues Bild, das immer einen Teil mehr von sich preisgibt. Bei aufwändigen Zeichenoperationen sind nun viele Rasterstrahldurchläufe nötig bis das Bild komplett ist.
Hinweis Double-Buffering: Eine einfache und elegante Methode, diesem Flackern zu entkommen, ist die Technik des Double-Buffering. Eine zweite Zeichenebene wird angelegt und auf dieser dann gezeichnet. Ist die Zeichnung komplett, wird sie zur passenden Zeit in den sichtbaren Bereich hineinkopiert.

Über Double-Buffering vermeiden wir zusätzliche Zeichenoperationen auf der sichtbaren Fläche, in dem wir alle Operationen auf einem Hintergrundbild durchführen. Immer dann, wenn das Bild, beispielsweise eine Konstruktionszeichnung, fertig ist, kopieren wir das Bild in den Vordergrund. Dann kann nur noch bei dieser Kopiermethode Flackern auftreten. Glücklicherweise ist das Zeichnen auf Hintergrundbildern nicht schwieriger als auf Vordergrundbildern, denn die Operationen sind auf ein beliebiges Image erlaubt.

Zunächst benötigen wir einen Offscreen-Puffer für Grafiken als Image-Objekt, auf dem wir die Zeichenoperationen anwenden. Zum Beispiel durch die folgenden Zeilen:

Graphics offscreenGraphics;
Image offscreenImage;

Innerhalb der paint()-Methode – oder bei einem Applet gerne in der init()-Funktion – erzeugen wir die Zeichenfläche mit der Funktion createImage(). Die Größe der Fläche muss übergeben werden. Diese können wir über die getSize()-Methode erfragen. Alle von Component abgeleiteten Objekte implementieren getSize().

Neben dem Bild müssen wir noch das Graphics-Objekt initialisieren:

offscreenImage = createImage( 400, 
400 );
offscreenGraphics = offscreenImage.getGraphics();

Wo wir vorher innerhalb der paint()-Methoden immer die Grafikoperationen mit dem Graphics g der Methode paint() benutzten, ersetzen wir dieses g durch offscreenGraphics. Unsere Zeichenoperationen verschieben wir von der paint()-Methode in eine eigene Methode, zum Beispiel offPaint(). So werden die drei Linien in der paint()-Methode

public void paint( Graphics g )
{
g.drawLine( 10, 20, 100, 200 );
g.drawLine( 100, 200, 60, 100 );
g.drawLine( 60, 100, 10, 20 );
}

zu

private void offPaint()
{
offscreenGraphics.drawLine( 10, 20, 100, 200 );
offscreenGraphics.drawLine( 100, 200, 60, 100 );
offscreenGraphics.drawLine( 60, 100, 10, 20 );
}

Die Urimplementation der update()-Methode ist so programmiert, dass sie den Bildschirm löscht und anschließend paint() aufruft. Genauer: Der Code der update()-Methode ist in Component durch den Zweizeiler

public void update( Graphics g 
)
{
clearBackground();
paint( g );
}

gegeben. clearBackground() zeichnet ein gefülltes Rechteck in der Hintergrundfarben über die Zeichenfläche. Auch dieses Löschen ist für das Flackern verantwortlich. Es macht Sinn, aus der update()-Methode sofort paint() aufzurufen. Die meisten Applikationen überschreiben daher die Implementierung von update().

public void update( Graphics g 
)
{
paint( g );
}

Somit fällt das lästige und Zeit kostende Bildschirmlöschen weg. Da in unserer paint()-Methode ohnehin das gesamte Rechteck gezeichnet wird, können keine Bereiche ungeschrieben bleiben. Der Code der paint()-Methode ist daher nicht mehr spektakulär. Wir haben die Grafik im Hintergrund aufgebaut, und sie muss nun in den eigentlichen Zeichenbereich mit drawImage() kopiert werden. Aus paint() heraus haben wir den aktuellen Graphic-Kontext g und dann zeichnet

public void paint( Graphics g )
{
if ( offscreenImage != null )
g.drawImage( offscreenImage, 0, 0, this );
}

das Bild. Wohlgemerkt, dieser Funktionsaufruf ist der einzige in paint().


Galileo Computing

14.11.5 Bilder skalieren  downtop

Die Methode getScaledInstance() der Klasse Image gibt ein neues Image-Objekt mit größeren oder kleineren Ausmaßen zurück. Das neue Bild wird wieder nur dann berechnet, wenn es auch benötigt wird – das Verhalten ist also ebenso asynchron wie bei der gesamten Bildverwaltung über die Image-Klasse. Beim Vergrößern oder Verkleinern kommt es zu Pixelfehlern und das Vergrößern der Pixel beeinflusst das Endergebnis und die Geschwindigkeit. Stellen wir uns vor, ein Bild der Größe 100 x 100 Pixel soll um das doppelte vergrößert werden. Das Resultat ist ein Bild 200 x 200 Pixel, doch aus einem Bildpunkt muss nun die Information für drei weitere Punkte abgeleitet werden. Eine Lösung wäre, die Farbwerte der Punkte einfach zu duplizieren, dann bleibt die Schärfe, aber das Bild wirkt wie in groben Blöcken. Eine andere Möglichkeit wäre, die Farbinformationen für die neuen Punkte aus den Informationen der Nachbarpunkte zu errechnen. Das Bild wirkt glatter, aber auch etwas unschärfer bei hoher Skalierung. Und ebenso wie beim Vergrößern der Bilder sollte auch beim Verkleinern die Bildinformationen nicht einfach wegfallen, sondern, wenn möglich, zu neuen Farbwerten zusammengefasst werden. So erwarten wir von einem Algorithmus, dass dieser bei einer Schrumpfung von drei Farbwerten zu einem Farbwert, die drei Informationen zu einem neuen Wert zusammenlegt.

Damit diese Anforderungen erfüllt werden können, verlangt getScaledInstance() nicht nur die neue Breite und Höhe, sondern auch eine Konstante für die Art der Skalierung. Der Parameter bestimmt den Algorithmus und ist aus SCALE_DEFAULT, SCALE_FAST, SCALE_SMOOTH, SCALE_REPLICATE, SCALE_AREA_AVERAGING.

Tabelle 14.5   Parameter für getScaledImage()
Skalierungs-Parameter Bedeutung
SCALE_DEFAULT Verwendet einen Standard-Skalierungs-Algorithmus.
SCALE_FAST Verwendet einen Skalierungs-Algorithmus, der mehr Wert auf Geschwindigkeit als auf Glätte des Bildes legt.
SCALE_SMOOTH Verwendet einen Algorithmus mit guter Bildqualität und legt weniger Wert auf Geschwindigkeit.
SCALE_REPLICATE Benutzt für den Skalierungs-Algorithmus den ReplicateScaleFilter.
SCALE_AREA_AVERAGING Verwendet den AreaAveragingScaleFilter.

Mit Hilfe dieser Konstanten lässt sich die Funktion mit Parametern füllen:

class java.awt.Image

gp  Image getScaledInstance( int width, int height, int hints )
Liefert ein skaliertes Bild mit den neuen Ausmaßen width und height. Das neue Bild kann asynchron gefördert werden. hints gibt den Skalierungsalgorithmus als Konstante an. Ist die Höhe oder Breite negativ, so berechnet sich der Wert aus dem anderen, um das Seitenverhältnis beizubehalten.
Beispiel Betrachten wir ein paar Zeilen Quellcode, der eine Grafik lädt und zwei neue Image-Exemplare konstruiert.

Die erste Skalierung soll das Original um einen Prozentwert verändern, und die zweite Skalierung soll – unabhängig von der korrekten Wiedergabe der Seitenverhältnisse – das Bild auf die Größe des Bildschirms bringen. Wir wollen das Bild immer Image.SCALE_SMOTH skaliert haben.

String filename = "ottosHaus.jpg"
Image image = new ImageIcon( filename ).getImage(),

int percent = 175;

Image scaled1 =
image.getScaledInstance(
(image.getWidth() * percent) / 100,
(image.getHeight() * percent) / 100,
Image.SCALE_SMOTH );

Image scaled2 = image.getScaledInstance(
Toolkit.getDefaultToolkit().getScreenSize().width,
Toolkit.getDefaultToolkit().getScreenSize().height,
Image.SCALE_SMOTH );

Hinter den Kulissen

Was auf den ersten Blick so aussieht wie die Wahl zwischen unglaublich vielen Varianten entpuppt sich als typische Informatikerlösung: entweder schnell und schmutzig oder schön und lahm. Aber so ist nun mal das Leben. Der Quelltext macht dies deutlich:

public Image getScaledInstance(int 
width, int height, int hints)
{
ImageFilter filter;

if ((hints & (SCALE_SMOOTH | SCALE_AREA_AVERAGING)) != 0)
filter = new AreaAveragingScaleFilter(width, height);
else
filter = new ReplicateScaleFilter(width, height);

ImageProducer prod;
prod = new FilteredImageSource(getSource(), filter);

return Toolkit.getDefaultToolkit().createImage(prod);
}

Bei der Wahl zwischen sanftem Bild und schnellem Algorithmus wird auf die zwei Filterklassen AreaAveragingScaleFilter und ReplicateScaleFilter abgebildet. Sie berechnen jeweils das neue Bild über einen Bildproduzenten. ReplicateScaleFilter ist der Einfachere von beiden. Bei der Vergrößerung werden die Pixel einer Zeile oder Spalte einfach verdoppelt, wird verkleinert, werden einfach Reihen oder Spalten weggelassen. Mit einem AreaAveragingScaleFilter erhalten wir die besseren Resultate, da Pixel nicht einfach kopiert werden, sondern weil wir eingefügte Pixel aus einer Mittelwertberechnung erhalten. Der Algorithmus heißt im Englischen auch nearest neighbor algorithm.


Galileo Computing

14.12 Programmicon setzen  downtop

Unter Windows ist jedem Fenster ein kleines Bildchen zugeordnet, das links am Fenster neben den Menüs untergebracht ist. Dies ist ein Programm-Icon und es lässt sich in Java durch die setIconImage()-Funktion setzen. Der Methode wird ein Image-Objekt übergeben, welches die Grafik der Größe 16 ¥ 16 Pixel beinhaltet. Doch hier gilt, was für andere Bilder gilt: Durch einen Aufruf von getImage() wird eine Grafik zwar vorbereitet, aber noch nicht physikalisch geladen. Bei der drawImage()-Methode wird der Ladevorgang durchgeführt, setIconImage() könnte sich nun ähnlich verhalten – macht es aber nicht. Versuchen wir etwa nachstehenden Code, der direkt in der Erweiterung von Frame liegt, so führt

image = Toolkit.getDefaultToolkit().getImage( 
"image.gif" );
setIconImage( image );
show();

mitunter zum Absturz. Erstaunlicherweise kann die Vertauschung der zwei Zeilen setIcon Image() und show() korrekt verlaufen, ohne einen Laufzeitfehler zu produzieren. Wir müssen wieder mit der Funktion prepareImage() darauf achten, dass tatsächlich von der Datei oder vom Netz geladen wird. Erst dann dürfen wir setIconImage() aufrufen. Praktisch ist in diesem Falle jedoch die ImageIcon-Klasse, da sie automatisch das Bild lädt.

class java.awt.Frame
extends Window implements MenuContainer

gp  void setIconImage( Image )
Ordnet dem Fenster eine kleine Grafik zu. Nicht alle grafischen Oberflächen erlauben diese Zuordung, so ist dies bisher nur bei Microsoft Windows geläufig.

Die nachfolgende Applikation erstellt ein einfaches Fenster ohne großen Rahmen mit einem Programm-Icon.

Listing 14.13   IconImage.java
import java.awt.*;

public class IconImage
{
public static void main( String args[] )
{
Frame f = new Frame();
Toolkit tk = f.getToolkit();
Image image = tk.getImage( "BookIcon.gif" );

while ( !tk.prepareImage( image, -1, -1, f ) ) {
try {
Thread.sleep( 100 );
} catch ( Exception e ) {}
}
f.setIconImage( image );

f.show();
}
}

Galileo Computing

14.12.1 VolatileImage  downtop

Die unter 1.4 neu eingeführte abstrakte Klasse VolatileImage mit ihren Unterklassen bietet eine effiziente Möglichkeit, Bilder direkt im Speicher der Grafikkarte abzulegen, sofern dies vom Betriebssystem unterstützt wird. Eine Konsequenz ist, dass die Informationen dann natürlich verloren gehen, wenn beispielsweise ein Fenster den Inhalt überdeckt. Doch der Einsatz dieser direkten Manipulation liegt im Bereich von schnellen Bildschirmdarstellungen, wie sie zum Beispiel bei Spielen oder Video-Übertragungen nötig sind. Die VolatileImage-Objekte erkennen defekte Darstellungen und leiten sie an die Software weiter, die dann wiederum eine Bildschirmaktualisierung vornehmen kann.

Damit VolatileImage-Objekte erstellt werden können, gibt es die Methode createVolatileImage(w, h) in den Klassen Component und ComponentPeer. Da ein VolatileImage sich genauso wie ein Image-Objekt verhält, ändert sich am Zeichnen mit drawImage() nichts.

Mehr über die Einsatzgebiete und einer Beispielimplementierung findet sich unter der Webadresse ftp://ftp.java.sun.com/docs/j2se/1.4/VolatileImage.pdf.


Galileo Computing

14.13 Grafiken speichern  downtop


Galileo Computing

14.13.1 Bilder im GIF-Format speichern  downtop

Java bietet uns als nette Hilfe das Laden von GIF und JPG kodierten Grafiken an. Leider blieben Routinen zum Speichern in den einen oder anderen Dateityp auf der Strecke – und auch erst seit Java 1.2 hilft uns die Klasse JPEGImageEncoder beim Sichern von JPGs. Doch ist das Laden von GIF-Dateien überhaupt gestattet? Da UNISYS das Patent auf den Kompressionalgorithmus Welch-LZW für GIF-Dateien hält, ist es eine rechtliche Frage, ob wir UNISYS Geld für das Laden von GIF-Dateien zum Beispiel aus Applets bezahlen müssen. Auf die an UNISYS gestellte Frage »If I make an applet (for profit) which loads a GIF image using the Java API function, will I need a license from you?« antwortet Cheryl D. Tarter von UNISYS: »Yes, you need a license from Unisys.« Das heißt im Klartext, dass eigentlich alle bezahlen müssten. Eine weitere Anfrage an die für Lizenzen zuständige Stelle bestätigte dies. Mit einer Klage Seitens UNISYS ist jedoch nicht zu rechnen und beim Lesen von GIF-Dateien ist somit keine Gefahr zu erwarten. Wer jedoch Bibliotheken zum Schreiben von LZW komprimierten GIF-Dateien anbietet, sollte vorsichtig sein. Der Patentinhaber ist im Jahr 2000 dazu übergegangen, von Betreibern von Webseiten pauschal 5.000 Dollar Lizenzgebühren einzufordern, wenn sie nicht nachweisen können, dass die verwendeten GIF-Grafiken mit lizensierter Software erstellt wurden. Eine nette Webseite zu dem Thema findet sich unter http://burnallgifs.org.

Der GIFEncoder von Adam Doppelt

Bei der schwierigen Lizenzfrage von GIF ist das schon verständlich, aber doch nicht minder tröstend, wenn wir einmal eine Routine brauchen. Um Problemen aus dem Weg zu gehen, hat Sun also gleich die Finger von einer GIF-Sichern Routine gelassen bzw. hat eine Speicherroutine ohne Komprimierung implementiert. Um dennoch ohne zusätzliche Bibliotheken eine GIF-Datei im GIF87a Format non-interlaced zu sichern, hat Adam Doppelt (E-Mail: amd@marimba.com) die Klasse GIFEncoder geschrieben, die es gestattet, beliebige Image-Objekte oder Bytefelder zu speichern. (Der Java Quellcode basiert auf dem Programm gifsave.c von Sverre H. Huseby (sverrehu@ifi.uio.no).) Die Klasse liegt zum Beispiel unter http://www.gurge.com/amd/old/java/ GIFEncoder/index.html.

Um Daten zu sichern, wird ein Exemplar der GIFEncoder-Klasse angelegt. Die Klasse besitzt zwei Konstruktoren, wobei entweder ein geladenes Image-Objekt gesichert werden kann oder drei Felder mit den RGB-Werten. Über die Write()-Funktion der Klasse wird die Datei dann in einen Ausgabestrom geschrieben. Dieser sollte gepuffert sein, da die Kodierung sowieso schon lange genug dauert. Folgende Zeilen leisten das Gesuchte:

GIFEncoder encode = new GIFEncoder( 
image );
OutputStream output = new BufferedOutputStream(
new FileOutputStream( "DATEI" ) );
encode.Write( output );

Da beim herkömmlichen GIF-Format die Bilder nicht mehr als 256 Farben besitzen können (GIF24 behebt das Problem, ist aber nicht sehr verbreitet), müssen 24 Bit Grafiken umgewandelt werden. Hier wird ein Quantization-Algorithmus verwendet. Eine Referenz findet der Leser auf der Web Seite von Adam Doppelt. Die API-Dokumentation ist jedoch hier etwas widersprüchlich, da der Autor angibt, ein Bild mit mehr als 256 Farben würde eine AWTException ergeben.

class GIFEncoder

gp  GIFEncoder( byte r[][], byte g[][], byte b[][] )
Erzeugt ein GIFEncoder-Objekt aus drei Feldern mit getrennten roten, grünen und blauen Farben. Somit bezieht sich etwa r[x][y] auf die Rotintensität des Pixels in der Spalte x und Zeile y.
gp  GIFEncoder( Image )
Erzeugt ein GIFEncoder-Objekt aus einem Image-Objekt.
gp  void Write( OutputStream) throws IOException
Schreibt das Bild in den Dateistrom.
Listing 14.14   giftest.java
import java.awt.*;
import java.io.*;
import java.net.*;

// This app will load the image URL given as the first argument, and
// save it as a GIF to the file given as the second argument. Beware
// of not having enough memory!

public class giftest
{
public static void main(String args[]) throws Exception {
if (args.length != 2) {
System.out.println("giftest [url to load] [output file]");
return;
}

// need a component in order to use MediaTracker
Frame f = new Frame("GIFTest");
// load an image
Image image = f.getToolkit().getImage(new URL(args[0]));

// wait for the image to entirely load
MediaTracker tracker = new MediaTracker(f);
tracker.addImage(image, 0);
try
tracker.waitForID(0);
catch (InterruptedException e);
if (tracker.statusID(0, true) != MediaTracker.COMPLETE)
throw new AWTException("Could not load: "+args[0]+" "+
tracker.statusID(0, true));

// encode the image as a GIF
GIFEncoder encode = new GIFEncoder(image);
OutputStream output = new BufferedOutputStream(
new FileOutputStream(args[1]));
encode.Write(output);

System.exit(0);
}
}

Ganz unproblematisch ist die Klasse von Adam Doppelt nicht. Da Image-Objekte komplett im Speicher liegen müssen, bekommt GIFEncoder schon mal Probleme mit großen Bildern. So kann etwa folgende Fehlermeldung auftreten:

java.awt.AWTException: Grabber returned false: 192.

Galileo Computing

14.13.2 Gif speichern mit dem ACME-Paket  downtop

Jef Poskanzer, bekannt ist auch seine Firma ACME Laboratories, hat ebenfalls einen GIF- und PPM-Konverter veröffentlicht. Eine Beschreibung des GIF-Konverters im JavaDoc Format liegt unter http://www.acme.com/java/software/Acme.JPM.Encoders.GifEncoder.html und für das PPM-Format heißt die HTML-Datei »Acme.JPM.Encoders.PpmEncoder.html«. Auf den Seiten finden sich auch die Links zum Download der Java-Klassen. Diese liegen im Quellcode vor und müssen von uns compiliert werden. Der Vorteil ist, dass wir die Paketanweisung ändern können, sodass die Klasse auf unsere Paket-Struktur angepasst werden kann. So schön die Klasse auch ist, sie hängt leider noch von der Klasse ImageEncoder ab, sodass hier gleich mehrere Klassen installiert werden müssen. Die Alternative von Adam Doppelt bietet den Vorteil, dass hier nur eine Klasse eingesetzt wird. Die ACME-Klassen haben jedoch den Vorteil, dass das Bild auch von einem ImageProducer erzeugt werden kann und dass das Bild dann auch interlaced sein darf.


Galileo Computing

14.13.3 JPEG-Dateien mit dem Sun-Paket schreiben  downtop

Da es rechtliche Probleme mit dem GIF-Format beim Schreiben gibt, wollte Sun keine Lizenzen zahlen und hat sich gegen Schreibmethoden entschieden. JPEG dagegen ist vom Komitee Joint Photographic Experts Group als freies Format für Bildkompressionen entworfen worden. Daher haben sich die Entwickler der Java-Bibliotheken für JPEG-Klassen zum Kodiereren und Enkodieren (auch Dekodieren genannt) entschieden. Sie sind (noch) nicht in den Core-APIs eingebunden, sondern liegen im Paket com.sun.image.codec.jpeg, das nur Teil des JDK bzw. JRE von Sun ist und somit nur von Lizenznehmern zusätzlich angeboten wird. Alternative Bibliotheken sind dann nicht mehr nötig. Und da auch JPG nicht komprimierend (allerdings immer noch mit einer leichten Farbverfälschung) speichern kann, bietet es sich als Alternative zur GIF an.

Damit wir mit JPEG-Bildern arbeiten können, benötigen wir einen Decoder. Dazu liefert die Fabrikmethode JPEGCodec.createJPEGEncoder() ein JPEGImageEncoder-Objekt. JPEGImageEncoder selbst ist eine Schnittstelle, die JPEG-Dateien liest oder im Falle von JPEG ImageDecoder schreibt. Dazu verwendet die Klasse intern einen Datenpuffer, der vom Typ BufferedImage sein muss. BufferedImage ist eine Erweiterung der Image-Klasse. Transparenz ist für die Bilder nicht erlaubt. Mit einem konkreten Objekt können dann die Image-Daten geschrieben werden. Dazu ist nur ein beliebiges OutputStream-Objekt nötig.

JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder( out );
encoder.encode( img );

Diese beiden Zeilen schreiben ein JPG. Einfacher kann dies nicht sein.

JPEG-Bilder sind im Gegensatz zu GIF-Bildern verlustkomprimiert. Über eine diskrete Kosinustransformation werden 8 x 8 große Pixelblöcke vereinfacht. Die Komprimierung nutzt die Unfähigkeit des Auges aus, Farbunterschiede so stark wahrzunehmen wie Helligkeitsunterschiede. So können Punkte, die eine ähnliche Helligkeit, aber eine andere Farbe besitzen, zu einem Wert werden. Bei einer hohen Kompression treten so genannte Artefakte (engl. auch Degradation genannt) auf, die unschön wirken. Bei einer sehr hohen Kompression ist das Bild sehr klein (aber auch hässlich).

Um nun noch die Qualität des Bildes einzustellen, wird eine Schnittstelle JPEGEncodeParam eingeführt. Das Encoder-Objekt bietet die Methode getDefaultJPEGEncodeParam() an, mit der wir an die Standard-Parameter kommen. Das Einstellen der Qualität geht über die Methode setQuality(qualiy, true).

JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img);
param.setQuality( qualiy, true );

Der Qualitätsfaktor ist ein Float und kann sich zwischen 0 und 1 bewegen. 1 bedeutet im Prinzip keine Kompression und somit höchste Qualität. Ein Wert um 0,75 ist ein hoher Wert für Qualitätsbilder, der Wert 0,5 für mittlere Bilder und 0,25 für stärkere Artefakte und hohe Kompression.

Bilder in verschiedenen Kompressionsstufen speichern

Wir wollen nun ein Programm entwickeln, das eine Zufallsgrafik aus gefüllten Rechtecken erzeugt und in den Qualitätsstufen 1,0 bis 0,0 in 0,25 Schritten speichert.

Listing 14.15   CodecDemo.java
import java.io.*;
import java.awt.*;
import java.text.*;
import java.awt.image.*;
import com.sun.image.codec.jpeg.*;

class JPEGCodecDemo
{
public static void main( String args[] ) throws Exception
{
int n = 400;

BufferedImage img = new BufferedImage( n, n,
BufferedImage.TYPE_INT_RGB );

// Placebografik anlegen

Graphics g = img.getGraphics();

g.setColor( Color.white );
g.fillRect( 0, 0, n-1, n-1 );

for ( int i=0; i<100; i++ )
{
g.setColor( new Color( (int)(Math.random()*256),
(int)(Math.random()*256), (int)(Math.random()*256) ) );

g.fillRect( (int)(Math.random()*n), (int)(Math.random()*n),
(int)(Math.random()*n/2), (int)(Math.random()*n/2) );
}

g.dispose();

// Bild in ein Array schreiben

int size = 0;

for ( float quality = 1f; quality >= 0; quality -= 0.25 )
{
ByteArrayOutputStream out = new ByteArrayOutputStream( 0xfff );

JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder( out );

JPEGEncodeParam param;
param = encoder.getDefaultJPEGEncodeParam( img );

param.setQuality( quality, true );
encoder.encode( img, param );

FileOutputStream fos = new FileOutputStream("JPG"+quality+".jpg");
fos.write( out.toByteArray() );
fos.close();
out.close();

System.out.print( "Quality: " + quality +
" Size: " + out.size() + "k " +
" Ratio: " );

size = (size == 0 ) ? size = out.size() : size ;

DecimalFormat df = new DecimalFormat( "##.##%" );
float ratio = (float)out.size()/size;
System.out.println( df.format(ratio) );
}
}
}

Die Ausgabe des Programms für ein Bild ist etwa Folgendes:

Quality: 1.0 Size: 34636k  Ratio: 
100%
Quality: 0.75 Size: 14573k Ratio: 42,07%
Quality: 0.5 Size: 11366k Ratio: 32,82%
Quality: 0.25 Size: 8586k Ratio: 24,79%
Quality: 0.0 Size: 4336k Ratio: 12,52%

Da die Zufallsgrafik immer anders aussieht, werden natürlich auch die Dateigrößen immer anders aussehen. Es lässt sich ablesen, dass beispielsweise eine Datei mit einem Qualitätsfaktor 0,75 etwa 42 % der Größe der Ursprungsdatei entspricht.


Galileo Computing

14.13.4 Java Image Management Interface (JIMI)  downtop

JIMI (Java Image Management Interface) ist eine 100 %-ige Java-Klassenbibliothek, die hauptsächlich Lade- und Speicherroutinen für Bilder zur Verfügung stellt. Die Klasse JimiUtils stellt beispielsweise eine getThumbnail()-Methode bereit, die zu einer Datei ein Vorschaubild als Image-Objekt berechnet. Ebenso stellt JIMI Möglichkeiten zur Anzeige bereit, um etwa sehr große Grafiken speichersparend zu verwalten. Diese Technik nennt sich Smart-Scrolling und kann von der JimiCanvas-Komponente übernommen werden. So wird nur der Bildteil im Speicher gehalten, der gerade sichtbar ist. Für die Speicherverwaltung stellt JIMI ein eigenes Speicherverwaltungssystem, das VMM (Virtual Memory Management) bereit, ebenso wie eine eigene Image-Klasse, die schnelleren Zugriff auf die Pixelwerte erlaubt. Zusätzlich bietet JIMI eine Reihe von Filtern für Rotation, Helligkeitsanpassung, die auf JIMI und AWT-Bildern arbeiten. Auch Farbreduktion ist ein Teil von JIMI. JIMI-Bilder lassen sich im Gegensatz zu den bekannten AWT-Bildern serialisieren.

Ursprünglich vertrieb Activated Intelligence das Paket, doch Sun stellt es für die Allgemeinheit unter http://java.sun.com/products/jimi zur Verfügung. Die von JIMI unterstützten Formate sind vielfältig: Activated Pseudo Format (APF), BMP, Windows .ico-Format (CUR und ICO), GIF (nicht komprimierend), JPEG, Windows .pcx-Format für Paintbrush-Dateien (PCX), Portable Network Graphics (PNG), PICT, Adobe Photoshop (PSD), Sunraster, Targa (TGA), Tag Image File Format (TIFF), X-BitMap und X-Pixmap (XBM, XPM). Nicht für alle Formate gibt es gleichfalls Dekodierer und Kodierer. Ein Teil der Kodierer und Dekodierer befindet sich schon in der Java Advanced Imaging API. Das Paket in der Jai ist com.sun.media.jai.codec. Längerfristig stellt sich die Frage, ob JIMI in JAI integriert wird oder ob es ein Extrapaket bleiben wird.

Die Installation einer Java-Bibliothek ist immer ganz einfach, so auch bei der JIMI-Bibliothek. Die Datei Jimi/JimiProClasses.zip muss im Pfad aufgenommen werden, und dann können schon in den Java-Programmen die Klassen genutzt werden.

Listing 14.16   JimiDemo.java
import java.awt.*;
import java.awt.image.*;
import com.sun.jimi.core.Jimi;

public class JimiDemo
{
public static void main( String args[] ) throws Exception
{
// Bild erzeugen

BufferedImage image = new BufferedImage( 500, 500,
BufferedImage.TYPE_3BYTE_BGR );

// Bild bemalen

Graphics g = image.getGraphics();

for ( int i=0; i<2000; i++ )
{
int x = rand(500), y=rand(500);

g.setColor( new Color(rand(256*32)) );
g.drawRect( x, y, rand(500)-x, rand(500)-y );
}

g.dispose();


// Bild speichern

String mimes[] = { "bmp","pcx","png","psd","tga","xbm" };
// "jpg" funktioniert so nicht.
// für gif, tiff gibt es keinen Encoder
// xpm kodiert nur paletten-basierte Grafiken

for ( int i=0; i<mimes.length; i++ )
{
String mime = "image/"+mimes[i];
String filename = "JimiDemoGfx." + mimes[i];

System.out.print( "Saving " + filename + "..." );
Jimi.putImage( mime, image, filename );
System.out.println( "done" );
}
System.exit( 0 );
}

private static int rand( int max )
{
return (int) (Math.random()*max);
}
}

Galileo Computing

14.14 Von Produzenten, Konsumenten und Beobachtern  downtop

Bisher kamen die angezeigten Grafiken irgendwie vom Datenträger auf den Schirm. Im Folgenden wollen wir dies etwas präziser betrachten. An den verschiedensten Stellen haben wir bereits von der Eigenschaft der drawImage()-Methode gesprochen, erst bei der ersten Benutzung das Bild zu laden. In Java kommt hinter den Kulissen ein Modell zum Tragen, welches komplex, aber auch sehr leistungsfähig ist. Es ist das Modell vom Erzeuger (engl. Producer) und Verbraucher (engl. Consumer). Ein Beispiel aus der realen Welt: Katzenpfötchen wird von Katjes produziert und von mir konsumiert. Oder etwas technischer: Ein Objekt, das vom Netzwerk eine Grafik holt oder auch ein Objekt, das aus einem Array mit Farbinformationen das Bild aufbaut. Ist was? Der Konsument ist die Zeichenfunktion, die das Bild darstellten möchte.

Abbildung


Galileo Computing

14.14.1 Producer und Consumer für Bilder  downtop

Ein besonderer Produzent, der sich um alles kümmert, was das Bilderzeugen angeht, ist der Image Producer. Im Gegensatz dazu sind es die Image Consumer, die etwaige Bilddaten benutzen. Zu diesen Bild Konsumenten zählen in der Regel Low-Level Zeichenroutinen, die auch die Grafik auf den Schirm bringen. In der Bibliothek von Java ist die Aufgabe der Bildproduzenten und -konsumenten durch die Schnittstelle ImageProducer und ImageConsumer abgebildet. Das Interface ImageProducer beschreibt Methoden, um Pixel eines Bildes bereitzustellen. Klassen, die nun die Schnittstelle implementieren, stellen somit die Bildinformationen einer speziellen Quelle da. Die Klasse MemoryImageSource ist eine vorgefertigte Klasse, die ImageProducer implementiert. Sie produziert Bildinformationen aus einem Array von Pixeln, die im Speicher gehalten werden.

Im Gegenzug beschreibt die Schnittstelle ImageConsumer Methoden, die einem Objekt den Zugriff auf die Bilddaten des Produzenten erlauben. Objekte, die ImageConsumer implementieren, hängen somit immer an einem Bilderzeuger. Der Produzent liefert die Daten über Methoden zum Konsumenten, indem spezielle, im Interface ImageConsumer vorgeschriebene Methoden aufgerufen werden.


Galileo Computing

14.14.2 Beispiel für die Übermittlung von Daten  downtop

Damit für uns das Verfahren deutlich wird, beschreiben wir zunächst das Prinzip der Übermittlung von Daten vom Produzenten zum Konsumenten an einem Beispiel. Wir entwickeln eine Klasse Produzent mit einer Methode beginne() und eine Klasse Konsument, die vom Produzenten Daten haben möchte. Wenn der Produzent etwas für den Konsumenten erzeugen soll, dann ruft der Konsument die erzeugeFür()-Routine mit einem Verweis auf sich auf. Danach ruft der Konsument die Funktion beginne() auf. Über diesen Verweis an erzeugeFür() weiß dann der Produzent, an wen er die Daten schicken muss. Nach dem Aufruf von beginne() sendet der Produzent an alle Konsumenten die Daten, in dem er die Methode briefkasten() aller Konsumenten aufruft und somit die Daten abliefert.

class Konsument
{
irgendwo()
{
Produzent nudeln
nudeln.erzeugeFür( this )
nudeln.beginne()
}

briefkasten( int data )
{
ausgabe( "Ich habe ein " + data + " bekommen" )
}
}

class Produzent
{
erzeugeFür( Konsument einKonsument )
{
// merke sich alle Konsumenten in einer Liste
}

beginne()
{
data = erzeugeDatum()
für alle interessierten Konsumeten
konsument.briefkasten( data )
}
}

Wie der ImageProducer dem ImageConsumer die Daten beschreibt

Das Interface ImageProducer benutzt die Methode setPixels() im ImageConsumer, um das Bild dem Konsumenten zu beschreiben. Ein gutes Beispiel für das Modell ist das Laden eines Bildes über ein Netzwerk. So verlangt etwa die Zeichenfunktion drawImage() das Bild. Nehmen wir eine konkrete Klasse an, die ein Bild laden kann. Diese implementiert natürlich dann das Interface ImageProducer. Zunächst beginnt dann die Klasse mit dem Lesevorgang, indem sie eine Netzwerkverbindung aufbaut und einen Kommunikationskanal öffnet. Das Erste, was das Programm dann vom Server liest, ist die Breite und Höhe des Bildes. Seine Informationen über die Dimension berichtet es dem Konsumenten mit der Methode setDimensions(). Uns sollte bewusst sein, dass es zu einem Produzenten durchaus mehrere Konsumenten geben kann. Korrekter heißt das: Die Information über die Dimension wird zu allen horchenden Konsumenten gebracht.

Abbildung

Als Nächstes liest der Produzent die Farbinformationen für das Bild. Über die Farbtabelle findet er heraus, welches Farbmodell das Bild benutzt. Dies teilt er über den Aufruf von setColorModel() jedem Konsument mit. Danach lassen sich die Pixel des Bildes übertragen. Die verschieden Formate nutzen dabei allerdings unterschiedliche Techniken. Sie heißen Hints. Die Übermittlung der Hints an den Konsumenten geschieht mit der Methode setHints(). Jeder Konsument kann daraufhin seine Handhabung mit den Bildpunkten optimieren. So könnte etwa ein Konsument, der ein Bild skalieren soll, genau in dem Moment die Bildzeile skalieren und die Werte neu berechnen, während der Produzent eine Zeile erzeugt. Mögliche Werte für die Hints sind Folgende:

interface java.awt.image.ImageConsumer

gp  ImageConsumer.TOPDOWNLEFTRIGHT
Die Pixellieferung ist von oben nach unten und von links nach rechts.
gp  ImageConsumer.COMPLETESCANLINES
Mehrere Zeilen (Scanlines) bauen das Bild auf. Eine Scanline besteht aus mehreren Pixeln, die dann in einem Rutsch anliegen. Es wird also sooft setPixels() aufgerufen wie es Bildzeilen gibt.
gp  ImageConsumer.SINGLEPASS
Die Pixel des gesamten Bildes können wir nach einem Aufruf von setPixels() erwarten. Niemals liefern mehrere Aufrufe dieselben Bildinformationen. Ein progressives JPEG-Bild fällt nicht in diese Kategorie, da es ja in mehreren Durchläufen erst komplett vorliegt.
gp  ImageConsumer.SINGLEFRAME
Das Bild besteht aus genau einem statischen Bild. Ein Programm, welches also nicht schrittweise Zeilen zur Verfügung stellt, benutzt dieses Flag. Der Consumer ruft also einmal setPixels() vom Producer auf und danach steht das Bild bereit. Ein Bild aus einer Videoquelle würde, da es sich immer wieder ändert, niemals SINGLEFRAME sein.
gp  ImageConsumer.RANDOMPIXELORDER
Die Bildpunkte kommen in beliebiger Reihenfolge an. Der ImageConsumer kann somit keine Optimierung vornehmen, die von der Reihenfolge der Pixel abhängt. Ohne Bestätigung einer anderen Reihenfolge müssen wir von RANDOMPIXELORDER ausgehen. Erst nach Abschluss durch einen Aufruf von imageComplete() – siehe unten – lässt sich mit dem Bild weiterarbeiten.
Abbildung

Nun kann der Producer anfangen, mittels setPixels() Pixel zu produzieren. Da der Produzent die setPixels()-Methode aufruft, die im Consumer implementiert ist, wird der Konsument dementsprechend all den Programmcode enthalten, der die Bildinformationen benötigt. Entsprechend erinnern wir uns an die Methode briefkasten() aus unserem ersten Beispiel.

Wir haben oben nur das erlangte Datum ausgegeben. Ein wirklicher Konsument allerdings sammelt alle Daten bis das Bild geladen ist, und verwendet es dann weiter, indem er es zum Beispiel anzeigt. In der Regel ist erst nach vielen Aufrufen das Bild aufgebaut, und zwar genau dann, wenn der Consumer jeweils nur eine Zeile des Bildes liefert. Es kann aber auch nur ein Aufruf genügen, nämlich genau dann, wenn das Bild in einem Rutsch geliefert wird (ImageConsumer.SINGLEPASS).

Nachdem das Bild geladen ist, ruft der Producer die imageComplete()-Methode für den Konsumenten auf, um anzuzeigen, dass das Bild geladen ist. Nun sind also keine Aufrufe mehr für setPixels() möglich, um das Bild vollständig zu erhalten. Der Methode imageComplete() wird immer ein Parameter übergeben und für ein gültiges Bild ist der Parameter ImageConsumer.STATICIMAGEDONE. Auch Multi-Frames-Images (etwa Animated GIF) ist dies gestattet und zeigt an, dass das letzte Bild der Sequenz geladen ist. Besteht das Bild aus mehreren Teilen, es folgen aber noch weitere Frames, ist der Parameter SINGLEFRAMEDONE. Hier zeigt SINGLEFRAMEDONE also nur den Abschluss eines Einzelbildes an. Über setHints() ist dann aber schon ein Multi-Frame angekündigt gewesen.

Mehrere Fehler können beim Produzieren auftreten. IMAGEERROR bzw. IMAGEABORT zeigen an, dass ein schwerer Fehler auftrat und das Bild nicht erzeugt werden konnte. Die Unterscheidung der beiden Fehlerquellen ist nicht eindeutig.

interface java.awt.image.ImageConsumer

gp  void imageComplete( int status )
Wird aufgerufen, wenn der ImageProducer alle Daten abgeliefert hat. Auch, wenn ein einzelner Rahmen einer Multi-Frame-Animation beendet ist oder ein Fehler auftrat.
gp  void setColorModel( ColorModel model )
Das ColorModel bestimmt wie setPixels() die Pixelinformationen wertet.
gp  void setDimensions( int width, int height )
Die Ausmaße der Bildquelle.
gp  void setHints( int hintflags )
Reihenfolge der Bildinformationen.
gp  void setPixels( int x, int y, int w, int h, ColorModel model, byte[] pixels, int off, int scansize)
Die Bildpunkte werden durch einen oder mehrer Aufrufe der Funktion überliefert.
gp  void setPixels( int x, int y, int w, int h, ColorModel model, int[] pixels, int off, int scansize )
Die Bildpunkte werden durch einen oder mehrer Aufrufe der Funktion überliefert.
gp  void setProperties( Hashtable props )
Setzt eine Liste von Eigenschaften, die mit dem Bild verbunden sind. Dies kann etwa eine Zeichenkette über den Bilderzeuger sein, die Geschwindigkeit eines Bildaufbaus oder die Information, wie viel Konsumenten an einem Produzenten hängen können.

Galileo Computing

14.14.3 Bilder selbst erstellen  downtop

Bisher haben wir über unsere bekannten Zeichenfunktionen wie drawLine() usw. auf die Oberfläche gezeichnet. Die paint()-Methode gab uns den Grafikkontext in die Hand, mit dem wir die Operation durchführen konnten. Nun kann es aber von Vorteil sein, wenn wir direkt in eine Zeichenfläche malen könnten und nicht immer über die Elementarfunktionen gehen müssten. Es ist intuitiv klar, dass dieser Weg bei bestimmten Grafikoperationen schneller ist. So können wir nicht existierende Grafikfunktionen – beispielsweise eine weiche Linie – durch Punktoperationen direkt auf dem Raster durchführen, ohne immer die drawLine()- und setColor()-Funktionen für einen Punkt zu bemühen. Wesentlich schneller sind wir wieder mit Bildern im Hintergrund, so wie wir im letzten Abschnitt flackerfreie Bilder produzierten.

Was wir dazu brauchen, ist eine Klasse aus dem awt.image-Paket, nämlich MemoryImageSource.

Listing 14.17   MemImage.java
import java.awt.*;
import java.awt.image.*;

class MemImage extends Frame
{
final static int a = Color.white.getRGB();
final static int b = Color.black.getRGB();
final static int c = Color.yellow.getRGB();
int imageData[] = {
a,a,a,a,a,a,a,a,a,a,b,b,b,b,b,b,b,b,b,b,b,a,a,a,a,a,a,a,a,a,a,a,
a,a,a,a,a,a,a,b,b,b,b,b,c,c,c,c,c,c,c,b,b,b,b,b,a,a,a,a,a,a,a,a,
a,a,a,a,a,b,b,b,c,c,c,c,c,b,c,c,c,b,c,c,c,c,c,b,b,b,a,a,a,a,a,a,
a,a,a,b,b,b,c,c,b,b,c,c,c,b,b,b,b,b,c,c,c,b,b,c,c,b,b,b,a,a,a,a,
a,a,b,b,c,c,c,b,b,c,c,c,c,b,c,b,c,b,c,c,c,c,b,b,c,c,c,b,b,a,a,a,
a,b,b,c,c,b,b,b,b,c,c,c,c,b,b,b,b,b,c,c,c,c,b,b,b,b,c,c,b,b,a,a,
a,b,c,b,b,b,b,b,b,c,c,c,c,b,b,b,b,b,c,c,c,c,b,b,b,b,b,b,c,b,a,a,
b,b,c,b,b,b,b,b,b,b,c,c,b,b,b,b,b,b,b,c,c,b,b,b,b,b,b,b,c,b,b,a,
b,c,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,c,b,a,
b,c,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,c,b,a,
b,c,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,c,b,a,
b,c,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,c,b,a,
b,c,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,c,b,a,
b,b,c,b,b,b,b,c,c,b,b,b,b,b,b,b,b,b,b,b,b,b,c,c,b,b,b,b,c,b,b,a,
a,b,c,b,b,b,c,c,c,c,b,c,c,b,b,b,b,b,c,c,b,c,c,c,c,b,b,b,c,b,a,a,
a,b,b,c,c,b,c,c,c,c,b,c,c,c,b,b,b,c,c,c,b,c,c,c,c,b,c,c,b,b,a,a,
a,a,b,b,c,c,b,c,c,c,c,c,c,c,c,b,c,c,c,c,c,c,c,c,b,c,c,b,b,a,a,a,
a,a,a,b,b,b,c,c,c,c,c,c,c,c,c,b,c,c,c,c,c,c,c,c,c,b,b,b,a,a,a,a,
a,a,a,a,a,b,b,b,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,b,b,b,a,a,a,a,a,a,
a,a,a,a,a,a,a,b,b,b,b,b,c,c,c,c,c,c,c,b,b,b,b,b,a,a,a,a,a,a,a,a,
a,a,a,a,a,a,a,a,a,a,b,b,b,b,b,b,b,b,b,b,b,a,a,a,a,a,a,a,a,a,a,a
};

Image icon;
MemImage()
{
super( "Mit freundlicher Unterstuetzung von..." );
icon = createImage(
new MemoryImageSource( 32, 21, imageData, 0, 32 ) );
setBackground( Color.white );
setSize( 300, 300 );
show();
}
public void paint( Graphics g )
{
g.drawImage( icon, 100, 100, 64, 64, this );
}
public static void main( String args[] )
{
MemImage mi = new MemImage();
}
}

Bildpunkte ansprechen

Zunächst wird das Bild im Speicher, dass heißt, in einem Integer-Feld gezeichnet. So bereiten die drei Zeilen

int breite   = 100;
int höhe = 100;
int pixels[] = new int[ breite * höhe ];

ein Ganzzahl-Array mit 100 x 100 Bildpunkten vor.

Die Farben sind durch die Grundfarben Rot, Grün und Blau in den Abstufungen 0 bis 255 gegeben. Sie setzen sich zu einer 24 Bit-Zahl zusammen, die den Farbwert repräsentiert. Jede Farbe hat eine eigene Position.

pixels[ x*width + y ] = (r << 
24) | (g << 16) | b;

Wir dürfen allerdings nicht vergessen, die Transparenz auf 0xFF zu setzen.

Die Methode, die aus den Rot-, Grün- und Blau-Werten das Integer berechnet, welches in das Array positioniert wird, lässt sich aus oberer Zeile ableiten:

static public int rgbToInt( byte 
r, byte g, byte b ) {
return (int)(0xFF000000 + r<<16 + g<<8 + b );
}

Nachdem das Feld mit Inhalt gefüllt ist, machen wir daraus einen ImageProducer. Dies ist ein Interface für Objekte, die aus Daten ein Image erzeugen können. Ein ImageProducer kann ein Bild immer neu konstruieren, wenn es benötigt wird, beispielsweise wenn sich die Breite oder Höhe ändert, da das Bild umskaliert wurde. Nachdem wir einen ImageProducer für unser handberechnetes Bild haben, müssen wir es natürlich noch in ein Image umwandeln, damit wir es zeichnen können. Die Umwandlung wird mit einer Funktion createImage() vorgenommen; sie erzeugt aus dem ImageProducer ein wirkliches Image. Die Zeile verdeutlicht dies:

Image img = createImage(
new MemoryImageSource(breite, höhe, pixels, 0, breite) );

Dieses Image-Objekt kann nun wieder mit drawImage() gezeichnet werden.

class java.awt.image.MemoryImageSource
implements ImageProducer

gp  MemoryImageSource( int Breite, int Höhe, int Feld[], int Verschiebung, int
Scanline )
Erzeugt eine ImageProducer-Objekt, das aus einem Integer-Feld ein Image-Objekt erzeugt. Die Elemente des Arrays repräsentieren die RGB-Farben des aktuellen ColorModels. Scansize ist der Abstand von einer Pixel-Zeile zur nächsten im Feld.
class java.awt.Component
implements ImageObserver, MenuContainer, Serializable

gp  Image createImage( ImageProducer )
Erzeugt ein Image vom angegebenen ImageProducer.
Abbildung

Das folgende Programm erzeugt ein Feld mit Farben, die von Schwarz nach Blau auf der X-Achse und von Schwarz nach Rot auf der Y-Achse verlaufen. Anschließend wird die Grafik auf den Schirm gebracht.

Listing 14.18   MemoryImage.java
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

public class MemoryImage extends Frame
{
final int width = 400;
final int height = 400;
int pixels[] = new int[width * height];
Image img;

public MemoryImage() {
setTitle("ImageViewer");
setSize( width + 20, height + 40);
addWindowListener(new WindowAdapter() {
public void windowClosing ( WindowEvent e) {
System.exit(0);
}
});
drawPoints();
}

private void drawPoints() {
int r=0, g=255, b=0,
index = 0;

for ( int y = 0; y < height; y++ ) {
r = (y * 255) / (width – 1);
for ( int x = 0; x < width; x++ ) {
b = (x * 255) / (width – 1);
pixels[index++] = (g << 24) | (r << 16) | b;
}
}
img = createImage(new MemoryImageSource( width, height,
pixels, 0, width) );
}

public void update( Graphics g ) {
paint( g );
}

public void paint( Graphics g ) {
g.drawImage( img, 10, 30, this );
}

public static void main( String args[] ) {
MemoryImage f = new MemoryImage();
f.show();
}
}
Abbildung 14.7   Ein aus dem Speicher erzeugtes Bild
Abbildung


Galileo Computing

14.14.4 Die Bildinformationen wieder auslesen  downtop

Das letzte Beispiel zeigt den Weg in die eine Richtung; das Bild wird konstruiert und dann in ein Image konvertiert. Doch auch der umgekehrte Weg ist notwendig und muss beispielsweise beim Speichern eines Bildes verwendet werden. Auch Grafikfilter müssen Farben von existierenden Bildpunkten mit in die Berechnung hineinbringen.

Das Gegenteil vom MemoryImageSource ist ein PixelGrabber. Dieser wird auf ein Image-Objekt angesetzt und füllt ein Ganzzahl-Feld mit den Farbwerten, die die Anteile der Farben Rot, Grün und Blau enthalten.

Beispiel Bilddaten aus einer Grafik auslesen und in ein Feld legen
PixelGrabber grabber =
new PixelGrabber( image,0,0,breite,höhe,pixels,0,breite );

Das Auslesen der Farbwerte wird durch die Funktion grabPixels() initiiert. So füllt

grabber.grabPixels();

unser Feld image mit den Farbwerten. Der PixelGrabber implementiert die Klasse Image-Consumer, die allgemein Bilder »verbraucht«. Die Farben werden ausgelesen, in dem wir wieder das Feld mit RGB-Farben entschlüsseln.

int alpha = (pixel >> 24) 
& 0xff;
int red = (pixel >> 16) & 0xff;
int green = (pixel >> 8) & 0xff;
int blue = (pixel) & 0xff;
class java.awt.image.PixelGrabber
implements ImageConsumer

gp  PixelGrabber( Image, int x, int y, int Breite, int Höhe,
int Feld[], int Verschiebung, int Scansize )
Erzeugt ein PixelGrabber-Objekt, welches ein Rechteck von RGB-Farben aus dem Feld holt. Das Rechteck ist durch die Ausmaße x, y, Breite, Höhe beschrieben. Die Farben für einen Punkt (i,j) sind im Feld an der Position (j – y) * Scansize + (i – x) + Verschiebung. Mit der Umwandlung wird noch nicht begonnen. Sie muss mit der Funktion grabPixles anregt werden.
gp  boolean grabPixels() throws InterruptedException
Die Werte von einem Image oder ImageProducer werden geholt. Da das Kodieren einige Zeit in Anspruch nimmt, kann die Funktion von außen unterbrochen werden. Daher ist eine try-Anweisung notwendig, die InterruptedException abfängt. Geht alles gut, wird true zurückgegeben.
gp  int getHeight()
Liefert die Höhe des Pixelfelds. Ist die Höhe nicht verfügbar, ist das Ergebnis –1.
gp  int getWidth()
Liefert die Breite eines Pixelfelds – ist diese nicht verfügbar, ist das Ergebnis –1.

Ein Grabber-Beispiel

Das nachfolgende Programm lädt ein Bild und gibt die Farbinformationen – also die Anteile Rot, Grün, Blau – auf der Konsole aus. Dabei müssen wir nur die Maus im Bild bewegen. Die Ereignisbehandlung wird über einen MouseMotionListener übernommen. Im nächsten Kapitel werden wir die Ereignisbehandlung genauer kennen lernen. Um das Bild zu laden, nutzen wir eine Methode aus Swing, die später noch genauer erklärt wird.

Abbildung

Listing 14.19   DuAlterGrabber.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

class DuAlterGrabber extends Frame
{
Image image;
int width, height;
int pixels[];

DuAlterGrabber()
{
image = new ImageIcon("Macke.jpg").getImage();

width = image.getWidth( this );
height = image.getHeight( this );

pixels = new int[width * height];

PixelGrabber grabber
= new PixelGrabber( image,0,0,width,height,pixels,0,width );

try {
grabber.grabPixels();
} catch ( InterruptedException e ) {
System.err.println( "Error getting pixels" );
}

addMouseMotionListener( new MouseMotionAdapter() {
public void mouseMoved( MouseEvent e )
{
int pixel = pixels[e.getY() * width + e.getX()];
int red = (pixel >> 16) & 0xff,
green = (pixel >> 8) & 0xff,
blue = (pixel) & 0xff;
System.out.println( "R=" +red+ " G=" +green+ " B=" +blue );
}
} );
setSize( width, height );
}

public void paint( Graphics g )
{
if ( image != null )
g.drawImage( image, 0, 0, this );
}

public static void main( String args[] )
{
new DuAlterGrabber().show();
}
}

Galileo Computing

14.15 Filter  downtop


Galileo Computing

14.15.1 Grundlegende Eigenschaft von Filtern  downtop

ImageFilter liegen zwischen Produzenten und Konsumenten und verändern Bildinformationen oder nehmen Einfluss auf die Größe. Sie verhalten sich ähnlich wie ein Proxy, der zwischen Quelle und Ziel liegt. Für Produzenten treten die Filter als Konsumenten auf, da sie die Schnittstelle ImageConsumer implementieren und die wichtige Methode setPixel()ausprogrammieren.

Um einen Filter anzuwenden, nutzen wir die Klasse FilteredImageSource. Im Konstruktor geben wir das Bild und den Filter an. Anschließend können wir den zurückgegebenen Produzenten an createImage() übergeben und wir haben ein neues Bild.

Beispiel Die Realisierung eines Filters
Image src = getImage( "ullinackig.gif" 
);
ImageFilter colorfilter = new GrayFilter();
ImageProducer imageprod =
new FilteredImageSource( src.getSource(), colorfilter );
Image img = createImage( imageprod );


Galileo Computing

14.15.2 Konkrete Filterklassen  downtop

Von der Klasse ImageFilter gibt es einige Unterklassen, die für unsere Arbeit interessant sind.

Abbildung

gp  BufferedImageFilter
Diesem Filter lässt sich ein Objekt vom Typ BufferedImageOp übergeben, mit dem unterschiedliche Manipulationen ermöglicht werden. BufferedImageOp ist eine Schnittstelle, die von AffineTransformOp, ConvolveOp, BandCombineOp und LookupOp implementiert wird. AffineTransformOp ist am Attraktivsten, da es mit einem AffineTransform konstruiert wird, sodass leicht Vergrößerungen oder Rotationen ermöglicht werden. Über AffineTransform Objekte erfahren wir im 2D-Kapitel mehr.
gp  CropImageFilter
Bildteile werden herausgeschnitten.
Abbildung

gp  ReplicateScaleFilter
Zum Vergrößern oder Verkleinern von Bildern. Ein einfacher Algorithmus wird angewendet. Eine weiche Vergrößerung oder Verkleinerung lässt sich mit der Unterklasse AreaAveragingScaleFilter erreichen.
gp  RGBImageFilter
Dieser allgemeine Filter ist für die eigene Filterklasse gedacht. Wir müssen lediglich eine filterRGB()-Methode angeben, die die RGB-Bildinformationen für jeden Punkt (x,y) modifizieren. Benötigt der Filter auch die Nachbarpunkte, so können wir mit RGBImageFilter nicht arbeiteten.
Abbildung

Beispiel Ein Filter, der den Rot- und Blauanteil in einem Bild vertauscht.
class RedBlueSwapFilter extends 
RGBImageFilter
{
public RedBlueSwapFilter()
{
canFilterIndexColorModel = true;
}

public int filterRGB( int x, int y, int rgb )
{
return ( (rgb & 0xff00ff00)
| ((rgb & 0xff0000) >> 16)
| ((rgb & 0xff) << 16));
}
}


Galileo Computing

14.15.3 Mit CropImageFilter Teile ausschneiden  downtop

Mit CropImageFilter lassen sich Teile des Bildes ausschneiden. Wir definieren dazu vom Bild einen Ausschnitt mit den Koordinaten x, y, und der Breite und Höhe. Wie die anderen Bildfilter, so wird auch CropImageFilter mit dem FilteredImageSource als Produzent verwendet.

Beispiel Erzeuge für die Grafik big.gif in einem Applet ein neues Image-Objekt.

Das Original hat die Größe 100 x 100 Pixel. Das neue Bild soll einen Rand von 10 Pixeln haben.

Image origImage = getImage( getDocumentBase(), 
"big.gif" );
ImageFilter cropFilter = new CropImageFilter( 10, 10, 90, 90 );
Image cropImage = createImage(
new FilteredImageSource(origImage.getSource(),cropFilter) );

Bildausschnitte über PixelGrabber ausschneiden

Nicht nur über CropImageFilter lassen sich Bildausschnitte auswählen. Eine andere Lösung geht über PixelGrabber, da dieser auch einen Ausschnitt erlaubt. Darüber lässt sich dann mit MemoryImageSource wieder ein neues Bild erzeugen.

Beispiel Schneide aus dem Image img das passende Rechteck mit den Startkoordinaten x, y und der Breite width und der Höhe height aus.
int pix[] = new int[width * height];

PixelGrabber pg = new PixelGrabber( img, x, y, width, height,
pix, 0, width );
try {
pg.grabPixels();
}
catch( InterruptedException e ) {}

newImg = createImage(
new MemoryImageSource(width, height, pix, 0, width) );

An diese Stelle sollten wir noch einmal den Unterschied zwischen den beiden Möglichkeiten betonen. PixelGrabber implementiert die Schnittestelle ImageConsumer, sodass er ein Bildkonsument ist und Daten in einem Integer-Feld ablegt. CropImageFilter ist allerdings ein Filter, der ein anderes Image-Objekt konstruiert und kein Feld.


Galileo Computing

14.15.4 Transparenz  downtop

Um eine bestimmte Farbe eines Bildes durchsichtig zu machen (also die Transparenz zu bestimmen), nutzen wir einen RGBImageFilter. Dabei implementieren wir einen Konstruktor, der die Farbe sichert, die transparent werden soll. Sie wird später in der Implementierung von filterRGB() verwendet. Die Methode, die ja für jeden Bildpunkt aufgerufen wird, liefert dann entweder die Farbe ohne Alpha-Kanal zurück (rgb|0xff000000) oder eben nur den Alpha-Kanal (rgb&0xffffff) für Transparenz. Eine interessante Erweiterung ist die Einführung einer Toleranzauswertung um einen »Zauberstab«, der ähnlich wie in Photoshop zu realisieren ist.

public class TransparentFilter 
extends RGBImageFilter
{
public TransparentFilter( Color color )
{
this.color=color;
}

public int filterRGB( int x, int y, int rgb )
{
if ( rgb != color )
return rgb | 0xff000000;
else
return rgb & 0xffffff; //transparent
}

private Color color;
}

Galileo Computing

14.16 Alles wird bunt mit Farbmodellen  downtop

Als wir uns mit dem Produzenten- und Konsumenten-Modell bei Image-Objekten beschäftigt haben, standen die Daten über die Pixel immer in einem Byte- oder Integer-Feld. Eher übersprungen wurde das Farbmodell bei MemoryImageSource und einem create Image(). Die Einträge der Felder sind Pixel und die Werte standen für Farbinformationen, genauer gesagt, für Rot, Grün und Blau. Wir haben uns bisher wenig Gedanken über das Format gemacht und haben stillschweigend angenommen, dass diese in 24 Bit abgelegt sein müssen. Dies muss jedoch nicht so sein und die Interpretation der Farbwerte in einem Informationswort bestimmt ein Farbmodell. Für Farbmodelle gibt es in Java die Klasse ColorModel. Mit der Klasse lassen sich dann aus einem Pixel die roten, grünen, blauen und transparenten Anteile bestimmen. Der transparente Teil, auch Alpha-Komponente genannt, bestimmt, in welcher Intensität die Farbinformationen wirken. Alpha-Werte lassen sich nur in Zusammenhang mit Bildern anwenden. Mit der Graphics-Klasse lässt sich ein Alpha-Wert nicht einstellen, der dann alte Zeichenoperationen beeinflusst. Bei den Farbmodellen ist der Anteil der Transparenz genauso lang wie ein Farbwert, nämlich 8 Bit. Ein Wert von 255 sagt aus, dass der Farbwert zu 100 % sichtbar ist. Ist der Wert 0, so ist die Farbe nicht zu sehen.

Java macht das Programmierleben so plattformunabhängig wie möglich. Bei wenigen oder vielen Farben auf der Zielplattform wird eine optimale Annäherung an unsere Wunschfarben errechnet. So können wir alles in 24 Bit Farbtiefe errechnen, die Dislay-Komponente sucht die wichtigsten Farben heraus und fasst Gruppen ähnlicher Farben zusammen.


Galileo Computing

14.16.1 Die abstrakte Klasse ColorModel  downtop

Die abstrakte Klasse ColorModel beschreibt alle Methoden für konkrete Farbklassen, sodass die Informationen über die Farbwerte und Transparenz erreichbar sind. Obwohl die Klasse abstrakt ist, besitzt sie zwei Konstruktoren, die von den Unterklassen benutzt werden. Direkte Unterklassen sind ComponentColorModel, IndexColorModel und PackedColorModel.

abstract class java.awt.image.ColorModel
implements Transparency

gp  ColorModel(int pixel_bits, int[] bits, ColorSpace cspace,boolean hasAlpha,
boolean isAlphaPremultiplied,int transparency,int transferType)
gp  ColorModel(int bits)

Der zweite Konstruktor ist praktisch, da dieser nur die Farbtiefe in Bits erwartet. Diese abstrakte Klasse besitzt jedoch die Fabrik-Methode – die statisch ist – getRGBdefault(), die ein ColorModel-Objekt zurückliefert. Das Standard-Farbmodell, auch sRGB genannt, ist ein Farbmodell, welches die Werte als 24 Bit Tupel mit den Komponenten Alpha, Rot, Grün und Blau hält. Dieses Farbmodell lässt sich etwa für ein Memory-Image einsetzen. Der erste Konstruktor ist noch leistungsfähiger und ist erst seit Java 1.2 dabei. Mit seiner Hilfe muss ein Farbwert nicht zwingend in einem Integer kodiert sein.

Die Methode getPixelSize() liefert die Farbtiefe eines Farbmodells. Das Standard-Modell besitzt eine Tiefe von 32 Bit (24 für die Farben und dann noch den Alpha-Kanal). So ergibt auch die folgende Zeile als Anwort auf die Frage nach der Anzahl der Farben im Standard-Modell 32 Bit:

System.out.println( ColorModel.getRGBdefault().getPixelSize());

Die Hauptaufgabe einer Farb-Modell-Klasse ist die Auswertung der Farbinformationen aus einem Speicherwort. Mit drei Methoden lassen sich die verschiedenen Farben auslesen. getRed(int pixel), getGreen(int pixel), getBlue(int pixel). Zusätzlich kommt noch getAlpha(int pixel) hinzu. Jede dieser Methoden ist abstrakt und liefert eine Ganzzahl mit dem Farbwert zurück. Wie wir später sehen werden ist das einfachste Modell genau jenes, das wir bisher immer benutzt haben. Dieses liest nämlich genau von den Stellen 24, 16 und 8 die Farbwerte aus. Da die Methoden abstrakt sind, müssen Unterklassen dieses Verhalten programmieren.

Eine weitere Methode ist getRGB(), welche ein int mit allen Farben im entsprechenden Farbformat zurückliefert. Die Implementierung basiert auf den Anfrage-Methoden.

public int getRGB(int pixel) {
return (getAlpha(pixel) << 24)
| (getRed(pixel) << 16)
| (getGreen(pixel) << 8)
| (getBlue(pixel) << 0);
}

Im Folgenden sehen wir eine Auflistung der wichtigsten Operationen:

abstract class java.awt.image.ColorModel
implements Transparency

gp  abstract int getAlpha( int pixel )
Liefert den Alpha-Wert im Bereich 0 bis 255.
gp  abstract int getBlue( int pixel )
Liefert den Blauanteil des Pixels.
gp  ColorSpace getColorSpace()
Liefert den Farbraum, der mit dem ColorModel verbunden ist.
gp  int[] getComponents( int pixel, int[] components, int offset )
Liefert ein Feld mit unnormalisierter Farb- und Alpha-Komponente für ein Pixel.
gp  abstract int getGreen( int pixel )
Liefert den Grünanteil.
gp  int getNumColorComponents()
Gibt die Anzahl der Farben zurück.
gp  int getNumComponents()
Liefert die Anzahl der Komponenten (mit Alpha).
gp  int getPixelSize()
Wie viele Pixel beschreiben eine Farbe?
gp  abstract int getRed( int pixel )
Liefert den Rotanteil.
gp  int getRGB( int pixel )
Gibt Farbe- und Alpha-Komponente des Pixels im sRGB-Farbmodell wieder.
gp  static ColorModel getRGBdefault()
Liefert eie DirectColorModel mit dem sRGB-Modell.
gp  int getTransparency()
Liefert die Art der Transparenz. Dies ist entweder OPAQUE, BITMASK oder TRANSLUCENT. Es sind Konstanten aus der Schnittstelle Transparency. Sie können aber auch über ColorModel verwendet werden, da ColorModel diese Schnittstelle implementiert.
gp  boolean hasAlpha()
Fragt an, ob das Farbmodell Transparenz unterstützt.
gp  boolean isCompatibleRaster( Raster raster )
Liefert true, falls das Raster mit dem Farbmodell kompatibel ist.

Nun lassen sich auf der Basis dieser Klassen verschiedene Farbmodelle entwerfen. Einige sind von den Entwicklern der Java-Bibliotheken schon vorgefertigt, wie etwa eine Farbklasse, die die Informationen gleich im Pixel selbst speichern, wie im Beispiel RGB, oder eine Klasse, die einen Index auf einen Farbwert verwaltet. Als eigene Ergänzung können wir Farbklassen implementieren, die Graustufen direkt unterstützen oder etwa andere Farbräume wie HSB (Hue, Saturation, Brightness). Die einzige Aufgabe, die uns als Implementierer der abstrakten Methoden übrig bleibt, ist, die Farbwerte aus dem Pixelwert zu extrahieren. Im Falle von HSB ist das einfach. Die Methoden getRed(), getGreen() und getBlue() müssen nur aus dem internen HSB-Wert den Anteil liefern.


Galileo Computing

14.16.2 Farbwerte im Pixel mit der Klasse DirectColorModel  downtop

Mit Hilfe der Klasse DirectColorModel werden die Farbwerte Rot, Grün, Blau und Alpha direkt aus dem Farbtupel extrahiert. Die Klasse gehört zu einer der größten im Image-Paket. Als Beispiel für das direkte Format kennen wir Standard-RGB. Für dieses gilt, dass die Farben jeweils 8 Bit in Anspruch nehmen. Dass muss aber nicht so sein und im Konstruktor vom DirectColorModel lässt sich bestimmen, wie und an welcher Stelle die Bits für die Farben sitzen. Wir dürfen dies jedoch nicht damit verwechseln, dass wir die Anzahl der Bits angeben. Nur die Positionen sind möglich. Daraus ergibt sich auch, dass die Werte zusammenhängend sind und nicht etwa Folgendes Auftreten kann: 0xrrbgbg. Die Bitanzahl kann aber für die Farben unterschiedlich sein. Auch der Alpha-Wert kann frei gewählt werden. Für das Standard-Modell ergibt sich eine einfache Zeile:

DirectColorModel rgbModel = new 
DirectColorModel(32,
0xff0000, 0x00ff00, 0x0000ff, 0xff000000);

Ist das Objekt einmal angelegt, so sind nun die Anfrage-Methoden wie getRed() möglich, da DirectColorModel als konkrete Klasse, von der auch ein Exemplar erzeugt werden kann, diese abstrakte Methoden alle überschreibt und mit Implementierung versieht. Eine wichtige Eigenschaft dieser Methoden ist, dass sie final sind und ihren Farbwert mit dem Alpha-Wert kombinieren. Da sie final sind, können sie von Unterklassen nicht mehr überschrieben werden. Letzteres verlangt aber die aktuelle Implementierung der AWT-Bibliothek.

Beispiel Implementierung von getRed().
final public int getRed(int pixel) 
{
int r = ((pixel & maskArray[0]) >>> maskOffsets[0]);
if (scaleFactors[0] != 1.)
r = (int)(r * scaleFactors[0]);
if (isAlphaPremultiplied) {
int a = getAlpha(pixel);
r = (a == 0) ? 0 : (r * 255/a);
}
return r;
}

Im Parameter pixel ist die Farbe Rot an einer Bitposition (meistens ab 24 Bit) abgelegt. Damit wir diesen Wert auslesen können und mit dem Alpha-Wert kombinieren können, muss er zunächst einmal ausmaskiert werden. Daher wird pixel mit der Maske verknüpft, sodass nur die Bits übrig bleiben, die auch wirklich die Farbe Rot beschreiben. Anschließend verschieben wir die Rot-Pixel so weit nach rechts, dass die Grün- und Blau-Werte verschwinden. Die Felder maskArray und maskOffsets sowie scaleFactors sind in der direkten abstrakten Oberklasse PackedColorModel angelegt. Im Übrigen finden wir dort auch eine countBits()-Methode, die wir ja am Anfang unserer Inselrundfahrt bei den Bitoperationen kennen gelernt haben. Doch bleiben wir bei getRed(). Hier sehen wir noch deutlich, wie der Alpha-Wert in die Berechnung mit eingeht. Ist der Farbwert 0, so ist auch das Ergebnis 0. Ist er ungleich 0, so wird die Farbe nach dem Apha-Wert gewichtet. Der Skalierungsfaktor skaliert die Werte auf 256. Denn haben wir beispielsweise nur zwei Bits für einen Farbwert, dann müssen wir mit 128 multiplizieren, um wieder eine 8-Bit-Darstellung zu bekommen.

Abbildung


Galileo Computing

14.16.3 Die Klasse IndexColorModel  downtop

Im Gegensatz zur Klasse DirectColorModel verwaltet ein IndexColorModel die Farben und Transparenzen nicht im Pixel, sondern in einer eigenen Tabelle, die auch Color-Map oder Palette genannt wird. Das Modell ist vergleichbar mit dem Dateiformat GIF. Dort stehen 256 Farben maximal in einer Tabelle zur Verfügung und alle Punkte in einem GIF-Bild müssen einer dieser Farben entsprechen. Eine GIF-Datei mit zwei Farben definiert etwa eine Farbe mit schweinchen-rosa und eine zweite Farbe mit hornhaut-umbra. Der Pixel selbst ist dann nur ein Index auf einen Eintrag. Dieses Verfahren ist sehr speicherschonend, ein Kriterium das vor ein paar Jahrzehnten noch zählte. An Stelle von 24 Bit für einen Pixel wird der Index etwa 10 Bit breit gemacht und stellt dann bis zu 1024 Farben dar. Das ist immerhin eine Reduktion des Bildschirmspeichers um die Hälfte. Leider sind aber damit auch hohe Berechnungskosten verbunden. Für eine Verwendung dieser Klasse spricht eine Abstraktion von den konkreten Farben. Ein Beispiel dafür wäre ein Fraktalprogramm. Einer berechneten Zahl wird direkt ein Farbwert zugeordnet. Somit lässt sich leicht eine Farbverschiebung programmieren, die sich auf Englisch Color-Cycle nennt.

Wenn wir ein IndexColorModel verwenden wollen, geben wir im Konstruktor eine Anzahl Bits pro Pixel zusammen mit einer Tabelle an, die die Komponenten Rot, Grün und Blau sowie optional die Transparenzen enthält. Die Farbtabelle, die über einen Index die Farbe verrrät, kann maximal 256 Farben aufnehmen. Dies ist leider eine Einschränkung, beschränkt aber den Speicher, da nur ein byte anstelle eines short belegt wird.

class java.awt.image.IndexColorModel
extends ColorModel

gp  IndexColorModel( int bits, int size,
byte[] r, byte[] g, byte[] b, byte[] a )
gp  IndexColorModel( int bits,int size,
byte[] r, byte[] g, byte[] b, int trans )
gp  IndexColorModel( int bits, int size
byte[] r, byte[] g, byte[] b )
gp  IndexColorModel( int bits, int size, byte[] cmap,
int start, boolean hasalpha, int trans )
gp  IndexColorModel( int bits, int size, byte[] cmap,
int start, boolean hasalpha )
gp  IndexColorModel( int bits, int size, int[] cmap,
int start,boolean hasalpha, int trans,
int transferType )
Abbildung 14.8   es Bild
Abbildung

An den Konstruktoren lässt sich ablesen, dass mehrere Wege gegangen werden können. Die Farben können als Einzelfelder einem IndexColorModel übergeben werden oder als zusammengepacktes Feld. Dann erfolgt die Speicherung nach dem Standard RGB-Modell. Vorsicht ist bei einem Alpha-Wert geboten. Dieser folgt nach dem Blauton. So ist die Reihenfolge bei Transparenz 0xRRGGBBAA. Dies ist sehr verwirrend, da wir es gewohnt sind, den Alpha-Wert vor dem Rotwert zu setzen.

Intern werden die Werte in einem Feld gehalten. Der erste Wert gibt die Anzahl der Bits an, die einen Pixel beschreiben. Er darf 8 Bit nicht überschreiten, da die Längenbeschränkung 2^8 = 256 maximale Farben vorgibt. Der nächste Wert size ist die Größe der Tabelle. Sie sollte mindestens 2^bits groß sein. Andernfalls werden Farben fehlerhaft zugeordnet. Präziser heißt dies, dass sie Null sind, da ja für das Feld der new-Operator das Feld mit Null-Werten automatisch belegt. Sind in der Farbtabelle Apha-Werte abgelegt, dann sollte hasalpha den Wert true annehmen. Sind alle Werte in einer Tabelle, berechnet sich der Farbwert zu einem Index wie folgt: Betrachten wir keinen Alpha-Wert und unser Pixel hat den Wert f(arbe).

gp  Dann ist der Rotwert an der Stelle colorMap[start+3*f] und
gp  der Grünwert an der Stelle colorMap[start+3*f+1] und
gp  der Blauwert schließlich bei colorMap[start+3*f+2].

Um Informationen über die internen Werte und die Größe der Tabelle zu bekommen, reicht ein toString(). Die Größe der Tabelle liefert die Methode getMapSize().

Mit den finalen Methoden getReds(byte[]), getGreens(byte[]), getBlues(byte[] blue Array) und getAlphas(byte[] alphaArray), deren Rückgabewert void ist, lassen sich die Farbinformationen auslesen und als Ergebnis in das Feld legen. Die Felder müssen schon die passende Größe haben, die sich jedoch mit final int getMapSize() erfragen lässt. Die Methode getTransparentPixel() liefert den Index des transparenten Pixels. Gibt es keinen, ist der Wert -1.

Werfen wir zur Demonstration noch einen Blick auf die Methode getGreens(). Wir sehen deutlich, dass das Feld eine passende Größe haben muss.

final public void getGreens(byte 
g[]) {
for (int i = 0; i < map_size; i++)
g[i] = (byte) (rgb[i] >> 8);
}

An getRed()sehen wir auch, dass der Pixel auch direkt ein Index für das private Feld rgb ist. Wenn der Index über die Feldgröße läuft müssen wir den Fehler selber behandeln.

final public int getRed(int pixel) 
{
return (rgb[pixel] >> 16) & 0xff;
}

Wenden wir unsere Aufmerksamkeit auf ein Programm, welches ein Bytefeld erzeugt und aus sechs Farben die Pixel in das Feld schreibt. Zum Schluss konvertieren wir das Bytefeld mit einem MemoryImageSource in ein Image-Objekt. Für diese Klasse können wir ein IndexColorModel angeben, dass dann folgendes Format hat:

ColorModel cm = IndexColorModel( 
8, colorCnt, r, g, b );

Hier handelt es sich um ein Farbmodell mit 8 Bits und sechs Farben. Die folgenden Werte zeigen auf die drei Felder mit den Farbwerten. Anschließend erzeugt createImage() mit diesem Farbmodell das Image-Objekt.

Image i = createImage(new MemoryImageSource(w,h,cm,pixels,0,w));
Listing 14.20   IndexColorModelDemo.java
import java.awt.*;
import java.awt.image.*;

public class IndexColorModelDemo extends Frame
{
Image i;
static int w = 400, h = 400;

int pixels[] = new int [w*h];

Color colors[] = {
Color.red, Color.orange, Color.yellow,
Color.green, Color.blue, Color.magenta
};

IndexColorModelDemo()
{
int colorCnt = colors.length;

byte r[] = new byte[colorCnt],
g[] = new byte[colorCnt],
b[] = new byte[colorCnt];

for ( int i=0; i<colorCnt; i++ )
{
r[i] = (byte) colors[i].getRed();
g[i] = (byte) colors[i].getGreen();
b[i] = (byte) colors[i].getBlue();
}

int index = 0;
for ( int y=0; y<h; y++ )
for ( int x=0; x<w; x++ )
pixels[index++] = (int)(Math.random() * colorCnt);
i = createImage( new MemoryImageSource( w, h,
new IndexColorModel(8, colorCnt, r, g, b),
pixels, 0, w) );
}

public void paint( Graphics g )
{
if ( i != null )
g.drawImage( i, 0, 0, this );
}

public static void main( String args[] )
{
IndexColorModelDemo d = new IndexColorModelDemo();
d.setSize( w, h );
d.show();
}
}

Galileo Computing

14.17 Drucken  downtop

Zum Drucken von Inhalten in Java gibt es zwei unterschiedliche Ansätze. Zunächst ist es der klassische Weg über Klassen im AWT, die recht bescheidene Möglichkeiten bieten, aber einfach zu nutzen sind. Bei diesem Ansatz zeichnen wir über einen Graphics-Kontext des Druckers. Hier gibt es einen Unterschied zwischen einer leichtgewichtigen gezeichneten Swing-Komponente und den harten nativen, schwergewichtigen AWT-Elementen. Seit Java 2 existiert die ereignisgesteuerte Möglichkeit. Sie behandelt selbstgezeichnete und die leichtgewichtigen Komponenten gleich.


Galileo Computing

14.17.1 Drucken mit dem einfachen Ansatz  downtop

Seit Java 1.1 gibt es eine einfache Möglichkeit, aus Applikationen zu drucken. Bei Applets verhindert standardmäßig eine Sicherheitssperre den Zugriff auf den Drucker.

Von der Anwendung zum gedruckten Blatt sind es sechs Schritte, die wir im Folgenden skizzieren wollen:

1. Wir benötigen ein Fenster.

Die Schnittstelle zum Drucker gelingt in Java über die Klasse PrintJob, die mit getGraphics() Zugang auf einen Grafik-Kontext gibt. Zu Beginn eines Ausdrucks benötigen wir daher ein konkretes PrintJob-Objekt. PrintJob ist eine abstrakte Klasse und über eine Methode getPrintJob() vom Toolkit bekommen wir eine plattformabhängige Implementierung. Daher verfügt PrintJob über keinen Konstruktor, sondern wird indirekt über Toolkit erzeugt. Wir erkennen hier wieder eine Fabrik-Methode.

abstract class java.awt.Toolkit

gp  PrintJob getPrintJob( Frame frame, String jobtitle,
JobAttributes jobAttributes,
PageAttributes pageAttributes )
Wie für Dialoge üblich, benötigen wir eine Vaterklasse. Diese legen wir in die Variable frame. Mit title spezifizieren wir eine Überschrift für den Dialog. Dieser kann null sein und entspricht dem String »«. Mit den Argumenten in props lassen sich zusätzliche Eigenschaften einführen, so wie es unter Unix üblich ist. Unter Windows funktioniert das nicht. frame kann null sein, wenn jobAttributes gültig ist und jobAttributes.getDialog() den Wert JobAttributes.DialogType.NONE oder JobAttributes.DialogType.COMMON liefert. In der Regel heißt das aber, dass Drucken mit grafischen Oberflächen verbunden ist.

Galileo Computing

14.17.2 Ein PrintJob  downtop

Folgende Anweisungen liefert eine Unterklasse vom PrintJob.

Toolkit tk = Toolkit.getDefaultToolkit();
PrintJob pj = tk.getPrintJob( new Frame(), "", null );

Sie öffnet ein Fenster mit dem Dialogfeld der aktuellen Maschine. Dort lässt sich zum Beispiel einstellen, wie viele Kopien wir möchten oder welchen Drucker wir ansprechen. Bricht der Anwender den Dialog ab, so ist der Rückgabewert null.

Abbildung

Informationen über die Seite

Über ein PrintJob-Objekt gelangen wir dann an Informationen über das Ausmaß einer Seite oder die Auflösung des Druckers. Rufen wir getGraphics() auf, so kommen wir an ein Graphics-Objekt. Jetzt können wie die Aufrufe, die wir sonst in paint() an das konkrete Graphics der grafischen Oberfläche gemacht haben, an das Drucker-Device schicken. Dazu muss die Methode dispose() von Graphics abschließend aufgerufen werden. Mehrere Seiten werden dann mit einer Reihe von Aufrufen von getGraphics() und dispose() realisiert. Das bedeutet, dass getGraphics() immer genau für eine Seite gilt.

Die abstrakte Klasse PrintJob schreibt lediglich sechs Funktionen vor, von denen drei Eigenschaften des Druckers beschreiben.

abstract class java.awt.PrintJob

gp  abstract Dimension getPageDimension()
Liefert die Ausmaße der Seite in Pixeln.
gp  abstract int getPageResolution()
Liefert die Auflösung der Seite in Pixel pro Inch.
gp  abstract boolean lastPageFirst()
Liefert true, wenn die letzte Seite zuerst gedruckt wird.

Mit getPageResolution() bekommen wir Auflösungen von typischerweise 300 oder 600 Pixeln. Der Rückgabewert von getPageDimension() liefert ein Dimension-Objekt mit den Ausmaßen der Seite, also Höhe oder Breite. Die Einträge liefern die Anzahl der druckbaren Pixel (keine Inches) pro Seite. Mit folgenden Zeilen erfragen wir die Auflösung:

Listing 14.21   PrinterInfo.java
import java.awt.*;

class PrinterInfo
{
public static void main( String args[] )
{
Toolkit tk = Toolkit.getDefaultToolkit();
PrintJob pj = tk.getPrintJob( new Frame(), "", null );

if ( pj != null )
{
int resolution = pj.getPageResolution();
Dimension d = pj.getPageDimension();

System.out.println( "Resolution : " + resolution + "\n" +
"Width : " + d.width + "\n" +
"Height : " + d.height + "\n" +
"Pixel on page : " +
(resolution * d.width * d.height) );
}

System.exit( 0 );
}
}

Für meinen Drucker ergeben sich folgende Werte :

Resolution : 72
Width : 595
Height : 842
Pixel on page : 36071280

Galileo Computing

14.17.3 Drucken der Inhalte  downtop

Die Methode getGraphics() von PrintJob liefert ein Graphics-Objekt für den Druck. Was wir vorher auf den Bildschirm gebracht haben, leiten wir mit Drucker-Graphics auf den Drucker um. Dann lassen sich die bekannten Methoden wie drawLine(), drawString() usw. nutzen. Bei Zeichensätzen ist Vorsicht geboten, da kein Standard-Zeichensatz eingestellt ist.

Nach dem Ende eines gesamten Druckvorgangs beendet end() die Sitzung und die belegten Ressourcen werden freigegeben.

abstract class java.awt.PrintJob

gp  abstract Graphics getGraphics()
Liefert ein Graphics für eine Seite.
gp  abstract void end()
Beendet den Druck und gibt Ressourcen frei.

Galileo Computing

14.17.4 Komponenten drucken  downtop

Bisher haben wir mit getGraphics() einen Kontext bekommen und selbstständig die Elemente gezeichnet. Doch was machen wir, wenn Standard-Komponenten wie Schaltflächen oder Textfelder gedruckt werden sollen? Für diese Aufgabe bietet jede Komponente über die Oberklasse Component entweder print() oder printAll() an.

abstract class java.awt.Component
implements ImageObserver, MenuContainer, Serializable

gp  void print( Graphics g )
Druckt alle Komponenten. Die Unterklassen überschreiben diese Methode.
gp  void printAll( Graphics g )
Druckt alle Komponenten und Unterkomponenten. Die Unterklassen überschreiben diese Methode.

Diese Methoden werden auf dem Graphics-Objekt des Druckers aufgerufen.

Drucker von Containern

Da Container ihre Kinder selbstständig verwalten, gibt es auch hier eine Druckfunktion. Sie heißt printComponents() und druckt alle Komponenten im Container. Sie kann jedoch nur das ausgeben, was tatsächlich sichtbar ist.

Falls wir ein Fenster mit allen Elementen zu Papier bringen wollen schreiben wir:

Toolkit tk = Toolkit.getDefaultToolkit();
PrintJob pj = tk.getPrintJob( new Frame(), "", null );

if ( pj != null ) {
Graphics g = pj.getGraphics();
f.printComponents( g );
g.dispose();
pj.end();
}

Galileo Computing

14.17.5 Den Drucker am Parallelport ansprechen  downtop

Es ist natürlich immer aufwendig, für einen einfachen 10cpi Text ein Printer-Objekt zu erzeugen und dann all den Text als Grafik zu erzeugen. Das braucht nicht nur lange, sondern ist auch sehr umständlich. Um einen Drucker am Parallelport oder im Netzwerk direkt anzusprechen, konstruieren wir einfach ein FileOutputStream wie folgt:

FileOutputStream fos = new FileOutputStream( 
"PRN:" );
PrintWriter pw = new PrintWriter( fos );
pw.println( "Hier bin ich" );
pw.close();

Hängt dann am Printer-Port ein Drucker, so schreiben wir den Text in den Datenstrom. Anstelle von PRN: funktioniert auch LTP1: beziehungsweise auch ein Druckername im Netzwerk. Unter UNIX kann entsprechend /dev/lp verwendet werden.

Natürlich sehen wir auf den ersten Blick, dass dies eine Windows- bzw. DOS-Version ist. Um das Ganze auch systemunabhängig zu steuern, entwickelte Sun die Communications API. Obwohl sie in erster Linie für die serielle Schnittstelle gedacht ist, unterstützt sie auch die parallele Schnittstelle. Hier bietet sich auch die Chance, den Zugriff zu synchronisieren.


Galileo Computing

14.18 Java 2D API  downtop

Seit dem JDK 1.2 existiert in den Java Foundation Classes (JFC) die 2D API, mit der sich zweidimensionale Grafiken zeichnen lassen. Damit erreichen wir eine ähnliche Funktionalität wie etwa mittels der Sprache Postscript. Als wichtige Erweiterungen gegenüber den alten Zeichenfunktionen sind Transformationen auf beliebig definierbaren Objekten, Füllmustern und Kompositionen definiert. Die Zeichenoperationen sind optional weichgezeichnet.

Viele der 2D-Klassen sind im java.awt.geom-Packet untergebracht. Daher steht in den Programmen ganz zu Beginn:

import java.awt.geom.*;

Wenn nun die grafische Oberfläche Objekte zeichnet, wird für alle Komponenten die paint()-Methode mit dem passenden Grafikkontext aufgerufen. Unter Java 2D wurde dies um einen neuen Grafikkontext mit der Klasse Graphics2D erweitert. Um diese zu nutzen, muss nur das bekannte Graphics-Objekt in ein Graphics2D gecastet werden.

public void paint( Graphics g )
{
Graphics2D g2 = (Graphics2D) g;
...
}

Da Graphics2D eine Unterklasse von Graphics ist, lassen sich natürlich noch alle AWT-Operationen weiter verwenden.


Galileo Computing

14.18.1 Grafische Objekte zeichnen  downtop

Um in den herkömmlichen Java-Versionen grafische Primitive auf dem Schirm zu zeichnen, standen uns diverse drawXXX()- und fillXXX()-Methoden aus der Graphics-Klasse zur Verfügung. Eine blaue Linie entstand daher etwa so:

public void paint( Graphics g )
{
g.setColor( Color.blue );
g.drawLine( 20, 45, 324, 96 );
}

Die Methode setColor() setzt nun eine interne Variable im Graphics-Objekt und ändert so den Zustand. Anschließend zeichnet drawLine() mit dieser Farbe in den Speicher. Die Koordinaten sind in Pixel angegeben. Bei der 2D API ist dies nun anders. Hier werden die Objekte in einem Kontext gesammelt und nach Bedarf gezeichnet. Der Kontext bestimmt anschließend für diese Form noch den Zeichenbereich (clipping), die Transformationen, die Komposition von Objekten und die Farben und Muster.

Das erste 2D Programm

Beginnen wir mit einem einfachen Programm, welches eine einfache Linie zeichnet.

Listing 14.22   First2Ddemo.java
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;

class First2DDemo extends JFrame
{
public void paint( Graphics g )
{
super.paint( g );

Graphics2D g2 = (Graphics2D) g;

g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

g2.draw( new Line2D.Double( 10, 40, getWidth()-10, 70 ) );
}

public static void main( String args[] )
{
JFrame f = new First2DDemo();
f.setSize( 200, 100 );
f.show();
}
}

Das Programm ist wie andere AWT-Programme aufgebaut. Prinzipiell hätten wir auch Frame anstatt JFrame erweitern können. Wir erkennen auch die Umwandlung von Graphics in Graphics2D. Da normalerweise die Ausgabe nicht weichgezeichnet ist, setzen wie dies durch setRenderingHint(). Die Parameter und die Funktionen wird später näher beschrieben.

Abbildung 14.9   Eine weiche Linie
Abbildung

Wirklich wichtig ist die draw()-Methode. draw() aus der Klasse Graphics2D nimmt ein Shape-Objekt und zeichnet es. Shape-Objekte sind etwa Linien, Polygone oder Kurven.

abstract class java.awt.Graphics2D
extends Graphics

gp  abstract void draw( Shape s )
Zeichnet die Form im aktuellen Graphics2D Kontext. Die Attribute umfassen Clipping, Transformation, Zeichen, Zusammensetzung und Stift (Stroke)-Attribute.

Galileo Computing

14.18.2 Geometrische Objekte durch Shape gekennzeichnet  downtop

Die geometrischen Objekte, die sich alle von der Klasse Shape ableiten, sind Polygon, RectangularShape, Rectangle, Area, Line2D, QuadCurve2D und CubicCurve2D. Ein Beispiel für Line2D haben wir im oberen Programm schon aufgeführt. Eine besondere Klasse, die auch von Shape abgeleitet ist, heißt GeneralPath. Damit lassen sich mehrere Objekte zu einer Figur zusammensetzen. Die Klassen sind im Paket java.awt.geom definiert.

Die Klassen Rectangle2D, RoundRectangle2D, Arc2D und Ellipse2D erben alle von der Klasse RectangularShape und sind dadurch Objekte, die durch eine rechteckige Box umgeben sind. RectangularShape selbst ist abstrakt, gibt aber Methoden vor, die das Rechteck verändern und abfragen. Unter anderem gibt es Methoden, die Abfragen, ob ein Punkt im Rechteck ist (contains()), wie die Ausmaße sind oder wo das Rechteck seine Mitte besitzt.

Kurven

Mit der Klasse QuadCurve2D können wir quadratische und kubische Kurvensegmente beschreiben. Dies sind Kurven, die durch zwei Endpunkte und durch dazwischen liegende Kontrollpunkte gegeben sind. Kubische Kurvensegmente werden auch Bézier-Kurven genannt.

Pfade

Eine Polygon-Klasse wie unter AWT gibt es unter der 2D API nicht. Hier wird ein neuer Weg eingeschlagen, der über die Klasse GeneralPath geht. Damit lassen sich beliebige Formen bilden. Dem Pfad werden verschiedene Punkte zugefügt, die dann verbunden werden. Die Punkte müssen nicht zwingend wie bei Polygonen mit Linien verbunden werden, sondern lassen sich auch durch quadratische oder kubische Kurven verbinden.

Abbildung

Beispiel Zeichnen einer Linie unter der 2D API
public void paint( Graphics g )
{
Graphics2D g2 = (Graphics2D) g;
GeneralPath p = new GeneralPath( 1 );
p.moveTo( x1, y1 );
p.lineTo( x2, y2 );
g2.setColor( Color.black );
g2.draw( p );
}

Natürlich hätten wir in diesem Fall auch ein Line2D-Objekt nehmen können. Doch dieses Beispiel zeigt einfach, wie ein Pfad aufgebaut ist. Zunächst bewegen wir den Zeichenstift mit moveTo() auf eine Position und anschließend zeichnen wir eine Linie mit lineTo(). Um eine Kurve zu einem Punkt zu ziehen, nehmen wir quadTo() oder für Bézier-Kurves curveTo(). Die Methoden erwarten Parameter vom Typ float. Ist der Pfad einmal gezogen, zeichnet draw() die Form, und fill() füllt das Objekt aus.


Galileo Computing

14.18.3 Eigenschaften geometrischer Objekte  downtop

Windungs-Regel

Eine wichtige Eigenschaft für gefüllte Objekte ist die Windungs-Regel (engl. Winding Rule). Diese Regel kann entweder WIND_NON_ZERO oder WIND_EVEN_ODD sein. Konstanten aus dem GeneralPath-Objekt werden dabei einfach der Methode setWindingRule() übergeben.

p.setWindingRule( GeneralPath.WIND_NON_ZERO );

Wenn Zeichenoperationen aus einer Form herausführen und wir uns dann wieder in der Figur befinden, sagt WIND_EVEN_ODD aus, dass dann innen und außen umgedreht wird. Wenn wir also zwei Rechtecke ineinander durch einen Pfad positionieren und der Pfad wird gefüllt, bekommt die Form ein Loch in der Mitte.

Betrachten wir dazu den folgenden Programmcode, der für die Mittelpunktkoordinaten x und y zwei Rechtecke zeichnet. Das erste Rechteck besitzt die Breite width, und die Höhe height und das innere Rechteck ist halb so groß:

GeneralPath p = new GeneralPath();

p.moveTo( x + (width/2), y – (height/2) );
p.lineTo( x + (width/2), y + (height/2) );
p.lineTo( x – (width/2), y + (height/2) );
p.lineTo( x – (width/2), y – (height/2) );

p.moveTo( x + (width/4), y – (height/4) );
p.lineTo( x + (width/4), y + (height/4) );
p.lineTo( x – (width/4), y + (height/4) );
p.lineTo( x – (width/4), y – (height/4) );

Mit moveTo() bewegen wir uns zum ersten Punkt. Die anschließenden lineTo()-Direktiven formen das Rechteck. Die Form muss nicht geschlossen werden, da sie mit fill() automatisch geschlossen wird. Wir können dies jedoch mit closePath() noch zusätzlich machen. Wenn wir das Objekt nur zeichnen, ist dies selbstverständlich notwendig. Dieses Beispiel macht durch das innere Rechteck deutlich, dass die Figuren eines GeneralPath-Objekts nicht zusammenhängend sein müssen. Das innere Rechteck wird nun genauso gezeichnet wie das äußere.

Mit der Konstanten WIND_NON_ZERO wird nun das innere Rechteck mit ausgefüllt. Ausschlaggebend, ob nun das innere Rechteck gezeichnet wird, ist die Anzahl der Schnittpunkte nach außen – »außen« heißt in diesem Fall unendlich viele Schnittpunkte. Diese Regel wird aber nur dann wichtig, wenn wir mit nicht konvexen Formen arbeiten. Solange sich die Linien nicht schneiden, ist dies kein Problem.

Beispiel Das nachfolgende Programm zeichnet mit dem oben genannten Rechteck-Pfad zwei Rechtecke. Ein blaues mit GeneralPath.WIND_NON_ZERO und ein anderes rotes mit GeneralPath.WIND_EVEN_ODD.

Abbildung 14.10   Die Windungsregeln WIND_NO_ZERO und WIND_EVEN_ODD
Abbildung

Listing 14.23   WindDemo.java
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;

class WindDemo extends JFrame
{
static GeneralPath makeRect( int x, int y, int width, int height )
{
GeneralPath p = new GeneralPath();

p.moveTo( x + width/2, y – height/2 );
p.lineTo( x + width/2, y + height/2 );
p.lineTo( x – width/2, y + height/2 );
p.lineTo( x – width/2, y – height/2 );
p.closePath();

p.moveTo( x + width/4, y – height/4 );
p.lineTo( x + width/4, y + height/4 );
p.lineTo( x – width/4, y + height/4 );
p.lineTo( x – width/4, y – height/4 );

return p;
}


public void paint( Graphics g )
{
Graphics2D g2 = (Graphics2D) g;

g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

g2.clearRect( 0, 0, getSize().width-1, getSize().height-1 );

g2.setColor( Color.yellow );
g2.fill( new Rectangle( 70, 70, 130, 50 ) );

GeneralPath p;

// Erstes Rechteck

p = makeRect( 100, 80, 50, 50 );

p.setWindingRule( GeneralPath.WIND_NON_ZERO );

g2.setColor( Color.blue );

g2.fill( p );


// Zweites Rechteck

p = makeRect( 200, 80, 50, 50 );

p.setWindingRule( GeneralPath.WIND_EVEN_ODD );

g2.setColor( Color.red );

g2.fill( p );
}

public static void main( String args[] )
{
JFrame f = new WindDemo();
f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
f.setSize( 300, 150 );
f.show();
}
}

Das Objekt RenderingHints

Bisher haben wir still schweigend eine Zeile eingefügt, die das Weichzeichnen (engl. Antialiasing) einschaltet. Dadurch erscheinen die Bildpunkte weicher nebeneinander, sind aber etwas dicker, da in der Nachbarschaft Pixel eingefügt werden.

Beispiel Es soll alles weich gezeichnet werden
g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON );

In der Programmzeile nutzen wir die setRenderingHint()-Methode der Klasse Graphics2D. Die Methode nimmt immer einen Schlüssel (daher beginnen die Konstanten mit KEY_XXX) und einen Wert (VALUE_XXX).

abstract class java.awt.Graphics2D
extends Graphics

gp  abstract void setRenderingHint( RenderingHints.Key hintKey, Object hintValue)
Setzt ein Eigenschaft des Rendering-Algorithmus.

Im Beispiel setzen wir den Hinweis auf das ANTIALIASING. Da durch das Weichzeichnen mehr Rechenaufwand nötig ist, empfiehlt es sich für eine schnelle Grafikausgabe auf das Antialiasing zu verzichten. Um dies zu erreichen, würden wir den Schlüssel ANTIALIAS_OFF als zweites Argument übergeben. Weitere Hinweise sind etwa

gp  KEY_ALPHA_INTERPOLATION
gp  KEY_COLOR_RENDERING
gp  KEY_DITHERING
gp  KEY_FRACTIONALMETRICS
gp  KEY_INTERPOLATION
gp  KEY_RENDERING
gp  KEY_TEXT_ANTIALIASING

Mit dem RENDERING-Schlüssel können wir etwa die Geschwindigkeit bestimmen, die direkt mit der Qualität der Ausgabe korreliert. Mögliche Werte sind RENDER_SPEED, RENDER_ QUALITY oder RENDER_DEFAULT.

Die Dicke und die Art der Linien bestimmen

Mit der 2D API lässt sich einfach mit der Methode setStroke() die Dicke (width), die Eigenschaft wie ein Liniensegment beginnt und endet (end caps), die Art, wie sich Linien verbinden (line joins) und ein Linien-Pattern (dash attributes) definieren.

Unterstützt wird diese Operation durch die Schnittstelle Stroke, die konkret durch BasicStroke implementiert wird. Für BasicStroke-Objekte gibt es neun Konstruktoren.

Abbildung

Die folgende Anweisung zeichnet die Elemente eines Pfads mit einer Dicke von 10 Pixel.

Stroke stroke = new BasicStroke( 
10 );
g2.setStroke( stroke );

Besonders bei breiten Linien ist es interessant, wie die Linie endet. Hier lässt sich aus CAP_BUTT, CAP_ROUND und CAP_SQUARE auswählen.

Die folgenden Zeilen aus dem Programm BasicStrokeDemo zeigen die drei Möglichkeiten auf:

g2.setStroke( new BasicStroke( 
20, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER ) );
g2.drawLine( 30, 50, 200, 50 );

g2.setStroke( new BasicStroke( 20, BasicStroke.CAP_SQUARE,
BasicStroke.JOIN_MITER ) );
g2.drawLine( 30, 150, 200, 150 );

g2.setStroke( new BasicStroke( 20, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_MITER ) );
g2.drawLine( 30, 100, 200, 100 );

Linien existieren aber nicht alleine, sondern sind etwa in einem Rechteck auch verbunden. Daher ist es wichtig, diese Eigenschaft auch bestimmen zu können. Da es keinen Konstruktor gibt, der nur den Linien-Ende-Typ angibt, aber nicht auch gleichzeitig den Verbindungstyp haben wir im oberen Beispiel schon eine Verbindung benutzt: JOIN_MITER. Diese ist aber nur Eine von Dreien. Die anderen lauten JOIN_ROUND und JOIN_BEVEL. MITER schließt die Linien so ab, dass sie senkrecht aufeinander stehen. Bei ROUND sind die Ecken abgerundet und bei BEVEL wird eine Linie zwischen den beiden äußeren Endpunkten gezogen.

Beispiel Das Programm BasicStrokeDemo.java zeigt unterschiedliche Abrundungsarten.

Listing 14.24   BasicStrokeDemo.java
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.util.*;

class BasicStrokeDemo extends JFrame
{
public void paint( Graphics g )
{
Graphics2D g2 = (Graphics2D) g;

g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

g2.setStroke( new BasicStroke( 20,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER ) );
g2.drawLine( 30, 50, 200, 50 );

g2.setStroke( new BasicStroke( 20,
BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER ) );
g2.drawLine( 30, 150, 200, 150 );

g2.setStroke( new BasicStroke( 20,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER ) );
g2.drawLine( 30, 100, 200, 100 );
}

public static void main( String args[] )
{
JFrame f = new BasicStrokeDemo();
f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
f.setSize( 260, 200 );
f.show();
}
}
Abbildung 14.11   Unterschiedliche Linienenden
Abbildung

Mit der Variablen BEVEL kann noch bestimmt werden, wie weit die Linien nach außen gezogen sind. Hier bestimmt die Variable miterlimit diese Verschiebung. Das Beispiel MiterlimitDemo.java zeigt diese Eigenschaft von miterlimit.

Listing 14.25   MiterlimitDemo.java
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.util.*;

class MiterlimitDemo extends JFrame
{
public void paint( Graphics g )
{
Graphics2D g2 = (Graphics2D) g;

g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

BasicStroke stroke = new BasicStroke( 15,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
1 ); // Mitterlimit = 15

g2.setStroke( stroke );

g2.draw( new Rectangle2D.Float( 50, 50, 50, 50 ) );


stroke = new BasicStroke( 15,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
10 ); // Mitterlimit = 15

g2.setStroke( stroke );

g2.draw( new Rectangle2D.Float( 150, 50, 50, 50 ) );

}

public static void main( String args[] )
{
JFrame f = new MiterlimitDemo();
f.setSize( 250, 150 );
f.show();
}
}
Abbildung 14.12   Unterschiedliche Miterlimit
Abbildung

Füllmuster

Auch die Muster, mit denen die Linien oder Kurven gezeichnet werden, lassen sich ändern. Dazu erzeugen wir vorher ein Feld und übergeben dies einem Konstruktor. Damit auch die Muster abgerundet werden, muss CAP_ROUND gesetzt sein. Folgende Zeilen erzeugen ein Rechteck mit einem einfachen Linienmuster – 10 Punkte gesetzt und 2 Punkte frei:

float dash[] = { 10, 2 };

BasicStroke stroke = new BasicStroke( 2,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
1,
dash, 0 );

g2.setStroke( stroke );

g2.draw( new Rectangle2D.Float( 50, 50, 50, 50 ) );

Als letzter Parameter hängt am Konstruktor noch eine Verschiebung an. Diese bestimmt, ob im Muster Pixel übersprungen werden sollen. Geben wir dort für unser Beispiel etwa 10 an, so beginnt die Linie gleich mit zwei nicht gesetzten Pixeln. Eine 12 ergibt eine Verschiebung wieder an den Anfang. Bei nur einer Zahl im Feld ist der Abstand der Linien und die Breite einer Linie genau so lang wie diese Zahl angibt. Bei gepunkteten Linien ist das Feld also 1. Hier eignet sich ganz gut ein anonymes Feld, wie die nächsten Zeilen zeigen:

stroke = new BasicStroke( 1,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
1, new float[]{ 1 }, 0 );

Bei feinen Linien sollten wir das Weichzeichnen besser ausschalten.

Abbildung 14.13   Zwei Linienmuster
Abbildung


Galileo Computing

14.18.4 Transformationen mit einem AffineTransform-Objekt  downtop

Eine affine Transformation eines Objekts ist entweder eine Translation (Verschiebung), Rotation, Skalierung oder Scherung . Bei diesen Transformationen bleiben parallele Linien nach der Transformation auch parallel. Um diese Operationen durchzuführen, existiert eine Klasse AffineTransform. Dem Graphics2D-Kontext können diese Transformationen vor dem Zeichnen zugewiesen werden, etwa über die Methode setTransform(). Aber auch Grafiken lassen sich mit drawImage() vor dem Zeichnen ein AffineTransform-Objekt übergeben und so einfach bearbeiten. Mit wenigen Zeilen Programmcode lassen sich dann beliebige Formen, Text und Grafiken verändern.

Abbildung

Die zweidimensionalen Objekte können durch die Operationen Translation, Rotation, Skalierung oder Scherung verändert werden. Diese Operationen sind durch eine 3x3-Matrix gekennzeichnet. Die Klasse AffineTransform bietet nun Methoden an, damit wir diese Matrix selber erzeugen können sowie Hilfsmethoden, die uns die Arbeit abnehmen.

AffineTransform trans = new AffineTransform();
trans.rotate( 0.1 );
g2.setTransform( trans );
g2.fill( new Rectangle2D.Float( 150, 100, 60, 60 ) );

Konstruktoren

Die Klasse AffineTransform besitzt sechs Konstruktoren. Zunächst einen Standard-Konstruktor und einen Konstruktor mit einem schon vorhandenen AffineTransform-Objekt. Dann jeweils einen Konstruktor für eine Matrix mit dem Datentyp float und mit dem Datentyp double sowie zwei Konstruktoren mit allen sechs Werten der Matrix für float und double. Eine eigene Matrix macht nur dann Sinn, wenn wir mehrere Operationen hintereinander ausführen lassen wollen. So nutzen wir in der Regel den Standard-Konstruktor wie oben und ändern die Form durch die Methoden rotate(), scale(), shear() oder translate(). Wird nach dem Erzeugen des AffineTransform-Objekts direkt eine der Methoden aufgerufen, so geht dies auch einfacher über die statischen Erzeugungsmethoden getRotateInstance(), getScaleInstance(), getShearInstance() und getTranslateInstance(). Sie füllen dann die Matrix mit den passenden Einträgen. Ein Transformationsobjekt kann mit setToIdentity() wieder initialisiert werden, sodass AffineTransform wieder verwendbar ist.


Galileo Computing

14.19 Graphic Layers Framework  downtop

Da viele Effekte von allen Programmierern immer wieder programmiert werden, hat Sun die Graphic Layers Framework (GLF) entwickelt. Sie entstand für das Java 2D-Graphics Buch, das von Sun Microsystems Press in der Java Serie herausgegeben wurde. Mit der Bibliothek lassen sich Rendereffekte in Applets und Applikationen verwenden. Sie lässt sich unter http://java.sun.com/products/java-media/2D/samples/glf/index.html laden.


Galileo Computing

14.20 Grafikverarbeitung ohne grafische Oberfläche  downtop

Im AWT sind viele Operationen fest mit einer konkreten grafischen Oberfläche verbunden und lassen sich nicht getrennt davon behandeln. Nahezu jeder Aufruf einer Graphics-Methode wie drawLine() ist nativ von einer Graphics-Unterklasse auf der Host-Plattform implementiert. So führt drawLine()direkt nach ein paar Umwandlungen auf eine Funktion auf der Plattform.

Am stärksten wird diese Einschränkung sichtbar, wenn ein Offscreen-Bild bearbeitet oder erstellt werden soll. Die Klassen dafür sind MemoryImageSource und PixelGrabber. Wenn wir nur ein Bild vom Dateisystem laden wollen (getImage() von Toolkit), dann dieses Bild in ein Integer-Feld umwandeln (MemoryImageSource), dieses dann über Filter laufen lassen und als Bitmap-Datei abspeichern wollen, hat das zunächst einmal nicht viel mit einer grafischen Oberfläche gemeinsam. Alle Operationen laufen im Hintergrund und ein Bildschirm ist nicht nötig. Diese Aufgaben sind ganz typisch für Server-Applikationen, etwa Servlets. Die erzeugen zum Beispiel Grafiken für eine Serverstatistik und erstellen Börseninformationen. Leider sind aber alle Grafikoperationen, insbesondere das createImage(), für Hintergrundbilder von einer grafischen Benutzeroberfläche abhängig. Unter einem Windows- oder Macintosh-Server ist das erst einmal kein Problem, da beide von Haus aus mit einer grafischen Oberfläche ausgestattet wurden. Im Serverbetrieb laufen jedoch sehr häufig Unix-Server, bei denen eine grafische Oberfläche nur Spielerei ist. Zudem öffnet ein X11-System unter Unix viele Sicherheitslöcher, die besser vermieden werden sollten.


Galileo Computing

14.20.1 Xvfb-Server  downtop

Eine Möglichkeit ist, auf dem Unix-Rechner einen speziellen X-Server zu installieren. Die gute Nachricht ist, dass es kein vollständiger X-Server sein muss, sondern, dass es unter X einen speziellen Server für die Shell gibt, den so genannten X-Virtual-Framebuffer-Server, kurz Xvfb-Server. Mit ihm können dann alle Grafikoperationen verwendet werden, da die Java-Implementierung dann den Bildschirmspeicher dieses virtuellen Servers nutzt. Um den virtuellen Server einzusetzen, muss Xvfb nur compiliert werden; der Quellcode liegt unter htp://www.x.org. Xvfb lässt sich als Dämon einsetzen und etwa mit folgender Anweisung aktivieren:

# /usr/X11R6/bin/Xvfb :1 -screen 
0 1152x900x8 2>>/var/adm/Xvfb.log &

Für das AWT muss die DISPLAY-Variable noch gesetzt werden.

DISPLAY='hostname':1.0

Galileo Computing

14.20.2 Pure Java AWT Toolkit (PJA)  toptop

Eine andere Lösung für das Problem ist das Pure Java AWT Toolkit (kurz PJA). Diese 100 %-ige Java-Implementierung ist eine Unterklasse von Graphics und implementiert die Methoden ohne nativen Code. So lässt sich auf einen speziellen Server verzichten, lassen sich Ressourcen schonen und dennoch Bilder im Hintergrund berechnen. Die Installation ist dabei nicht kompliziert und da die Bibliothek unter der GNU General Public Licence liegt, lassen sich die Quellen zur Not auch modifizieren. Das Ersatz-AWT wird dabei einfach über die Umgebungsvariable java.awt eingeführt. Der Verweis zeigt auf com.eteks.awt.PJAToolkit und dann sind keine Modifikationen an den Programmen nötig. Die grafische Klasse nutzt dann nicht mehr das native Standard-AWT, sondern PJA.

Das PJAToolkit rendert in einigen Fällen die grafischen Primitiven anders als es die Java-API-Dokumentation beschreibt. So beschreibt die Hilfe von Eteks etwa, dass unter MacOS SansSerif anders dargestellt wird und dass die Ellispsen etwas anders aussehen könnten, da sie mit Bresenham gezeichnet werden.

Die Bibliothek kann mit Beispielprogrammen (unter anderem GIF generieren, Font-Auszüge erstellen, Servlet erstellt Diagramm), FAQ, Quellcode, Jar-Archiven unter http://www.eteks.com/pja/en bezogen werden. Im Download befindet sich auch eine Batch-Datei, die zeigt, wie die Variable gesetzt wird und wie die beiden Jar-Dateien heißen, die eingebunden werden müssen.






1    Das Wort erspare ich den Lesern.

2    Zur Bewegung des Grafik-Cursors wird gerne eine XOR-Operation eingesetzt. Obwohl dies absolut einfach erscheint, ist die Realisierungsidee patentiert.

3    Obwohl quadratische B-Splines eine Untermenge von kubischen Bézierkurzen ist, macht die Konvertierung der Zeichensätze den Anbietern viele Sorgen. Zudem lassen sich die Hints nur sehr schwer umsetzen.

4    Benannt nach den Erfindern Lempel, Ziv und Welch.

5    Das große »W« ist kein Tippfehler.

6    Hört sich an wie ein Fußballverein, ist aber keiner.

7    Ein Objekt wird geschert, wenn es entlang einer Koordinatenachse verzogen wird. Im Zweidimensionalen gibt es zwei Scherungsarten: Entlang der x-Achse und der y-Achse.

  

Java 2




Copyright © Galileo Press GmbH 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de