1

I'm developing a program for work (notice: I can not share complete code, as it is in large part protected work product, but I will share everything I can). In the application, I have JPanels that have background images applied to them. Some of these panels also have mouse listeners attached & my management wants there to be a visual clue that the panel can be clicked on to initiate an action. To that end, I've overlaid a transparent JPanel on top of the JPanel with the background image, and attached a MouseListener to that, keying off the mouseEntered/Exited events. When the mouse enters the image panel, the overlaid panel will switch from transparent to translucent, and back when the mouse exits.

Under Linux, this works perfectly. Under Windows... it's a good thing I'm bald so I can't tear my hair out. What appears to be happening is that some kind of image caching is occuring as I move the mouse around, and the mouseEnter event is causing whatever the mouse was over a second ago to be painted into the frame; i.e. if I place the mouse over a nearby button on the GUI & then mouse over the panel, I'll see the button appear in the panel, with the surrounding GUI.

The Image Panel is contained within a JInternalFrame that is opaque.

One other thing to note, if I do something in the application that would cause the image to change (e.g. making a new selection from a JComboBox), the image repaints as expected. Whatever the trouble is, it seems related to the highlighting and how the image is being repainted/redrawn.

What am I not doing for Windows that I should be?

Thanks in advance.

Here is a link to some images. Start.png is what it looks like in both OS's when the panel opens. GoodMouseEnter.png is what the mouseEnter event looks like under Linux. BadMouseEnter.png & badMouseExit.png are what it looks like under Windows.

This is the Class I am using to create the image panel (EDIT: This example is self contained):

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class ImageTest {
JFrame frm = null;
ImagePanel imgP = null;

public static void main(String[] args) throws MalformedURLException 
{
    ImageTest it = new ImageTest();
    it.initialize();
}

public void initialize() throws MalformedURLException
{
    String path = "file:/C:/CylinderTank.png";
    URL imagePath = new URL(path);
    System.out.println(imagePath.toString());
    frm = new JFrame();
    imgP = new ImagePanel(true);
    int fW = 500;
    int fH = 700;
    int pW = 450;
    int pH = 650;

    frm.setLayout(null);
    frm.setPreferredSize(new Dimension(fW,fH));
    frm.setSize(fW,fH);

    imgP.getFilterPanel().addMouseListener(new PanelListener());
    imgP.useCustomSizing(pW, pH);
    imgP.setImageURL(imagePath);
    imgP.setBounds(0, 0, pW, pH);

    frm.add(imgP);

    frm.pack();
    frm.setVisible(true);
}

private class PanelListener implements MouseListener {

    public PanelListener() {        }
    @Override
    public void mouseClicked(MouseEvent e) {        }
    @Override
    public void mousePressed(MouseEvent e) {        }
    @Override
    public void mouseReleased(MouseEvent e) {        }
    @Override
    public void mouseEntered(MouseEvent e) 
    {
        imgP.highlightImage(true);
        imgP.repaint();
    }
    @Override
    public void mouseExited(MouseEvent e) 
    {
        imgP.highlightImage(false);
        imgP.repaint();
    }
}    
}



