Search code examples
javaswingjpanelrepaintjslider

JSlider changeListener won't update - Java


I have a fractal tree generator and I am trying to make a slider control the number of iterations, but I can not get it to work. Also, the layout gets messed up whenever the repaint() method is called. Any thoughts on how to fix this?

public class FractalTree extends JPanel implements ChangeListener {

    static JSlider slider = new JSlider(0,12);

    static  int slideVal=7;
    public FractalTree()
    {
        super();
        slider.addChangeListener(this);
    }

    public void paint(Graphics g)
    {
        g.setColor(Color.green);
        drawTree(g, 400, 750, 200, Math.toRadians(-90), Math.toRadians(45), slideVal); //Don't let # of iterations exceed 12, it is useless
    }

    private void drawTree(Graphics g, int x1, int y1, double l, double t, double dt, double iterations) {
        if (iterations > 0) {
            int x2 = x1 + (int) (l * Math.cos(t));
            int y2 = y1 + (int) (l * Math.sin(t));
            g.drawLine(x1, y1, x2, y2);
            drawTree(g, x2, y2, l / 1.5, t + dt, Math.PI / 4, iterations - .5);
            drawTree(g, x2, y2, l / 1.5, t - dt, Math.PI / 4, iterations - .5);
        }
    }

    @Override
    public void stateChanged(ChangeEvent e) {
        slideVal=slider.getValue();
        repaint();
    }

    public static void main(String[] args) {
        JFrame t = new JFrame("Some swaggy fractal shit");
        FractalTree tree = new FractalTree();
        slider.setValue(slideVal);
        slider.setMinorTickSpacing(1);
        slider.setPaintTicks(true);
        slider.setPaintLabels(true);

        tree.add(slider);
        t.add(tree);
        t.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        t.setResizable(false);
        t.setLocationByPlatform(true);
        t.setSize(800, 800);
        t.setBackground(Color.black);
        t.setVisible(true);
    }
}

Solution

  • Two main problems:

    1. You're overriding paint instead of paintComponent.
    2. You're not calling super.paintComponent(g) (or in your case, super.paint(g)) as the first thing in your overridden method.

    This is what you need to have:

    @Override
    public void paintComponent(Graphics g) {
    
        super.paintComponent(g);
        g.setColor(Color.green);
        drawTree(g, 400, 750, 200, Math.toRadians(-90), Math.toRadians(45), slideVal);
    }
    

    Other things to consider:

    • Add the slider to the frame on position BorderLayout.PAGE_START instead of to the panel. If you add it to the panel, you risk drawing where the slider is.
    • Set the background color on the panel and not on the frame.
    • No need to call super() in the constructor, it's automatic.
    • setResizable(false) on the frame should generally be avoided. No need to restrict the user's space.
    • Call pack() on the frame instead of setSize(...). The latter is too dependent on the local graphics configuration.
      • You will need to override the panel's getPreferredSize method to return the correct size for the drawing.
    • Your pixel calculation should be adjusted to that the tree is aligned to the upper left corner of the panel, and not start it from an arbitrary location on the bottom, which causes you to lose a lot of screen real estate:

    enter image description here

    Response to comments

    Why paintComponent should be used?

    See these:

    public void paint(Graphics g)

    This method actually delegates the work of painting to three protected methods: paintComponent, paintBorder, and paintChildren. They're called in the order listed to ensure that children appear on top of component itself. [...] A subclass that just wants to specialize the UI (look and feel) delegate's paint method should just override paintComponent.

    You saw that if you override paint and added the slider to the panel, you got issues with the slider painting because you ignored paintChildren.

    What calling the superclass constructor does?

    Best to answer is the JLS:

    JLS 8.8.7. Constructor Body

    If a constructor body does not begin with an explicit constructor invocation and the constructor being declared is not part of the primordial class Object, then the constructor body implicitly begins with a superclass constructor invocation "super();", an invocation of the constructor of its direct superclass that takes no arguments.

    So calling super() does nothing.