如何在Java RichTextFX的CodeArea行指示器处添加折叠展开图标并实现代码折叠
使用RichTextFX CodeArea实现括号/尖括号代码折叠功能
核心思路
实现IDE风格的括号折叠需要三个核心环节:
- 解析成对的括号/尖括号,定位折叠块的起止行
- 自定义行指示器(Gutter)绘制折叠/展开图标,处理点击交互
- 基于折叠状态动态控制CodeArea的段落可见性
关键代码实现
1. 括号匹配工具类
用于定位指定位置的匹配括号,返回折叠块的起止行号:
import org.fxmisc.richtext.CodeArea; import java.util.Stack; public class BracketMatcher { private static final char[][] BRACKET_PAIRS = {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'<', '>'}}; public static int[] findMatchingBlock(CodeArea codeArea, int caretPosition) { char currentChar = codeArea.getText().charAt(caretPosition); char matchingChar = getMatchingChar(currentChar); if (matchingChar == 0) return null; Stack<Integer> bracketStack = new Stack<>(); String fullText = codeArea.getText(); int startLine = -1, endLine = -1; if (isOpeningBracket(currentChar)) { bracketStack.push(caretPosition); for (int i = caretPosition + 1; i < fullText.length(); i++) { char c = fullText.charAt(i); if (c == currentChar) bracketStack.push(i); else if (c == matchingChar) { bracketStack.pop(); if (bracketStack.isEmpty()) { startLine = codeArea.getCurrentParagraph(); endLine = codeArea.offsetToParagraph(i); break; } } } } else if (isClosingBracket(currentChar)) { bracketStack.push(caretPosition); for (int i = caretPosition - 1; i >= 0; i--) { char c = fullText.charAt(i); if (c == currentChar) bracketStack.push(i); else if (c == matchingChar) { bracketStack.pop(); if (bracketStack.isEmpty()) { startLine = codeArea.offsetToParagraph(i); endLine = codeArea.getCurrentParagraph(); break; } } } } return bracketStack.isEmpty() ? new int[]{startLine, endLine} : null; } private static char getMatchingChar(char c) { for (char[] pair : BRACKET_PAIRS) { if (pair[0] == c) return pair[1]; if (pair[1] == c) return pair[0]; } return 0; } private static boolean isOpeningBracket(char c) { for (char[] pair : BRACKET_PAIRS) { if (pair[0] == c) return true; } return false; } private static boolean isClosingBracket(char c) { for (char[] pair : BRACKET_PAIRS) { if (pair[1] == c) return true; } return false; } }
2. 自定义折叠行指示器
扩展RichTextFX的Gutter组件,实现折叠图标绘制与交互:
import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.Gutter; import org.fxmisc.richtext.GutterRow; import javafx.scene.control.Label; import javafx.scene.paint.Color; import javafx.scene.shape.Polygon; import java.util.HashMap; import java.util.Map; public class FoldGutter { private final Map<Integer, Boolean> foldStates = new HashMap<>(); private final CodeArea codeArea; public FoldGutter(CodeArea codeArea) { this.codeArea = codeArea; setupGutter(); } private void setupGutter() { Gutter<CodeArea> gutter = codeArea.getGutter(); gutter.setFactory(this::createFoldableGutterRow); } private GutterRow createFoldableGutterRow(int paragraphIndex) { GutterRow row = new GutterRow(); if (isFoldStartLine(paragraphIndex)) { Label foldIcon = createFoldToggleIcon(paragraphIndex); row.getChildren().add(foldIcon); } return row; } private boolean isFoldStartLine(int paragraphIndex) { String lineText = codeArea.getParagraph(paragraphIndex).getText().trim(); return lineText.endsWith("(") || lineText.endsWith("[") || lineText.endsWith("{") || lineText.endsWith("<"); } private Label createFoldToggleIcon(int startLine) { Label icon = new Label(); updateFoldIcon(icon, foldStates.getOrDefault(startLine, false)); icon.setOnMouseClicked(e -> { boolean isFolded = foldStates.getOrDefault(startLine, false); foldStates.put(startLine, !isFolded); updateFoldIcon(icon, !isFolded); applyFoldState(startLine, !isFolded); }); icon.setStyle("-fx-cursor: hand;"); return icon; } private void updateFoldIcon(Label icon, boolean isFolded) { if (isFolded) { Polygon expandIcon = new Polygon(0, 0, 8, 0, 4, 6); expandIcon.setFill(Color.GRAY); icon.setGraphic(expandIcon); } else { Polygon foldIcon = new Polygon(0, 0, 6, 4, 0, 8); foldIcon.setFill(Color.GRAY); icon.setGraphic(foldIcon); } } private void applyFoldState(int startLine, boolean isFolded) { int lineEndOffset = codeArea.getParagraph(startLine).getLength(); int[] blockRange = BracketMatcher.findMatchingBlock( codeArea, codeArea.paragraphToOffset(startLine) + lineEndOffset ); if (blockRange == null || blockRange[0] != startLine) return; int endLine = blockRange[1]; for (int i = startLine + 1; i < endLine; i++) { codeArea.setParagraphVisible(i, !isFolded); } codeArea.getGutter().refresh(); } }
3. 初始化CodeArea并启用折叠功能
在UI初始化代码中集成折叠功能:
import org.fxmisc.richtext.CodeArea; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; public class CodeFoldDemo extends Application { @Override public void start(Stage stage) { CodeArea codeArea = new CodeArea(); // 填充示例代码 codeArea.appendText("public class FoldDemo {\n"); codeArea.appendText(" public void testMethod() {\n"); codeArea.appendText(" String[] arr = {\"a\", \"b\", \"c\"};\n"); codeArea.appendText(" if (arr.length > 0) {\n"); codeArea.appendText(" for (String s : arr) {\n"); codeArea.appendText(" System.out.println(s);\n"); codeArea.appendText(" }\n"); codeArea.appendText(" }\n"); codeArea.appendText(" }\n"); codeArea.appendText("}\n"); // 绑定折叠 gutter new FoldGutter(codeArea); StackPane root = new StackPane(codeArea); Scene scene = new Scene(root, 800, 600); stage.setScene(scene); stage.setTitle("Code Fold Demo"); stage.show(); } public static void main(String[] args) { launch(args); } }
优化提示
- 简化版的括号匹配逻辑未处理字符串、注释中的括号,实际项目可结合语法解析器(如ANTLR)优化匹配精度
- 可扩展折叠状态持久化功能,通过保存
foldStates实现重启后恢复折叠状态 - 嵌套折叠场景下可调整图标样式,区分不同层级的折叠块
内容的提问来源于stack exchange,提问作者garawaa garawaa
相关产品推荐
相关产品推荐

