1. 程式人生 > >用Java2D畫出樹的結構(是不是感覺標題很熟悉)

用Java2D畫出樹的結構(是不是感覺標題很熟悉)

前言

感覺標題很熟悉的就對了,因為其實這是我碰到了一個作業要畫出樹,然後就百度了一下,參考了另一位學者kakashi8841(姑且就這麼叫吧)的文章和程式碼,才做完了作業。下面是連結:

本文的內容就是改進了原文的Bug,所以說大部分和原文很像。我也是第一次用這個部落格希望能和大家分享學習經驗(大佬可以無視這個文章的內容,因為很簡單),給遇到苦難的同學學者們一點小小的幫助(以後還指望你們幫我呢)。

效果預覽

一個語法樹的整體

一個語法樹的部分

程式碼部分

1. 樹的資料結構Tnode

package tree;

import MutableInteger.MutableInteger;

import java.util.LinkedList;
import java.util.List;

public
class Tnode { private String name; //該結點名字 private int layer = 0; //該結點層級 private int x = -1; //x座標 private List<Tnode> childs = null; //儲存該結點的孩子 public Tnode(String name) { this.name = name; } public Tnode() { this.name = null; } public
void add(Tnode n) { if (childs==null) childs = new LinkedList<Tnode>();//這裡可以改為ArrayList n.layer = this.layer + 1; setChildLayer(n); childs.add(n); } private void setChildLayer(Tnode n) {//遞迴設定層級,深度優先 if (n.hasChild()) { List<Tnode> c = n.getChilds(); for
(Tnode node : c) { node.layer = n.layer + 1; setChildLayer(node); } } } public void CoordinateProcess(MutableInteger maxX, MutableInteger maxY) { CoordinateProcess(this, maxX, maxY); } public static void CoordinateProcess(Tnode n, MutableInteger maxX, MutableInteger maxY) { //max其實是用來佈置畫布的大小而設定的返回值 //預設的根節點座標是(0,0),即x=0,layer=0 setx(n, new MutableInteger(0), maxX, maxY); } private static void setx(Tnode n, MutableInteger va, MutableInteger maxX, MutableInteger maxY) {//va其實只是用來儲存中間結果用來呼叫的 if (n.hasChild()) { List<Tnode> c = n.getChilds(); c.get(0).x = va.value; setx(c.get(0), va, maxX, maxY); for (int i=1; i<c.size(); i++) { setx(c.get(i), va, maxX, maxY); } n.x = c.get(0).x;//本結點的x是第一個孩子的x } else { n.x = va.value++; } //儲存最大的x,y返回 if (n.getX()>maxX.value) { maxX.value = n.getX(); } if (n.getLayer()>maxY.value) { maxY.value = n.getLayer(); } } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getLayer() { return layer; } public int getX() { return x; } public void setLayer(int layer) { this.layer = layer; } public List<Tnode> getChilds() { return childs; } public void setChilds(List<Tnode> childs) { this.childs = childs; } public boolean hasChild() { return childs==null ? false : true; } public void printAllNode(Tnode n) {//遞迴列印所有結點,深優 System.out.println(n.toString()); if (n.hasChild()) { List<Tnode> c = n.getChilds(); for (Tnode node : c) { printAllNode(node); } } } public void printAllNode() { printAllNode(this); } public String getAllNodeName(Tnode n) { String s = n.toString()+"/n"; if (n.hasChild()) { List<Tnode> c = n.getChilds(); for (Tnode node : c) { s += getAllNodeName(node)+"/n"; } } return s; } public String getAllNodeName() { return getAllNodeName(this); } public String toString() { return name; } }

一般的學生應該都挺熟悉這種資料結構,基本沿用原文的內容。相比原文我多了一個畫樹的關鍵步驟:CoordinateProcess。後來這個函式又跳到了setx。其實很好理解,在原來的基礎下我們可以計算出每個數結點的y座標,我只不過加了一個計算x座標的方法。看上去呼叫有點冗餘,這也是有原因的:

  • 內部方法public void CoordinateProcess(MutableInteger maxX, MutableInteger maxY) 是為了方便其他地方呼叫,但是使用時要邏輯清晰。根節點使用這個方法才有效,否則只會計算一部分的x依然無法得到正確的座標。正確寫法應該是root.CoordinateProcess(maxX, maxY);
  • 靜態方法public static void CoordinateProcess(Tnode n, MutableInteger maxX, MutableInteger maxY) 其實是遞迴的外殼,因為遞迴需要儲存中間引數進行計算但是外部卻不需要,所以這個外殼才是外部呼叫是用到的函式。正確寫法是CoordinateProcess(root, maxX, maxY);
  • 具體的實現是遞迴的靜態方法private static void setx(Tnode n, MutableInteger va, MutableInteger maxX, MutableInteger maxY)

說實話這個部分程式碼的核心就是計算結點的座標(x, y)。計算y其實就是計算結點的層數,這個很好理解,在原始碼中也有計算。計算x座標就稍微需要思考一下。因為存在畫出來的問題,所以要保證同一層的結點能夠放下,就是座標x不能重疊,否則會出現原文的類似錯誤。於是改進方法如下:

  1. 規定每一層從左往右開始畫,而不是像原文可以從中間開始
  2. 規定子樹的第一個結點(姑且稱之為長子)跟在父結點的下面一個,即x不變,y+1
  3. 父結點的下一個結點(姑且稱為他弟弟)實際上不能直接跟在哥哥的後面。這是因為如果弟弟存在子結點,各個也存在多個子結點,弟弟的子結點就會與各個的子結點位置衝突。所以弟弟的位置必須跟在哥哥所有子樹中最右邊一個的右邊(也就是x座標)。

具體的演算法其實是我自己想出來的,並沒有考慮演算法的效率

    private static void setx(Tnode n, MutableInteger va, MutableInteger maxX, MutableInteger maxY) {//va其實只是用來儲存中間結果用來呼叫的
        if (n.hasChild()) {
            List<Tnode> c = n.getChilds();
            c.get(0).x = va.value;//本結點沿用父結點座標,見方法第1條
            setx(c.get(0), va, maxX, maxY);//先對哥哥的子結點遞迴計算,見方法第3條
            for (int i=1; i<c.size(); i++) {//之後再對弟弟機器子結點遞迴計算,見方法第3條
                setx(c.get(i), va, maxX, maxY);
            }
            n.x = c.get(0).x;//本結點的x是其第一個孩子的x,是不是感覺和上面重複了,其實並不是。這一步其實很重要,因為計算後並不是每個孩子都和父親一樣,只有長子是和父親一樣的x,其他都不是,所以必須要這一步。
        } else {
            n.x = va.value++;
        }
        //儲存最大的x,y返回,這個只是為了確定畫板大小的附加功能
        if (n.getX()>maxX.value) {
            maxX.value = n.getX();
        }
        if (n.getLayer()>maxY.value) {
            maxY.value = n.getLayer();
        }
    }

2. MutableInteger(只是一個為了傳遞可變整數的工具)

package MutableInteger;

public class MutableInteger {//為了函式返回值而寫的類
    public int value;
    public MutableInteger(int x) { value = x; }
    public MutableInteger() { value = 0; }
    public boolean equals(int x) {
        if ( x==value )
            return true;
        else
            return false;
    }
    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; }
}

