在圆形 JLabel 图像周围创建一个可点击区域

Create a clickable area around round JLabel image

我正在尝试创建一个带有椭圆形图像的 JLabel,如下所示。

我的代码如下:

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);
    }
}

此代码创建一个包含上述图像的 JLabel。每次单击按钮时,JLabel 的文本应根据添加到 JLabelMouseListener 交替显示。

我面临的问题是,即使我在图像外部(也在 JLabel 外部)单击,也会触发 MouseListener,并且文本会交替出现。

我想要实现的目标是: 一个圆形按钮,只要在其表面的任何地方单击它就会响应 MouseListener

我试过你的代码。你得到这种行为是因为你的 JLabel 实际上填满了整个框架。您需要为框架设置布局;像这样:

// ...
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 是最简单的布局管理器之一,其中有很多。您可以在这里阅读它们:https://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html

希望这对您有所帮助。

PS:你想调试这个问题是个好主意,添加这样的边框:

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

不确定您为什么将其注释掉。也许你没有注意到整个框架区域周围有一个黑色边框。尝试将颜色设置为 Color.RED 或更明显的颜色。

由于 pointed out, the JLabel 填满了整个内容窗格,因此单击内容窗格中的任意位置会导致标签发生鼠标事件。

一些话:

1) 您也可以使用 GridBagLayout, instead of FlowLayout, in case you want the JLabel centered in its parent component (both vertically and horizontally). This solution is here

像这样:

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

2) 此外,如果您想忽略鼠标点击标签图标的任何透明像素(如果有),您可以执行以下操作:

使用 BufferedImage.getRGB(int x, int y), BufferedImage.getColorModel() and ColorModel.getAlpha(int pixel) 方法确定每个鼠标事件的单击像素的 alpha 值。如果 alpha 值等于 0,则像素是完全透明的,否则 alpha 值介于 1 和 255(包括两者)之间,这反过来意味着像素不是完全透明的。示例代码如下。

而不是 Image.getScaledInstance(...) to scale the image (which returns Image, but we need BufferedImage), use the solution provided here

像这样:

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;
}  

同时将引用 biImage to BufferedImage 更改。

然后是标签的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++;
        }
    }
}

请注意 上述实现:图像的所有透明像素都将被忽略(包括透明像素 "inside" 形状,如果有的话)。这意味着用户可以点击图像的中心,如果点击的像素是完全透明的,则什么也不会发生。所以我假设你这里的图片不是这种情况。

3) 对于 Image 的缩放,您可以使用方法 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;
    }
}

用这个方法:
a) 缩放后 Image 的宽度将小于或等于容器的尺寸宽度 (resultDim.width <= containerDim.width)。
b) 高度相同 (resultDim.height <= containerDim.height).
c) 原始 Image 尺寸的宽高比将保留在新的 Image 尺寸中 (resultDim.width / resultDim.height == originalDim.width / originalDim.height ).
使用 Image.getScaledInstance(...) 和之前的 getScaledBufferedImage(...)Image 的纵横比可能会改变。
所以我编辑 getScaledBufferedImage(...) 以使用 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;
}

以上所有三个的完整代码:

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);
    }
}

或者,您也可以覆盖 JPanelpaintComponent(...) 并使用 Graphics.drawImage(...)Image bi 绘制到 JPanel 像这样:

@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:
    //
    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);
}

其中 imgbitext 是按钮的文本(即 "Hello World" 和 "Bye")。
然后按原样将 MouseListener 添加到 JPanel

完整代码(如“RoundedButtonDemo1”class,在同一个包中):

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:
            //
            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);
    }
}

或者,如果您想要普通按钮而不是标签,看来 here 您也可以重塑按钮 !

除此之外,我还尝试了以下方法:
1) 使用 JLayeredPanes. 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 或类似的东西,但他们没有。
2) 在 MouseListener 中使用 Container.getComponentAt(Point)Container.findComponentAt(Point),但是(正如我在测试后发现的) 这些方法不会 "see through" 非不透明(+透明)像素。
3) 在 JPanels (inspired by How to Create Translucent and Shaped Windows) 中搜索重塑或半透明,但没有找到。
4) How to Decorate Components with the JLayer Class 第一行说:

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

看起来很有希望,但我已经完成了。

我现在就到此为止。再见。