在 Java 中渲染表达式树

Rendering an expression tree in Java

我以前编写过将采用数学表达式并将其转换为解析树的代码,我现在正尝试以可视方式显示通过在 JPanel 上绘图创建的树。表达式由用户输入到控制台,它输出后缀,我还想显示树。然而,当我 运行 我当前的程序时,树没有呈现在 JPanel 上。我没有从编译器收到任何错误,所以我不确定问题出在哪里。

public class TreeView extends JPanel {
//Class for drawing the Tree onto a panel
private int radius = 20;
private int levelGap = 50;
ExpTree t;

public TreeView(ExpTree t) {
    this.t = t;
}

protected void paintComponent(Graphics g) {
    super.paintComponents(g);

    if (t.getRoot() != null) {
        displayTree(g, t.getRoot(), getWidth() / 2, 30, getWidth() / 4);
    }
}

private void displayTree(Graphics g, ExpTree t, int x, int y, int gap) {
    g.drawOval(x - radius, y - radius, 2 * radius, 2 * radius);
    g.drawString(t.getLeafVal() + "", x - 6, y + 4);

    if (t.getlChild() != null) {
        connectCircles(g, x - gap, y + levelGap, x, y);
        displayTree(g, t.lChild, x - gap, y + levelGap, gap /2);
    }

    if (t.getrChild() != null) {
        connectCircles(g, x + gap, y + levelGap, x, y);
        displayTree(g, t.rChild, x + gap, y + levelGap, gap /2);
    }
}

private void connectCircles(Graphics g, int x1, int y1, int x2, int y2) {
    double d = Math.sqrt(levelGap * levelGap + (x2 - x1) * (y2 - y1));
    int x11 = (int)(x1 - radius * (x1 - x2) / d);
    int y11 = (int)(y1 - radius * (y1 - y2) / d);
    int x21 = (int)(x2 + radius * (x1 - x2) / d);
    int y21 = (int)(y2 + radius * (y1 - y2) / d);
    g.drawLine(x11, y11, x21, y21);
  }
}

public class Test extends JFrame {

public Test() {
    setSize(400, 400);
    setLayout(new BorderLayout());
    JPanel jp = new JPanel();
    add(jp);
    setVisible(true);
}

public static void main(String[] args) {


    Test test = new Test();

    //create parse trees from input in console
    boolean done = false;
    boolean valid = false;
    Parser p = new Parser();
    ExpTree myTree;
    System.out.println("Enter an expression to convert into postfix notation");
    do {
        System.out.println("Enter an expression: ");
        try {
            myTree = p.parseLine();
        }
        catch (ParseException e) {
            System.out.println("Invalid Expression: Ensure it ends with a semicolon");
            continue;
        }
        System.out.println(myTree.toPostfix(myTree));
        TreeView view = new TreeView(myTree);
        test.add(view);
        view.repaint();

        System.out.println("Do you want to enter another expression? (y/n)");
        do {
            String s = p.getLine();
            switch (s) {
                case "y" : valid = true;
                    done = false;
                    continue;
                case "n" : valid = true;
                    done = true;
                    continue;
                default: valid = false;
                    done = false;
                    System.out.println("Invalid input: Must be y or n");
            }
        } while (!valid);
    } while (!done);
  }
}


//Setup of the tree incase it's useful
public class ExpTree {
    //Tree object that is created when an expression is parsed
    private int type;
    private Object leafVal;
    public ExpTree lChild, rChild;
    public static final int NUM = 0, VAL = 1, OP = 2;
    private StringBuffer sb = new StringBuffer();

    public ExpTree(int type, Object leafVal, ExpTree l, ExpTree r) {
        this.type = type;
        this.leafVal = leafVal;
        this.lChild = l;
        this.rChild = r;
    }
   //return the forth expression, a postfix expression
public String toPostfix(ExpTree t) {
    if (t != null) {
        toPostfix(t.lChild);
        toPostfix(t.rChild);
        sb.append(t.leafVal);
        sb.append(" ");
    }
    return sb.toString();
}

public ExpTree getRoot() {
    return this;
}

public Object getLeafVal() {
    return leafVal;
}

public ExpTree getlChild() {
    return lChild;
}

public ExpTree getrChild() {
    return rChild;
}
}

class ParseException extends RuntimeException
{ public ParseException(String s)
{ super("Invalid expression: "+s);
}
}

public class Parser
{ private Lexer lex;

  public Parser()
  { lex = new Lexer();
  }

  public ExpTree parseLine()
  { lex.init();
    lex.getToken();
    ExpTree result = parseExp(true);
    if (lex.token==Lexer.where)
    { lex.getToken();
      ExpTree defs = parseDefList();
      result = makeWhereTree(result, defs);
    }
    if (lex.token!=Lexer.semico)
    { throw new ParseException("semicolon expected");
    }
    return result;
  }

  public String getLine()
  { return lex.getLine();
  }

  private ExpTree parseExp(boolean idsAllowed)
  { ExpTree result = parseTerm(idsAllowed);
    { while (lex.token==Lexer.plus || lex.token==Lexer.minus)
    { int op = lex.token;
      lex.getToken();
      if (op==Lexer.plus)
        result = makePlusTree(result, parseTerm(idsAllowed));
      else
        result = makeMinusTree(result, parseTerm(idsAllowed));
    }
    }
    return result;
  }

  private ExpTree parseTerm(boolean idsAllowed)
  { ExpTree result = parseOpd(idsAllowed);
    { while (lex.token==Lexer.times || lex.token==Lexer.div || lex.token==Lexer.mod)
    { int op = lex.token;
      lex.getToken();
      if (op==Lexer.times)
        result = makeTimesTree(result, parseOpd(idsAllowed));
      else if (op==Lexer.mod)
        result = makeModTree(result, parseOpd(idsAllowed));
      else
        result = makeDivideTree(result, parseOpd(idsAllowed));
    }
    }
    return result;
  }

  private ExpTree parseOpd(boolean idsAllowed)
  { ExpTree result;
    switch(lex.token)
    { case Lexer.num:
      result = makeNumberLeaf(lex.numval);
      lex.getToken();
      return result;
      case Lexer.id:
        if (!idsAllowed)
          throw new ParseException("identifier not allowed in identifier defintion");
        result = makeIdLeaf(lex.idval);
        lex.getToken();
        return result;
      case Lexer.lp:
        lex.getToken();
        result = parseExp(idsAllowed);
        if (lex.token!=Lexer.rp)
          throw new ParseException("right parenthesis expected");
        lex.getToken();
        return result;
      default:
        throw new ParseException("invalid operand");
    }
  }

  private ExpTree parseDefList()
  { ExpTree result = parseDef();
    while (lex.token==Lexer.and)
    { lex.getToken();
      result = makeAndTree(result, parseDef());
    }
    return result;
  }

  private ExpTree parseDef()
  { if (lex.token!=Lexer.id)
    throw new ParseException("definition must start with identifier");
    char id = lex.idval;
    if (Character.isUpperCase(id))
      throw new ParseException("upper-case identifiers cannot be used in defintion list");
    lex.getToken();
    if (lex.token!=Lexer.eq)
      throw new ParseException("'=' expected");
    lex.getToken();
    return makeDefTree(id, parseExp(false));
  }

  // the next seven methods need to be modified for part 3 of the assignment
  static ExpTree makeNumberLeaf(int n)
  { return new ExpTree(ExpTree.NUM, n, null, null);
    // this method should return a new number leaf with value n created using your constructor
    // if you've used the abstract class approach you will probably need something like
    // return new NumLeaf(n);
    // if you've used an ExpTree class that stores the node kind you will probably need something like
    // return new ExpTree(ExpTree.numNode, n , null, null);
  }

  static ExpTree makeIdLeaf(char c)
  { return new ExpTree(ExpTree.VAL, c, null, null);
    // this method should return a new id leaf with value c
  }

  static ExpTree makePlusTree(ExpTree l, ExpTree r)
  { return new ExpTree(ExpTree.OP, '+', l, r);
    // this method should return a new plus node with children l and r created using your constructor
    // if you've used the abstract class approach you will probably need something like
    // return new OpNode('+', l, r);
    // or
    // return new PlusNode(l, r);
    // if you've used an ExpTree class that stores the node kind you will probably need something like
    // return new ExpTree(ExpTree.opMode, '+', l, r);
  }

  static ExpTree makeMinusTree(ExpTree l, ExpTree r)
  { return new ExpTree(ExpTree.OP, '-', l, r);
    // this method should return a new minus node with children l and r
  }

  static ExpTree makeTimesTree(ExpTree l, ExpTree r)
  { return new ExpTree(ExpTree.OP, '*', l, r);
    // this method should return a new times node with children l and r
  }