@SuppressWarnings("serial")
class ImagePanel extends JPanel implements ImageObserver {
private final JPanel filterPanel;
private BufferedImage image;
private Dimension panelSize;
private final Toolkit kit;
private boolean highlight = false;
private URL imagePath;
private int imgW, imgH;
private final Color blueFilter = new Color(0, 0, 255, 38);
private final Color redFilter = new Color(255, 0, 0, 38);
private final Color greenFilter = new Color(0, 255, 0, 38);
private final Color clear = new Color(0, 0, 0, 0);
private final Color bgColor = new Color(116, 169, 255, 255);
private boolean customSize = false;

public ImagePanel(boolean opaque)
{
    super();
    this.kit = Toolkit.getDefaultToolkit();
    setLayout(null);
    setOpaque(opaque);
    setBackground(bgColor);
    filterPanel = new JPanel();
    filterPanel.setBackground(clear);
}

public ImagePanel(URL imagePath, boolean opaque)
{
    super();
    this.imagePath = imagePath;
    this.kit = Toolkit.getDefaultToolkit();
    setLayout(null);
    setOpaque(opaque);
    setBackground(bgColor);
    filterPanel = new JPanel();
    filterPanel.setBackground(clear);

    readImage();

}

@Override
protected void paintComponent(Graphics g)
{
    Graphics2D g2D = (Graphics2D) g;

    if (highlight)
        filterPanel.setBackground(blueFilter);
    else
        filterPanel.setBackground(clear);

    int X = 0, Y = 0;
    if (image != null)
    {
        image.flush();
        kit.prepareImage(image, -1, -1, this);

        if (customSize)
        {
            X = (panelSize.width - imgW) / 2;
            Y = (panelSize.height - imgH) / 2;
        }

        if (isOpaque())
            g2D.drawImage(image, X, Y, bgColor, this);
        else
            g2D.drawImage(image, X, Y, this);
    }
    else
        super.paintComponent(g2D);
}

public void highlightImage(boolean highlight)
{
    this.highlight = highlight;

}

private void readImage()
{
    try
    {
        image = ImageIO.read(imagePath);
        imgW = image.getWidth();
        imgH = image.getHeight();

        if (customSize)
            panelSize = getPreferredSize();
        else
            panelSize = new Dimension(imgW, imgH);

        setPreferredSize(panelSize);
        setMinimumSize(panelSize);
        setMaximumSize(panelSize);

        int X = (panelSize.width - imgW) / 2;
        int Y = (panelSize.height - imgH) / 2;
        filterPanel.setBounds(X, Y, imgW, imgH);

        add(filterPanel);
    }
    catch (IOException ex)  
    {
        Logger.getLogger(ImagePanel.class.getName()).log(Level.SEVERE, null, ex);
    }
}


public void setImageURL(URL img)
{
    this.imagePath = img;
    readImage();
}

public Dimension getDisplayedImageSize()
{
    if (image == null)
        return null;

    return new Dimension(imgW, imgH);
}

public JPanel getFilterPanel()
{
    return filterPanel;
}

public void useCustomSizing(int W, int H)
{
    if (W < 0)
        W = getPreferredSize().width;
    if (H < 0)
        H = getPreferredSize().height;

    if ((W>0) || (H>0))
        customSize = true;

    Dimension cDim = new Dimension(W,H);
    setPreferredSize(cDim);
    setMinimumSize(cDim);
    setMaximumSize(cDim);

    repaint();
}
}//end class ImagePanel
sngillis
  • 13
  • 1
  • 5
  • 3
    It "looks" like a layout issue, but I can't be sure, consider providing a [runnable example](https://stackoverflow.com/help/mcve) which demonstrates your problem. This will result in less confusion and better responses. Also calling `invalidate`, `revalidate`, `validate` on a component with no layout manager is kind of pointless – MadProgrammer Oct 28 '14 at 21:19
  • @MadProgrammer True, but when you have no clue why it's not behaving, you start trying things... I'll get up a test class in a few. – sngillis Oct 28 '14 at 22:10
  • @MadProgrammer That should be better – sngillis Oct 28 '14 at 22:38
  • I cant post an answer for you, but what I will say is that Swing and drawing are maddening concepts. If you dont understand 100 percent of Swing, then drawing anything will rarely work how you want it to. For example, calling paint, or repaint doesn't necessarily do anything; its more a suggestion to the underlying mechanics to paint, and it can choose not to if it thinks it shouldnt have to. Another thing, I dont remember the specifics, but any Swing UI should be run in its own special thread. If you look up "event dispatch thread java" that will probably tell you better than I ever could – searchengine27 Oct 28 '14 at 22:45
  • @searchengine27 Threading I understand, and all GUI events are happening on the EDT (this example code is obviously but a small piece of the overall application & not representative of the threading I am doing). That said, if you have an answer more specific than "That's Swing for ya!", I'm all ears. Specifically, I have a check in my code that makes it easy to know if I'm running under Windows or Linux, so I can setup two different paint/drawImage processes if I need to. – sngillis Oct 28 '14 at 23:00
  • 1
    Avoid using `null` layouts, pixel perfect layouts are an illusion within modern ui design. There are too many factors which affect the individual size of components, none of which you can control. Swing was designed to work with layout managers at the core, discarding these will lead to no end of issues and problems that you will spend more and more time trying to rectify. Have a look at [Why is it frowned upon to use a null layout in SWING?](http://stackoverflow.com/questions/6592468/why-is-it-frowned-upon-to-use-a-null-layout-in-swing) for more details – MadProgrammer Oct 28 '14 at 23:10
  • @MadProgrammer Thank you! Coloring the rectangle did the trick. – sngillis Oct 29 '14 at 17:17

1 Answers1

2

There are a cascade of problems presenting themselves...

  1. The over use of null layouts, I'll get back to that...
  2. The use of a "overlay" panel
  3. Modification of a component from within the context of the a paint method
  4. Reliance of "magic" numbers instead of known values
  5. Breaking the paint chain...

All these things are conspiring against you...

The first thing you need to know is Swing only knows how to paint opaque or transparent components and only when those components are flagged as been opaque or not. If you use a color with an alpha value, Swing doesn't know that needs to paint under the component as well.

The second thing you need to know is that the Graphics context is a shared resource. That is, every component been updated during a paint cycle get the SAME Graphics context.

The MAIN problem is, your filterPanel is using a alpha color, but Swing doesn't know that it should be painting under it, so it simply "fills" the available area with the color you have chosen, but because it's a alpha color, it doesn't completely clean the Graphics context, so you will end up with paint artifacts been left behind...

The fact is, you don't need the filterPane or, more importantly, don't need to use it the way you are. You should NEVER update the state of any component from within a paint method, this will cause the component to request a repaint, every time, which will quickly consume your CPU cycles till you system won't run...

You could achieve a simular resulting using something like...

@Override
protected void paintComponent(Graphics g) {
    Graphics2D g2D = (Graphics2D) g;
    super.paintComponent(g2D);

    int X = 0, Y = 0;
    if (image != null) {

        if (customSize) {
            X = (panelSize.width - imgW) / 2;
            Y = (panelSize.height - imgH) / 2;
        }

        g2D.drawImage(image, X, Y, this);
    }


    if (highlight) {
        g2D.setColor(blueFilter);
        g2D.fillRect(X, Y, image.getWidth(), image.getHeight());
    }

}

Take a look at Painting in AWT and Swing, Performing Custom Painting and 2D Graphics for more details.

The reliance on pre-calculated values could also cause you issues. The size of a component should be determined by using it's getWidth and getHeight properties as the values could have changed between paint cycles...

I would also encourage you to use ImageIO over Toolkit, it will...

  1. Return a full realised image instead of off loading the loading to a background thread
  2. Raise an exception if it can't read the file for some reason, rather the silently failing...

See Reading/Loading an Image for more details

This is a basic example of how I might approach the problem...

Mouse in the house

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class MouseOverTest {

    public static void main(String[] args) {
        new MouseOverTest();
    }

    public MouseOverTest() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                try {
                    BufferedImage background = ImageIO.read(new File("C:\\hold\\thumbnails\\_MTCGAC__Pulling_Cords_by_Dispozition.png"));

                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(new TestPane(background));
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException exp) {
                    exp.printStackTrace();
                }
            }
        });
    }

    public static class TestPane extends JPanel {

        protected static final Color BLUE_FILTER = new Color(0, 0, 255, 38);

        private BufferedImage background;
        private Rectangle imageBounds;
        private boolean mouseInTheHouse;

        public TestPane(BufferedImage background) {
            this.background = background;
            MouseAdapter ma = new MouseAdapter() {
                @Override
                public void mouseMoved(MouseEvent e) {
                    mouseInTheHouse = getImageBounds().contains(e.getPoint());
                    repaint();
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    mouseInTheHouse = false;
                    repaint();
                }

            };
            addMouseMotionListener(ma);
            addMouseListener(ma);
        }

        @Override
        public Dimension getPreferredSize() {
            return background == null ? new Dimension(200, 200) : new Dimension(background.getWidth(), background.getHeight());
        }

        @Override
        public void invalidate() {
            imageBounds = null;
            super.invalidate();
        }

        protected Rectangle getImageBounds() {

            if (imageBounds == null) {

                if (background != null) {

                    int x = (getWidth() - background.getWidth()) / 2;
                    int y = (getHeight() - background.getHeight()) / 2;
                    imageBounds = new Rectangle(x, y, background.getWidth(), background.getHeight());

                } else {

                    imageBounds = new Rectangle(0, 0, 0, 0);

                }

            }

            return imageBounds;

        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            Rectangle bounds = getImageBounds();
            if (background != null) {
                g2d.drawImage(background, bounds.x, bounds.y, this);
            }
            if (mouseInTheHouse) {
                g2d.setColor(BLUE_FILTER);
                g2d.fill(bounds);
            }
            g2d.dispose();
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366