如何在JTextArea每行添加不可编辑编号前缀并自动调整编号
解决JTextArea自动添加不可编辑步骤前缀并自动调整编号的问题
核心实现思路
通过自定义DocumentFilter拦截所有文档修改操作,实现两个核心目标:
- 禁止用户编辑每行的前缀部分(
Step X:) - 每次文档修改后,重新计算并更新所有行的编号,保证编号连续有序
完整代码实现
自定义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); }); } }
关键功能说明
- 前缀保护:通过正则匹配前缀区域,任何试图修改、删除前缀的操作都会被直接拦截,确保前缀不可编辑。
- 自动编号调整:每次文档内容变化后,重新遍历所有行,替换原有的编号为连续的序号,包括空行也会自动添加正确前缀。
- 光标位置维护:修改文档后自动调整光标位置,保证光标始终停留在用户输入的内容区域,不会因为编号更新而错位。
注意事项
- 该实现每次修改后会重新生成整个文档内容,适合步骤列表这类文本量不大的场景,若需处理大文本可优化局部更新逻辑。
- 前缀模板
prefixTemplate可根据需求修改,比如改成"【步骤%d】: "等格式。
内容的提问来源于stack exchange,提问作者Arkadya
相关产品推荐
相关产品推荐

