从包含透明像素的图像创建自定义JButton

7

阅读编辑2以了解我实际需要的内容

我目前正在尝试使用在Photoshop中创建的带有alpha参数的图像创建一些自定义JButtons。

到目前为止,通过覆盖paint()方法来绘制图像已经起作用,因为按钮显示了正确的图像。然而,我想通过使其形状(可点击区域)与图像上可见像素相同来改进它(现在如果绘制按钮的边框,它是一个正方形)。

有没有简单的方法可以做到这一点,或者我必须解析图像并找到透明像素以制作自定义边框?

我需要重写哪些方法才能使它按照我想要的方式工作?

另外,我以后还会有另一个问题:是否最好使用某种算法来更改图像的颜色,以使人们在单击它时看起来像是被单击了,还是我最好创建第二个图像,并在按钮处于活动状态时绘制该图像?

编辑:我刚刚在其他问题上读到,我应该重新定义paintComponent()而不是paint(),我想知道为什么,因为重新定义paint()也可以正常工作?

编辑2:我改变了一切,以确保我的JButtons使用带有图标的默认构造函数创建。我想要做的是获取注册单击的X和Y位置,并抓取该位置处的图标像素,并检查其alpha通道是否为0(如果是,则不执行任何操作,否则执行它应该执行的操作)。

问题在于,alpha通道总是返回255(透明像素上的蓝色、红色和绿色为238)。在其他像素上,一切都返回应该返回的值。

这里是一个重现我的问题的示例(如果您愿意,请尝试使用另一张图像):

public class TestAlphaPixels extends JFrame
{
  private final File FILECLOSEBUTTON = new File("img\\boutonrondX.png");  //My round button with transparent corners
  private JButton closeButton = new JButton(); //Creating it empty to be able to place it and resize the image after the button size is known


  public TestAlphaPixels() throws IOException
  {
    setLayout(null);
    setSize(150, 150);

    closeButton.setSize(100, 100);
    closeButton.setContentAreaFilled(false);
    closeButton.setBorderPainted(false);

    add(closeButton);

    closeButton.addMouseListener(new MouseListener()
      {

        public void mouseClicked(MouseEvent e)
        {
        }

        public void mousePressed(MouseEvent e)
        {
        }

        public void mouseReleased(MouseEvent e)
        {
          System.out.println("Alpha value of pixel (" + e.getX() + ", " + e.getY() + ") is: " + clickAlphaValue(closeButton.getIcon(), e.getX(), e.getY()));
        }

        public void mouseEntered(MouseEvent e)
        {
        }

        public void mouseExited(MouseEvent e)
        {
        }
      });
    Image imgCloseButton = ImageIO.read(FILECLOSEBUTTON);

    //Resize the image to fit the button
    Image newImg = imgCloseButton.getScaledInstance((int)closeButton.getSize().getWidth(), (int)closeButton.getSize().getHeight(), java.awt.Image.SCALE_SMOOTH);
    closeButton.setIcon(new ImageIcon(newImg));


  }

  private int clickAlphaValue(Icon icon, int posX, int posY) 
  {
    int width = icon.getIconWidth();
    int height = icon.getIconHeight();

    BufferedImage tempImage = (BufferedImage)createImage(width, height);
    Graphics2D g = tempImage.createGraphics();

    icon.paintIcon(null, g, 0, 0);

    g.dispose();

    int alpha = (tempImage.getRGB(posX, posY) >> 24) & 0x000000FF;

    return alpha;
  } 
  public static void main(String[] args)
  {
    try
    {
      TestAlphaPixels testAlphaPixels = new TestAlphaPixels();
      testAlphaPixels.setVisible(true);
      testAlphaPixels.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    catch(IOException ioe)
    {
      ioe.printStackTrace();
    }
  }
}

这个示例实际显示的内容

这只是一个猜测,但是当我的图像被转换为图标时,它是否失去了Alpha属性,因此没有返回正确的值?无论如何,如果有人能真正帮助我并告诉我应该改变什么来获得正确的值,我将不胜感激。

我猜这是因为当我尝试使用原始图像时,Alpha通道的值是正确的,但是我实际上不能使用那个BufferedImage,因为我调整了它的大小,所以我实际上获得的是具有原始大小的图像的通道值...


如果您只想让按钮成为图像,为什么不使用 JLabel 并将图像设置为其图标呢? - Moonbeam
第一个原因是我将有一些按钮,它们具有从数组生成的图像上方的文本。第二个原因是我不知道将其设置为JLabel只会绘制非透明像素(因此Alpha像素不会被视为点击)。我可能会尝试这样做,但我仍然需要编写方法来绘制它,使其看起来像正在被单击。 - Jumbala
关于您的第二次编辑,为什么要测试 alpha 呢?如果这是为了查看是否单击了圆形按钮,则有更好的方法可以实现。如果是这种情况,我会将其发布为答案。测试 alpha 只有在形状不典型时才有用。 (我看到你的是圆形?) - rtheunissen
@paranoid-android 你的解决方案很好用,肯定会派上用场。问题是,我以后在这个程序中还会有其他奇形怪状的按钮,只是因为我还没有创建它们,所以圆形按钮在那些按钮上不起作用。我可能会使用你的方法来制作圆形按钮,因为它更简单。谢谢! - Jumbala
@Adam Smith:很高兴听到这个消息。:) 如果你使用JButton的扩展并开始集成“形状”,那么你就走在了正确的道路上。希望你的应用能够成功。 - rtheunissen
6个回答