  static ExpTree makeDivideTree(ExpTree l, ExpTree r)
  { return new ExpTree(ExpTree.OP, '/', l, r);
    // this method should return a new divide node with children l and r
  }

  static ExpTree makeModTree(ExpTree l, ExpTree r)
  { return new ExpTree(ExpTree.OP, '%', l, r);
    // this method should return a new mod (%) node with children l and r
  }

  // the next three methods need to be modified for part 6 of the assignment - do not modify them if you have not attempted part 6

  static ExpTree makeWhereTree(ExpTree l, ExpTree r)
  { // remove the following line if you modify this method; leave it here if you do not attempt part 6
    System.out.println("Part 6 not attempted");
    return null;
    // this method should return a new 'where' node with children l and r
  }

  static ExpTree makeAndTree(ExpTree l, ExpTree r)
  { return null;
    // this method should return a new 'and' node with children l and r
  }

  static ExpTree makeDefTree(char c, ExpTree t)
  { return null;
    // this method should return a new definition node with identifier c and child t
    // if your definition nodes have 2 children you should put a new id leaf in the left child and use t as the right child
  }
}

class Lexer
{ static final int err = 0, num = 1, id = 2, plus = 3, minus = 4, times = 5, div = 6, mod = 7,
        lp = 8, rp = 9, semico = 10, where = 11, and = 12, eq = 13;
  int token;
  char idval;
  int numval;
  private String line = "";
  private BufferedReader buf;

  Lexer()
  { buf = new BufferedReader(new InputStreamReader(System.in));
  }

  void init()
  { do
    try
    { line = buf.readLine().trim();
    }
    catch(Exception e)
    { System.out.println("Error in input");
      System.exit(1);
    }
  while (line.length()==0);
  }

  String getLine()
  { init();
    return(line);
  }

  void getToken()
  { if (line.length()==0)
    token = err;
  else switch (line.charAt(0))
    { case '+':
      token = plus;
      line = line.substring(1).trim();
      break;
      case '-':
        token = minus;
        line = line.substring(1).trim();
        break;
      case '*':
        token = times;
        line = line.substring(1).trim();
        break;
      case '/':
        token = div;
        line = line.substring(1).trim();
        break;
      case '%':
        token = mod;
        line = line.substring(1).trim();
        break;
      case '(':
        token = lp;
        line = line.substring(1).trim();
        break;
      case ')':
        token = rp;
        line = line.substring(1).trim();
        break;
      case ';':
        token = semico;
        line = line.substring(1).trim();
        break;
      case '=':
        token = eq;
        line = line.substring(1).trim();
        break;
      default:
        if (Character.isDigit(line.charAt(0)))
        { token = num;
          numval = line.charAt(0) - '0';
          int i = 1;
          while (i<line.length()&&Character.isDigit(line.charAt(i)))
          { numval = numval*10+line.charAt(i)-'0';
            i++;
          }
          line = line.substring(i).trim();
        }
        else if (Character.isLowerCase(line.charAt(0)))
        { char c = line.charAt(0);
          if (c=='w' && line.length()>=5 && line.charAt(1)=='h' && line.charAt(2)=='e' && line.charAt(3)=='r' &&
                  line.charAt(4)=='e')
          { token = where;
            line = line.substring(5).trim();
          }
          else if (c=='a' && line.length()>=3 && line.charAt(1)=='n' && line.charAt(2)=='d')
          { token = and;
            line = line.substring(3).trim();
          }
          else if (line.length()>1 && Character.isLetter(line.charAt(1)))
          { token = err;
          }
          else
          { token = id;
            idval = c;
            line = line.substring(1).trim();
          }
        }
        else
          token = err;
    }
  }
}

哦,很久以前,我做过秋千的东西。

在Test.main中,如果我替换

        System.out.println(myTree.toPostfix(myTree));
        TreeView view = new TreeView(myTree);
        test.add(view);
        view.repaint();

与:

        System.out.println (myTree.toPostfix(myTree));
        TreeView view = new TreeView(myTree);
        test.add (view);
        // view.repaint();
        test.invalidate ();

我得到了一个分裂图 - 可能是开始的一个步骤。

4+2*3-9*4+2*3-9;
4 2 3 * + 9 4 * - 2 3 * + 9 - 

但只有在全尺寸之后。调整大小会破坏图形并且程序会挂起。

