如何在Java中可视化渲染ANTLR4生成的无规则名算术表达式解析树?
嘿,刚接触ANTLR4就能搞定Visitor转后缀表达式,已经很棒了!关于解析树可视化且不显示规则名的需求,我刚好有两种实用的实现方式,给你一步步拆解:
方法一:基于ANTLR自带TreeViewer自定义节点显示
ANTLR自带的TreeViewer默认会显示规则名称,但我们可以通过自定义文本提供者来替换成我们想要的内容(比如操作符、数字,而非规则名)。
步骤1:实现自定义ParseTreeTextProvider
这个类负责决定每个节点在可视化时显示的文本:
import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.gui.ParseTreeTextProvider; import org.antlr.v4.runtime.tree.TerminalNode; public class CustomTreeTextProvider implements ParseTreeTextProvider { @Override public String getText(ParseTree node) { // 叶子节点(比如数字、操作符)直接显示token文本 if (node instanceof TerminalNode) { return ((TerminalNode) node).getSymbol().getText(); } // 非叶子节点根据上下文类型显示对应的操作符(空字符串代表不显示规则名) String nodeClassName = node.getClass().getSimpleName(); return switch (nodeClassName) { case "AddContext" -> "+"; case "SubContext" -> "-"; case "MulContext" -> "*"; case "DivContext" -> "/"; case "ParenContext" -> "()"; default -> ""; // 根节点或其他规则节点不显示文本 }; } }
注意:这里的上下文类名(比如
AddContext)是ANTLR根据你的语法规则自动生成的,比如你语法里写add : exp '+' exp;,就会生成AddContext,要和你自己的语法对应上哦。
步骤2:用TreeViewer显示自定义解析树
在你的主程序里获取解析树,然后传入自定义文本提供者:
import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.gui.TreeViewer; import javax.swing.*; import java.util.Arrays; public class ExpressionVisualizer { public static void main(String[] args) { // 1. 解析表达式得到ParseTree CharStream input = CharStreams.fromString("3+4*(5-2)"); ExpAnalyserLexer lexer = new ExpAnalyserLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); ExpAnalyserParser parser = new ExpAnalyserParser(tokens); ParseTree parseTree = parser.exp(); // 这里的exp是你语法里的根规则 // 2. 初始化自定义文本提供者和TreeViewer CustomTreeTextProvider textProvider = new CustomTreeTextProvider(); TreeViewer viewer = new TreeViewer(Arrays.asList(parser.getRuleNames()), parseTree); viewer.setParseTreeTextProvider(textProvider); // 3. 用Swing窗口展示 JFrame frame = new JFrame("中缀表达式解析树"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(viewer); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }
方法二:生成Graphviz DOT文件渲染为图片
如果需要把解析树保存为图片或者嵌入到其他应用中,生成DOT文件再转成图片是更灵活的方式。
步骤1:编写解析树转DOT的工具类
import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; public class ParseTreeToDot { private int nodeCounter = 0; private final StringBuilder dotBuilder = new StringBuilder(); public String generateDot(ParseTree tree) { dotBuilder.append("digraph ExpressionParseTree {\n"); dotBuilder.append(" node [shape=ellipse, style=filled, fillcolor=lightcyan];\n"); traverseTree(tree); dotBuilder.append("}\n"); return dotBuilder.toString(); } private int traverseTree(ParseTree node) { int currentNodeId = nodeCounter++; String nodeLabel = getNodeLabel(node); dotBuilder.append(String.format(" node%d [label=\"%s\"];\n", currentNodeId, nodeLabel)); // 遍历子节点并添加边 for (int i = 0; i < node.getChildCount(); i++) { ParseTree child = node.getChild(i); int childNodeId = traverseTree(child); dotBuilder.append(String.format(" node%d -> node%d;\n", currentNodeId, childNodeId)); } return currentNodeId; } private String getNodeLabel(ParseTree node) { if (node instanceof TerminalNode) { return ((TerminalNode) node).getSymbol().getText(); } // 同样根据上下文类型返回对应文本 String className = node.getClass().getSimpleName(); return switch (className) { case "AddContext" -> "+"; case "SubContext" -> "-"; case "MulContext" -> "*"; case "DivContext" -> "/"; case "ParenContext" -> "()"; default -> ""; }; } }
步骤2:生成并渲染DOT文件
你可以选择调用本地安装的Graphviz命令,或者用Java库graphviz-java来处理:
import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Paths; public class DotRenderer { public static void main(String[] args) throws IOException, InterruptedException { // 1. 获取解析树 CharStream input = CharStreams.fromString("3+4*(5-2)"); ExpAnalyserLexer lexer = new ExpAnalyserLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); ExpAnalyserParser parser = new ExpAnalyserParser(tokens); ParseTree parseTree = parser.exp(); // 2. 生成DOT内容 ParseTreeToDot dotGenerator = new ParseTreeToDot(); String dotContent = dotGenerator.generateDot(parseTree); // 3. 保存为DOT文件 try (FileWriter writer = new FileWriter("expression_tree.dot")) { writer.write(dotContent); } // 方式A:调用本地Graphviz的dot命令生成PNG(需提前安装Graphviz并加入PATH) Process process = new ProcessBuilder("dot", "-Tpng", "expression_tree.dot", "-o", "expression_tree.png").start(); process.waitFor(); // 方式B:用graphviz-java库(无需本地安装),先添加依赖 // 比如Maven依赖:<dependency><groupId>com.github.jabrena</groupId><artifactId>graphviz-java</artifactId><version>0.1.7</version></dependency> // Graphviz.fromString(dotContent).render(Format.PNG).toFile(Paths.get("expression_tree.png")); } }
小提示
- 如果你语法里的规则名不同(比如根规则是
expression而非exp),记得调整代码里的parser.exp()为parser.expression()。 - 非叶子节点的文本可以根据你的需求调整,比如括号节点可以选择不显示,或者显示
(),完全看你想要的可视化效果。
内容的提问来源于stack exchange,提问作者AaySquare
相关产品推荐
相关产品推荐

