Java-Tipp 109: Zeigen Sie Bilder mit JEditorPane an

Sie können die aktuelle JEditorPaneKomponente verwenden, um HTML-Markups anzuzeigen. Um jedoch kompliziertere Aufgaben auszuführen, JEditorPanemüssen einige Verbesserungen vorgenommen werden. Vor kurzem musste ich eine XML Form Builder-Anwendung erstellen. Eine notwendige Komponente war ein WYSIWYG-HTML-Editor, der den HTML-Markup-Inhalt in einigen XML-Tags bearbeiten konnte. JEditorPanewar die offensichtliche Wahl der Java-Komponente für die Anzeige des HTML-Markups, da diese Funktionalität bereits integriert war. Leider konnten beim Einfügen in das HTML-Markup JEditorPanekeine Bilder mit relativen Pfaden angezeigt werden. Wenn beispielsweise das folgende Bild mit einem relativen Pfad in einem XML-Tag enthalten wäre, würde es nicht richtig angezeigt:


  

Umgekehrt würde ein absoluter Pfad funktionieren (vorausgesetzt, der angegebene Pfad und das Bild existieren tatsächlich):


  

In meiner Anwendung wurden Bilder immer in einem Unterverzeichnis relativ zum Speicherort der XML-Datei gespeichert. Daher wollte ich immer einen relativen Pfad verwenden. In diesem Artikel wird erläutert, warum dieses Problem besteht und wie es behoben werden kann.

Warum passiert das?

Ein genauerer Blick auf die Konstruktoren für JEditorPanehilft uns zu verstehen, warum Bilder nicht in relativen Pfaden angezeigt werden können.

  1. JEditorPane()schafft eine neue JEditorPane.
  2. JEditorPane(String url)Erstellt eine JEditorPanebasierend auf einer Zeichenfolge, die eine URL-Spezifikation enthält.
  3. JEditorPane(String type, String text)Erstellt eine JEditorPane, die mit dem angegebenen Text initialisiert wurde.
  4. JEditorPane(URL initialPage)Erstellt eine JEditorPanebasierend auf einer angegebenen URL für die Eingabe.

Der zweite und vierte Konstruktor initialisieren das Objekt mit einem Verweis auf eine entfernte oder lokale HTML-Datei. An HTMLDocumentbefindet sich in jedem JEditorPaneund seine Basis wird auf die Basis des URL-Konstruktorparameters gesetzt. JEditorPaneMit diesen Konstruktoren erstellte s können relative Pfade verarbeiten, da die Basis der HTMLDocumentmit dem relativen Pfad kombinierten Pfade einen absoluten Pfad erstellt.

Wenn der erste Konstruktor verwendet wird, muss der angezeigte Text nach dem Erstellen des Objekts eingefügt werden. Der dritte Konstruktor akzeptiert a Stringals Inhalt, aber die Basis wird nicht initialisiert. Da ich das HTML-Markup von einem XML-Tag und nicht von einer Datei erhalten wollte, musste ich entweder den ersten oder den dritten Konstruktor verwenden.

Wie beheben wir das Problem?

Bevor ich fortfahre, wollen wir ein weiteres kleineres Problem enthüllen und lösen. Der naheliegendste Weg, um Markup in das einzufügen, JEditorPaneist die Verwendung von setText(String text). Diese Methode erfordert jedoch, dass Sie bei jeder Änderung das gesamte angezeigte Markup eingeben. Idealerweise sollten die neuen Tags in den vorhandenen Text eingefügt werden. Sie können den folgenden Code verwenden, um das neue Markup hinzuzufügen:

private void insertHTML (JEditorPane-Editor, String html, int location) löst eine IOException aus {// setzt voraus, dass der Editor bereits auf "text / html" gesetzt ist Typ HTMLEditorKit kit = (HTMLEditorKit) editor.getEditorKit (); Dokument doc = editor.getDocument (); StringReader reader = neuer StringReader (html); kit.read (Leser, Dokument, Speicherort); }}

Kommen wir nun zum Kern der Sache: Wie wird JEditorPaneHTML gerendert? Jede Art von JEditorPaneReferenzen sowohl a Documentals auch an EditorKit. Wenn JEditorPane"text / html" eingegeben wird, enthält es ein HTMLDocument, das das Markup enthält, und ein HTMLEditorKit, das bestimmt, welche Klassen jedes im Markup enthaltene Tag rendern. Insbesondere HTMLEditorKitenthält die Klasse eine HTMLFactoryinnere Klasse, deren create(Element elem)Methode tatsächlich jedes einzelne Tag untersucht. Hier ist der Code aus dieser Factory-Klasse, die Bild-Tags verarbeitet:

 sonst wenn (kind == HTML.Tag.IMG) neues ImageView (elem) zurückgibt; 

Wie Sie jetzt sehen können, ImageViewlädt die Klasse das Bild tatsächlich. Um die Position des Bildes zu bestimmen, wird die getSourceURL()Methode aufgerufen:

private URL getSourceURL () {String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); if (src == null) gibt null zurück; URL-Referenz = ((HTMLDocument) getDocument ()). getBase (); try {URL u = neue URL (Referenz, src); gib u zurück; } catch (MalformedURLException e) {return null; }}