2 项额外改进,一项是关键改进,一项是用户友好性:

    setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);

应添加到 CTor

public Test() {

在 setVisible 之前,因此您不必使用 Ctrl-C 停止程序。那是关键。

第二个在词法分析器中。如果您在 line.endsWith (";") 上修剪后测试输入,您可以自己默默地添加分号,而不是告诉用户,做什么以及如何做。 更好的是:在 BorderLayout (NORTH) 或 SOUTH 为公式添加一个 JTextField,以便用户可以更新它。出于测试目的,最好预先填充它。

更新

同时我玩得很开心,改进了它,这可能主要是品味和优先级的问题,但也许你感兴趣。一步很可能比上面的 invalidate-command 更好。

这里是需要的导入:

import javax.swing.*;
import java.awt.*;
import java.io.*;
import javax.swing.border.*;

这里是 TestTreeView(我有很多 类 调用 'Test'):

public class TestTreeView extends JFrame {

    JTextField jtf = new JTextField (30);
    JButton jb = new JButton ("go!");

    public TestTreeView () {
        setSize (900, 600); // a
        Container cp = getContentPane (); // b
        JPanel jp = new JPanel ();
        jp.setLayout (new BorderLayout());
        cp.add (jp);
        jp.setBorder (BorderFactory.createEtchedBorder ()); // c
        JPanel tp = new JPanel ();
        tp.setLayout (new FlowLayout ());
        tp.add (jtf);
        jtf.setText ("1+2*3/4%5");
        jtf.setFont ((jtf.getFont()).deriveFont (24.0f)); // I like big fonts, maybe need stronger glasses :) 
        tp.add (jb);
        jp.add (tp, BorderLayout.NORTH);
        actions (jp); // see below
        setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        setVisible (true);
    }

    public void actions (JPanel jp) {
        jb.addActionListener (ae -> {
            String s = jtf.getText ();
            System.out.println (s);
            if (s.length () > 1) {
                //create parse trees from input in JTextField
                Parser p = new Parser (s); // redefined, see below 
                ExpTree myTree;

                try {
                    myTree = p.parseLine ();
                    System.out.println (myTree.toPostfix(myTree));

                    TreeView view = new TreeView (myTree);
                    jp.add (view, BorderLayout.CENTER);
                    // view.repaint();
                    // jp.invalidate ();
                    jp.revalidate (); // c
                }
                catch (ParseException e) {
                    System.out.println ("Invalid Expression: Ensure it ends with a semicolon");
                }
            }
        });
    }

    public static void main (String[] args) {
        TestTreeView test = new TestTreeView ();
    }
}

备注:

  • a) 我需要更多 space,因此需要 900x600
  • b) 至少到 Java-1.5,你不应该添加到主框架,而是添加到 contentPane。也许它随着 1.6、1.7 或 1.8
  • 发生了变化
  • c) 重新验证是必经之路。直接在前面绘画,而不仅仅是在第一次调整大小之后。这应该适用,即使您不喜欢使用 JTextField 和按钮。

Parser 发现了一个新的 CTor,它需要一个字符串,我们从 JTextField 传递过来:

class Parser
{
    private Lexer lex;

    public Parser()
    {
        lex = new Lexer();
    }

    public Parser (String s)
    {
        lex = new Lexer (s);
    }

Lexer 找到了一个新的 CTor,它需要一个从解析器传递过来的字符串:

class Lexer
void init()
{
    do
        try
        {
            line = buf.readLine ().trim ();
            if (! line.endsWith (";"))
                line = String.format ("%s;", line);
        }
        catch(Exception e)
        {
            System.out.println("Error in input");
            System.exit (1);
        }
    while (line.length () == 0);
}

{
    // ...  
    Lexer ()
    {
        buf = new BufferedReader (new InputStreamReader(System.in));
    }

    Lexer (String s)
    {
        buf = new BufferedReader (new StringReader (s));
    }

//加上自动分号修复:

    void init()
    {
        do
            try
            {
                line = buf.readLine ().trim ();
                if (! line.endsWith (";"))
                    line = String.format ("%s;", line);
            }
            catch(Exception e)
            {
                System.out.println("Error in input");
                System.exit (1);
            }
        while (line.length () == 0);
    }

最后的名言。在网上,有很多教程,如何将 ActionListener 中的动作工作从主事件循环中分离出来。你应该考虑完成这样的 material。这里的代码不是最先进的。 :)