You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JTextArea每行添加不可编辑编号前缀并自动调整编号

解决JTextArea自动添加不可编辑步骤前缀并自动调整编号的问题

核心实现思路

通过自定义DocumentFilter拦截所有文档修改操作,实现两个核心目标:

  1. 禁止用户编辑每行的前缀部分(Step X:)
  2. 每次文档修改后,重新计算并更新所有行的编号,保证编号连续有序

完整代码实现

自定义DocumentFilter类

import javax.swing.text.*;
import java.util.regex.Pattern;

public class StepDocumentFilter extends DocumentFilter {
    private static final Pattern STEP_PREFIX_PATTERN = Pattern.compile("^Step \\d+: ");
    private final String prefixTemplate = "Step %d: ";

    @Override
    public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
        if (isInPrefixArea(fb.getDocument(), offset)) {
            return;
        }
        super.insertString(fb, offset, string, attr);
        updateStepNumbers(fb);
    }

    @Override
    public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
        if (isReplacingPrefix(fb.getDocument(), offset, length)) {
            return;
        }
        super.replace(fb, offset, length, text, attrs);
        updateStepNumbers(fb);
    }

    @Override
    public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
        if (isRemovingPrefix(fb.getDocument(), offset, length)) {
            return;
        }
        super.remove(fb, offset, length);
        updateStepNumbers(fb);
    }

    private boolean isInPrefixArea(Document doc, int offset) throws BadLocationException {
        int line = doc.getDefaultRootElement().getElementIndex(offset);
        Element lineElement = doc.getDefaultRootElement().getElement(line);
        int lineStart = lineElement.getStartOffset();
        String lineText = doc.getText(lineStart, offset - lineStart);
        return STEP_PREFIX_PATTERN.matcher(lineText).matches() || lineText.startsWith("Step ");
    }

    private boolean isReplacingPrefix(Document doc, int offset, int length) throws BadLocationException {
        int endOffset = offset + length;
        int lineStart = getLineStartOffset(doc, offset);
        int prefixLength = getPrefixLength(lineStart, doc);
        return offset < lineStart + prefixLength && endOffset > lineStart;
    }

    private boolean isRemovingPrefix(Document doc, int offset, int length) throws BadLocationException {
        int endOffset = offset + length;
        int lineStart = getLineStartOffset(doc, offset);
        int prefixLength = getPrefixLength(lineStart, doc);
        return offset < lineStart + prefixLength && endOffset > lineStart;
    }

    private int getLineStartOffset(Document doc, int offset) throws BadLocationException {
        int line = doc.getDefaultRootElement().getElementIndex(offset);
        return doc.getDefaultRootElement().getElement(line).getStartOffset();
    }

    private int getPrefixLength(int lineStart, Document doc) throws BadLocationException {
        String lineText = doc.getText(lineStart, Math.min(20, doc.getLength() - lineStart));
        var matcher = STEP_PREFIX_PATTERN.matcher(lineText);
        return matcher.find() ? matcher.end() : 0;
    }

    private void updateStepNumbers(FilterBypass fb) throws BadLocationException {
        Document doc = fb.getDocument();
        String fullText = doc.getText(0, doc.getLength());
        String[] lines = fullText.split("\\n", -1);
        StringBuilder newText = new StringBuilder();

        for (int i = 0; i < lines.length; i++) {
            String line = lines[i];
            String newLine = STEP_PREFIX_PATTERN.matcher(line).replaceAll(String.format(prefixTemplate, i + 1));
            if (newLine.isEmpty()) {
                newLine = String.format(prefixTemplate, i + 1);
            }
            newText.append(newLine);
            if (i < lines.length - 1) {
                newText.append("\n");
            }
        }

        int caretPosition = doc.getProperty("caretPosition") != null
                ? (int) doc.getProperty("caretPosition")
                : doc.getLength();
        fb.replace(0, doc.getLength(), newText.toString(), null);
        doc.putProperty("caretPosition", adjustCaretPosition(caretPosition, fullText, newText.toString()));
    }

    private int adjustCaretPosition(int oldPos, String oldText, String newText) {
        int oldLineCount = oldText.substring(0, oldPos).split("\\n", -1).length;
        String[] newLines = newText.split("\\n", -1);
        if (oldLineCount > newLines.length) {
            return newText.length();
        }

        String oldLine = oldText.split("\\n", -1)[oldLineCount - 1];
        int oldLinePos = oldPos - (oldText.substring(0, oldPos).lastIndexOf("\n") + 1);
        int oldPrefixLength = STEP_PREFIX_PATTERN.matcher(oldLine).find() ? STEP_PREFIX_PATTERN.matcher(oldLine).end() : 0;
        int relativePos = Math.max(0, oldLinePos - oldPrefixLength);

        String newLine = newLines[oldLineCount - 1];
        int newPrefixLength = STEP_PREFIX_PATTERN.matcher(newLine).find() ? STEP_PREFIX_PATTERN.matcher(newLine).end() : 0;

        int newPos = 0;
        for (int i = 0; i < oldLineCount - 1; i++) {
            newPos += newLines[i].length() + 1;
        }
        newPos += newPrefixLength + relativePos;
        return Math.min(newPos, newText.length());
    }
}

使用示例

import javax.swing.*;
import javax.swing.text.AbstractDocument;
import java.awt.*;

public class StepTextAreaDemo {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Step Text Area");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(400, 300);

            JTextArea textArea = new JTextArea();
            textArea.setLineWrap(true);
            textArea.setWrapStyleWord(true);

            AbstractDocument doc = (AbstractDocument) textArea.getDocument();
            doc.setDocumentFilter(new StepDocumentFilter());

            try {
                doc.insertString(0, "Step 1: ", null);
            } catch (BadLocationException e) {
                e.printStackTrace();
            }

            frame.add(new JScrollPane(textArea), BorderLayout.CENTER);
            frame.setVisible(true);
        });
    }
}

关键功能说明

  1. 前缀保护:通过正则匹配前缀区域,任何试图修改、删除前缀的操作都会被直接拦截,确保前缀不可编辑。
  2. 自动编号调整:每次文档内容变化后,重新遍历所有行,替换原有的编号为连续的序号,包括空行也会自动添加正确前缀。
  3. 光标位置维护:修改文档后自动调整光标位置,保证光标始终停留在用户输入的内容区域,不会因为编号更新而错位。

注意事项

  • 该实现每次修改后会重新生成整个文档内容,适合步骤列表这类文本量不大的场景,若需处理大文本可优化局部更新逻辑。
  • 前缀模板prefixTemplate可根据需求修改,比如改成"【步骤%d】: "等格式。

内容的提问来源于stack exchange,提问作者Arkadya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 22:30:53