Hier versucht die getSourceURL()Methode, eine neue URL zu erstellen, um das Bild mithilfe der HTMLDocumentBasis zu referenzieren . Wenn diese Basis null ist, wird null zurückgegeben und der Bildladevorgang wird abgebrochen. Sie möchten dieses Verhalten überschreiben.

Im Idealfall würden Sie die ImageViewKlasse in Unterklassen unterteilen und die initialize(Element elem)Methode überschreiben , bei der das Laden des Bildes erfolgt. Leider ist diese Klasse paketgeschützt, sodass Sie eine völlig neue Klasse erstellen müssen. Der einfachste Weg, dies zu tun, besteht darin, den Code aus der ursprünglichen ImageViewKlasse auszuleihen und dann zu ändern . Nennen wir es MyImageView.

Schauen Sie sich zunächst den Code an, der das Bild geladen hat. Folgendes wird aus der initialize(Element elem)Methode entnommen :

URL src = getSourceURL (); if (src! = null) {Dictionary cache = (Dictionary) getDocument (). getProperty (IMAGE_CACHE_PROPERTY); if (cache! = null) fImage = (Image) cache.get (src); sonst fImage = Toolkit.getDefaultToolkit (). getImage (src); }}

Hier erhalten Sie die URL; Wenn es null ist, überspringen Sie das Laden des Bildes. In MyImageViewsollten Sie diesen Code nur ausführen, wenn Ihre Bildreferenz eine URL ist. Die folgende Methode können Sie hinzufügen, um die Bildquelle zu testen:

private boolean isURL () String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); Rückgabe von src.toLowerCase (). StartsWith ("Datei")  

Grundsätzlich erhalten Sie den Verweis auf das Bild in Form von a Stringund testen, ob er mit einem der beiden URL-Typen beginnt: Datei für lokale Bilder und http für Remote-Bilder. Jens Alfke, Autor der ursprünglichen javax.swing.text.html.ImageViewKlasse, verwendet globale Klassenvariablen, sodass die Übergabe von Parametern an Funktionen nicht erforderlich ist. Hier ist die globale Variable fElement.

Sie können Code schreiben, der besagt , aber was setzen Sie in die else-Anweisung für einen relativen Pfad? Es ist ganz einfach - laden Sie das Bild einfach wie gewohnt in eine Anwendung:if (isURL()) { }

sonst {String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); fImage = Toolkit.getDefaultToolkit (). createImage (src); }}

Hier gibt es keine wirkliche Magie, aber es gibt einen Haken. Die createImage(src)Funktion kann zurückkehren, bevor alle Pixel des Bildes gefüllt wurden. In diesem Fall wird ein fehlerhaftes Bild angezeigt. Um das Problem zu beheben, können Sie einfach warten, bis die Pixel des Bildes vollständig ausgefüllt sind. Meine erste Neigung bestand darin, mit MediaTrackerzu erkennen, wann das Bild fertig war, aber der MediaTrackerKonstruktor des Bilds erfordert, dass die Komponente das Bild als Parameter rendert. Also habe ich mir noch einmal Code von Jim Graham ausgeliehen java.awt.MediaTrackerund meine eigene Methode geschrieben, um das Problem zu umgehen:

private void waitForImage () löst InterruptedException aus {int w = fImage.getWidth (this); int h = fImage.getHeight (this); while (true)}

Diese Methode funktioniert im Grunde die gleiche Arbeit wie die MediaTracker‚s - waitForID(int id)Methode, aber keine übergeordnete Komponente erfordern. Ein Aufruf dieser Methode kann unmittelbar nach der Erstellung des Bildes erfolgen.

Es gibt ein kleines Problem, das ich erwähnen sollte, bevor ich fortfahre. Es war unmöglich, eine Unterklasse ImageViewaus dem javax.swing.text.htmlPaket zu erstellen, daher habe ich die gesamte Datei kopiert, um meine eigene Klasse mit dem Namen zu erstellen MyImageView, die ich nicht in ein Paket eingefügt habe. ImageViewWenn im Originalcode ein Bild nicht angezeigt werden kann, weil es nicht vorhanden ist oder sich verzögert, wird ein standardmäßig fehlerhaftes Bild aus dem javax.swing.text.html.iconsPaket geladen. Um das fehlerhafte Bild zu laden, verwendet die Klasse die getResourceAsStream(String name)Methode aus der ClassKlasse. Der eigentliche Code sieht folgendermaßen aus:

 InputStream resource = HTMLEditorKit.class.getResourceAsStream (MISSING_IMAGE_SRC); 