3. 實現把樹畫到畫板上的TreePanel

package tree;

import java.awt.*;
import java.util.List;

import javax.swing.JPanel;

public class TreePanel extends JPanel {

    private static final long serialVersionUID = 1L;

    private Tnode tree;             //儲存整棵樹
    private int gridWidth = 170;    //每個結點的寬度
    private int gridHeight = 20;    //每個結點的高度
    private int vGap = 50;          //每2個結點的垂直距離
    private int hGap = 30;          //每2個結點的水平距離

    private int startY = 10;        //根結點的Y,預設距離頂部10畫素
    private int startX = 10;        //根結點的X,預設距離左端10畫素

    //改進之後的程式呢就不是原文的對對齊方式啦,所以下面幾行是沒用的
    //private int childAlign;                     //孩子對齊方式
    //public static int CHILD_ALIGN_ABSOLUTE = 0; //相對Panel居中
    //public static int CHILD_ALIGN_RELATIVE = 1; //相對父結點居中

    private Font font = new Font("微軟雅黑",Font.BOLD,14);  //描述結點的字型

    private Color gridColor = Color.BLACK;      //結點背景顏色
    private Color linkLineColor = Color.BLACK;  //結點連線顏色
    private Color stringColor = Color.WHITE;    //結點描述文字的顏色
    /*放棄了原文的內容,這是由於我們只有一種畫法,而不是中間對其或是左對齊等
    public TreePanel() { this(null,CHILD_ALIGN_ABSOLUTE); }
    public TreePanel(Tnode n) { this(n,CHILD_ALIGN_ABSOLUTE); }
    public TreePanel(int childAlign) { this(null,childAlign); }
    public TreePanel(Tnode n, int childAlign) {
        super();
        setTree(n);
        this.childAlign = childAlign;
    }
    */
    public TreePanel() { this(null); }
    public TreePanel(Tnode n) {
        super();
        setTree(n);
    }
    public void setTree(Tnode n) { tree = n; }

