Java-Tipp 109: Zeigen Sie Bilder mit JEditorPane an
Sie können die aktuelle JEditorPane
Komponente verwenden, um HTML-Markups anzuzeigen. Um jedoch kompliziertere Aufgaben auszuführen, JEditorPane
mü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. JEditorPane
war 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 JEditorPane
keine 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 JEditorPane
hilft uns zu verstehen, warum Bilder nicht in relativen Pfaden angezeigt werden können.
JEditorPane()
schafft eine neueJEditorPane
.JEditorPane(String url)
Erstellt eineJEditorPane
basierend auf einer Zeichenfolge, die eine URL-Spezifikation enthält.JEditorPane(String type, String text)
Erstellt eineJEditorPane
, die mit dem angegebenen Text initialisiert wurde.JEditorPane(URL initialPage)
Erstellt eineJEditorPane
basierend 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 HTMLDocument
befindet sich in jedem JEditorPane
und seine Basis wird auf die Basis des URL-Konstruktorparameters gesetzt. JEditorPane
Mit diesen Konstruktoren erstellte s können relative Pfade verarbeiten, da die Basis der HTMLDocument
mit 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 String
als 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, JEditorPane
ist 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 JEditorPane
HTML gerendert? Jede Art von JEditorPane
Referenzen sowohl a Document
als 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 HTMLEditorKit
enthält die Klasse eine HTMLFactory
innere 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, ImageView
lä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 HTMLDocument
Basis 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 ImageView
Klasse 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 ImageView
Klasse 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 MyImageView
sollten 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 String
und 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.ImageView
Klasse, 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 MediaTracker
zu erkennen, wann das Bild fertig war, aber der MediaTracker
Konstruktor des Bilds erfordert, dass die Komponente das Bild als Parameter rendert. Also habe ich mir noch einmal Code von Jim Graham ausgeliehen java.awt.MediaTracker
und 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 ImageView
aus dem javax.swing.text.html
Paket 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. ImageView
Wenn 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.icons
Paket geladen. Um das fehlerhafte Bild zu laden, verwendet die Klasse die getResourceAsStream(String name)
Methode aus der Class
Klasse. Der eigentliche Code sieht folgendermaßen aus:
InputStream resource = HTMLEditorKit.class.getResourceAsStream (MISSING_IMAGE_SRC);
wobei der MISSING_IMAGE_SRC
Parameter a String
mit Inhalt ist:
MISSING_IMAGE_SRC = "Symbole" + System.getProperty ("file.separator", "/") + "image-failed.gif";
Der folgende Auszug aus dem ImageView
Quellcode 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 MyHTMLEditorKit
erstellen 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 JEditorPane
zu verwendende Einstellung festlegen MyHTMLEditorKit
. Der Code ist ganz einfach: