Suggerimento Java 109: visualizza le immagini utilizzando JEditorPane

È possibile utilizzare il JEditorPanecomponente corrente per visualizzare il markup HTML, ma per eseguire attività più complicate è JEditorPanenecessario migliorare. Recentemente, ho dovuto creare un'applicazione per la creazione di moduli XML. Un componente necessario era un editor HTML WYSIWYG in grado di modificare il contenuto del markup HTML all'interno di alcuni tag XML. JEditorPaneera l'ovvia scelta del componente Java per visualizzare il markup HTML, perché quella funzionalità era già incorporata in esso. Sfortunatamente, una volta inserito nel markup HTML, JEditorPanenon poteva visualizzare immagini con percorsi relativi. Ad esempio, se la seguente immagine con un percorso relativo fosse contenuta in un tag XML, non verrebbe visualizzata correttamente:


  

Al contrario, un percorso assoluto funzionerebbe (supponendo che il percorso e l'immagine dati esistessero davvero):


  

Nella mia applicazione, le immagini erano sempre archiviate in una sottodirectory relativa alla posizione del file XML. Quindi, ho sempre voluto utilizzare un percorso relativo. Questo articolo spiegherà perché questo problema esiste e come risolverlo.

Perché succede questo?

Dare uno sguardo più da vicino ai costruttori JEditorPaneci aiuterà a capire perché non può visualizzare le immagini in percorsi relativi.

  1. JEditorPane()crea un nuovo JEditorPane.
  2. JEditorPane(String url)crea un JEditorPanebasato su una stringa contenente una specifica URL.
  3. JEditorPane(String type, String text)crea un JEditorPaneche è stato inizializzato al testo dato.
  4. JEditorPane(URL initialPage)crea un JEditorPanebasato su un URL specificato per l'input.

Il secondo e il quarto costruttore inizializzano l'oggetto con un riferimento a un file HTML remoto o locale. Un HTMLDocumentè all'interno di ogni JEditorPanee la sua base è impostata sulla base del parametro costruttore dell'URL. JEditorPaneI file creati utilizzando questi costruttori possono gestire percorsi relativi, poiché la base delle HTMLDocumentcombina con il percorso relativo per creare un percorso assoluto.

Se viene utilizzato il primo costruttore, il testo visualizzato deve essere inserito dopo la creazione dell'oggetto. Il terzo costruttore accetta un Stringcontenuto come, ma la base non è inizializzata. Poiché volevo ottenere il markup HTML da un tag XML e non da un file, avevo bisogno di utilizzare il primo o il terzo costruttore.

Come risolviamo il problema?

Prima di continuare, sveliamo e risolviamo un altro problema più piccolo. Il modo più ovvio per inserire markup nel file JEditorPaneè usare il setText(String text). Tuttavia, questo metodo richiede di inserire l'intero markup visualizzato ogni volta che si apporta una modifica. Idealmente, i nuovi tag dovrebbero essere inseriti nel testo esistente. È possibile utilizzare il codice seguente per aggiungere il nuovo markup:

private void insertHTML (editor JEditorPane, String html, int location) genera IOException {// presume che l'editor sia già impostato su "text / html" tipo HTMLEditorKit kit = (HTMLEditorKit) editor.getEditorKit (); Documento doc = editor.getDocument (); StringReader reader = nuovo StringReader (html); kit.read (lettore, doc, posizione); }

Ora, arrivando al cuore della questione: come viene JEditorPanevisualizzato l'HTML? Ogni tipo di JEditorPaneriferimento fa sia a Documentche a EditorKit. Quando JEditorPaneè impostato sul tipo "text / html", contiene un HTMLDocument, che contiene il markup e un HTMLEditorKitche determina quali classi rendono ogni tag contenuto nel markup. In particolare, la HTMLEditorKitclasse contiene una HTMLFactoryclasse interna il cui create(Element elem)metodo esamina effettivamente ogni tag separato. Ecco il codice di quella classe factory, che gestisce i tag immagine:

 altrimenti se (kind == HTML.Tag.IMG) restituisce il nuovo ImageView (elem); 

Come puoi vedere ora, la ImageViewclasse carica effettivamente l'immagine. Per stabilire la posizione dell'immagine, il getSourceURL()metodo viene chiamato:

URL privato getSourceURL () {String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); if (src == null) restituisce null; Riferimento URL = ((HTMLDocument) getDocument ()). getBase (); prova {URL u = nuovo URL (riferimento, src); return u; } catch (MalformedURLException e) {return null; }}

Qui, il getSourceURL()metodo tenta di creare un nuovo URL per fare riferimento all'immagine utilizzando la HTMLDocumentbase. Se quella base è null, viene restituito null e l'operazione di caricamento dell'immagine viene interrotta. Vuoi sovrascrivere quel comportamento.

Idealmente, dovresti sottoclassare la ImageViewclasse e sovrascrivere il initialize(Element elem)metodo, dove viene eseguito il caricamento dell'immagine. Sfortunatamente, quella classe è protetta da un pacchetto, quindi devi creare una classe completamente nuova. Il modo più semplice per farlo è prendere in prestito, quindi modificare, il codice dalla ImageViewclasse originale . Chiamiamolo MyImageView.

Per prima cosa, guarda il codice che ha caricato l'immagine. Quanto segue è tratto dal initialize(Element elem)metodo:

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

Qui ottieni l'URL; se è nullo, salti il ​​caricamento dell'immagine. In MyImageView, dovresti eseguire questo codice solo se la tua immagine di riferimento è un URL. Il seguente è un metodo che puoi aggiungere per testare l'origine dell'immagine:

private booleano isURL () String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); return src.toLowerCase (). startsWith ("file")  

Fondamentalmente, si ottiene il riferimento all'immagine sotto forma di a Stringe si verifica se inizia con uno dei due tipi di URL: file per immagini locali e http per immagini remote. Jens Alfke, autore della javax.swing.text.html.ImageViewclasse originale , utilizza variabili globali di classe, quindi non è necessario passare parametri alle funzioni. Qui, la variabile globale è fElement.

Puoi scrivere codice che dice , ma cosa metti nell'istruzione else per un percorso relativo? È abbastanza semplice: carica l'immagine come faresti normalmente in un'applicazione:if (isURL()) { }

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

Non c'è vera magia qui, ma c'è un problema. La createImage(src)funzione può tornare prima che tutti i pixel dell'immagine siano stati popolati. In tal caso, verrà visualizzata un'immagine interrotta. Per risolvere il problema, puoi semplicemente aspettare che i pixel dell'immagine siano completamente popolati. La mia prima inclinazione è stata quella di utilizzare il MediaTrackerper rilevare quando l'immagine era pronta, ma il MediaTrackercostruttore di richiede che il componente restituisca l'immagine come parametro. Quindi, ancora una volta, ho preso in prestito del codice da Jim Graham java.awt.MediaTrackere ho scritto il mio metodo per aggirare il problema:

private void waitForImage () genera InterructedException {int w = fImage.getWidth (this); int h = fImage.getHeight (questo); while (true)}

Questo metodo fondamentalmente fa lo stesso lavoro del metodo MediaTrackers waitForID(int id), ma non richiede un componente genitore. Una chiamata a questo metodo può essere effettuata subito dopo la creazione dell'immagine.

C'è un piccolo problema che dovrei menzionare prima di continuare. Era impossibile creare una sottoclasse ImageViewdal javax.swing.text.htmlpacchetto, quindi ho copiato l'intero file per creare la mia classe, chiamata MyImageView, che non ho inserito in un pacchetto. Nel ImageViewcodice originale , se un'immagine non può essere visualizzata perché non esiste o è ritardata, carica un'immagine danneggiata predefinita dal javax.swing.text.html.iconspacchetto. Per caricare l'immagine interrotta, la classe utilizza il getResourceAsStream(String name)metodo dalla Classclasse. Il codice effettivo ha questo aspetto:

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

dove il MISSING_IMAGE_SRCparametro è un Stringcon contenuto:

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

Il seguente estratto dal ImageViewcodice sorgente spiega il ragionamento di Sun per l'utilizzo del getResourceAsStream(String name)metodo per caricare le immagini rotte.

/ * Copia la risorsa in una matrice di byte. Questo è * necessario perché diversi browser considerano * Class.getResource un rischio per la sicurezza perché * può essere utilizzato per caricare classi aggiuntive. * Class.getResourceAsStream restituisce solo * byte grezzi, che possiamo convertire in un'immagine. * /

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, quali sottoclassi HTMLEditorKit. All'interno di MyHTMLEditorKit, crei una nuova classe interna chiamata MyHTMLFactory, which subclasses HTMLFactory. In quella classe interna, puoi creare il tuo create(Element elem)metodo, che assomiglia a questo:

public View create (Element elem) {Object o = elem.getAttributes (). getAttribute (StyleConstants.NameAttribute); if (o instanceof HTML.Tag) {HTML.Tag kind = (HTML.Tag) o; if (kind == HTML.Tag.IMG) restituisce new MyImageView (elem); } return super.create (elem); }

L'unica cosa che resta da fare ora è impostare JEditorPaneda usare MyHTMLEditorKit. Il codice è abbastanza semplice: