文档

Java™教程
隐藏目录
文本组件特性
路径: 使用Swing创建GUI
课程: 使用Swing组件
章节: 使用文本组件

文本组件功能

JTextComponent类是Swing文本组件的基础。该类为其所有子类提供以下可定制功能:

查看名为TextComponentDemo的示例以探索这些功能。尽管TextComponentDemo示例包含了一个自定义的JTextPane实例,但本节讨论的功能都是由所有JTextComponent的子类继承的。

TextComponentDemo的快照,其中包含一个自定义的文本窗格和一个标准文本区域

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


试一试: 
  1. 点击“Launch”按钮使用Java™ Web Start运行TextComponentDemo(下载JDK 7或更高版本)。或者,要自己编译和运行示例,请参考示例索引启动TextComponentDemo应用程序
  2. 使用鼠标选择文本并将光标放置在文本窗格中。选择和光标的相关信息将显示在窗口底部。
  3. 通过键盘输入文本。可以使用键盘上的箭头键或四个emacs键绑定(Ctrl-B向后一个字符,Ctrl-F向前一个字符,Ctrl-N向下一行,Ctrl-P向上一行)来移动插入符。
  4. 打开“Edit”菜单,使用其中的菜单项编辑文本窗格中的文本。在窗口底部的文本区域中进行选择。因为文本区域不可编辑,所以只有一些“Edit”菜单的命令(如复制到剪贴板)可用。但是需要注意的是,菜单对两个文本组件都起作用。
  5. 使用“Style”菜单中的项将不同的样式应用于文本窗格中的文本。

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

将文本操作与菜单和按钮关联

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

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

private HashMap<Object, Action> createActionTable(JTextComponent textComponent) {
        HashMap<Object, Action> actions = new HashMap<Object, Action>();
        Action[] actionsArray = textComponent.getActions();
        for (int i = 0; i < actionsArray.length; i++) {
            Action a = actionsArray[i];
            actions.put(a.getValue(Action.NAME), a);
        }
        return actions;
    }

以下是通过名称从哈希映射中检索动作的方法:

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

您可以在您的程序中直接使用这两个方法。

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

