1

Event detection on opaque pixels in JButton

Using the code example found in my question above, I have created several buttons with irregular edges that interlock, and am using a null layout in order to position the buttons properly. The issue I am encountering is that, although the mouse clicks are not being detected on transparent pixels in the bufferedimage, the button is still taking the shape of a rectangle. This means that buttons that are added to the panel later block portions of buttons they are adjacent to.

My question is: is there a way to force the mouse event to propagate down the entire physical arrangement of JButtons until it comes to one with opaque pixels, or is another solution required? I've looked at solutions involving Shape, but they seem very expensive, which is why I'm wondering about another way.

I'm not too attached to using JButtons, if the solution requires me to leave them, but I would like to find an inexpensive solution, if one exists.

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;


public class JButtonExample {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                final JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                MyButton button1 = null, button2 = null;

                try {
                    button1 = new MyButton(ImageIO.read(new URL("https://dl.dropbox.com/s/dxbao8q0xeuzhgz/button1.png")));
                } catch (Exception ex) {
                    ex.printStackTrace();
                }

                button1.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent me) {
                        super.mouseClicked(me);
                        MyButton mb = ((MyButton) me.getSource());
                        if (!isAlpha(mb.getIconImage(), me.getX(), me.getY()))
                            JOptionPane.showMessageDialog(frame, "You clicked button 1");
                    }

                    private boolean isAlpha(BufferedImage bufImg, int posX, int posY) {
                        int alpha = (bufImg.getRGB(posX, posY) >> 24) & 0xFF;
                        return alpha == 0 ? true : false;
                    }
                });

                button1.setBounds(10, 10, 72, 77);

                try {
                    button2 = new MyButton(ImageIO.read(new URL("https://dl.dropbox.com/s/v16kyha0ojx1gza/button2.png")));
                } catch (Exception ex) {
                    ex.printStackTrace();
                }

                button2.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent me) {
                        super.mouseClicked(me);
                        MyButton mb = ((MyButton) me.getSource());
                        if (!isAlpha(mb.getIconImage(), me.getX(), me.getY()))
                            JOptionPane.showMessageDialog(frame, "You clicked button 2");
                    }

                    private boolean isAlpha(BufferedImage bufImg, int posX, int posY) {
                        int alpha = (bufImg.getRGB(posX, posY) >> 24) & 0xFF;
                        return alpha == 0 ? true : false;
                    }
                });

                button2.setBounds(65, 0, 122, 69);

                frame.getContentPane().setLayout(null);

                frame.add(button1);
                frame.add(button2);

                frame.setSize(210, 130);
                frame.setVisible(true);
            }
        });
    }
}

class MyButton extends JButton {

    BufferedImage icon;

    MyButton(BufferedImage bi) {
        this.icon = ((BufferedImage) bi);
        setContentAreaFilled(false);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(icon.getWidth(), icon.getHeight());
    }

    public BufferedImage getIconImage() {
        return icon;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(icon, 0, 0, null);
        g.dispose();
    }
}
Community
  • 1
  • 1
