Petua Java 109: Paparkan gambar menggunakan JEditorPane

Anda boleh menggunakan JEditorPanekomponen semasa untuk menampilkan markup HTML, tetapi untuk melakukan tugas yang lebih rumit, JEditorPaneperlu diperbaiki. Baru-baru ini, saya terpaksa membina aplikasi pembentuk borang XML. Salah satu komponen yang diperlukan ialah editor HTML WYSIWYG yang dapat mengedit kandungan markup HTML di dalam beberapa tag XML. JEditorPaneadalah pilihan komponen Java yang jelas untuk menampilkan markup HTML, kerana fungsi itu sudah ada di dalamnya. Sayangnya, ketika dimasukkan ke markup HTML, JEditorPanetidak dapat memaparkan gambar dengan jalan relatif. Sebagai contoh, jika gambar berikut dengan jalur relatif terkandung dalam tag XML, gambar tersebut tidak akan dipaparkan dengan betul:


  

Sebaliknya, jalan mutlak akan berfungsi (dengan andaian bahawa jalan dan imej yang diberikan benar-benar ada):


  

Dalam aplikasi saya, gambar selalu disimpan dalam subdirektori berbanding lokasi fail XML. Oleh itu, saya selalu mahu menggunakan jalan relatif. Artikel ini akan menerangkan mengapa masalah ini wujud dan bagaimana memperbaikinya.

Mengapa perkara ini berlaku?

Melihat lebih dekat pada konstruktor JEditorPaneakan membantu kita memahami mengapa ia tidak dapat memaparkan gambar dalam jalan relatif.

  1. JEditorPane()mencipta yang baru JEditorPane.
  2. JEditorPane(String url)membuat JEditorPaneberdasarkan rentetan yang mengandungi spesifikasi URL.
  3. JEditorPane(String type, String text)mencipta a JEditorPaneyang telah diinisialisasi dengan teks yang diberikan.
  4. JEditorPane(URL initialPage)membuat JEditorPaneberdasarkan URL yang ditentukan untuk input.

Pembina kedua dan keempat menginisialisasi objek dengan merujuk kepada fail HTML jauh atau tempatan. An HTMLDocumentada di dalam setiap JEditorPane, dan dasarnya ditetapkan ke dasar parameter pembina URL. JEditorPanes yang dibuat menggunakan konstruktor tersebut dapat menangani jalan relatif, kerana dasar HTMLDocumentmenggabungkan dengan jalan relatif untuk membuat jalan mutlak.

Sekiranya konstruktor pertama digunakan, teks yang dipaparkan mesti dimasukkan setelah objek dibuat. Pembina ketiga menerima Stringkandungan sebagai isi, tetapi asasnya tidak diinisialisasi. Kerana saya ingin mendapatkan markup HTML dari tag XML dan bukan fail, saya perlu menggunakan konstruktor pertama atau ketiga.

Bagaimana kita menyelesaikan masalahnya?

Sebelum saya meneruskan, mari kita bongkar dan selesaikan masalah lain yang lebih kecil. Kaedah yang paling jelas untuk memasukkan markup JEditorPaneadalah dengan menggunakan setText(String text). Namun, kaedah itu menghendaki anda memasukkan keseluruhan markup yang dipaparkan setiap kali anda membuat perubahan. Sebaik-baiknya, tag baru harus dimasukkan ke dalam teks yang ada. Anda boleh menggunakan kod berikut untuk menambahkan markup baru:

