views:

119

answers:

2

Hi,

I'm making a visualization for a BST implementation (I posted another question about it the other day). I've created a GUI which displays the viewing area and buttons. I've added code to the BST implementation to recursively traverse the tree, the function takes in coordinates along with the Graphics object which are initially passed in by the main GUI class. My idea was that I'd just have this function re-draw the tree after every update (add, delete, etc...), drawing a rectangle over everything first to "refresh" the viewing area. This also means I could alter the BST implementation (i.e by adding a balance operation) and it wouldn't affect the visualization.

The issue I'm having is that the draw function only works the first time it is called, after that it doesn't display anything. I guess I don't fully understand how the Graphics object works since it doesn't behave the way I'd expect it to when getting passed/called from different functions. I know the getGraphics function has something to do with it.

Relevant code:

    private void draw(){
    Graphics g = vPanel.getGraphics();      
    tree.drawTree(g,ORIGIN,ORIGIN);
}

vPanel is what I'm drawing on

private void drawTree(Graphics g, BinaryNode<AnyType> n, int x, int y){
    if( n != null ){
        drawTree(g, n.left, x-10,y+10 );
            if(n.selected){
                g.setColor(Color.blue);
            }
            else{
                g.setColor(Color.gray);
            }        
            g.fillOval(x,y,20,20);
            g.setColor(Color.black);
            g.drawString(n.element.toString(),x,y);
        drawTree(g,n.right, x+10,y+10);
    }
}

It is passed the root node when it is called by the public function. Do I have to have:

Graphics g = vPanel.getGraphics();

...within the drawTree function? This doesn't make sense!!

Thanks for your help.

+4  A: 

This is not the right way of doing it. If you want a component that displays the tree, you should make your own JComponent and override the paintComponent-method.

Whenever the model (the tree / current node etc) changes, you invoke redraw() which will trigger paintComponent.

I actually don't think you are allowed to fetch the Graphics object from anywhere else than the argument of the paintComponent method.

Try out the following program

import java.awt.Graphics;

public class FrameTest {

    public static void main(String[] args) {
        final JFrame f = new JFrame("Frame Test");
        f.setContentPane(new MyTreeComponent());
        f.setSize(400, 400);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);

        new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    f.repaint();
                }
            }
        }.start();
    }
}

class MyTreeComponent extends JComponent {
    public void paintComponent(Graphics g) {
        // Draw your tree. (Using random here to visualize the updates.)
        g.drawLine(30, 30, 50, 30 + new Random().nextInt(20));
        g.drawLine(30, 30, 50, 30 - new Random().nextInt(20));
    }
}

The best starting point is probably http://java.sun.com/docs/books/tutorial/uiswing/painting/index.html

aioobe
So you're saying I should extend the JComponent class and put my drawing code in the paintComponent method which can then be called with redraw?Edit: didn't see your last edit.
primehunter326
Yes. That's exactly the way to go :) It's along with the model-view-controller way of thinking: The tree and the current selection etc constitutes the model, and the component is the view. Preferably you listen for changes in the model (clicks in the tree etc) and then call `repaint()`. (**Don't** call `paintComponent` yourself! You wouldn't be able to hand it a proper `Graphics`-object.)
aioobe
I should have clarified that I'm not doing updates in real time or interacting with the tree directly with the mouse, I'm just using buttons to add, delete etc... from the tree. I just need a way to keep the image matched up with the actual tree structure. It seems like I could still do this through the method you describe, I'm just not familiar doing it this way.
primehunter326
I see. This is still the way to go though. At the same place where you change the data that the visualization depends on and varies with, you should invoke `repaint`.
aioobe
Yes I think I sort of understand now, the Graphics object can't be passed as an argument into any function with the exception of paintComponent. It seems like I need to move the drawTree function out of the BST implementation (probably wasn't a good idea in the first place) in order to make it work.My original idea had been to modify the BST so I could pull out individual nodes, allowing me to do the traversal in the GUI but this would have required more modification of the BST implementation.
primehunter326
Yes. You seem to have understood the basic concept :-) Though you may very well pass around the Graphics object as you wish, but you must keep in mind that the drawing can never be initialized by you (you wouldn't know what graphics-object to use for drawing!) The drawing can only be initiated by the Swing system, however, you may *ask* the swing system to initiate the drawing (or calling paintComponent with a proper graphics object) by calling repaint.
aioobe
So basically the key is that if in order to have my own custom drawing function like I wrote above I have to define it in paintComponent and call it with repaint in order for it to work the way I want it to.
primehunter326
Yes. Now you got it. Except, I would phrase it like "I have to tell swing to call paintComponent (by calling repaint)" :)
aioobe
+1 @aioobe: I understand it's an example, but shouldn't `repaint()` be invoked on the EDT, say via `EventQueue.invokeLater()`?
trashgod
+2  A: 

@aioobe's approach is sound and the example is compelling. In addition to the cited tutorial, Performing Custom Painting, I would add that drawing should take place on the Event Dispatch Thread (EDT). In the variation below, note how the GUI is built using EventQueue.invokeLater. Similarly, the actionPerformed() method of javax.swing.Timer invokes repaint() on the EDT to display recently added nodes. A more elaborate example may be found here.

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.*;

public class StarPanel extends JPanel implements ActionListener {

    private static final Random rnd = new Random();
    private final Timer t = new Timer(100, this);
    private final List<Node> nodes = new ArrayList<Node>();

    private static class Node {
        private Point p;
        private Color c;

        public Node(Point p, Color c) {
            this.p = p;
            this.c = c;
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame f = new JFrame("Star Topology");
                f.add(new StarPanel());
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.pack();
                f.setVisible(true);
            }
        });
    }

    public StarPanel() {
        this.setPreferredSize(new Dimension(400, 400));
        t.start();
    }

    @Override // Respond to the Timer
    public void actionPerformed(ActionEvent e) {
        int w = this.getWidth();
        int h = this.getHeight();
        nodes.add(new Node(
            new Point(rnd.nextInt(w), rnd.nextInt(h)),
            new Color(rnd.nextInt())));
        this.repaint();
    }

    @Override
    public void paintComponent(Graphics g) {
        int w2 = this.getWidth() / 2;
        int h2 = this.getHeight() / 2;
        for (Node n : nodes) {
            g.setColor(n.c);
            int x = n.p.x;
            int y = n.p.y;
            g.drawLine(w2, h2, x, y);
            g.drawLine(w2, h2, x, y);
            g.drawRect(x - 2, y - 2, 4, 4);
        }
    }
}
trashgod
Do I still need to use a timer and/or RNG if my visualization is just responding to button presses? Otherwise that makes sense.
primehunter326
No timer needed unless you do animations (but then you're better off with a SwingWorker). The timer simply serves as an example in this case.
aioobe
@primehunter326: @aioobe's comment is correct. Like the `actionPerformed()` method of `javax.swing.Timer`, the `process() method of `SwingWorker` runs on the EDT.
trashgod