    //重寫,呼叫自己的繪製方法
    public void paintComponent(Graphics g) {
        //startX = (getWidth()-gridWidth)/2;//這是居中方式的設定,放棄原文方法
        super.paintComponent(g);
        g.setFont(font);
        drawAllNode(tree, g);
    }

    /**
     * 遞迴繪製整棵樹
     * n 被繪製的Node
     * xPos 根節點的繪製X位置
     * g 繪圖上下文環境
     */
    public void drawAllNode(Tnode n, Graphics g) {
        /*
        int y = n.getLayer()*(vGap+gridHeight)+startY;
        int fontY = y + gridHeight - 5;     //5為測試得出的值,你可以通過FM計算更精確的,但會影響速度


        g.setColor(gridColor);
        g.fillRect(x, y, gridWidth, gridHeight);    //畫結點的格子

        g.setColor(stringColor);
        g.drawString(n.toString(), x, fontY);       //畫結點的名字

        if (n.hasChild()) {
            List<Tnode> c = n.getChilds();
            int size = n.getChilds().size();
            int tempPosx = childAlign == CHILD_ALIGN_RELATIVE
                         ? x+gridWidth/2 - (size*(gridWidth+hGap)-hGap)/2
                         : (getWidth() - size*(gridWidth+hGap)+hGap)/2;

            int i = 0;
            for (Tnode node : c) {
                int newX = tempPosx+(gridWidth+hGap)*i; //孩子結點起始X
                g.setColor(linkLineColor);
                g.drawLine(x+gridWidth/2, y+gridHeight, newX+gridWidth/2, y+gridHeight+vGap);   //畫連線結點的線
                drawAllNode(node, newX, g);
                i++;
            }
        }
        */
        //改進一個非遞迴演算法,用我們自己計算的座標畫樹,這樣結點就不會重疊啦
        int y = n.getLayer()*(vGap+gridHeight)+startY;
        int x = n.getX()*(hGap+gridWidth)+startX;
        int fontY = y + gridHeight - 5;     //5為測試得出的值,你可以通過FM計算更精確的,但會影響速度


        g.setColor(gridColor);
        g.fillRoundRect(x, y, gridWidth, gridHeight, 10, 10);    //畫結點的格子

        g.setColor(stringColor);
        g.drawString(n.toString(), x+5, fontY);       //畫結點的名字


        if (n.hasChild()) {
            g.setColor(linkLineColor);
            g.drawLine(x+gridWidth/2, y+gridHeight, x+gridWidth/2, y+gridHeight+vGap/2);

            List<Tnode> c = n.getChilds();
            int i = 0;
            for (Tnode node : c) {
                int newX = node.getX()*(hGap+gridWidth)+startX; //孩子結點起始X
                g.setColor(linkLineColor);
                g.drawLine(newX+gridWidth/2, y+gridHeight+vGap/2, newX+gridWidth/2, y+gridHeight+vGap);
                drawAllNode(node, g);
                i++;
                if (i==c.size()) {
                    g.setColor(linkLineColor);
                    g.drawLine(x+gridWidth/2, y+gridHeight+vGap/2, newX+gridWidth/2, y+gridHeight+vGap/2);
                }
            }
        }
    }

    public Color getGridColor() { return gridColor; }
    public void setGridColor(Color gridColor) { this.gridColor = gridColor; }
    public Color getLinkLineColor() { return linkLineColor; }
    public void setLinkLineColor(Color gridLinkLine) { this.linkLineColor = gridLinkLine; }
    public Color getStringColor() { return stringColor; }
    public void setStringColor(Color stringColor) { this.stringColor = stringColor; }
    public int getStartY() { return startY; }
    public void setStartY(int startY) { this.startY = startY; }
    public int getStartX() { return startX; }
    public void setStartX(int startX) { this.startX = startX; }
    public int getGridWidth() { return gridWidth; }
    public void setGridWidth(int gridWidth) { this.gridWidth = gridWidth; }
    public int getGridHight() { return gridHeight; }
    public void setGridHeight(int gridHeight) { this.gridHeight = gridHeight; }
    public int getVGap() { return vGap; }
    public void setVGap(int vGap) { this.vGap = vGap; }
    public int getHGap() { return hGap; }
    public void setHGap(int hGap) { this.hGap = hGap; }

}