private void insertHTML (JEditorPane editor, String html, int location) membuang IOException {// menganggap editor sudah ditetapkan ke "text / html" jenis HTMLEditorKit kit = (HTMLEditorKit) editor.getEditorKit (); Dokumen doc = editor.getDocument (); Pembaca StringReader = StringReader baru (html); kit.read (pembaca, doc, lokasi); }

Sekarang, sampai pada intinya: Bagaimana JEditorPanemembuat HTML? Setiap jenis JEditorPanerujukan a Documentdan an EditorKit. Ketika JEditorPanediatur untuk mengetik "text / html", itu berisi HTMLDocument, yang berisi markup dan HTMLEditorKityang menentukan kelas mana yang memberikan setiap tag yang ada di markup. Secara khusus, HTMLEditorKitkelas mengandungi kelas HTMLFactorydalaman yang create(Element elem)kaedahnya benar-benar memeriksa setiap teg yang berasingan. Berikut adalah kod dari kelas kilang yang mengendalikan tag gambar:

 lain jika (kind == HTML.Tag.IMG) mengembalikan ImageView (elem) baru; 

Seperti yang anda lihat sekarang, ImageViewkelas sebenarnya memuatkan gambar. Untuk menentukan lokasi gambar, getSourceURL()kaedah ini disebut:

URL peribadi getSourceURL () {String src = (String) fElement.getAttributes (). getAttribute (HTML.Attribute.SRC); jika (src == null) kembali null; Rujukan URL = ((HTMLDocument) getDocument ()). getBase (); cuba {URL u = URL baru (rujukan, src); pulangkan u; } tangkapan (MalformedURLException e) {return null; }}

Di sini, getSourceURL()kaedah ini cuba membuat URL baru untuk merujuk gambar menggunakan HTMLDocumentpangkalan. Sekiranya pangkalan itu nol, null dikembalikan dan operasi memuatkan gambar dibatalkan. Anda mahu mengatasi tingkah laku itu.

Sebaik-baiknya, anda akan menundukkan ImageViewkelas dan mengganti initialize(Element elem)kaedah, di mana pemuatan gambar selesai. Malangnya, kelas itu dilindungi pakej, jadi anda mesti membuat kelas yang sama sekali baru. Kaedah termudah untuk melakukannya ialah meminjam, kemudian mengubah suai, kod dari ImageViewkelas asal . Mari kita sebut MyImageView.

Pertama, lihat kod yang memuatkan gambar. Perkara berikut diambil dari initialize(Element elem)kaedah:

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

Di sini, anda memperoleh URL; jika batal, anda melangkau pemuatan gambar. Di MyImageView, anda hanya boleh menjalankan kod ini jika rujukan gambar anda adalah URL. Berikut ini adalah kaedah yang boleh anda tambahkan untuk menguji sumber gambar:

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

Pada asasnya, anda memperoleh rujukan pada gambar dalam bentuk a Stringdan ujian untuk melihat apakah ia bermula dengan salah satu daripada dua jenis URL: fail untuk gambar tempatan dan http untuk gambar jauh. Jens Alfke, pengarang javax.swing.text.html.ImageViewkelas asal , menggunakan pemboleh ubah global kelas, jadi tidak perlu memasukkan parameter ke fungsi. Di sini, pemboleh ubah global adalah fElement.

Anda boleh menulis kod yang mengatakan , tetapi apa yang anda masukkan ke dalam pernyataan lain untuk jalan relatif? Cukup mudah - muatkan gambar seperti biasa dalam aplikasi:if (isURL()) { }

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

Tidak ada sihir sebenar di sini, tetapi ada satu tangkapan. The createImage(src)fungsi yang boleh kembali sebelum semua piksel imej yang telah penduduk. Sekiranya itu berlaku, gambar yang rosak akan dipaparkan. Untuk menyelesaikan masalah, anda boleh menunggu sehingga piksel gambar diisi sepenuhnya. Kecenderungan pertama saya adalah menggunakan MediaTrackeruntuk mengesan kapan gambar siap, tetapi MediaTrackerkonstruktor memerlukan komponen yang menjadikan gambar sebagai parameter. Jadi sekali lagi, saya meminjam beberapa kod dari Jim Graham java.awt.MediaTrackerdan menulis kaedah saya sendiri untuk mengatasi masalah tersebut:

private void waitingForImage () membuang InterruptException {int w = fImage.getWidth (ini); int h = fImage.getHeight (ini); sementara (benar)}

Kaedah ini pada dasarnya melakukan kerja yang sama dengan MediaTracker's waitForID(int id)kaedah, tetapi tidak memerlukan komponen ibu bapa. Panggilan ke kaedah ini dapat dilakukan sejurus gambar dibuat.

Terdapat masalah kecil yang harus saya sebutkan sebelum meneruskannya. Tidak mungkin subkelas ImageViewdari javax.swing.text.htmlpakej, jadi saya menyalin keseluruhan fail untuk membuat kelas saya sendiri, yang dipanggil MyImageView, yang belum saya masukkan ke dalam pakej. Dalam ImageViewkod asal , jika gambar tidak dapat ditampilkan kerana tidak ada atau ditangguhkan, ia memuatkan gambar patah lalai dari javax.swing.text.html.iconspaket. Untuk memuatkan gambar yang rosak, kelas menggunakan getResourceAsStream(String name)kaedah dari Classkelas. Kod sebenar kelihatan seperti ini:

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

di mana MISSING_IMAGE_SRCparameternya adalah Stringdengan kandungan:

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

Petikan berikut dari ImageViewkod sumber menerangkan alasan Sun untuk menggunakan getResourceAsStream(String name)kaedah memuat gambar yang rosak.

/ * Salin sumber ke dalam array bait. Ini * perlu kerana beberapa penyemak imbas menganggap * Class.getResource adalah risiko keselamatan kerana * dapat digunakan untuk memuat kelas tambahan. * Class.getResourceAsStream hanya mengembalikan bait * mentah, yang boleh 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 mana subkelas HTMLEditorKit. Di dalam anda MyHTMLEditorKit, anda membuat kelas dalaman baru yang dipanggil MyHTMLFactory, yang merupakan subkelas HTMLFactory. Di kelas dalaman itu, anda boleh membuat create(Element elem)kaedah anda sendiri , yang kelihatan seperti ini:

public View create (Element elem) {Object o = elem.getAttributes (). getAttribute (StyleConstants.NameAttribute); if (o instanceof HTML.Tag) {HTML.Tag kind = (HTML.Tag) o; jika (kind == HTML.Tag.IMG) kembalikan MyImageView (elem) baru; } kembali super.create (elem); }

Satu-satunya perkara yang perlu dilakukan sekarang ialah menetapkan JEditorPaneuntuk digunakan MyHTMLEditorKit. Kodnya agak mudah: