文本组件功能

原文: https://docs.oracle.com/javase/tutorial/uiswing/components/generaltext.html

JTextComponent 类是 Swing 文本组件的基础。此类为其所有后代提供以下可自定义的功能:

  • 一种称为文档的模型,用于管理组件的内容。
  • 一个视图,在屏幕上显示组件。
  • 一种称为编辑器套件的控制器,可以通过动作读取和写入文本并实现编辑功能。
  • 支持无限撤消和重做。
  • 可插入的插入符号和对插入符号的支持更改了监听器和导航过滤器。

请参阅名为TextComponentDemo的示例以探索这些功能。虽然TextComponentDemo示例包含JTextPane的自定义实例,但本节中讨论的功能由所有JTextComponent子类继承。

A snapshot of TextComponentDemo, which contains a customized text pane and a standard text area

上部文本组件是自定义文本窗格。下部文本组件是JTextArea的实例,它用作报告对文本窗格内容所做的所有更改的日志。窗口底部的状态行报告选择的位置或插入符号的位置,具体取决于是否选择了文本。


Try this:

  1. 单击“启动”按钮以使用 Java™Web Start下载 JDK 7 或更高版本)运行 TextComponentDemo。或者,要自己编译并运行示例,请参考示例索引Launches the TextComponentDemo Application

  2. 使用鼠标选择文本并将光标放在文本窗格中。有关选择和光标的信息显示在窗口的底部。

  3. 通过在键盘上键入来输入文本。您可以使用键盘上的箭头键或四个 emacs 键绑定来移动插入符:Ctrl-B(向后一个字符),Ctrl-F(向前一个字符),Ctrl-N(向下一行)和 Ctrl- P(上行一行)。
  4. 打开“编辑”菜单,然后使用其菜单项在文本窗格中编辑文本。在窗口底部的文本区域中进行选择。由于文本区域不可编辑,因此只有一些“编辑”菜单的命令(如“复制到剪贴板”)才有效。但值得注意的是,菜单对两个文本组件都有效。
  5. 使用“样式”菜单中的项目可以将不同的样式应用于文本窗格中的文本。

使用TextComponentDemo示例作为参考点,本节包含以下主题:

所有 Swing 文本组件都支持标准编辑命令,如剪切,复制,粘贴和插入字符。每个编辑命令由Action对象表示和实现。 (要了解有关操作的更多信息,请参阅如何使用操作。)操作允许您将命令与 GUI 组件(例如菜单项或按钮)相关联,从而围绕文本组件构建 GUI。

您可以在任何文本组件上调用getActions方法以接收包含此组件支持的所有操作的数组。也可以将操作数组加载到HashMap中,以便程序可以按名称检索操作。以下是TextComponentDemo示例中的代码,该示例从文本窗格中获取操作并将其加载到HashMap中。

  1. private HashMap<Object, Action> createActionTable(JTextComponent textComponent) {
  2. HashMap<Object, Action> actions = new HashMap<Object, Action>();
  3. Action[] actionsArray = textComponent.getActions();
  4. for (int i = 0; i < actionsArray.length; i++) {
  5. Action a = actionsArray[i];
  6. actions.put(a.getValue(Action.NAME), a);
  7. }
  8. return actions;
  9. }

以下是从哈希映射中按名称检索操作的方法:

  1. private Action getActionByName(String name) {
  2. return actions.get(name);
  3. }

您可以在程序中逐字使用这两种方法。

以下代码显示了如何创建剪切菜单项并将其与从文本组件中删除文本的操作相关联。

  1. protected JMenu createEditMenu() {
  2. JMenu menu = new JMenu("Edit");
  3. ...
  4. menu.add(getActionByName(DefaultEditorKit.cutAction));
  5. ...

此代码使用前面显示的方便方法按名称获取操作。然后它将操作添加到菜单中。这就是你需要做的。菜单和动作照顾其他一切。请注意,动作的名称来自 DefaultEditorKit 。该工具包提供了基本文本编辑的操作,是 Swing 提供的所有编辑工具包的超类。因此,除非被自定义覆盖,否则它的功能可用于所有文本组件。

为了提高效率,文本组件共享操作。 getActionByName(DefaultEditorKit.cutAction)返回的Action对象由窗口底部的不可编辑JTextArea共享。这种共享特征有两个重要的后果:

  • 通常,您不应修改从编辑器工具包中获得的Action对象。如果这样做,更改将影响程序中的所有文本组件。
  • Action对象可以在程序中的其他文本组件上运行,有时比您预期的更多。在此示例中,即使它不可编辑,JTextArea也与JTextPane共享操作。 (在文本区域中选择一些文本,然后选择剪切到剪贴板菜单项。您将听到蜂鸣声,因为文本区域不可编辑。)如果您不想共享,请自行实例化Action对象。 DefaultEditorKit定义了许多有用的Action子类。

以下是创建“样式”菜单并将“粗体”菜单项放入其中的代码:

  1. protected JMenu createStyleMenu() {
  2. JMenu menu = new JMenu("Style");
  3. Action action = new StyledEditorKit.BoldAction();
  4. action.putValue(Action.NAME, "Bold");
  5. menu.add(action);
  6. ...

StyledEditorKit提供Action子类来实现样式化文本的编辑命令。您将注意到,此代码不是从编辑器工具包中获取操作,而是创建BoldAction类的实例。因此,此操作不与任何其他文本组件共享,并且更改其名称不会影响任何其他文本组件。

除了将动作与 GUI 组件相关联之外,您还可以使用文本组件的输入映射将动作与键击关联。输入映射在如何使用键绑定中描述。

TextComponentDemo示例中的文本窗格支持默认情况下未提供的四个键绑定。

  • Ctrl-B 将插入符向后移动一个字符
  • Ctrl-F 将插入符号向前移动一个字符
  • Ctrl-N 将插入符号向下移动一行
  • Ctrl-P 将插入符号向上移动一行

以下代码将 Ctrl-B 键绑定添加到文本窗格。添加上面列出的其他三个绑定的代码是类似的。

  1. InputMap inputMap = textPane.getInputMap();
  2. KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_B,
  3. Event.CTRL_MASK);
  4. inputMap.put(key, DefaultEditorKit.backwardAction);

首先,代码获取文本组件的输入映射。接下来,它找到表示 Ctrl-B 键序列的 KeyStroke 对象。最后,代码将键击绑定到向后移动光标的Action

实现 undo 和 redo 有两个部分:

为了支持撤消和重做,文本组件必须记住每次发生的编辑,编辑顺序以及撤消每个编辑所需的内容。示例程序使用 UndoManager 类的实例来管理其可撤销编辑列表。创建撤消管理器,其中声明成员变量:

  1. protected UndoManager undo = new UndoManager();

现在,让我们看一下程序如何发现可撤销的编辑并将它们添加到撤消管理器中。

只要在文档内容上发生可撤销编辑,文档就会通知感兴趣的听众。实现撤消和重做的一个重要步骤是在文本组件的文档上注册可撤消的编辑监听器。以下代码将MyUndoableEditListener的实例添加到文本窗格的文档中:

  1. doc.addUndoableEditListener(new MyUndoableEditListener());

我们示例中使用的可撤消编辑监听器将编辑添加到撤消管理器列表中:

  1. protected class MyUndoableEditListener
  2. implements UndoableEditListener {
  3. public void undoableEditHappened(UndoableEditEvent e) {
  4. //Remember the edit and update the menus
  5. undo.addEdit(e.getEdit());
  6. undoAction.updateUndoState();
  7. redoAction.updateRedoState();
  8. }
  9. }

请注意,此方法更新两个对象:undoActionredoAction。这些是分别附加到撤消和重做菜单项的操作对象。下一步将向您展示如何创建菜单项以及如何实现这两个操作。有关可撤消编辑监听器和可撤消编辑事件的一般信息,请参阅如何编写可撤消编辑监听器


Note:

默认情况下,每个可撤消编辑撤消单个字符条目。可以通过一些努力对编辑进行分组,以便将一系列击键组合成一个可撤消的编辑。以这种方式对编辑进行分组将要求您定义一个类,该类拦截文档中的可撤消编辑事件,并在适当时将它们组合并将结果转发给可撤消的编辑监听器。


实现撤消和重做的第一步是创建要放在“编辑”菜单中的操作。

  1. JMenu menu = new JMenu("Edit");
  2. //Undo and redo are actions of our own creation
  3. undoAction = new UndoAction();
  4. menu.add(undoAction);
  5. redoAction = new RedoAction();
  6. menu.add(redoAction);
  7. ...

撤消和重做操作分别由自定义AbstractAction子类实现:UndoActionRedoAction。这些类是示例主类的内部类。

当用户调用undo命令时,调用UndoAction类的actionPerformed方法:

  1. public void actionPerformed(ActionEvent e) {
  2. try {
  3. undo.undo();
  4. } catch (CannotUndoException ex) {
  5. System.out.println("Unable to undo: " + ex);
  6. ex.printStackTrace();
  7. }
  8. updateUndoState();
  9. redoAction.updateRedoState();
  10. }

此方法调用撤消管理器的undo方法并更新菜单项以反映新的撤消/重做状态。

类似地,当用户调用redo命令时,RedoAction类的actionPerformed方法被调用:

  1. public void actionPerformed(ActionEvent e) {
  2. try {
  3. undo.redo();
  4. } catch (CannotRedoException ex) {
  5. System.out.println("Unable to redo: " + ex);
  6. ex.printStackTrace();
  7. }
  8. updateRedoState();
  9. undoAction.updateUndoState();
  10. }

此方法与 undo 类似,只是它调用撤消管理器的redo方法。

UndoActionRedoAction类中的大部分代码专用于根据当前状态启用和禁用操作,以及更改菜单项的名称以反映要撤消或重做的编辑。


Note:

TextComponentDemo示例中的 undo 和 redo 的实现取自 JDK 软件附带的NotePad演示。许多程序员也可以无需修改即可复制此 undo / redo 实现。


与其他 Swing 组件一样,文本组件将其数据(称为模型)与其数据视图分开。如果您还不熟悉 Swing 组件使用的模型 - 视图分割,请参阅使用模型

文本组件的模型称为文档,是实现 Document 接口的类的实例。文档为文本组件提供以下服务:

  • 包含文字。文档将文本内容存储在Element对象中,这些对象可以表示任何逻辑文本结构,例如段落或共享样式的文本运行。我们这里没有描述Element对象。
  • 支持通过removeinsertString方法编辑文本。
  • 通知文档监听器和可撤消的编辑监听器对文本的更改。
  • 管理Position对象,即使文本被修改,也会跟踪文本中的特定位置。
  • 允许您以字符串形式获取有关文本的信息,例如文本的长度和文本段。

Swing 文本包包含DocumentStyledDocument 的子接口,它增加了对使用样式标记文本的支持。一个JTextComponent子类JTextPane要求其文件为StyledDocument,而不仅仅是Document

javax.swing.text包提供以下文档类层次结构,它们为各种JTextComponent子类实现专用文档:

The hierarchy of document classes that javax.swing.text provides.

PlainDocument是文本字段,密码字段和文本区域的默认文档。 PlainDocument为文本提供了一个基本容器,其中所有文本都以相同的字体显示。即使编辑器窗格是样式化文本组件,它默认使用PlainDocument的实例。标准JTextPane的默认文档是DefaultStyledDocument的实例 - 用于没有特定格式的样式文本的容器。但是,任何特定编辑器窗格或文本窗格使用的文档实例都取决于绑定到它的内容类型。如果使用setPage方法将文本加载到编辑器窗格或文本窗格中,则窗格使用的文档实例可能会更改。有关详细信息,请参阅如何使用编辑器窗格和文本窗格

虽然您可以设置文本组件的文档,但通常更容易自动设置它,如果需要,可以使用文档过滤器来更改文本组件数据的设置方式。您可以通过安装文档过滤器或将文本组件的文档替换为您自己的文档来实现某些自定义。例如,TextComponentDemo示例中的文本窗格有一个文档过滤器,用于限制文本窗格可以包含的字符数。

要实现文档过滤器,请创建 DocumentFilter 的子类,然后使用AbstractDocument类中定义的setDocumentFilter方法将其附加到文档。尽管可以使文档不是来自AbstractDocument,但默认情况下,Swing 文本组件使用AbstractDocument子类作为其文档。

TextComponentDemo应用程序有一个文档过滤器 DocumentSizeFilter ,它限制文本窗格可以包含的字符数。以下是创建过滤器并将其附加到文本窗格文档的代码:

  1. ...//Where member variables are declared:
  2. JTextPane textPane;
  3. AbstractDocument doc;
  4. static final int MAX_CHARACTERS = 300;
  5. ...
  6. textPane = new JTextPane();
  7. ...
  8. StyledDocument styledDoc = textPane.getStyledDocument();
  9. if (styledDoc instanceof AbstractDocument) {
  10. doc = (AbstractDocument)styledDoc;
  11. doc.setDocumentFilter(new DocumentSizeFilter(MAX_CHARACTERS));
  12. }

要限制文档中允许的字符,DocumentSizeFilter会覆盖DocumentFilter类的insertString方法,每次将文本插入文档时都会调用该方法。它还会覆盖replace方法,当用户粘贴新文本时,最有可能调用该方法。通常,当用户在新文本中键入或粘贴时,或者在调用setText方法时,可能会导致文本插入。这是DocumentSizeFilter类的insertString方法的实现:

  1. public void insertString(FilterBypass fb, int offs,
  2. String str, AttributeSet a)
  3. throws BadLocationException {
  4. if ((fb.getDocument().getLength() + str.length()) <= maxCharacters)
  5. super.insertString(fb, offs, str, a);
  6. else
  7. Toolkit.getDefaultToolkit().beep();
  8. }

replace的代码类似。 DocumentFilter类定义的方法的 FilterBypass 参数只是一个对象,它使文档能够以线程安全的方式更新。

由于前面的文档过滤器涉及文档数据的添加,因此它仅覆盖insertStringreplace方法。大多数文档过滤器也会覆盖DocumentFilterremove方法。

您可以在文档上注册两种不同类型的监听器:文档监听器和可撤消的编辑监听器。本小节描述了文档监听器。有关可撤消编辑监听器的信息,请参阅实现撤消和重做

文档通知注册文档监听器文档的更改。使用文档监听器在文档插入或删除文本时或文本样式更改时创建反应。

每当对文本窗格进行更改时,TextComponentDemo程序都会使用文档监听器来更新更改日志。以下代码行在文本窗格的文档中将MyDocumentListener类的实例注册为监听器:

  1. doc.addDocumentListener(new MyDocumentListener());

这是MyDocumentListener类的实现:

  1. protected class MyDocumentListener implements DocumentListener {
  2. public void insertUpdate(DocumentEvent e) {
  3. displayEditInfo(e);
  4. }
  5. public void removeUpdate(DocumentEvent e) {
  6. displayEditInfo(e);
  7. }
  8. public void changedUpdate(DocumentEvent e) {
  9. displayEditInfo(e);
  10. }
  11. private void displayEditInfo(DocumentEvent e) {
  12. Document document = (Document)e.getDocument();
  13. int changeLength = e.getLength();
  14. changeLog.append(e.getType().toString() + ": "
  15. + changeLength + " character"
  16. + ((changeLength == 1) ? ". " : "s. ")
  17. + " Text length = " + document.getLength()
  18. + "." + newline);
  19. }
  20. }

监听器实现了三种处理三种不同类型文档事件的方法:插入,删除和样式更改。 StyledDocument实例可以触发所有三种类型的事件。 PlainDocument实例仅针对插入和移除触发事件。有关文档监听器和文档事件的一般信息,请参见如何编写文档监听器

请记住,此文本窗格的文档过滤器限制了文档中允许的字符数。如果您尝试添加的文本超出文档过滤器允许的范围,则文档过滤器会阻止更改,并且不会调用监听器的insertUpdate方法。仅当更改已发生时,才会通知文档监听器更改。

您可能希望在文档监听器中更改文档的文本。 但是,不应该从文档监听器中修改文本组件的内容。 如果你这样做,该程序可能会陷入僵局。相反,您可以使用带格式的文本字段或提供文档过滤器。

TextComponentDemo程序使用插入符听取器显示插入符的当前位置,或者,如果选择了文本,则显示选择的范围。

此示例中的插入符监听器类是JLabel子类。以下是创建插入符监听器标签并使其成为文本窗格的插入监听器的代码:

  1. //Create the status area
  2. CaretListenerLabel caretListenerLabel = new CaretListenerLabel(
  3. "Caret Status");
  4. ...
  5. textPane.addCaretListener(caretListenerLabel);

插入符号监听器必须实现一个方法caretUpdate,每次插入符号移动或选择更改时都会调用该方法。这是caretUpdateCaretListenerLabel实现:

  1. public void caretUpdate(CaretEvent e) {
  2. //Get the location in the text
  3. int dot = e.getDot();
  4. int mark = e.getMark();
  5. if (dot == mark) { // no selection
  6. try {
  7. Rectangle caretCoords = textPane.modelToView(dot);
  8. //Convert it to view coordinates
  9. setText("caret: text position: " + dot +
  10. ", view location = [" +
  11. caretCoords.x + ", " + caretCoords.y + "]" +
  12. newline);
  13. } catch (BadLocationException ble) {
  14. setText("caret: text position: " + dot + newline);
  15. }
  16. } else if (dot < mark) {
  17. setText("selection from: " + dot + " to " + mark + newline);
  18. } else {
  19. setText("selection from: " + mark + " to " + dot + newline);
  20. }
  21. }

如您所见,此监听器更新其文本标签以反映插入符号或选择的当前状态。监听器从插入事件对象获取要显示的信息。有关插入符号监听器和插入符事件的一般信息,请参阅如何编写插入监听器

与文档监听器一样,插入符听众是被动的。它会对插入符号或选择中的更改做出反应,但不会更改插入符号或选择本身。如果要更改插入符号或选择,请使用导航过滤器或自定义插入符号。

实现导航过滤器类似于实现文档过滤器。首先,编写 NavigationFilter 的子类。然后使用setNavigationFilter方法将子类的实例附加到文本组件。

您可以创建自定义插入符来自定义插入符的外观。要创建自定义插入符,请编写一个实现 Caret 接口的类 - 可能通过扩展 DefaultCaret 类。然后提供类的实例作为文本组件上setCaret方法的参数。

文本组件使用EditorKit将文本组件的各个部分绑定在一起。编辑器工具包提供视图工厂,文档,插入符和操作。编辑器工具包还可以读取和写入特定格式的文档。虽然所有文本组件都使用编辑器套件,但某些组件隐藏了它们。您无法设置或获取文本字段或文本区域使用的编辑器工具包。编辑器窗格和文本窗格提供getEditorKit方法以获取当前编辑器工具包和setEditorKit方法来更改它。

对于所有组件,JTextComponent类为您提供间接调用或自定义某些编辑器工具包功能的 API。例如,JTextComponent提供readwrite方法,它们调用编辑器工具包的readwrite方法。 JTextComponent还提供了一种方法getActions,它返回组件支持的所有操作。

Swing 文本包提供以下编辑器包:

DefaultEditorKit

Reads and writes plain text, and provides a basic set of editing commands. Details about how the text system treats newlines can be found in the DefaultEditorKit API documentation. Briefly, the ‘\n’ character is used internally, but the document or platform line separators are used when writing files. All the other editor kits are descendants of the DefaultEditorKit class.

StyledEditorKit

Reads and writes styled text, and provides a minimal set of actions for styled text. This class is a subclass of DefaultEditorKit and is the editor kit used by JTextPane by default.

HTMLEditorKit

Reads, writes, and edits HTML. This is a subclass of StyledEditorKit.

上面列出的每个编辑器套件都已在JEditorPane类中注册,并与套件读取,写入和编辑的文本格式相关联。将文件加载到编辑器窗格中时,窗格会根据其注册的工具包检查文件的格式。如果找到支持该文件格式的已注册工具包,则该窗格使用该工具包读取文件,显示并编辑它。因此,编辑器窗格有效地将其自身转换为该文本格式的编辑器。您可以通过为其创建编辑工具包来扩展JEditorPane以支持您自己的文本格式,然后使用JEditorPaneregisterEditorKitForContentType将您的工具包与文本格式相关联。