如果沒有接觸過這個部分,的確有可能看不懂。稍微解釋一下,這個JPanel就是指畫板,我們就是在上面作畫。之前的一些常數只是設定結點的的長寬和間隙之類的,不是特別重要。最後的部分也只是一些普通方法的重寫,沒什麼重大意思。
其實在知道整個數的座標之後,畫樹的任務就非常簡單了,只要讀取座標然後畫出來就行了。注意每個結點之間相連線的線的畫法和遵循的數學規律。

4. 建立視窗測試Test

import java.awt.*;
import javax.swing.*;
import java.io.File;
import java.util.ArrayList;

import MutableInteger.MutableInteger;
import tree.*;

public class TestForCompile extends JFrame{

    public Tnode tree;

    private static final long serialVersionUID = 1L;

    public TestForCompile(Tnode t){
        super("Test Draw Tree");
        tree = t;
        MutableInteger maxx = new MutableInteger();
        MutableInteger maxy = new MutableInteger();
        Tnode.CoordinateProcess(t, maxx, maxy);//畫樹前必須對樹的座標處理
        initComponents(maxx, maxy);
    }
    public static void main(String[] args){
        File source = new File("input.txt");
        File target = new File("output.txt");
        //File target2 = new File("SyntacticTree.txt");
        ArrayList<ID> iDList = new ArrayList<ID>();
        new LexicalAnalysis(source,target,iDList).analysisBegin();
        SyntacticAnalysis sa = new SyntacticAnalysis(target,iDList);
        sa.analysisBegin();
        //以上是我作業的內容,當然和本章內容無關。也不能讓大家光抄程式碼,好學的大家只要修改一下就能畫出來啦
        if ( sa.correctnessFlag ) {
            //new TreePrinter(sa.SyntacticTree, target2).PrintBegin();
            TestForCompile frame = new TestForCompile(sa.SyntacticTree);//呼叫建構函式其實就完成了繪圖

            frame.setSize(800, 600);//設定視窗的大小,其實視窗這麼小放不下我們這麼大的樹,所以我們只要讓畫板可以有滾動條就能顯示全了(這個原文沒有哦)
            frame.setVisible(true);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }
    } 
    public void initComponents(MutableInteger maxX, MutableInteger maxY){
        TreePanel panel1 = new TreePanel(tree);
        /*
        TreePanel panel2 = new TreePanel(tree);
        panel2.setBackground(Color.BLACK);
        panel2.setGridColor(Color.WHITE);
        panel2.setLinkLineColor(Color.WHITE);
        panel2.setStringColor(Color.BLACK);
        */
        JPanel contentPane = new JPanel();
        contentPane.setLayout(new GridLayout());
        contentPane.add(panel1);//我們就畫一棵樹,原文畫了兩棵樹我都註釋掉了
        //contentPane.add(panel2); 


        JScrollPane scrollPane = new JScrollPane(
                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
        //上一步設定了滾動條是否可見,大家可以改成as_needed看看效果
        scrollPane.setViewportView(contentPane);//這一步是能看到畫面的必要一步
        //這一步開始設定了畫板的大小,這個大小是根據最大的x,y和各個結點的長寬和間距計算的,然後把這個畫板add到一個帶滾動條的畫板裡就能滾動著看啦
        contentPane.setPreferredSize(
                new Dimension(
                        (maxX.getValue())*(panel1.getGridWidth()+panel1.getHGap()) + panel1.getGridWidth() + panel1.getStartX()*2,
                        (maxY.getValue())*(panel1.getGridHight()+panel1.getVGap()) + panel1.getGridHight() + panel1.getStartY()*2));
        contentPane.revalidate();
        //horizontalScandocDRPane.add(scrollPane);
        //this.add(scrollPane);
        this.add(scrollPane,BorderLayout.CENTER);
    }
}

總結

想要畫一棵樹有兩個關鍵點,一個是計算數每一個結點x,y座標的演算法和一個畫出樹的方法呼叫。這兩個關鍵點分別在Tnode和TreePanel中大家自己看哦。
我幾乎把完整的程式碼都給了大家,但也不是全部,各位同學們自己想辦法稍微修改一下,就可以把自己的樹給畫出來了。

後記

大家在我給的Test中也看到了這其實是一個語法分析生成的語法樹,至於我會不會在寫一個怎麼生成語法樹,看看評論在看看心情吧。到時候我在完善一下可以在語法樹中搜索詞(當然是用樹的資料結構的方法,這個其實我都沒有寫)。