Astuce Java 109: afficher des images à l'aide de JEditorPane

Vous pouvez utiliser le JEditorPanecomposant actuel pour afficher le balisage HTML, mais pour effectuer des tâches plus complexes, il JEditorPanefaut quelques améliorations. Récemment, j'ai dû créer une application de création de formulaires XML. Un composant nécessaire était un éditeur HTML WYSIWYG qui pouvait modifier le contenu du balisage HTML à l'intérieur de certaines balises XML. JEditorPaneétait le choix évident du composant Java pour afficher le balisage HTML, car cette fonctionnalité y était déjà intégrée. Malheureusement, une fois inséré dans le balisage HTML, JEditorPanene pouvait pas afficher les images avec des chemins relatifs. Par exemple, si l'image suivante avec un chemin relatif était contenue dans une balise XML, elle ne s'afficherait pas correctement:


  

Inversement, un chemin absolu fonctionnerait (en supposant que le chemin et l'image donnés existaient vraiment):


  

Dans mon application, les images étaient toujours stockées dans un sous-répertoire relatif à l'emplacement du fichier XML. Par conséquent, j'ai toujours voulu utiliser un chemin relatif. Cet article explique pourquoi ce problème existe et comment le résoudre.

Pourquoi cela arrive-t-il?

Un examen plus attentif des constructeurs de JEditorPanenous aidera à comprendre pourquoi il ne peut pas afficher les images dans des chemins relatifs.

  1. JEditorPane()crée un nouveau JEditorPane.
  2. JEditorPane(String url)crée un JEditorPanebasé sur une chaîne contenant une spécification d'URL.
  3. JEditorPane(String type, String text)crée un JEditorPanequi a été initialisé au texte donné.
  4. JEditorPane(URL initialPage)crée un JEditorPanebasé sur une URL spécifiée pour l'entrée.

Les deuxième et quatrième constructeurs initialisent l'objet avec une référence à un fichier HTML distant ou local. An HTMLDocumentest à l'intérieur de chaque JEditorPane, et sa base est définie sur la base du paramètre du constructeur d'URL. JEditorPaneLes s créés à l'aide de ces constructeurs peuvent gérer les chemins relatifs, car la base du se HTMLDocumentcombine avec le chemin relatif pour créer un chemin absolu.

Si le premier constructeur est utilisé, le texte affiché doit être inséré après la création de l'objet. Le troisième constructeur accepte un Stringcomme contenu, mais la base n'est pas initialisée. Parce que je voulais obtenir le balisage HTML à partir d'une balise XML et non d'un fichier, je devais utiliser le premier ou le troisième constructeur.

Comment résoudre le problème?

Avant de continuer, dévoilons et résolvons un autre problème plus petit. La manière la plus évidente d'insérer un balisage dans le JEditorPaneest d'utiliser le setText(String text). Cependant, cette méthode nécessite que vous saisissiez la totalité du balisage affiché chaque fois que vous apportez une modification. Idéalement, la ou les nouvelles balises doivent être insérées dans le texte existant. Vous pouvez utiliser le code suivant pour ajouter le nouveau balisage:

private void insertHTML (éditeur JEditorPane, String html, int location) jette IOException {// suppose que l'éditeur est déjà défini sur le type "text / html" HTMLEditorKit kit = (HTMLEditorKit) editor.getEditorKit (); Document doc = editor.getDocument (); Lecteur StringReader = nouveau StringReader (html); kit.read (lecteur, doc, emplacement); }

Maintenant, entrant dans le vif du sujet: comment JEditorPanerend le HTML? Chaque type de JEditorPaneréférence à la fois a Documentet an EditorKit. Lorsque JEditorPaneest défini sur le type "text / html", il contient un HTMLDocument, qui contient le balisage et un HTMLEditorKitqui détermine quelles classes rendent chaque balise contenue dans le balisage. Plus précisément, la HTMLEditorKitclasse contient une HTMLFactoryclasse interne dont la create(Element elem)méthode examine en fait chaque balise distincte. Voici le code de cette classe d'usine, qui gère les balises d'image:

 else if (kind == HTML.Tag.IMG) return new ImageView (elem); 

Comme vous pouvez maintenant le voir, la ImageViewclasse charge réellement l'image. Pour établir l'emplacement de l'image, la getSourceURL()méthode est appelée:

URL privée getSourceURL () {String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); if (src == null) renvoie null; Référence URL = ((HTMLDocument) getDocument ()). getBase (); essayez {URL u = nouvelle URL (référence, src); return u; } catch (MalformedURLException e) {return null; }}

Ici, la getSourceURL()méthode tente de créer une nouvelle URL pour référencer l'image en utilisant la HTMLDocumentbase. Si cette base est nulle, null est renvoyée et l'opération de chargement d'image est abandonnée. Vous souhaitez annuler ce comportement.

Idéalement, vous sous-classeriez la ImageViewclasse et remplaceriez la initialize(Element elem)méthode, où le chargement de l'image est effectué. Malheureusement, cette classe est protégée par package, vous devez donc créer une classe entièrement nouvelle. Le moyen le plus simple de le faire est d'emprunter, puis de modifier, le code de la ImageViewclasse d' origine . Appelons ça MyImageView.

Tout d'abord, regardez le code qui a chargé l'image. Ce qui suit est tiré de la initialize(Element elem)méthode:

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

Ici, vous obtenez l'URL; s'il est nul, vous ignorez le chargement de l'image. Dans MyImageView, vous ne devez exécuter ce code que si votre référence d'image est une URL. Voici une méthode que vous pouvez ajouter pour tester la source de l'image:

private boolean isURL () String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); retourne src.toLowerCase (). startsWith ("fichier")  

En gros, vous obtenez la référence à l'image sous la forme d'un Stringet testez pour voir si elle commence par l'un des deux types d'URL: fichier pour les images locales et http pour les images distantes. Jens Alfke, auteur de la javax.swing.text.html.ImageViewclasse d' origine , utilise des variables globales de classe, il n'est donc pas nécessaire de passer des paramètres aux fonctions. Ici, la variable globale est fElement.

Vous pouvez écrire du code qui dit , mais que mettez-vous dans l'instruction else pour un chemin relatif? C'est assez simple - chargez simplement l'image comme vous le feriez normalement dans une application:if (isURL()) { }

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

Il n'y a pas de vraie magie ici, mais il y a un piège. La createImage(src)fonction peut revenir avant que tous les pixels de l'image aient été remplis. Si cela se produit, une image cassée sera affichée. Pour résoudre le problème, vous pouvez simplement attendre que les pixels de l'image soient complètement remplis. Ma première inclination a été d 'utiliser l' MediaTrackerpour détecter quand l 'image était prête, mais le MediaTrackerconstructeur de s requiert le composant rendant l' image comme paramètre. Donc, encore une fois, j'ai emprunté du code à Jim Graham java.awt.MediaTrackeret j'ai écrit ma propre méthode pour contourner le problème:

private void waitForImage () lance InterruptedException {int w = fImage.getWidth (this); int h = fImage.getHeight (ceci); while (vrai)}

Cette méthode fait essentiellement le même travail que la méthode MediaTrackers waitForID(int id), mais ne nécessite pas de composant parent. Un appel à cette méthode peut être placé juste après la création de l'image.

Il y a un petit problème que je devrais mentionner avant de continuer. Il était impossible de sous- classer à ImageViewpartir du javax.swing.text.htmlpackage, j'ai donc copié le fichier entier pour créer ma propre classe, appelée MyImageView, que je n'ai pas mise dans un package. Dans le ImageViewcode d' origine , si une image ne peut pas être affichée car elle n'existe pas ou est retardée, elle charge une image cassée par défaut à partir du javax.swing.text.html.iconspackage. Pour charger l'image cassée, la classe utilise la getResourceAsStream(String name)méthode de la Classclasse. Le code réel ressemble à ceci:

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

où le MISSING_IMAGE_SRCparamètre est un Stringavec contenu:

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

L'extrait suivant du ImageViewcode source explique le raisonnement de Sun pour utiliser la getResourceAsStream(String name)méthode de chargement des images cassées.

/ * Copie la ressource dans un tableau d'octets. Ceci est * nécessaire car plusieurs navigateurs considèrent * Class.getResource comme un risque de sécurité car il * peut être utilisé pour charger des classes supplémentaires. * Class.getResourceAsStream renvoie juste des octets * bruts, que nous pouvons convertir en image. * /

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, qui sous-classe HTMLEditorKit. À l'intérieur de votre MyHTMLEditorKit, vous créez une nouvelle classe interne appelée MyHTMLFactory, qui sous-classe HTMLFactory. Dans cette classe interne, vous pouvez créer votre propre create(Element elem)méthode, qui ressemble à ceci:

public View create (Élément d'élément) {Objet o = elem.getAttributes (). getAttribute (StyleConstants.NameAttribute); if (o instanceof HTML.Tag) {HTML.Tag kind = (HTML.Tag) o; if (kind == HTML.Tag.IMG) return new MyImageView (elem); } return super.create (elem); }

La seule chose à faire maintenant est de configurer le JEditorPaneà utiliser MyHTMLEditorKit. Le code est assez simple: