简体   繁体   中英

Create a clickable area around round JLabel image

I am trying to create a JLabel with a oval shaped image, like this.

在此处输入图片说明

My code is the following:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public final class RoundedButtonDemo {
    private static Image bi;

    public static void main(String[] args) {
        try {
            loadImage();

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    createAndShowGUI();
                }
            });
        } catch (IOException e) {
            // handle exception
        }
    }

    private static void loadImage() throws IOException {
        int newWidth = 80;
        int newHeight = 40;
        bi = ImageIO.read(RoundedButtonDemo.class.getResource("/resources/login.png"));
        bi = bi.getScaledInstance(newWidth, newHeight, Image.SCALE_DEFAULT);

    }

    private static void createAndShowGUI() {
        final JFrame frame = new JFrame();
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JLabel label = new JLabel();
        label.setSize(new Dimension(5, 5));
        label.setIcon(new ImageIcon(bi));
        label.setText("Hello World");
        label.setHorizontalTextPosition(JLabel.CENTER);
        // label.setBorder(BorderFactory.createLineBorder(Color.BLACK));

        label.addMouseListener(new MouseListener() {

            private int count = 0;

            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getSource() == label) {
                    if (count % 2 == 0) {
                        label.setText("Bye");
                    } else {
                        label.setText("Hello World");
                    }

                    count++;
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mouseExited(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mousePressed(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mouseReleased(MouseEvent e) {
                // TODO Auto-generated method stub

            }
        });

        frame.add(label);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

This code creates a JLabel containing the above image. The text of the JLabel should alternate every time the button is clicked, based on a MouseListener added to the JLabel .

The problem I'm facing is that even when I click outside the image (also outside the JLabel ), the MouseListener is triggered, and the text alternates.

The big picture of what I want to achieve is : A rounded button which responds to a MouseListener , whenever it is clicked anywhere on its surface.

I tried your code. You are getting this behavior because your JLabel is actually filling the entire frame. You need to set a layout for your frame; something like this:

// ...
frame.setSize(400, 400);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());             // <-- you need this

final JLabel label = new JLabel();
label.setPreferredSize(new Dimension(80, 40)); // <-- also this
label.setIcon(new ImageIcon(bi));
label.setText("Hello World");
// ...

FlowLayout is one of the simplest layout managers, of which there are many. You can read about them here: https://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html

Hope this helps.

PS: you had a good idea trying to debug this problem, adding a border like this:

// label.setBorder(BorderFactory.createLineBorder(Color.BLACK));

Not sure why you commented it out. Maybe you didn't notice that there was a black border around the whole frame area. Try setting to color to Color.RED or something more noticeable.

As yoshi pointed out, the JLabel fills the entire content pane, so clicking anywhere in the content pane results in mouse events for the label.

Some words:

1) You could also use GridBagLayout , instead of FlowLayout , in case you want the JLabel centered in its parent component (both vertically and horizontally). This solution is here .

Like so:

final JPanel singleCenteredComponentJPanel = new JPanel(new GridBagLayout());
singleCenteredComponentJPanel.add(label);
frame.add(singleCenteredComponentJPanel);

2) If, furthermore, you want to ignore mouse clicks on any transparent pixels of the label icon (if any) you can do the following:

Use the BufferedImage.getRGB(int x, int y) , BufferedImage.getColorModel() and ColorModel.getAlpha(int pixel) methods to determine the alpha value of the clicked pixel for each mouse event. If the alpha value is equal to 0, then the pixel is completely transparent, otherwise the alpha value is between 1 and 255 (both inclusive) which in turn means the pixel is not completely transparent. Example code follows below.

And instead of Image.getScaledInstance(...) to scale the image (which returns Image , but we need BufferedImage ), use the solution provided here .

Like so:

private static void loadImage() throws IOException {
    int newWidth = 80;
    int newHeight = 40;
    bi = ImageIO.read(RoundedButtonDemo.class.getResource("/resources/login.png"));
    bi = getScaledBufferedImage(bi, newWidth, newHeight);

}

public static BufferedImage getScaledBufferedImage(final Image img,
                                                   final int newWidth,
                                                   final int newHeight) {
    final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
    final GraphicsDevice gdev = genv.getDefaultScreenDevice();
    final GraphicsConfiguration gcnf = gdev.getDefaultConfiguration();
    final BufferedImage simg = gcnf.createCompatibleImage(newWidth, newHeight, Transparency.TRANSLUCENT);

    //Painting input image to output image:
    final Graphics2D g2d = simg.createGraphics();
    g2d.drawImage(img, 0, 0, newWidth, newHeight, null); //@Docs "...is scaled if necessary.".
    g2d.dispose();

    return simg;
}  

Also change the reference bi from Image to BufferedImage .

And then the MouseListener of the label:

private int count = 0;

@Override
public void mouseClicked(MouseEvent e) {
    if (e.getSource() == label) {

        //Get the mouse click position (in pixels),
        //relative to the top-left corner of label:
        final Point relativeClickPoint = e.getPoint();

        //Obtain alpha value from the TYPE_INT_ARGB pixel:
        final int pixel = bi.getRGB(relativeClickPoint.x, relativeClickPoint.y);
        final int alpha = bi.getColorModel().getAlpha(pixel);

        if (alpha > 0) { //Check if the pixel is not transparent.
            if (count % 2 == 0) {
                label.setText("Bye");
            } else {
                label.setText("Hello World");
            }
            count++;
        }
    }
}

Be aware for the above implementation: All transparent pixels of the image will be ignored (including transparent pixels "inside" the shape, if any). Which means the user could click in the center of the image and nothing would happen if the clicked pixel was completely transparent. So I made the assumption that this is not the case with your image here.

3) For the scaling of the Image , you can determine the new size with the method collapseInside(...) :

/**
 * @param resultDim Output dimension (same aspect ratio as the original dimension, and inside containerDim).
 * @param originalDim Original dimension.
 * @param containerDim Dimension with the maximum width and maximum height.
 */
public static void collapseInside(final Dimension resultDim,
                                  final Dimension originalDim,
                                  final Dimension containerDim) {
    resultDim.setSize(originalDim);
    if (resultDim.width > containerDim.width) {
        //Adjusting height for max width:
        resultDim.height = ( resultDim.height * containerDim.width ) / resultDim.width;
        resultDim.width = containerDim.width;
    }
    if (resultDim.height > containerDim.height) {
        //Adjusting width for max height:
        resultDim.width = ( resultDim.width * containerDim.height ) / resultDim.height;
        resultDim.height = containerDim.height;
    }
}

With this method:
a) Scaled Image 's width will be less than or equal to container's size width ( resultDim.width <= containerDim.width ).
b) Same for height ( resultDim.height <= containerDim.height ).
c) Aspect ratio of the original Image size will be preserved in the new Image size (resultDim.width / resultDim.height == originalDim.width / originalDim.height).
With Image.getScaledInstance(...) and the previous getScaledBufferedImage(...) the aspect ratio of the Image could change.
So I edit the getScaledBufferedImage(...) to use collapseInside(...) :

public static BufferedImage getScaledBufferedImage(final Image img,
                                                   final int newWidth,
                                                   final int newHeight) {
    final Dimension originalDim = new Dimension(img.getWidth(null), img.getHeight(null)),
                    containerDim = new Dimension(newWidth, newHeight),
                    resultDim = new Dimension();
    collapseInside(resultDim, originalDim, containerDim);

    final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
    final GraphicsDevice gdev = genv.getDefaultScreenDevice();
    final GraphicsConfiguration gcnf = gdev.getDefaultConfiguration();
    final BufferedImage simg = gcnf.createCompatibleImage(resultDim.width, resultDim.height, Transparency.TRANSLUCENT);

    //Painting input image to output image:
    final Graphics2D g2d = simg.createGraphics();
    g2d.drawImage(img, 0, 0, resultDim.width, resultDim.height, null); //@Docs "...is scaled if necessary.".
    g2d.dispose();

    return simg;
}