user18
  • 119
  • 7
  • 3
    *"I've looked at solutions involving Shape, but they seem very expensive,"* That is what your profiler is telling you, or just a random guess? I suggest you get it working using `Shape` and try it out. – Andrew Thompson Jan 07 '13 at 05:37
  • 1
    I've used Shape for just this purpose in a jigsaw puzzle. It performs reasonably well even on older computers. My solution only checks the shapes whose bounding rectangles contain the point. – Jon Hulka Jan 07 '13 at 06:58
  • 1
    Try relacing the JButton with JLabel – MadProgrammer Jan 07 '13 at 08:11
  • @AndrewThompson I was basing that statement on comments I've seen on this site and others. Upon further research, it seems that it might be less problematic than I thought, although I am limited to older machines. – user18 Jan 07 '13 at 14:26
  • @MadProgrammer I tried using JLabel before posting the question in the first place, but it caused an identical problem. – user18 Jan 07 '13 at 14:29
  • You might start looking into using multiple `Shape` instances using [this source](http://stackoverflow.com/a/13796268/418556). – Andrew Thompson Jan 07 '13 at 16:27

1 Answers1

0

Alright, I've worked out a solution that does what I want it to do with enough accuracy for my usage case. While some of the particulars might be a little hackish, I think I've got it doing what I want now. Code to create Area is adapted from here.

For simplicity and ease of use in other areas of my larger program, I preserved each button as an independent JButton. When a mouse click is detected on any button, it calculates the position of the click as it appears on the parent panel, then passes that position as a Point to the parent. The parent then runs through the array of buttons until it finds a button that contains the point, and fires the appropriate method. If the click is not within the area contained by any button, there is no effect. If the original mouse click does not occur on the square area bounding a JButton, no processing occurs.

Although the calculations required to produce an Area are relatively expensive compared to the rest of the program, I can deal with this by creating all the Area objects on startup, since it is a very rare case that the program will be started and every area will not be used.

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;


public class Example {

    private static int[][] pos = {{10, 10, 72, 77}, {65, 0, 122, 69}};

    public static MyButton[] buttons;

    private static URL[] src = new URL[2]; 

    private static MapPanel pane;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                final JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                try{
                    src[0] = new URL("https://dl.dropbox.com/s/dxbao8q0xeuzhgz/button1.png");
                    src[1] = new URL("https://dl.dropbox.com/s/v16kyha0ojx1gza/button2.png");
                } catch (Exception e){
                    e.printStackTrace();
                    System.exit(0);
                }

                pane = new MapPanel();
                pane.setLayout(null);

                buttons = new MyButton[2];

                for(int i = 0 ; i < buttons.length ; i++){
                    final int j = i;
                    try{
                        buttons[j] = new MyButton((ImageIO.read(src[j])), j, pos[j][0], pos [j][1]);
                    } catch (Exception e){
                        e.printStackTrace();
                        System.exit(0);
                    }
                    buttons[j].addMouseListener(new MouseAdapter(){
                        @Override
                        public void mouseClicked(MouseEvent me){
                            Point p = new Point(me.getX() + buttons[j].getX(), me.getY() + buttons[j].getY());
                            pane.check(p);
                        }
                    });
                    buttons[j].setBounds(pos[j][0], pos[j][1], pos[j][2], pos[j][3]);
                    pane.add(buttons[j]);
                }

                frame.setContentPane(pane);

                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setSize(210, 130);
                frame.setVisible(true);
            }
        });
    }
}

class MapPanel extends JLabel{

    public MapPanel(){
        super();
        this.setOpaque(true);
    }

    public void check(Point p){
    for(int i = 0 ; i < Example.buttons.length ; i++){
    if(Example.buttons[i].contains(p)){
            Example.buttons[i].clickDetected();
            break;
        }
        }
    }
}

class MyButton extends JButton {

    private BufferedImage icon;
    private int x, y, index;
    private Area area;

    MyButton(BufferedImage bi, int index, int x, int y) {
        this.icon = ((BufferedImage) bi);
        this.x = x;
        this.y = y;
        this.index = index;
        setContentAreaFilled(false);
        createArea();
    }

    @Override
    public Dimension getPreferredSize() {
        if(icon != null){
            return new Dimension(icon.getWidth(), icon.getHeight());
        } else {
            return super.getPreferredSize();
            }
    }

    public BufferedImage getIconImage() {
        return icon;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(icon, 0, 0, null);
        g.dispose();
    }

    private void createArea(){      
        GeneralPath gp = new GeneralPath();
        boolean cont = false;

        for(int xx = 0 ; xx < icon.getWidth() ; xx++){
            for(int yy = 0 ; yy < icon.getHeight() ; yy++){
                if(getAlpha(xx, yy) != 0){
                    if(cont){
                        gp.lineTo(xx, yy);
                        gp.lineTo(xx, yy+1);
                        gp.lineTo(xx+1, yy+1);
                        gp.lineTo(xx+1, yy);
                        gp.lineTo(xx, yy);
                    } else{
                        gp.moveTo(xx, yy);
                    }
                    cont = true;
                } else {
                    cont = false;
                }
            }
            cont = false;
        }

        gp.closePath();

        area = new Area(gp);        
    }

    @Override
    public boolean contains(Point p){
        if(area.contains(new Point((int)(p.getX() - this.x), (int) (p.getY() - this.y)))){
            return true;
        }
        return false;
    }

    private int getAlpha(int posx, int posy){
        return(icon.getRGB(posx, posy) >> 24) & 0x000000FF;
    }

    public void clickDetected(){
        JOptionPane.showMessageDialog(null, "You clicked button " + Integer.toString(this.index + 1) + ".");
    }
}

I would also greatly appreciate if anyone could point out anything in this code (or outside of it) that is an issue. I assume that how the GeneralPath creates the area is by creating a 1px square on top of every pixel that meets the criteria (in this case, non-transparent pixels), and then creating an area object from the area contained by the path. Please correct me if I am wrong.

Community
  • 1
  • 1
user18
  • 119
  • 7