6

我认为你走错了方向。你不需要覆盖paint()或paintComponent()方法。JButton已经知道只显示图像:

ImageIcon cup = new ImageIcon("images/cup.gif");
JButton button2 = new JButton(cup);

请看下面的教程示例:http://www.apl.jhu.edu/~hall/java/Swing-Tutorial/Swing-Tutorial-JButton.html 此外,Swing是完全定制的。您可以控制透明度、边框、颜色等。您可能需要覆盖一些提到的方法来改变功能。但在大多数情况下,有更好、更简单的解决方案。

5

由于多个答案中都有好的元素,但没有一个答案是完整的,因此我将回答自己的问题,以便其他有相同问题的人可以尝试类似的解决方法。

我使用了一个新类来创建我的按钮,该类扩展了JButton,并提供了一个新的构造函数,该构造函数以BufferedImage作为参数,而不是图标。原因是当我像这样执行myButton.getIcon()时,它会返回一个Icon,然后我必须对其进行各种操作,以使其成为正确大小的BufferedImage,但最终它还是无法工作,因为似乎第一次转换为Icon时就丢失了像素中的alpha数据,所以我无法检查用户是否点击了透明像素。

因此,我为构造函数做了以下操作:

public class MyButton extends JButton
{
   private BufferedImage bufImg;

   public MyButton(BufferedImage bufImg)
   {
      super(new ImageIcon(bufImg));
      this.bufImg = bufImg;
   }
 }

然后我为我的bufImg创建了一个访问器,使用getSize()方法将图像调整大小以适应JButton,然后返回正确尺寸的已调整大小的图像。我在getBufImg()访问器中进行转换,因为当窗口大小更改时,图像大小可能会更改。当您调用getBufImg()时,通常是因为您单击了按钮,因此您当前不会调整窗口的大小。

类似这样的内容将返回正确大小的图像:

 public BufferedImage getBufImg()
    {
      BufferedImage newImg = new BufferedImage(getSize().getWidth(), getSize().getHeight(), BufferedImage.TYPE_INT_ARGB); //Create a new buffered image the right size
      Graphics2D g2d = newImg.createGraphics();
      g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

      g2d.drawImage(bufImg, 0, 0, getSize().getWidth(), getSize().getHeight(), null);
      g2d.dispose();

      return newImg;
    }

有了这个缓冲图像,您可以编写以下方法:

  private int clickAlphaValue(BufferedImage bufImg, int posX, int posY) 
  {
    int alpha;

    alpha = (bufImg.getRGB(posX, posY) >>24) & 0x000000FF; //Gets the bit that contains alpha information

    return alpha;
  }

当您调用实现MouseListener的按钮时,就像这样:

myButton.addMouseListener(new MouseListener()
    {

    public void mouseClicked(MouseEvent e)
    {
    }

    public void mousePressed(MouseEvent e)
    {
    }

    public void mouseReleased(MouseEvent e)
    {
      if(clickAlphaValue(((myButton)e.getSource()).getBufImg(), e.getX(), e.getY()) != 0) //If alpha is not set to 0
        System.exit(0); //Or other things you want your button to do
    }

    public void mouseEntered(MouseEvent e)
    {
    }

    public void mouseExited(MouseEvent e)
    {
    }
  });

瞧!只有在您点击非透明像素时,按钮才会执行操作。

感谢大家的帮助,否则我自己不可能想出这个解决方案。


3

如果您想要具有特定形状的点击点,最好使用Shape和它们的contains方法。如果您愿意,在创建自定义按钮类时可以创建一个形状,并通过包装该形状的contains方法来实现包含方法。

至于自定义JButton,请创建一个扩展JButton的类,如下所示:

import java.awt.*;
import javax.swing.*;

public class CustomButton extends JButton{

    /** Filename of the image to be used as the button's icon. */
    private String fileName;
    /** The width of the button */
    private int width;
    /** The height of the button. */
    private int height;

 public CustomButton(String fileName, int width, int height){
    this.fileName = fileName;
    this.width = width;
    this.height = height;
    createButton();
}

/**
 * Creates the button according to the fields set by the constructor.
 */
private void createButton(){
    this.setIcon(getImageIcon(filename));
    this.setPreferredSize(new Dimension(width, height));
    this.setMaximumSize(new Dimension(width, height));
    this.setFocusPainted(false);
    this.setRolloverEnabled(false);
    this.setOpaque(false);
    this.setContentAreaFilled(false);
    this.setBorderPainted(false);
    this.setBorder(BorderFactory.createEmptyBorder(0,0,0,0)); 
  }
}


如果您想这样做,下面是如何加载ImageIcon的方法。

  public ImageIcon getImageIcon(String fileName){
    String imageDirectory = "images/"; //relative to classpath
    URL imgURL = getClass().getResource(imageDirectory + fileName);
    return new ImageIcon(imgURL);
  }

这将为您提供一个按钮,该按钮至少看起来像您的图像。 我曾就基于图像的单击事件提出类似的问题,Shapes帮了大忙。我想这归结于您的按钮图像有多复杂。 无论如何,下面是参考:
如何在Java中检测Image对象上的鼠标单击事件? PS:也许可以研究一下从图像生成形状,这些形状围绕所有不透明的像素。 不知道这是否可能,但这意味着只有在用户单击其图像部分时,按钮才会被“按下”。 只是一个想法。

您实际上可以完全不使用 Shape 来完成这个操作。尝试设置一个空边框,看看是否只有在图像上点击才会触发 ActionEvent。 - rtheunissen

2
如果您想要按钮布局与图像中非透明像素的布局相同,则应重新定义paintComponent()方法。这是最正确的方法(虽然在旧时期重写paint()也可以,但现在不鼓励使用)。
但是我认为这不完全符合您的要求:您希望仅检测单击按钮时,它位于非透明像素上,对吗?在这种情况下,您需要解析您的图像,并在单击时将鼠标坐标与您的图像的像素alpha通道进行比较,因为JButton没有此功能。

这种方法似乎过于复杂。 - Moonbeam
在Java中的awt.image包中,有一些类可以让我们访问图像中像素的RGB和alpha值。 - Guillaume
@Moonbeam,有什么好的替代方案吗?Guillaume所说的似乎正是我要找的。我知道你提到了JLabels,但它们能够注册alpha像素上的点击吗?如果可以的话,我不明白为什么它会比Guillaume的方法更简单。 - Jumbala
这是我目前尝试实现的方式,但有些事情不按照我想要的方式工作。如果您能帮助我解决它,我将非常乐意将您的答案标记为已接受。 - Jumbala

1
如果您有一个圆形按钮,这正是您所需要的:
  public class RoundButton extends JButton {

       public RoundButton() {
         this(null, null);
      }
       public RoundButton(Icon icon) {
         this(null, icon);
      }
       public RoundButton(String text) {
         this(text, null);
      }
       public RoundButton(Action a) {
         this();
         setAction(a);
      }

       public RoundButton(String text, Icon icon) {
         setModel(new DefaultButtonModel());
         init(text, icon);
         if(icon==null) return;
         setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
         setContentAreaFilled(false);
         setFocusPainted(false);
         initShape();
      }

    protected Shape shape, base;
    protected void initShape() {
      if(!getBounds().equals(base)) {
        Dimension s = getPreferredSize();
        base = getBounds();
        shape = new Ellipse2D.Float(0, 0, s.width, s.height);
      }
    }
    @Override public Dimension getPreferredSize() {
      Icon icon = getIcon();
      Insets i = getInsets();
      int iw = Math.max(icon.getIconWidth(), icon.getIconHeight());
      return new Dimension(iw+i.right+i.left, iw+i.top+i.bottom);
    }

    @Override public boolean contains(int x, int y) {
      initShape();
      return shape.contains(x, y);
      //or return super.contains(x, y) && ((image.getRGB(x, y) >> 24) & 0xff) > 0;
    }
  }

JButton有一个contains()方法。覆盖它并在mouseReleased()上调用它;


为什么你的示例中使用了EmptyBorder,顺便+1。 - mKorbel
@mKorbel:因为你喜欢的任何边框都应该包含在用于按钮的图像中。我认为这与跨平台ActionEvent处理有关(当您单击按钮时激活按钮),但我可能错了。真的不确定,所以不要听我的。 - rtheunissen
为什么不使用LineBorder?这并不是攻击你的人格,我只是好奇... - mKorbel
我们想要创建一个边框,但通过将其宽度设置为0等方式使其有效地变得无形。EmptyBorder只是显而易见的选择。从Java API中可以看到:EmptyBorder:提供一个空的、透明的边框,占用空间但不进行绘制的类。 - rtheunissen
这并没有说服我,但是使用EmptyBorder是个好主意,谢谢。 - mKorbel

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接