Swing文本组件带有许多预定义的功能。例如,正如我们在第15章中看到的,尽管文本组件具有如cut(),copy()与paste()方法来使用系统剪切板,但是事实上我们并不使用这些方法。这是因为Swing文本组件带有他们自己预定义的Action对象集合,我们将会在本章中探讨这一集合。要使用Action对象,只需要将其关联到组件,例如一个按钮或是菜单项,然后简单的选中激发Action的组件。对于文本组件,Action对象是TextAction的一个实例,他具有知道哪一个组件最后具有输入焦点的额外特性。
在本章中,我们同时还会了解如何创建在JTextPane中显示的格式化文本。如果我们希望显示多种颜色的文本文档或是不同字体风格,JTextPane组件提供了一系列的接口与类来描述与文档相关联的属性。AttributeSet接口在只读基础上为我们提供这些功能,而MutableAttributeSet接口为了设置属性扩展了AttributeSet。我们将会看到SimpleAttributeSet类如何通过提供Hashtable存储文本属性来实现这些接口,以及StyleConstants类如何有助于配置我们可以应用的多种文本属性。而且,我们将会了解如何在我们的文档中使用Tab,包括如何定义起始字符以及文本如何对齐。
接下来,我们将会概略了解Swing所提供的不同的编辑器工具集,我们将会关注HTMLDocument的内部工作。当JEditorPane显示HTML时,HTMLEditorKit控制如何在HTMLDocument中载入与显示HTML内容。我们将会了解分析器如何载入内容以及如何在文档的不同标记间进行遍历。
最后,我们将会了解如何利用JFormattedTextField组件的格式化输入选项以及验证合法性。我们将会了解如何提供格式化日期与数字,以及隐藏类似于电话与社会安全号码的输入。
TextAction类是Action接口的一个特殊类,在第2章定义了Action接口以及其他的Swing事件处理功能并且在第15章进行了概述。TextAction类的目的就是提供可以用于文本组件的Action实现。这些实现是如此精巧,可以知道哪一个组件是最近具有输入焦点的,从而应是动作的目标。
对于所有的文本组件,我们需要一种方法将按键与特定的动作相关联。这是通过Keymap接口来实现的,他将KeyStroke映射到TextAction,从而为了监听组件,单独的KeyListener对象不需要与文本组件相关联。键盘映射可以在多个组件之间共享并且/或者为特定的观感进行定制。JTextComponent还具有允许我们读取或是自定义按键映射的getKeymap()与setKeymap()方法。
注意,尽管Swing文本组件使用TextAction,KeyStroke与Keymap,他们仍然支持关联KeyListener的功能。然而使用KeyListener通常并不合适,特别是我们希望限制输入来匹配特定的情况时更是如此。限制输入的更好的方法就是构建自定义的DocumentFilter,如第15章中演示所示,或是使用InputVerifier。另外,实际的Keymap实现仅是对在非文本Swing组件中按键动作映射所用的InputMap/ActionMap组合的包装。
文本组件带有许多预定义的TextAction实现。通过默认的按键映射,文本组件知道这些预定义的动作,从而他们知道如何插入或是移除内容,以及如何跟踪光标与Caret的位置。如果文本组件支持格式化内容,如JTextPane所做的那样,还有额外的默认动作来支持这些内容。所有这些实现派生于JFC/Swing技术编辑器工具集。正如本章稍后在“编辑器工具集”中所讨论的,编辑器工具集提供了编辑特定文本组件类型的各种方法的逻辑组合。
要确定JTextComponent支持哪些动作,我们仅需要通过public Action[] getActions()方法进行查询。这会返回一个Action对象的数组,通常是TextAction,他可以像其他的Action一样使用,例如在JToolBar上创建按钮。
图16-1显示了将会列出不同的预定义组件的动作的程序。由JRadioButton组合中选择一个组件,而其文本动作列表将会显示在文本区域中。对于每一个动作,程序会显示出动作名与类名。
相同的53个动作集合可以适用于所有的文本组件。JTextField,JFormattedTextField与JPasswordField还有一个额外的动作,被称为notify-field-accept,用于当在文本组件中按下Enter键时进行检测。JFormattedTextField具有第二个额外动作,reset-field-edit,用于内容不符合所提供的格式掩码的情况。JTextPane添加了他独有的20个动作集合用于处理多属性文本。
列表16-1显示了用于生成图16-1的源友。RadioButtonUtils类在第5章中创建。
/** * */ package swingstudy.ch16; import java.awt.BorderLayout; import java.awt.Container; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.Comparator; import javax.swing.Action; import javax.swing.JEditorPane; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JPasswordField; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.text.JTextComponent; /** * @author mylxiaoyi * */ public class ListActions { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("TextAction List"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); String components[] = { "JTextField", "JFormattedTextField", "JPasswordField", "JTextArea", "JTextPane", "JEditorPane" }; final JTextArea textArea = new JTextArea(); textArea.setEditable(false); JScrollPane scrollPane = new JScrollPane(textArea); frame.add(scrollPane, BorderLayout.CENTER); ActionListener actionListener = new ActionListener() { public void actionPerformed(ActionEvent event) { // Determine which component selected String command = event.getActionCommand(); JTextComponent component = null; if(command.equals("JTextField")) { component = new JTextField(); } else if(command.equals("JFormattedTextField")){ component = new JFormattedTextField(); } else if(command.equals("JPasswordField")) { component = new JPasswordField(); } else if(command.equals("JTextArea")) { component = new JTextArea(); } else if(command.equals("JTextPane")) { component = new JTextPane(); } else { component = new JEditorPane(); } // Process action list Action actions[] = component.getActions(); // Define comparator to sort actions Comparator<Action> comparator = new Comparator<Action>() { public int compare(Action a1, Action a2) { String firstName = (String)a1.getValue(Action.NAME); String secondName = (String)a2.getValue(Action.NAME); return firstName.compareTo(secondName); } }; Arrays.sort(actions, comparator); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw, true); int count = actions.length; pw.println("Count: "+count); for(int i=0; i<count; i++) { pw.print(actions[i].getValue(Action.NAME)); pw.print(" : "); pw.println(actions[i].getClass().getName()); } pw.close(); textArea.setText(sw.toString()); textArea.setCaretPosition(0); } }; final Container componentsContainer = RadioButtonUtils.createRadioButtonGrouping(components, "Pick to List Actions", actionListener); frame.add(componentsContainer, BorderLayout.WEST); frame.setSize(400, 250); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
到目前为止,我们已经了解对于各种文本组件有许多预定义的TextAction实现可用,但是我们还没有使用其中的任何一个。通过对列表16-1做一些小的修改,我们就可以对程序进行加强。修改后的程序显示在列表16-2中。在这个版本中,当一个单选按钮被选中,文本组件的类型就会显示在Action对象的文本列表显示在图16-1中。另外,不同的Action对象被添加到位于显示窗口顶部的新JMenuBar中。
注意,在列表16-2所显示的程序中,在所有的菜单按钮被激活之后,我们也许会停留在一个也许我们并不希望的文本标签上。然而,我们可以很容易的通过JMenuItem的public void setText(String label)方法来修改。如果我们这样做,记住我们需要知道哪些位于菜单项中从而将会标签修改为某些有意义的说明。
package swingstudy.ch16; import java.awt.BorderLayout; import java.awt.Container; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.Action; import javax.swing.JEditorPane; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JPasswordField; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.text.JTextComponent; public class ActionsMenuBar { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { final JFrame frame = new JFrame("TextAction Ussage"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final JScrollPane scrollPane = new JScrollPane(); frame.add(scrollPane, BorderLayout.CENTER); final JMenuBar menuBar = new JMenuBar(); frame.setJMenuBar(menuBar); ActionListener actionListener = new ActionListener() { JTextComponent component; public void actionPerformed(ActionEvent event) { // Determine which component selected String command = event.getActionCommand(); if(command.equals("JTextField")) { component = new JTextField(); } else if(command.equals("JFormattedTextField")) { component = new JFormattedTextField(); } else if(command.equals("JPasswordField")) { component = new JPasswordField(); } else if(command.equals("JTextArea")) { component = new JTextArea(); } else if(command.equals("JTextPane")) { component = new JTextPane(); } else { component = new JEditorPane(); } scrollPane.setViewportView(component); // Process action list Action actions[] = component.getActions(); menuBar.removeAll(); menuBar.revalidate(); JMenu menu = null; for(int i=0, n=actions.length; i<n; i++) { if((i%10)==0) { menu = new JMenu("From "+i); menuBar.add(menu); } menu.add(actions[i]); } menuBar.revalidate(); } }; String components[] = { "JTextField", "JFormattedTextField", "JPasswordField", "JTextArea", "JTextPane", "JEditorPane" }; final Container componentContainer = RadioButtonUtils.createRadioButtonGrouping(components, "Pick to List Actions", actionListener); frame.add(componentContainer, BorderLayout.WEST); frame.setSize(400, 300); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
图16-2显示了JTextArea的一些可用操作。当我们选择不同的菜单选项时,JTextComponent就会受到相应的影响。
这一技术十分有用,因为他显示了我们可以发现一个文本组件所支持的操作,并且可以在没有确切知道实际行为是什么的情况下提供到这种行为的访问。这仅是我们可以使用TextAction对象的许多方法中的一种演示。
尽管列出与使用与一个文本组件相关的Action对象是一个相当具有扩展性的过程,除非我们知道我们正在查找什么,否则这种技术并不是十分有用。幸运的是,DefaultEditorKit具有46个与所有的文本组件所共享的46个Action对象相匹配的类常量。这些类常量的名字或多或少的反映了他们的功能。JTextField添加了一个与JFormattedTextField和JPasswordField共享的Action的额外常量。不幸的是,与JTextPane可用的额外动作相关联的名字并不是任何文本组件的类常量,而仅是在StyledEditorKit内部使用,在那里我们可以看到定义的额外的Action实现。
注意,存在一个的Action仅是出于高度的目的。其Action名字为dump-model,并没有与其相关的类常量。当初始化时,方法会在内部导出文本组件的Document模型Element结构。
表16-1列出了可以帮助我们定位我们正在查找的预定义Action的47个常量。
有了这些常量列表,实际上我们要如何使用他们呢?首先我们要查找我们希望使用的预定义的TextAction的常量(如果没有常量则要了解必须的文本字符串)。这相对来说较为简单因为名字是自解释的。
为了演示,列表16-3包含了一个程序,显示了如何使用这些常量。这个程序有两个文本区域来显示TextAction对象确实知道使用具有输入焦点的文本组件。菜单项的一个集合包含两个用于将文本区域由只读切换到可写的选项。这个动作是通过使用DefaultEditorKit.readOnlyAction与DefaultEditorKit.writableAction名字来实现的。另一个菜单选项集合包含用于剪切,粘贴与复制支持的选项,其相应的常量分别为DefaultEditorKit.cutAction,DefaultEditorKit.copyAction与DefaultEditorKit.pasteAction。因为这些常量是String值,我们需要查找要使用的实际的Action对象。
查找的过程需要使用getActionMap()方法获得组件的ActionMap,然后使用ActionMap的get()方法查找键值,如下面的示例所示:
Action readAction = component.getActionMap().get(DefaultEditorKit.readOnlyAction);
package swingstudy.ch16; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.EventQueue; import javax.swing.Action; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.text.DefaultEditorKit; public class UseActions { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Use TextAction"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Dimension empty = new Dimension(0,0); final JTextArea leftArea = new JTextArea(); JScrollPane leftScrollPane = new JScrollPane(leftArea); leftScrollPane.setPreferredSize(empty); final JTextArea rightArea = new JTextArea(); JScrollPane rightScrollPane = new JScrollPane(rightArea); rightScrollPane.setPreferredSize(empty); JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScrollPane, rightScrollPane); JMenuBar menuBar = new JMenuBar(); frame.setJMenuBar(menuBar); JMenu menu = new JMenu("Options"); menuBar.add(menu); JMenuItem menuItem; Action readAction = leftArea.getActionMap().get(DefaultEditorKit.readOnlyAction); menuItem = menu.add(readAction); menuItem.setText("Make read-only"); Action writeAction = leftArea.getActionMap().get(DefaultEditorKit.writableAction); menuItem = menu.add(writeAction); menuItem.setText("Make writable"); menu.addSeparator(); Action cutAction = leftArea.getActionMap().get(DefaultEditorKit.cutAction); menuItem = menu.add(cutAction); menuItem.setText("Cut"); Action copyAction = leftArea.getActionMap().get(DefaultEditorKit.copyAction); menuItem = menu.add(copyAction); menuItem.setText("Copy"); Action pasteAction = leftArea.getActionMap().get(DefaultEditorKit.pasteAction); menuItem = menu.add(pasteAction); menuItem.setText("Paste"); frame.add(splitPane, BorderLayout.CENTER); frame.setSize(400, 250); frame.setVisible(true); splitPane.setDividerLocation(.5); } }; EventQueue.invokeLater(runner); } }
图16-3显示了程序运行时的样子。注意,每一个JMenuItem被创建后,文本标签被修改为一个更为用户友好的设置。
通过查找特定的TextAction实例,我们并不需要记录重复操作。事实上,如果我们发现我们在一个文本组件上一次次重复相同的操作,那么也许我们就需要考虑创建自己的TextAction对象了。