我们已经概略的看到了本章前面所介绍的TextAction对象的一些默认的EditorKit功能。EditorKit类扮演将文本组件的所有不同方面组合在一起的粘合剂。他创建文档,管理动作,并且创建文档或是视图的可视化表示。另外,EditorKit知道如何读取或是写入流。每一个文档类型需要其自己的EditorKit,所以JFC/Project Swing组件为HTML与RTF文本以及普通文本与格式化文本提供了不同的EditorKit。
Document内容的实际显示是通过EditorKit借助于ViewFactory来实现的。对于Document的每一个Element,ViewFatory确定哪一个View是为这个元素创建并且通过文本组件委托进行渲染。对于不同的元素类型,有不同的View子类。
在第15章中,我们看到JTextComponent的read()与write()方法如何允许我们读取或是写入一个文本组件的内容。尽管第15章中的列表15-3中的LoadSave示例显示了JTextField的这一过程,但是正如我们所希望的,他同样适用于所有的文本组件。确保载入与保存为正确的文档类型完成的唯一需求是为文档修改编辑器工具集。
为了演示,在这里我们显示了我们如何将一个HTML文件作为StyledDocument载入到JEditorPane中:
JEditorPane editorPane = new JEditorPane(); editorPane.setEditorKit(new HTMLEditorKit()); reader = new FileReader(filename); editorPane.read(reader, filename);
这非常简单。组件的内容类型被设置为text/html,并且由filename载入作为HTML内容显示。值得注意的一件事就是载入是异步完成的。
如果我们需要同步载入内容,从而我们可以等待所有的内容载入完成,例如用于分析目的,则这个过程有一些麻烦。我们需要使用HTML分析器(HTMLEditorKit.Parset类位于javax.swing.text.html包中),解析器委托器(ParsetDeleegator位于javax.swing.text.html.parset包中),以及我们可以由HTMLDocument(作为HTMLDocument.HTMLReader)中获取的解析器回调(HTMLEditorKit.ParsetCallback)。听起来要比实际复杂。为了演示,下面的代码异步载入一个文件到JEditorPane中。
reader = new FileReader(filename); // First create empty empty HTMLDocument to read into HTMLEditorKit htmlKit = new HTMLEditorKit(); HTMLDocument htmlDoc = (HTMLDocument)htmlKit.createDefaultDocument(); // Next create the parser HTMLEditorKit.Parser parser = new ParserDelegator(); // Then get HTMLReader (parser callback) from document HTMLEditorKit.ParserCallback callback = htmlDoc.getReader(0); // Finally load the reader into it // The final true argument says to ignore the character set parser.parse(reader, callback, true); // Examine contents
在我们载入HTML文档之后,除了在JEditorPane中显示内容以外,也许我们会发现我们需要我们自己解析文档内容。HTMLDocument通过HTMLDocument.Iterator与ElementIterator类支持两种遍历方式。
HTMLDocument.Iterator类
要使用HTMLDocument.Iterator类,我们请求HTMLDocument为特定的HTML.Tag提供迭代器。然后,对于文档中标记的每个实例, 我们可以查看标签的属性。
HTML.Tag类包含用于所有标准HTML标签(HTMLEditorKit可以理解的)的76个类常量,例如用于H1标签的HTML.Tag.H1。表16-7列出了这些常量。
在我们有了可以使用的特定迭代器之后,我们就可以借助于表16-8中所显示的类属性查看每一个标签实例的特定属性与内容。
迭代过程的另一个方面就是next()方法,这个方法可以使得我们获得文档中的下一个标签实例。使用这个迭代器的基本结构如下:
// Get the iterator HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A); // For each valid one while (iterator.isValid()) { // Process element // Get the next one iterator.next(); }
这也可以表示为一个基本的for循环结构:
for (HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A); iterator.isValid(); iterator.next()) { // Process element }
列表16-6演示了HTMLDocument.Iterator的用法。这个程序会在命令行提示我们输入URL,异步载入文件,查找所有的<A>标签,然后显示所有的以HREF属性列出的锚点。可以将这个程序看作一个简单的的爬虫程序,因而我们可以构建一个文档之间URL的数据库。起始与结束偏移也可以用来获取链接文本。输入我们需要浏览的URL来启动这个程序。
/** * */ package swingstudy.ch16; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import javax.swing.text.AttributeSet; import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.parser.ParserDelegator; /** * @author mylxiaoyi * */ public class DocumentIteratorExample { /** * @param args */ public static void main(String[] args) throws Exception{ // TODO Auto-generated method stub if(args.length != 1) { System.err.println("Usage: java DocumentIteratorExample input-URL"); } // Load HTML file synchronously URL url = new URL(args[0]); URLConnection connection = url.openConnection(); InputStream is = connection.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); HTMLEditorKit htmlKit = new HTMLEditorKit(); HTMLDocument htmlDoc = (HTMLDocument)htmlKit.createDefaultDocument(); HTMLEditorKit.Parser parser = new ParserDelegator(); HTMLEditorKit.ParserCallback callback = htmlDoc.getReader(0); parser.parse(br, callback, true); // Parser for(HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A); iterator.isValid(); iterator.next()) { AttributeSet attributes = iterator.getAttributes(); String srcString = (String)attributes.getAttribute(HTML.Attribute.HREF); System.out.print(srcString); int startOffset = iterator.getStartOffset(); int endOffset = iterator.getEndOffset(); int length = endOffset - startOffset; String text = htmlDoc.getText(startOffset, endOffset); System.out.println(" - "+text); } System.exit(0); } }
ElementIterator类
检测HTMLDocument内容的另一种方法就是使用ElementIterator(并不是特定于HTML文档)。当使用ElementIteartor时,我们会看到文档的所有的Element对象并且询问每一个是什么。如果对象是我们感兴趣的,我们就可以近距离查看。
要获得文档的迭代器,可以使用如下的代码:
ElementIterator iterator = new ElementIterator(htmlDoc);
ElementIterator并不意味着一个简单的顺序迭代器。他是具有next()与previous()方法的双向迭代器,并且支持使用first()方法回到起始处。尽管next()与previous()方法返回要处理的下一个或前一个元素,我们还可以通过current()方法获取当前位置的元素。下面的代码显示在一个文档中遍历的基本循环方法:
Element element; ElementIterator iterator = new ElementIterator(htmlDoc); while ((element = iterator.next()) != null) { // Process element }
我们如何确定我们获得了哪一个元素,并且如果不是我们感兴趣的我们希望忽略?我们需要由其属性集合中获取其名字与类型。
AttributeSet attributes = element.getAttributes(); Object name = attributes.getAttribute(StyleConstants.NameAttribute); if (name instanceof HTML.Tag) {
现在我们可以查找特定的标签类型,例如HTML.Tag.H1,HTML.Tag.H2等。标签的实际内容将会位于元素的子元素中。为了进行演示,下面的代码显示了如何在文档中查找H1,H2与H3标签,同时显示了与文档相关联的合适标题。
if ((name instanceof HTML.Tag) && ((name == HTML.Tag.H1) || (name == HTML.Tag.H2) || (name == HTML.Tag.H3))) { // Build up content text as it may be within multiple elements StringBuffer text = new StringBuffer(); int count = element.getElementCount(); for (int i=0; i<count; i++) { Element child = element.getElement(i); AttributeSet childAttributes = child.getAttributes(); if (childAttributes.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.CONTENT) { int startOffset = child.getStartOffset(); int endOffset = child.getEndOffset(); int length = endOffset - startOffset; text.append(htmlDoc.getText(startOffset, length)); } } }
要实际尝试,我们需要实际查找一个使用H1,H2或是H3标签的页面。
在第15章中,我们简单尝试了JFormattedTextField组件。现在,我们将会探讨该组件的其他方面。JFormattedTextField用来接收用户的格式化输入。这听起来简单,但是实际是这非常重要且复杂。如果没有JFormattedTextField,获得格式化输入就不会像听起来这样简单。如果考虑本地化需求则会使得事情更为有趣。
JFormattedTextField组件不仅支持格式化输入,但是还会允许用户使用键盘来增加或是减少输入值;例如,在日期月份中滚动。
对于JFormattedTextField,验证是通过focusLostBehavior属性来控制的。这可以设置为如下的四个值:
作为开始,我们先来看一下如何使用JFormattedTextField接收应进行本地化的输入。这包括所有的日期,时间与数字格式,基本是由DateFormat或是NumberFormat对象所获得的所有内容。
如果我们为JFormattedTextField构造函数提供了一个Date对象或是Number子类,组件会将输入String传递该对象类型的构造函数进行输入验证。相反,我们应通过向构造函数传递DateFormat或是NumberFormat来使用位于java.swing.text包中的InternationalFormatter类。这允许我们指定长的或是短的日期与时间,以及货币,百分比,数字的小数或是整数格式。
日期与时间格式
为了演示日期与时间格式,列表6-7的示例接收各种日期与时间输入。由上至下,输入分别为默认locale的短日期格式,对于美国英语的完全日期格式,意大利的中等日期格式,法国的星期以及默认locale的短时间格式。
/** * */ package swingstudy.ch16; import java.awt.EventQueue; import java.awt.FlowLayout; import java.text.DateFormat; import java.text.Format; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import javax.swing.BoxLayout; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; /** * @author mylxiaoyi * */ public class DateInputSample { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Date/Time Input"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JLabel label; JFormattedTextField input; JPanel panel; BoxLayout layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS); frame.setLayout(layout); Format shortDate = DateFormat.getDateInstance(DateFormat.SHORT); label = new JLabel("Short date:"); input = new JFormattedTextField(shortDate); input.setValue(new Date()); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format fullUSDate = DateFormat.getDateInstance(DateFormat.FULL, Locale.US); label = new JLabel("Full US date:"); input = new JFormattedTextField(fullUSDate); input.setValue(new Date()); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format mediumItalian = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ITALIAN); label = new JLabel("Medium Italian date:"); input = new JFormattedTextField(mediumItalian); input.setValue(new Date()); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format dayOfWeek = new SimpleDateFormat("E", Locale.FRENCH); label = new JLabel("French day of week:"); input = new JFormattedTextField(dayOfWeek); input.setValue(new Date()); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format shortTime = DateFormat.getTimeInstance(DateFormat.SHORT); label = new JLabel("Short time:"); input = new JFormattedTextField(shortTime); input.setValue(new Date()); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); frame.pack(); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
图16-6显示了程序运行结果。要使用不同的locale启动程序,我们可以在命令行使用类似下面的命令来设置user.language与user.country设置。
java -Duser.language=fr -Duser.country=FR DateInputSample
然而,这只会修改没有指定locale集合的输入格式。
数字格式
数字的使用类似于日期,所不同的是使用java.text.NumberFormat类,而不是DateFormat类。可以实现的本地化是通过getCurrencyInstance(),getInstance(),IntegerInstance(),getNumberInstance()与getPercentInstance()方法来实现的。
NumberFormat类会处理必要的逗号,句点,百分号等占位符。当输入数字时,并不需要输入额外的字符,例如用于千的逗号。组件会在输入之后在合适的位置进行添加,如图16-7的示例所示。注意,十进制的小数点以及逗号的位置会以及他们的格式会因locale的不同而不同。
列表16-8显示了生成图16-7的程序源码。所有的输入域都以值2425.50开始。在整数的情况下,输入值会进行近似处理。当设置JFormattedTextField的内容时,使用setValue()方法,而不是setText()方法。这会保证文本内容进行验证。
/** * */ package swingstudy.ch16; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Font; import java.text.Format; import java.text.NumberFormat; import java.util.Locale; import javax.swing.BoxLayout; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; /** * @author mylxiaoyi * */ public class NumberInputSample { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Number Input"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Font font = new Font("SansSerif", Font.BOLD, 16); JLabel label; JFormattedTextField input; JPanel panel; BoxLayout layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS); frame.setLayout(layout); Format currency = NumberFormat.getCurrencyInstance(Locale.UK); label = new JLabel("UK Currency"); input = new JFormattedTextField(currency); input.setValue(2424.50); input.setColumns(20); input.setFont(font); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format general = NumberFormat.getInstance(); label = new JLabel("General/Instance"); input = new JFormattedTextField(general); input.setValue(2424.50); input.setColumns(20); input.setFont(font); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format integer = NumberFormat.getIntegerInstance(Locale.ITALIAN); label = new JLabel("Italian integer:"); input = new JFormattedTextField(integer); input.setValue(2424.50); input.setColumns(20); input.setFont(font); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); Format number = NumberFormat.getNumberInstance(Locale.FRENCH); label = new JLabel("French Number:"); input = new JFormattedTextField(number); input.setValue(2424.50); input.setColumns(20); input.setFont(font); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); label = new JLabel("Raw Number:"); input = new JFormattedTextField(2424.50); input.setColumns(20); input.setFont(font); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); frame.pack(); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
图16-7中五个JFormattedTextField示例中的最后一个使用一个double初始化组件。值2424.50会被自动装箱为一个Double对象。向构造函数传递一个对象并没有错。然而,当我们在文本域内输入值的时候我们会发现一些不规则的地方。值似乎总是以一个十进制小数点开头,尽管已经接受更多的输入数字。我们不需要使用一个Format对象进行文本与Object对象的转换,在这里我们使用接收String的Double构造函数。
当我们将java.text.Format对象传递给JFormattedTextField构造函数时,这在内部会映射到DateFormatter或是NumberFormatter对象。这两个对象都是InternationalFormatter类的子类。名为JFormattedTextField.AbstractFormatterFactory的内联类在JFormattedTextField内管理格式化对象的使用。工厂会在用户输入JFormattedTextField的时候install()格式器并且在离开时uninstall()格式器,从而保证格式器每次只在一个文本域内被激活。install()与uninstall()方法是由所有格式器的JFormattedTextField.AbstractFormatter超类继承来的。
除了数字与日期,JFormattedTextField支持遵循某种模式或是隐藏的用户输入。例如,如果一个输入域是一个美国社会保险号(SSN),则他有一个典型的数字,数字,数字,短划线,数字,数字,短划线,数字,数字,数字,数字的模型。借助于MaskFormatter类,我们可以使用表16-9中所列的字符来指定输入隐藏。
例如,下面的格式器创建了SSN输入掩码:
new MaskFormatter("###'-##'-####")
输入掩码中单引号后的字符会被作为字面量处理,在这种情况是一个短划线。我们可以将这个格式器传递给JFormattedTextField的构造函数或是使用setMask()方法配置文本域。
为了演示,列表16-9包含了两个JFormattedTextField组件:一个来接受SSN而另一个接收美国电话号码。
/** * */ package swingstudy.ch16; import java.awt.EventQueue; import java.awt.FlowLayout; import java.text.ParseException; import javax.swing.BoxLayout; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.text.MaskFormatter; /** * @author mylxiaoyi * */ public class MaskInputSample { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Mask Input"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JLabel label; JFormattedTextField input; JPanel panel; MaskFormatter formatter; BoxLayout layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS); frame.setLayout(layout); try { label = new JLabel("SSN"); formatter = new MaskFormatter("###'-##'-####"); input = new JFormattedTextField(formatter); input.setValue("123-45-6789"); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); } catch(ParseException e) { System.err.println("Unable to add SSN"); } try { label = new JLabel("US Phone"); formatter = new MaskFormatter("'(###')' ###'-####"); input = new JFormattedTextField(formatter); input.setColumns(20); panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.add(label); panel.add(input); frame.add(panel); } catch(ParseException e) { System.err.println("Unable to add Phone"); } frame.pack(); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
图16-8显示了程序的输出。在这个例子中,SSN文本域以初始值开始,然而电话号码文本域却没有。
MaskFormatter提供了一些自定义选项。默认情况下,格式器处于覆写模式,所以当我们输入时,所输入的数字会替换文本域中的数字与空格。将overwriteModel属性设置为false可以禁止这一行为。通常情况下,这并没有必要,尽管对于输入长的日期会比较有帮助。
如果我们希望使用不同的字符作为占位符,在位置被填充到掩码之前,设置MaskFormatter的placeholderCharacter属性。为了演示,将下面的代码行添加到列表16-9中的电话号码格式器之前:
formatter.setPlaceholder('*');
我们将会看到显示在图16-9中底部的文本域的结果。
另一个比较有用的MaskFormatter属性就是validCharacters,用于限制哪一个字母数字字符对于输入域是合法的。
javax.swing.text包中的DefaultFormatterFactory类提供了一种方法来使得不同的格式器显示值,编辑以及null值的特殊情况。他提供了五个构造函数,由无参数的构造函数开始,然后为每一个构造函数添加了一个额外的AbstractFormatter参数。
public DefaultFormatterFactory() DefaultFormatterFactory factory = new DefaultFormatterFactory() public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat) DateFormat defaultFormat = new SimpleDateFormat("yyyy--MMMM--dd"); DateFormatter defaultFormatter = new DateFormatter(displayFormat); DefaultFormatterFactory factory = new DefaultFormatterFactory(defaultFormatter); public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat, JFormattedTextField.AbstractFormatter displayFormat) DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd"); DateFormatter displayFormatter = new DateFormatter(displayFormat); DefaultFormatterFactory factory = new DefaultFormatterFactory(displayFormatter, displayFormatter); public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat, JFormattedTextField.AbstractFormatter displayFormat, JFormattedTextField.AbstractFormatter editFormat) DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd"); DateFormatter displayFormatter = new DateFormatter(displayFormat); DateFormat editFormat = new SimpleDateFormat("MM/dd/yy"); DateFormatter editFormatter = new DateFormatter(editFormat); DefaultFormatterFactory factory = new DefaultFormatterFactory( displayFormatter, displayFormatter, editFormatter); public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat, JFormattedTextField.AbstractFormatter displayFormat, JFormattedTextField.AbstractFormatter editFormat, JFormattedTextField.AbstractFormatter nullFormat) DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd"); DateFormatter displayFormatter = new DateFormatter(displayFormat); DateFormat editFormat = new SimpleDateFormat("MM/dd/yy"); DateFormatter editFormatter = new DateFormatter(editFormat); DateFormat nullFormat = new SimpleDateFormat("'null'"); DateFormatter nullFormatter = new DateFormatter(nullFormat); DefaultFormatterFactory factory = new DefaultFormatterFactory( displayFormatter, displayFormatter, editFormatter, nullFormatter);
DefaultFormatterFactory的使用并没有什么神奇之处。只需要创建一个实例然后传递给JFormattedTextField的构造函数。然后文本域的状态将会决定使用哪种格式器来显示当前值。通常情况下,显示格式器会为默认设置进行重复。如果格式器中的任何一个为null或是没有设置,则会使用默认的格式器。
在本章中,我们了解了使用JFC/Project Swing文本组件的一些高级方面。我们了解了如何使用预定义的TextAction对象来创建工作用户界面,而不需要定义我们自己的事件处理功能。另外,我们了JTextPane以及如何通过AttributeSet,MutableAttributeSet,SimpleAttributeSet与StyleConstants在JTextPane内创建多属性文本。我们同时了解了如何在Document内创建tab stop以及Swing的EditorKit实用程序,特别探讨了HTMLEditorKit的细节。最后,我们了解了使用JFormattedTextField接收格式化输入。
在第17章中,我们将会探讨用于显示层次结构数据的Swing组件:JTree。