protected JMenu createEditMenu() {
    JMenu menu = new JMenu("Edit");
    ...
    menu.add(getActionByName(DefaultEditorKit.cutAction));
    ...

此代码使用之前展示的便利方法按名称获取动作。然后将动作添加到菜单中。这就是您需要做的全部。菜单和动作会处理其他所有事情。请注意,动作的名称来自DefaultEditorKit。该工具包提供基本文本编辑的操作,并且是Swing提供的所有编辑器工具包的超类。因此,除非被自定义覆盖,否则所有文本组件都可以使用其功能。

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

下面是创建Style菜单并将Bold菜单项放入其中的代码:

protected JMenu createStyleMenu() {
    JMenu menu = new JMenu("Style");
 
    Action action = new StyledEditorKit.BoldAction();
    action.putValue(Action.NAME, "加粗");
    menu.add(action);
    ...

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

将文本操作与按键绑定

除了将操作与GUI组件关联外,还可以通过使用文本组件的输入映射将操作与按键绑定关联。输入映射在如何使用按键绑定中有描述。

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

下面的代码将Ctrl-B按键绑定添加到文本窗格中。添加其他三个绑定的代码类似。

InputMap inputMap = textPane.getInputMap();

KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_B,
                                       Event.CTRL_MASK);
inputMap.put(key, DefaultEditorKit.backwardAction);

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

实现撤销和重做

实现撤销和重做需要分为两个部分:

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

protected UndoManager undo = new UndoManager();

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

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

doc.addUndoableEditListener(new MyUndoableEditListener());

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

protected class MyUndoableEditListener
          implements UndoableEditListener {
    public void undoableEditHappened(UndoableEditEvent e) {
        //记住编辑并更新菜单
        undo.addEdit(e.getEdit());
        undoAction.updateUndoState();
        redoAction.updateRedoState();
    }
}  

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


注意: 

默认情况下,每次可撤销的编辑会撤销一个字符输入。如果有必要,可以通过一些努力将一系列按键组合成一个可撤销的编辑。以这种方式分组编辑需要您定义一个类来拦截来自文档的可撤销编辑事件,如果适合的话将它们合并,并将结果转发给可撤销编辑监听器。


第二部分:实现撤销和重做命令
实现撤销和重做的第一步是创建要放在编辑菜单中的动作。

JMenu menu = new JMenu("编辑");

//撤销和重做是我们自己创建的操作
undoAction = new UndoAction();
menu.add(undoAction);

redoAction = new RedoAction();
menu.add(redoAction);
...

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

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

public void actionPerformed(ActionEvent e) {
    try {
        undo.undo();
    } catch (CannotUndoException ex) {
        System.out.println("无法撤销:" + ex);
        ex.printStackTrace();
    }
    updateUndoState();
    redoAction.updateRedoState();
}

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

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

public void actionPerformed(ActionEvent e) {
    try {
        undo.redo();
    } catch (CannotRedoException ex) {
        System.out.println("无法重做:" + ex);
        ex.printStackTrace();
    }
    updateRedoState();
    undoAction.updateUndoState();
}

该方法与撤销类似,只是调用撤销管理器的redo方法。

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


注意: 

TextComponentDemo示例中的撤销和重做实现是从JDK软件附带的NotePad演示中获取的。许多程序员也可以直接复制这个撤销/重做实现而无需修改。


概念:关于文档

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

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

Swing文本包含一个Document的子接口,StyledDocument,它可以为文本添加样式支持。一个JTextComponent子类JTextPane要求其文档必须是StyledDocument而不仅仅是Document

javax.swing.text包提供了以下的文档类层次结构,这些类为不同的JTextComponent子类实现了专门的文档:

javax.swing.text提供的文档类层次结构。

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

虽然你可以设置文本组件的文档,但通常让它自动设置会更容易,并且在必要时使用文档过滤器来更改文本组件的数据设置。你可以通过安装文档过滤器或替换文本组件的文档来实现某些自定义。例如,TextComponentDemo示例中的文本窗格具有一个文档过滤器,限制文本窗格可以包含的字符数。

实现文档过滤器

要实现文档过滤器,创建DocumentFilter的子类,然后使用AbstractDocument类中定义的setDocumentFilter方法将其附加到文档上。虽然可能有不是从AbstractDocument继承的文档,但默认情况下Swing文本组件使用AbstractDocument的子类作为它们的文档。

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

...//声明成员变量的位置:
JTextPane textPane;
AbstractDocument doc;
static final int MAX_CHARACTERS = 300;
...
textPane = new JTextPane();
...
StyledDocument styledDoc = textPane.getStyledDocument();
if (styledDoc instanceof AbstractDocument) {
    doc = (AbstractDocument)styledDoc;
    doc.setDocumentFilter(new DocumentSizeFilter(MAX_CHARACTERS));
} 

为了限制文档中允许的字符数,DocumentSizeFilter重写了DocumentFilter类的insertString方法,每当文本插入到文档中时都会调用该方法。它还重写了replace方法,当用户粘贴新文本时最有可能调用该方法。一般来说,文本插入可能发生在用户键入或粘贴新文本时,或者在调用setText方法时。下面是DocumentSizeFilter类对insertString方法的实现:

public void insertString(FilterBypass fb, int offs,
                         String str, AttributeSet a)
    throws BadLocationException {

    if ((fb.getDocument().getLength() + str.length()) <= maxCharacters)
        super.insertString(fb, offs, str, a);
    else
        Toolkit.getDefaultToolkit().beep();
}

replace方法的代码类似。DocumentFilter类定义的方法的参数FilterBypass是一个简单的对象,它使得文档可以以线程安全的方式进行更新。

因为前面的文档过滤器只关注对文档数据的添加,所以它只重写了insertStringreplace方法。大多数文档过滤器还会重写DocumentFilterremove方法。

监听文档的变化

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

文档通知已注册的文档监听器有关文档的变化。使用文档监听器在文档中插入或删除文本,或者更改文本样式时创建反应。

TextComponentDemo程序使用文档监听器在文本面板发生更改时更新更改日志。下面的代码行将MyDocumentListener类的一个实例注册为文本面板文档的监听器:

doc.addDocumentListener(new MyDocumentListener());

下面是MyDocumentListener类的实现:

protected class MyDocumentListener implements DocumentListener {
    public void insertUpdate(DocumentEvent e) {
        displayEditInfo(e);
    }
    public void removeUpdate(DocumentEvent e) {
        displayEditInfo(e);
    }
    public void changedUpdate(DocumentEvent e) {
        displayEditInfo(e);
    }
    private void displayEditInfo(DocumentEvent e) {
            Document document = (Document)e.getDocument();
            int changeLength = e.getLength();
            changeLog.append(e.getType().toString() + ":" 
                + changeLength + " 个字符"
                + ((changeLength == 1) ? "。" : "s。 ")
                + " 文本长度 = " + document.getLength()
                + "。" + newline);
    }
} 

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

请记住,此文本窗格的文档过滤器限制了文档中允许的字符数。如果您尝试添加超过文档过滤器允许的文本,文档过滤器将阻止更改,并且监听器的 insertUpdate 方法不会被调用。只有在更改已经发生时,文档监听器才会收到更改通知。

您可能希望在文档监听器中更改文档的文本。 但是,您绝不能从文档监听器中修改文本组件的内容。如果这样做,程序很可能会死锁。相反,您可以使用格式化的文本字段或提供文档过滤器。

监听插入符和选定内容的更改

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

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

//创建状态区域
CaretListenerLabel caretListenerLabel = new CaretListenerLabel(
                                                "插入符状态");
...
textPane.addCaretListener(caretListenerLabel);

插入符监听器必须实现一个方法 caretUpdate,该方法在每次插入符移动或选择更改时被调用。以下是 CaretListenerLabel 实现的 caretUpdate 方法:

public void caretUpdate(CaretEvent e) {
    //获取文本中的位置
    int dot = e.getDot();
    int mark = e.getMark();
    if (dot == mark) {  // 没有选择
        try {
            Rectangle caretCoords = textPane.modelToView(dot);
            //将其转换为视图坐标
            setText("插入符:文本位置:" + dot +
                    ",视图位置 = [" +
                    caretCoords.x + ", " + caretCoords.y + "]" +
                    newline);
        } catch (BadLocationException ble) {
            setText("插入符:文本位置:" + dot + newline);
        }
     } else if (dot < mark) {
        setText("选择范围从:" + dot + " 到 " + mark + newline);
     } else {
        setText("选择范围从:" + mark + " 到 " + dot + newline);
     }
}

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

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

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

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

概念:关于编辑器工具包

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

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

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

DefaultEditorKit
读取和写入纯文本,并提供一组基本的编辑命令。有关文本系统如何处理换行符的详细信息,请参阅DefaultEditorKit API文档。简单地说,内部使用'\n'字符,但在写入文件时使用文档或平台的换行符。所有其他的编辑器工具包都是DefaultEditorKit类的子类。
StyledEditorKit
读取和写入样式文本,并提供一组最小的样式文本操作。该类是DefaultEditorKit的子类,并且是JTextPane默认使用的编辑器工具包。
HTMLEditorKit
读取、写入和编辑HTML。这是StyledEditorKit的子类。

以上列出的每个编辑器工具包都已经在JEditorPane类中注册,并与工具包读取、写入和编辑的文本格式关联起来。当文件加载到编辑器窗格中时,窗格会检查文件的格式与其注册的工具包是否匹配。如果找到支持该文件格式的注册工具包,窗格将使用该工具包来读取、显示和编辑文件。因此,编辑器窗格实际上会将自身转换为该文本格式的编辑器。您可以通过为其创建一个编辑器工具包,并使用JEditorPaneregisterEditorKitForContentType方法将您的工具包与您的文本格式关联起来,从而扩展JEditorPane来支持您自己的文本格式。


上一页: 使用文本组件
下一页: 文本组件API