用Java2D画出树的结构(是不是感觉标题很熟悉)

前言

感觉标题很熟悉的就对了,因为其实这是我碰到了一个作业要画出树,然后就百度了一下,参考了另一位学者kakashi8841(姑且就这么叫吧)的文章和代码,才做完了作业。
原文链接: 用Java2D画出树的结构v0.1.0

本文的内容就是改进了原文的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中也看到了这其实是一个语法分析生成的语法树,至于我会不会在写一个怎么生成语法树,看看评论而定吧。到时候我在完善一下可以在语法树中搜索词(当然是用树的数据结构的方法,这个其实我都没有写)。

跑道通常被定义为一条长方形的区域,可以通过Java 2D绘图工具来绘制出来。下面是一个简单的实现示例: 首先,你需要创建一个扩展了JPanel类的跑道类,用于在屏幕上绘制跑道。在这个类中,你需要重写paintComponent方法以实现绘制跑道的逻辑。 在paintComponent方法中,你可以使用Graphics2D对象来进行绘图操作。你可以通过在画布上定义矩形的位置和大小来绘制跑道。例如,你可以使用以下代码来绘制一条长为100像素、宽为20像素的跑道: ```java @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g.create(); g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, getWidth(), getHeight()); // 清空画布,设置背景颜色为白色 g2d.setColor(Color.BLACK); g2d.fillRect(0, getHeight() / 2 - 10, getWidth(), 20); // 绘制跑道 g2d.dispose(); } ``` 你可以在自定义的JFrame类中添加这个跑道面板,以在窗口上显示跑道。你可以使用以下代码将跑道面板添加到窗口中: ```java public class MainFrame extends JFrame { public MainFrame() { super("跑道"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(800, 600); setLocationRelativeTo(null); TrackPanel trackPanel = new TrackPanel(); add(trackPanel); setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(MainFrame::new); } } ``` 最后,你可以在主类的main方法中创建一个MainFrame实例,并运行程序。这样,你就可以在窗口上看到被绘制的跑道了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值