3

This question is based on a problem I had a while back with a simple Swing dice program. The original question I posted is here and has an accepted answer, but I'd like to know exactly what is happening, why the problem occurs, and why the solution works.

I managed to whittle down the original code to get to the core of the problem and it now looks very different:

  • I have two ColorPanels that each draw a coloured square
  • when you click on a panel the box should change colour in this order: start at black, then >red>green>blue>red>green>blue> etc
  • once a box has changed colour it should never be black again

However when I just call repaint() in the MouseListener, the program behaves very strangely:

  • I click on one panel and the square's colour changes
  • I then click on the other and it's square changes colour, but the first square also changes, back to black
  • you can see this behaviour in the gif below:

buggy program

If you use getParent().repaint() instead this behaviour goes away and the program behaves as expected:

enter image description here

  • The problem only seems to occur if the panels/squares start off 'overlapping'.
  • If you use a layout that stops this or don't set the size small then the problem does not seem to occur.
  • the problem doesn't happen every time which initially made me think that concurrency problems might be involved.
  • The code that I had problems with in my original question did not seem to cause problems for everybody and so my IDE, jdk etc might be relevant as well: Windows 7, Eclipse Kepler, jdk1.7.0_03

The code minus imports etc is as follows:

public class ColorPanelsWindow extends JFrame{

    static class ColorPanel extends JPanel {

        //color starts off black
        //once it is changed should never be 
        //black again
        private Color color = Color.BLACK;

        ColorPanel(){
            //add listener
            addMouseListener(new MouseAdapter(){
                @Override
                public void mousePressed(MouseEvent arg0) {
                    color = rotateColor();
                    repaint();
                    //using getParent().repaint() instead of repaint() solves the problem
                    //getParent().repaint();
                }
            });
        }
        //rotates the color black/blue > red > green > blue
        private Color rotateColor(){
            if (color==Color.BLACK || color == Color.BLUE)
                return Color.RED;
            if (color==Color.RED)
                return Color.GREEN;
            else return Color.BLUE;
        }

        @Override
        public void paintComponent(Graphics g){
            g.setColor(color);
            g.fillRect(0, 0, 100, 100);
        }
    }

    ColorPanelsWindow(){
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        setLayout(new GridLayout(1,0));
        add(new ColorPanel());
        add(new ColorPanel());
        //the size must be set so that the window is too small
        // and the two ColorPanels are overlapping
        setSize(40, 40);
//      setSize(300, 200);

        setVisible(true);
    }

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

            @Override
            public void run() {
                new ColorPanelsWindow();
            }

        });
    }
}

So my question is, what on earth is going on here?

Community
  • 1
  • 1
mallardz
  • 1,070
  • 11
  • 21

2 Answers2

4

but I'd like to know exactly what is happening,

I see the same problems using JDK7u60 on Windows 7. Definitely seems like a bug to me.

My best guess is that it is a problem with the double buffering.

I added so debug code to the paintComponent() method.

1) When you click on the right component only its paintComponent() method is called and the panel is painted the proper color.

2) When you click on the left component only its paintComponent() method is called and the panel is painted the proper color, however the panel on the right reverts back to the black color, without invoking the paintComonent() method on the right panel. This leads me to believe that somehow an old buffer is being used (this would be the bug and I have no idea how to fix it).

The reason that getParent().repaint() works is because this forces both components to be repainted no matter which panel you click on.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • Well this makes sense. In my original code I had some random events and dice and things and had thought it might be a concurrency issue, but a buffer bug explains it much better. I'll wait a day or two to see if anyone can come up with the exact workings, otherwise I'll mark this as accepted. – mallardz Jun 15 '14 at 08:10
3

I'm not sure the cause of your problem, since I cannot reproduce the misbehavior with your code, but your paintComponent override neglects to call the super's paintComponent method. Put that in and see what happens.

    @Override  // method should be protected, not public
    protected void paintComponent(Graphics g) { 

        // ******* add
        super.paintComponent(g);

        g.setColor(color);
        g.fillRect(0, 0, 100, 100);
    }

Note that if this were my program, and this were the only painting behavior I desired, I'd simplify my program my not overriding paintComponent but rather simply calling setBackground(color); I'd also create a Color array or List and iterate through it:

public static final Color[] COLORS = {Color.RED, Color.Blue, Color.Green};
private int colorIndex = 0;

// .....

@Override
public void mousePressed(MouseEvent mEvt) {
   colorIndex++;
   colorIndex %= COLORS.length;
   color = COLORS[colorIndex];
   setBackground(color);
}

I would also override the getPreferredSize method.


e.g.,

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.*;

@SuppressWarnings("serial")
public class MyColorsPanelDemo extends JPanel {
   private static final int GAP = 20;

   public MyColorsPanelDemo() {
      setLayout(new GridLayout(1, 0, GAP, GAP));
      setBorder(BorderFactory.createEmptyBorder(GAP, GAP, GAP, GAP));
      add(new ColorPanel());
      add(new ColorPanel());
   }

   private static class ColorPanel extends JPanel {
      public static final Color[] COLORS = {Color.red, Color.green, Color.blue};
      private static final int PREF_W = 100;
      private static final int PREF_H = PREF_W;
      private static final Color INIT_BACKGROUND = Color.black;
      private int colorIndex = 0;

      public ColorPanel() {
         setBackground(INIT_BACKGROUND);
         addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent mEvt) {
               setBackground(COLORS[colorIndex]);
               colorIndex++;
               colorIndex %= COLORS.length;
            }
         });
      }

      @Override
      public Dimension getPreferredSize() {
         return new Dimension(PREF_W, PREF_H);
      }
   }

   private static void createAndShowGui() {
      MyColorsPanelDemo mainPanel = new MyColorsPanelDemo();

      JFrame frame = new JFrame("MyColorsPanelDemo");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • +1 for proper painting techniques (overriding `getPreferredSize()` and invoking `super.paintComponent()`). – camickr Jun 14 '14 at 20:21
  • Thanks for trying it out and the tips. This code isn't anything I've used but is just the smallest code I've got that makes the bug appear in an obvious way. Using sensible sizes and `getPreferredSize() ` and flow layouts etc will stop the bug from appearing. `super.paintComponent(g)` doesn't change anything unfortunately. – mallardz Jun 15 '14 at 08:05