How do I redraw things when my JFrame is restored

2019-09-09 05:24发布

问题:

I have a very basic little JFrame with JToggleButtons and subclassed JPanels that know how to draw what I want them to draw. Selecting a button causes an oval to appear in the corresponding panel. Unselecting the buttons makes the drawings disappear.

Unfortunately, minimizing (iconifying) and then restoring (deiconifying) causes any drawn shapes to disappear. So I need to trigger redrawings manually. The problem is that I can only get the redrawing done (that is, I only see it) if I show a message box first.

Here's the deiconify event for the JFrame:

private void formWindowDeiconified(java.awt.event.WindowEvent evt)
{                                       
    //having this message makes everything work
    JOptionPane.showMessageDialog(null, "Useless message this is.");
    //but if I skip it, I'm SOL
    //what's going on?
    drawAll();
}

This method goes over all of my buttons and asks for the redraws when necessary:

public void drawAll()
{
    for (int i=0; i<channels; i++)
    {
        if (buttons[i].isSelected())
        {
            lightboxes[i].drawMe();            
        }
    }
}

and here is my subclassed JPanel:

class MyJPanel extends JPanel {

    public void drawMe()
    {
        Graphics myGraphics = this.getGraphics();
        myGraphics.fillOval(0, 0, this.getWidth(), this.getHeight());    
    }

    public void unDraw()
    {
        this.invalidate();
        this.repaint();
    }
}

回答1:

Firstly, for speed I would use double buffering. It's best to paint your graphics off screen and display them to the screen when the drawing has completed. The below should sort you out.

public class MyPanel extends JPanel {
    private BufferedImage buffer;
    private Graphics2D canvas;

    @Override
    public void paintComponent(Graphics g) {
        if(buffer == null) {
            buffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
            canvas = buffer.createGraphics();
        }
        canvas.fillOval(0, 0, this.getWidth(), this.getHeight());
        g.drawImage(buffer, 0, 0, this);
    }
}


回答2:

The window should automatically be repainted once it is restored by the RepaintManager. The problem is you are not performing custom painting like you should...

This is not how to do custom painting...

public void drawMe()
{
    Graphics myGraphics = this.getGraphics();
    myGraphics.fillOval(0, 0, this.getWidth(), this.getHeight());    
}

getGraphics can return null and is, at best, a snapshot of the graphics state.

Painting in Swing can occur at any time for many different reasons, most of which you don't have control over (nor should you care).

Your job is simply to respond to these repaint requests and update your components state.

Swing has a detailed paint chain which is called automatically and which you can use.

You should be overriding paintComponent and performing all painting within this method

Take a look at Performing Custom Painting and Painting in AWT and Swing for more details



回答3:

I'm just providing this answer so people can see what I ended up doing. The major lesson pointed out by everyone was to use the component's paintComponent. See the comments for issues that you might be experiencing yourself.

Edit: Updated to reflect comments from MadProgrammer.

//with help from Slihp and MadProgrammer on StackOverflow
//http://stackoverflow.com/q/17331986/1736461
class MyJPanel extends JPanel {

    private boolean buttonSelected = false;

    @Override
    public void paintComponent(Graphics g) {
        //make sure the background gets painted (wacky results otherwise)        
        super.paintComponent(g);

        //now if the corresponding toggle button is on, plop on the circle
        if (buttonSelected)
        {
            g.fillOval(0, 0, this.getWidth(), this.getHeight());
        }        
    }

    //an action listener for the button calls this
    public void setButtonSelected(boolean buttonStateIn)
    {
        buttonSelected = buttonStateIn;    
    }
}

I subclassed the buttons too, so I can get its "ID" off of it from the event handler:

class MyJToggleButton extends JToggleButton
{       
    private int whoAmI;

    public MyJToggleButton(int whoAmIn)
    {
        //the string given to the constructor becomes the button's label
        //("1", "2", "3", etc..)
        super(Integer.toString(whoAmIn + 1));
        whoAmI = whoAmIn;      
    }

    public int getWho()
    {
        return whoAmI;
    }
}

The JFrame code that makes the buttons:

private void makeButtons(int howMany)
    {
        buttons = new MyJToggleButton[howMany];
        for (int i=0; i<howMany; i++)
        {
            buttons[i] = new MyJToggleButton(i);
            buttons[i].addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent evt) {   
                    //find out which button this is
                    MyJToggleButton button = (MyJToggleButton) evt.getSource();
                    int which = button.getWho();
                    //send the button state to the corresponding lightbox
                    lightboxes[which].setButtonSelected(button.isSelected());
                    //trigger its redrawing
                    lightboxes[which].invalidate();
                    lightboxes[which].repaint();
                }
            });
            this.add(buttons[i]);
        }
    }

And that's the only manual redrawing I have to do - resizing and reshowing and all those other fun things eventually hit up the paintComponent, and it just has to know if its button is pushed to know what to do. Super clean and just what I wanted.