Complete code for all three above:

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Point;
import java.awt.Transparency;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public final class RoundedButtonDemo {
    private static BufferedImage bi;

    public static void main(String[] args) {
        try {
            loadImage();

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    createAndShowGUI();
                }
            });
        } catch (IOException e) {
            // handle exception
        }
    }

    private static void loadImage() throws IOException {
        int newWidth = 80;
        int newHeight = 40;
        bi = ImageIO.read(RoundedButtonDemo.class.getResource("/resources/login.png"));
        bi = getScaledBufferedImage(bi, newWidth, newHeight);

    }

    public static BufferedImage getScaledBufferedImage(final Image img,
                                                       final int newWidth,
                                                       final int newHeight) {
        final Dimension originalDim = new Dimension(img.getWidth(null), img.getHeight(null)),
                        containerDim = new Dimension(newWidth, newHeight),
                        resultDim = new Dimension();
        collapseInside(resultDim, originalDim, containerDim);

        final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
        final GraphicsDevice gdev = genv.getDefaultScreenDevice();
        final GraphicsConfiguration gcnf = gdev.getDefaultConfiguration();
        final BufferedImage simg = gcnf.createCompatibleImage(resultDim.width, resultDim.height, Transparency.TRANSLUCENT);

        //Painting input image to output image:
        final Graphics2D g2d = simg.createGraphics();
        g2d.drawImage(img, 0, 0, resultDim.width, resultDim.height, null); //@Docs "...is scaled if necessary.".
        g2d.dispose();

        return simg;
    }

    /**
     * @param resultDim Output dimension (same aspect ratio as the original dimension, and inside containerDim).
     * @param originalDim Original dimension.
     * @param containerDim Dimension with the maximum width and maximum height.
     */
    public static void collapseInside(final Dimension resultDim,
                                      final Dimension originalDim,
                                      final Dimension containerDim) {
        resultDim.setSize(originalDim);
        if (resultDim.width > containerDim.width) {
            //Adjusting height for max width:
            resultDim.height = ( resultDim.height * containerDim.width ) / resultDim.width;
            resultDim.width = containerDim.width;
        }
        if (resultDim.height > containerDim.height) {
            //Adjusting width for max height:
            resultDim.width = ( resultDim.width * containerDim.height ) / resultDim.height;
            resultDim.height = containerDim.height;
        }
    }

    private static void createAndShowGUI() {
        final JFrame frame = new JFrame();
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JLabel label = new JLabel();
        label.setSize(new Dimension(5, 5));
        label.setIcon(new ImageIcon(bi));
        label.setText("Hello World");
        label.setHorizontalTextPosition(JLabel.CENTER);
        // label.setBorder(BorderFactory.createLineBorder(Color.BLACK));

        label.addMouseListener(new MouseListener() {

            private int count = 0;

            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getSource() == label) {

                    //Get the mouse click position (in pixels),
                    //relative to the top-left corner of label:
                    final Point relativeClickPoint = e.getPoint();

                    //Obtain alpha value from the TYPE_INT_ARGB pixel:
                    final int pixel = bi.getRGB(relativeClickPoint.x, relativeClickPoint.y);
                    final int alpha = bi.getColorModel().getAlpha(pixel);

                    if (alpha > 0) { //Check if the pixel is not transparent.
                        if (count % 2 == 0) {
                            label.setText("Bye");
                        } else {
                            label.setText("Hello World");
                        }
                        count++;
                    }
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mouseExited(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mousePressed(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mouseReleased(MouseEvent e) {
                // TODO Auto-generated method stub

            }
        });

        final JPanel singleCenteredComponentJPanel = new JPanel(new GridBagLayout());
        singleCenteredComponentJPanel.add(label);
        frame.add(singleCenteredComponentJPanel);

        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Alternatively, you may also override the paintComponent(...) of JPanel and use Graphics.drawImage(...) to paint the Image bi to the JPanel like so:

@Override
protected void paintComponent(final Graphics g) {
    super.paintComponent(g);

    //Drawing the image:
    g.drawImage(img, 0, 0, null);

    //Drawing the text:
    //For centering the text, I used code from:
    //https://stackoverflow.com/questions/27706197/how-can-i-center-graphics-drawstring-in-java
    final FontMetrics metrics = g.getFontMetrics(g.getFont());
    final int x = (img.getWidth(null) - metrics.stringWidth(text)) / 2;
    final int y = (img.getHeight(null) - metrics.getHeight()) / 2 + metrics.getAscent();
    g.drawString(text, x, y);
}

Where img is bi and text is the text of the button (ie "Hello World" and "Bye").
Then add the MouseListener to the JPanel as is.

Complete code for this (as " RoundedButtonDemo1 " class, in the same package):

import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Point;
import java.awt.Transparency;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public final class RoundedButtonDemo1 {
    private static BufferedImage bi;

    public static void main(String[] args) {
        try {
            loadImage();

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    createAndShowGUI();
                }
            });
        } catch (IOException e) {
            // handle exception
        }
    }

    private static void loadImage() throws IOException {
        int newWidth = 80;
        int newHeight = 40;
        bi = ImageIO.read(RoundedButtonDemo1.class.getResource("/resources/login.png"));
        bi = getScaledBufferedImage(bi, newWidth, newHeight);

    }

    public static BufferedImage getScaledBufferedImage(final Image img,
                                                       final int newWidth,
                                                       final int newHeight) {
        final Dimension originalDim = new Dimension(img.getWidth(null), img.getHeight(null)),
                        containerDim = new Dimension(newWidth, newHeight),
                        resultDim = new Dimension();
        collapseInside(resultDim, originalDim, containerDim);

        final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
        final GraphicsDevice gdev = genv.getDefaultScreenDevice();
        final GraphicsConfiguration gcnf = gdev.getDefaultConfiguration();
        final BufferedImage simg = gcnf.createCompatibleImage(resultDim.width, resultDim.height, Transparency.TRANSLUCENT);

        //Painting input image to output image:
        final Graphics2D g2d = simg.createGraphics();
        g2d.drawImage(img, 0, 0, resultDim.width, resultDim.height, null); //@Docs "...is scaled if necessary.".
        g2d.dispose();

        return simg;
    }

    /**
     * @param resultDim Output dimension (same aspect ratio as the original dimension, and inside containerDim).
     * @param originalDim Original dimension.
     * @param containerDim Dimension with the maximum width and maximum height.
     */
    public static void collapseInside(final Dimension resultDim,
                                      final Dimension originalDim,
                                      final Dimension containerDim) {
        resultDim.setSize(originalDim);
        if (resultDim.width > containerDim.width) {
            //Adjusting height for max width:
            resultDim.height = ( resultDim.height * containerDim.width ) / resultDim.width;
            resultDim.width = containerDim.width;
        }
        if (resultDim.height > containerDim.height) {
            //Adjusting width for max height:
            resultDim.width = ( resultDim.width * containerDim.height ) / resultDim.height;
            resultDim.height = containerDim.height;
        }
    }

    private static class CustomButton extends JPanel {
        private Image img;
        private String text;

        public void setImage(final Image img) {
            this.img = img;
            final Dimension imgDim = new Dimension(img.getWidth(null), img.getHeight(null));
            setMinimumSize(imgDim);
            setPreferredSize(imgDim);
            repaint();
        }

        public Image getImage() {
            return img;
        }

        public void setText(final String text) {
            this.text = text;
            repaint();
        }

        public String getText() {
            return text;
        }

        @Override
        protected void paintComponent(final Graphics g) {
            super.paintComponent(g);

            //Drawing the image:
            g.drawImage(img, 0, 0, null);

            //Drawing the text:
            //For centering the text, I used code from:
            //https://stackoverflow.com/questions/27706197/how-can-i-center-graphics-drawstring-in-java
            final FontMetrics metrics = g.getFontMetrics(g.getFont());
            final int x = (img.getWidth(null) - metrics.stringWidth(text)) / 2;
            final int y = (img.getHeight(null) - metrics.getHeight()) / 2 + metrics.getAscent();
            g.drawString(text, x, y);
        }
    }

    private static void createAndShowGUI() {
        final JFrame frame = new JFrame();
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final CustomButton button = new CustomButton();
        button.setSize(new Dimension(5, 5));
        button.setImage(bi);
        button.setText("Hello World");
        //button.setHorizontalTextPosition(SwingConstants.CENTER);
        //button.setOpaque(false);
        //button.setBorder(BorderFactory.createLineBorder(Color.BLACK));

        button.addMouseListener(new MouseListener() {

            private int count = 0;

            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getSource() == button) {

                    //Get the mouse click position (in pixels),
                    //relative to the top-left corner of label:
                    final Point relativeClickPoint = e.getPoint();

                    //Obtain alpha value from the TYPE_INT_ARGB pixel:
                    final int pixel = bi.getRGB(relativeClickPoint.x, relativeClickPoint.y);
                    final int alpha = bi.getColorModel().getAlpha(pixel);

                    if (alpha > 0) { //Check if the pixel is not transparent.
                        if (count % 2 == 0) {
                            button.setText("Bye");
                        } else {
                            button.setText("Hello World");
                        }
                        count++;
                    }
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mouseExited(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mousePressed(MouseEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void mouseReleased(MouseEvent e) {
                // TODO Auto-generated method stub

            }
        });


        final JPanel singleCenteredComponentJPanel = new JPanel(new GridBagLayout());
        singleCenteredComponentJPanel.add(button);
        frame.add(singleCenteredComponentJPanel);

        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Alternatively, if you want normal buttons, istead of labels, it seems here you can reshape buttons as well !

Other than that, I have also tried the following:
1) Using JLayeredPane s. There is a tutorial explaining them, and I guessed they could probably have a method to obtain the order of the visible pane for a given Point , or something like that, but they don't.
2) Using Container.getComponentAt(Point) and Container.findComponentAt(Point) in the MouseListener , but (as I found out after testing) these methods don't "see through" non-opaque (+transparent) pixels.
3) Searching for reshaping or translucency in JPanel s (inspired by How to Create Translucent and Shaped Windows ), but nothing found.
4) How to Decorate Components with the JLayer Class with first line saying:

... enables you to draw on components and respond to component events without modifying the underlying component directly.

seems promising, but I'm done here.

That's it for me for now. Goodbye.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM