![]() |
|
|||||
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; } 14.11.2 Grafiken zentrieren
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Bilder können in Gruppen organisiert werden. |
| Bilder könenn synchron oder asynchron geladen werden. |
| Die Bilder-Gruppen können unabhängig geladen werden. |
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?
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. Sehen wir uns einmal eine typische Programmsequenz an, die dem Medien-Überwacher ein Hintergrundbild sowie einige animierte Bilder ü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 Bild-Array anim[] wird genauso gefüllt und überwacht. Die ID des Felds 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 beziehungsweise 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 ist 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.
| 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 der Methode statusID(), welche ja den Zustand des Ladens überwacht, können wir leicht die Fälle herausfinden, in denen das Bild erfolgreich beziehungsweise nicht erfolgreich geladen werden konnte. Dazu verknüpfen wir einfach durch den Und-Operator die Konstante mit dem Rückgabewert von statusAll() oder statusID():
if ( (tracker.statusAll() & MediaTracker.ERRORED) != 0 )
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 |
| static final int ABORTED Flag, welches anzeigt, dass das Medium nicht geladen werden konnte. Rückgabewert von statusAll() oder statusID(). |
| static final int ERRORED Während des Ladens gab es Fehler. Rückgabewert von statusAll() und statusID(). |
| static final int COMPLETE Medium konnte geladen werden. Rückgabewert von statusAll() und statusID(). |
| MediaTracker( Component comp ) Erzeugt einen MediaTracker auf einer Komponente, auf der das Bild möglicherweise angezeigt wird. |
| void addImage( Image image, int id ) Fügt ein Bild nichtskaliert der Ladeliste hinzu. Ruft addImage(image, id, -1, -1) auf. |
| 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. |
| 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. |
| 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. |
| boolean isErrorAny() true, wenn eines der überwachten Bilder einen Fehler beim Laden verursachte. |
| Object[] getErrorsAny() Liefert eine Liste aller Objekte, die einen Fehler aufweisen. null, wenn alle korrekt geladen wurden. |
| 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. |
| 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. |
| 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. |
| 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. |
| boolean checkID( int id, boolean load ) Wie checkID(id). Allerdings werden nur die Bilder geladen, die bisher noch nicht geladen wurden. |
| boolean isErrorID( int id ) Liefert den Fehler-Status von allen Bildern mit der ID id. true, wenn eines der Bilder beim Laden einen Fehler aufweist. |
| Object[] getErrorsID( int id ) Liefert eine Liste aller Medien, die einen Fehler aufweisen. |
| 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 alle Bilder ordentlich geladen wurden. |
| boolean waitForID( int id, long ms ) throws InterruptedException Wie waitForID(), nur stoppt der Ladeprozess nach einer festen Anzahl von Millisekunden. |
| 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. |
| 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. |
| 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. |
| 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. |
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:
| Wenn wir Bildschirminhalte verschieben und Teile verdeckt werden, muss über die update()- und paint()-Methode der verdeckte Bildausschnitt neu aufgebaut werden. |
| 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, indem 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 einem beliebigen 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 ); }
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 Hintergrundfarbe ü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 zeitintensive 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().
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 sollten 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 und SCALE_AREA_AVERAGING.
| 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 Bilds 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 |
| 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 einige 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.
Image image = new ImageIcon("ottosHaus.jpg").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 );
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.
| << zurück |
Copyright © Galileo Press GmbH 2003
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.