wobei der MISSING_IMAGE_SRCParameter a Stringmit Inhalt ist:

 MISSING_IMAGE_SRC = "Symbole" + System.getProperty ("file.separator", "/") + "image-failed.gif"; 

Der folgende Auszug aus dem ImageViewQuellcode erläutert die Gründe von Sun für die Verwendung der getResourceAsStream(String name)Methode zum Laden der fehlerhaften Bilder.

/ * Ressource in ein Byte-Array kopieren. Dies ist * notwendig, da mehrere Browser * Class.getResource als Sicherheitsrisiko betrachten, da * es zum Laden zusätzlicher Klassen verwendet werden kann. * Class.getResourceAsStream gibt nur rohe * Bytes zurück, die wir in ein Bild konvertieren können. * /

If you haven't skipped through this section yet (I know, it's pretty nitty-gritty!), let me explain why I mention it. If you aren't aware of this behavior, you won't understand why broken images are not displayed correctly, and won't be able to fix the problem in your own code. To fix the problem, you must load your own images. I chose to continue using the same method, but it's not really necessary. The above warning is for browsers containing applets, which have security considerations that limit disk access (unless signed, of course). In any case, this article was intended for use with an application, so using an alternate image-loading method should not be a concern.

When a call to getResourceAsStream(String name) is made, you can include a relative path to the image, as illustrated above. In the above code, the broken image will always be loaded from the specified path relative to the HTMLEditorKit class. For example, since the HTMLEditorKit class is located in javax.swing.text.html, it will attempt to load the broken image image-failed.gif from javax.swing.text.html.icons. This also applies to simple directories; the classes do not have to be in packages. Lastly, since HTMLEditorKit is package protected, you do not have access to its getResourceAsStream(String name) method. Instead, you can use the MyImageView class and put your broken images in an icons subdirectory. The code line will look like this:

 InputStream resource = MyImageView.class.getResourceAsStream(MISSING_IMAGE_SRC); 

If you choose to use an implementation similar to mine, you will have to create your own icons. You can still use the icons bundled with Sun's JDK, but that requires changing the location of the resource to use an absolute path instead of a relative path. The absolute path is:

javax.swing.text.html.icons.imagename.gif 

To learn about using getResourceStream(String name), see the Javadoc information for the Class class; a link is provided in Resources.

This article is almost entirely about accommodating relative paths -- but what are they relative to? So far, if you use the code I have supplied, you will only be able to use paths relative to where you started the application. This is great if all your images are always located in those paths, but that is not always the case. I won't go into great detail on how to fix this problem, because it can be fixed easily. You can either set an application global variable somewhere in your application or set a system variable. In MyImageView, before loading the image, you concatenate the relative path to the image and the absolute path obtained from the global variable. If that doesn't make sense, look for the processSrcPath() method in the final source code for MyImageView.

At last, MyImageView is complete. However, you must figure out how to tell JEditorPane to use MyImageView instead of javax.swing.text.html.ImageView. The JEditorPane can support three text formats: plain, RTF, and HTML. If JEditorPane is displaying HTML, BasicHTML -- a subclass of TextUI -- is used to render the HTML. BasicHTML uses JEditorPane's HTMLEditorKit to create the View. The HTMLEditorKit contains a method called getViewFactory(), which returns an instance of an inner class called HTMLFactory. The HTMLFactory contains a method called create(Element elem), which returns a View according to the tag type. Specifically, if the tag is an IMG tag, it returns an instance of ImageView. To return an instance of MyImageView, you can create your own EditorKit called MyHTMLEditorKit, welche Unterklassen HTMLEditorKit. In Ihrem MyHTMLEditorKiterstellen Sie eine neue innere Klasse namens MyHTMLFactory, die Unterklassen HTMLFactory. In dieser inneren Klasse können Sie Ihre eigene create(Element elem)Methode erstellen, die ungefähr so ​​aussieht:

public View create (Element elem) {Objekt o = elem.getAttributes (). getAttribute (StyleConstants.NameAttribute); if (o Instanz von HTML.Tag) {HTML.Tag kind = (HTML.Tag) o; if (kind == HTML.Tag.IMG) gibt neues MyImageView (elem) zurück; } return super.create (elem); }}

Jetzt müssen Sie nur noch die JEditorPanezu verwendende Einstellung festlegen MyHTMLEditorKit. Der Code ist ganz einfach: