Tip Java 109: Tampilkan gambar menggunakan JEditorPane

Anda dapat menggunakan JEditorPanekomponen saat ini untuk menampilkan markup HTML, tetapi untuk melakukan tugas yang lebih rumit, JEditorPaneperlu beberapa perbaikan. Baru-baru ini, saya harus membangun aplikasi pembuat formulir XML. Salah satu komponen yang diperlukan adalah editor HTML WYSIWYG yang dapat mengedit konten markup HTML di dalam beberapa tag XML. JEditorPaneadalah pilihan komponen Java yang jelas untuk menampilkan markup HTML, karena fungsionalitas itu sudah ada di dalamnya. Sayangnya, saat dimasukkan ke dalam markup HTML, JEditorPanetidak dapat menampilkan gambar dengan jalur relatif. Misalnya, jika gambar berikut dengan jalur relatif terdapat dalam tag XML, itu tidak akan ditampilkan dengan benar:


  

Sebaliknya, jalur absolut akan berfungsi (dengan asumsi bahwa jalur dan gambar yang diberikan benar-benar ada):


  

Dalam aplikasi saya, gambar selalu disimpan dalam subdirektori relatif terhadap lokasi file XML. Oleh karena itu, saya selalu ingin menggunakan jalur relatif. Artikel ini akan menjelaskan mengapa masalah ini ada dan cara memperbaikinya.

Mengapa ini terjadi?

Melihat lebih dekat pada konstruktor untuk JEditorPaneakan membantu kita memahami mengapa itu tidak dapat menampilkan gambar di jalur relatif.

  1. JEditorPane()menciptakan yang baru JEditorPane.
  2. JEditorPane(String url)membuat JEditorPaneberdasarkan string yang berisi spesifikasi URL.
  3. JEditorPane(String type, String text)membuat JEditorPaneyang telah diinisialisasi ke teks yang diberikan.
  4. JEditorPane(URL initialPage)membuat JEditorPaneberdasarkan URL yang ditentukan untuk masukan.

Konstruktor kedua dan keempat menginisialisasi objek dengan referensi ke file HTML jarak jauh atau lokal. An HTMLDocumentada di dalam every JEditorPane, dan basisnya disetel ke basis parameter konstruktor URL. JEditorPanes yang dibuat menggunakan konstruktor tersebut dapat menangani jalur relatif, karena basis HTMLDocumentgabungannya dengan jalur relatif untuk membuat jalur absolut.

Jika konstruktor pertama digunakan, teks yang ditampilkan harus disisipkan setelah objek dibuat. Konstruktor ketiga menerima a Stringsebagai konten, tetapi basisnya tidak diinisialisasi. Karena saya ingin mendapatkan markup HTML dari tag XML dan bukan file, saya perlu menggunakan konstruktor pertama atau ketiga.

Bagaimana cara kami mengatasi masalah tersebut?

Sebelum saya melanjutkan, mari kita ungkapkan dan selesaikan masalah lain yang lebih kecil. Cara paling jelas untuk menyisipkan markup ke dalam JEditorPaneadalah dengan menggunakan setText(String text). Namun, metode itu mengharuskan Anda memasukkan seluruh markup yang ditampilkan setiap kali Anda membuat perubahan. Idealnya, tag baru harus dimasukkan ke dalam teks yang sudah ada. Anda dapat menggunakan kode berikut untuk menambahkan markup baru:

private void insertHTML (editor JEditorPane, String html, int location) melempar IOException {// mengasumsikan editor sudah disetel ke "text / html" ketik HTMLEditorKit kit = (HTMLEditorKit) editor.getEditorKit (); Dokumen doc = editor.getDocument (); Pembaca StringReader = new StringReader (html); kit.read (reader, doc, location); }

Sekarang, ke inti permasalahan: Bagaimana JEditorPanemerender HTML? Setiap jenis JEditorPanereferensi baik a Documentdan an EditorKit. Saat JEditorPanedisetel untuk mengetik "teks / html", ini berisi HTMLDocument, yang berisi markup dan HTMLEditorKityang menentukan kelas mana yang merender setiap tag yang terkandung dalam markup. Secara khusus, HTMLEditorKitkelas berisi kelas HTMLFactorydalam yang create(Element elem)metodenya benar-benar memeriksa setiap tag terpisah. Berikut adalah kode dari kelas pabrik itu, yang menangani tag gambar:

 else if (kind == HTML.Tag.IMG) mengembalikan ImageView baru (elem); 

Seperti yang sekarang Anda lihat, ImageViewkelas sebenarnya memuat gambar. Untuk menetapkan lokasi gambar, getSourceURL()metode ini disebut:

URL pribadi getSourceURL () {String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); jika (src == null) mengembalikan null; Referensi URL = ((HTMLDocument) getDocument ()). getBase (); coba {URL u = URL baru (referensi, src); kembali u; } catch (MalformedURLException e) {return null; }}

Di sini, getSourceURL()metode mencoba membuat URL baru untuk mereferensikan gambar menggunakan HTMLDocumentbasis. Jika basis tersebut adalah null, null dikembalikan dan operasi pemuatan gambar dibatalkan. Anda ingin mengesampingkan perilaku itu.

Idealnya, Anda akan membuat ImageViewkelas subkelas dan mengganti initialize(Element elem)metode, tempat pemuatan gambar selesai. Sayangnya, kelas tersebut dilindungi oleh paket, jadi Anda harus membuat kelas yang sama sekali baru. Cara termudah untuk melakukannya adalah dengan meminjam, kemudian memodifikasi, kode dari ImageViewkelas aslinya . Sebut saja MyImageView.

Pertama, lihat kode yang memuat gambar. Berikut ini diambil dari initialize(Element elem)metode tersebut:

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

Di sini, Anda mendapatkan URL; jika nol, Anda melewatkan pemuatan gambar. Di MyImageView, Anda hanya boleh menjalankan kode ini jika referensi gambar Anda adalah URL. Berikut ini adalah metode yang dapat Anda tambahkan untuk menguji sumber gambar:

private boolean isURL () String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); kembalikan src.toLowerCase (). beginWith ("file")  

Pada dasarnya, Anda mendapatkan referensi ke gambar dalam bentuk a Stringdan menguji untuk melihat apakah itu dimulai dengan salah satu dari dua jenis URL: file untuk gambar lokal dan http untuk gambar jarak jauh. Jens Alfke, pembuat javax.swing.text.html.ImageViewkelas asli , menggunakan variabel global kelas, jadi tidak perlu meneruskan parameter ke fungsi. Di sini, variabel globalnya adalah fElement.

Anda dapat menulis kode yang mengatakan , tetapi apa yang Anda masukkan ke dalam pernyataan lain untuk jalur relatif? Ini cukup sederhana - cukup muat gambar seperti yang biasa Anda lakukan dalam aplikasi:if (isURL()) { }

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

Tidak ada keajaiban nyata di sini, tetapi ada satu hal yang perlu diperhatikan. The createImage(src)Fungsi dapat kembali sebelum semua piksel gambar ini telah dipilih dihuni. Jika itu terjadi, gambar rusak akan ditampilkan. Untuk memperbaiki masalah, Anda bisa menunggu sampai piksel gambar terisi penuh. Kecenderungan pertama saya adalah menggunakan MediaTrackeruntuk mendeteksi ketika gambar sudah siap, tetapi MediaTrackerkonstruktor memerlukan komponen rendering gambar sebagai parameter. Jadi sekali lagi, saya meminjam beberapa kode dari Jim Graham java.awt.MediaTrackerdan menulis metode saya sendiri untuk menghindari masalah:

private void waitForImage () melempar InterruptedException {int w = fImage.getWidth (this); int h = fImage.getHeight (ini); sementara (benar)}

Metode ini pada dasarnya melakukan pekerjaan yang sama sebagai MediaTracker's waitForID(int id)metode, tetapi tidak memerlukan komponen induk. Panggilan ke metode ini dapat dilakukan tepat setelah gambar dibuat.

Ada masalah kecil yang harus saya sebutkan sebelum saya melanjutkan. Tidak mungkin untuk membuat subkelas ImageViewdari javax.swing.text.htmlpaket, jadi saya menyalin seluruh file untuk membuat kelas saya sendiri, yang disebut MyImageView, yang belum saya masukkan ke dalam paket. Dalam ImageViewkode asli , jika gambar tidak dapat ditampilkan karena tidak ada atau tertunda, itu memuat gambar rusak default dari javax.swing.text.html.iconspaket. Untuk memuat gambar rusak, kelas menggunakan getResourceAsStream(String name)metode dari Classkelas. Kode sebenarnya terlihat seperti ini:

 Sumber daya InputStream = HTMLEditorKit.class.getResourceAsStream (MISSING_IMAGE_SRC); 

dimana MISSING_IMAGE_SRCparameternya adalah Stringdengan konten:

 MISSING_IMAGE_SRC = "ikon" + System.getProperty ("file.separator", "/") + "gambar-gagal.gif"; 

Kutipan berikut dari ImageViewkode sumber menjelaskan alasan Sun menggunakan getResourceAsStream(String name)metode untuk memuat gambar yang rusak.

/ * Salin sumber daya ke dalam array byte. Ini * diperlukan karena beberapa browser menganggap * Class.getResource sebagai risiko keamanan karena * dapat digunakan untuk memuat kelas tambahan. * Class.getResourceAsStream hanya mengembalikan * byte mentah, yang dapat kita ubah menjadi gambar. * /

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, yang merupakan subclass HTMLEditorKit. Di dalam Anda MyHTMLEditorKit, Anda membuat kelas dalam baru yang disebut MyHTMLFactory, yang merupakan subkelas HTMLFactory. Di kelas dalam itu, Anda dapat membuat create(Element elem)metode Anda sendiri , yang terlihat seperti ini:

publik Lihat buat (Elemen elem) {Objek o = elem.getAttributes (). getAttribute (StyleConstants.NameAttribute); if (o instanceof HTML.Tag) {HTML.Tag kind = (HTML.Tag) o; if (kind == HTML.Tag.IMG) mengembalikan MyImageView (elem) baru; } kembali super.create (elem); }

Satu-satunya hal yang harus dilakukan sekarang adalah mengatur JEditorPaneuntuk digunakan MyHTMLEditorKit. Kodenya cukup sederhana: