-1

I'm trying to paint a Windows 11 button on my JFrame. That worked well, but I can't properly add actionlisteners to it. Here is my code:

public class MyButton extends JButton implements MouseListener, ActionListener {
    public MyButton(String text) {
        //super(label);
        enableInputMethods(true);
        addMouseListener(this);

        setFocusPainted(false);
        setBorderPainted(false);

        buttonText = text;
    }

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

        Graphics2D g2 = (Graphics2D) g;
        FontMetrics metrics = g2.getFontMetrics(Constants.arialFont);
        g2.setStroke(new BasicStroke(2.0f));
        g2.setFont(Constants.arialFont);

        g2.setPaint(Constants.buttonNormal);
        rect = new RoundRectangle2D.Double(0, 0, metrics.stringWidth(buttonText) + 20, 27, 10, 10);
        g2.fill(rect);

        g2.setPaint(Constants.fontColor);
        g2.drawString(buttonText, Functions.getMiddleFromX(g2, rect, buttonText), Functions.getMiddleFromY(g2, rect, buttonText));
    }

I have tried to add mouse listeners to it as follows:

    @Override
    public void mouseClicked(MouseEvent e) {
        // Check if the click is on the button.
        Point p = e.getPoint();
        if(rect.contains(p)) System.out.println("Triangle contains point");
        else { return; }
    }

But when I run this class with this code:

        MyButton myButton = new MyButton("Epic button!");
        myButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Action Performed!");
            }
        });
        frame.add(myButton);

I can click anywhere on the canvas (or JFrame) and still see "Action Performed!" printed on the console.

How would I filter the incoming events and only do stuff if the click is on my RoundRectangle2D.Double()?

  • This probably isn't the right approach, instead, you should be implementing a custom button UI look and feel delegate. As a side note, `JButton` has a [`fireActionPerformed`](https://docs.oracle.com/javase/8/docs/api/?javax/swing/JButton.html) method – MadProgrammer May 02 '23 at 01:00
  • @MadProgrammer Would I put `fireActionPerformed()` in my `actionPerformed()` method? –  May 02 '23 at 01:02
  • [`ButtonUI` example](https://stackoverflow.com/questions/47339013/why-paintcomponent-is-defined-on-jcomponent/47341727#47341727) – MadProgrammer May 02 '23 at 01:04
  • You would put `fireActionPerformed` where you want to trigger the action event. The problem you have is figuring out how to otherwise stop it, which is my I would consider `ButtonUI` a better place to start – MadProgrammer May 02 '23 at 01:07
  • I would suggest having a look at `BasicButtonUI` and `BasicButtonListener` – MadProgrammer May 02 '23 at 01:12
  • More `ButtonUI` [example](https://stackoverflow.com/questions/70429378/java-jbutton-set-text-background-color/70429542#70429542), [example](https://stackoverflow.com/questions/46448044/swing-create-a-uwp-metro-like-button/46458112#46458112) and [example](https://stackoverflow.com/questions/44384073/java-swing-drawing-playbutton-for-a-basic-music-player/44404175#44404175) – MadProgrammer May 02 '23 at 01:51

1 Answers1

1

Proof of concept

This is intended as a proof of concept based on the limited understand of the requirements and is not intended to be a complete, production ready solution - just saying

This is somewhat of an assumption on my part, but, it seems you what to have some kind of "shape based" button which can only be triggered by the mouse if it's over the "shape" in question.

This is somewhat more involved, as the only realistic way to modify the mouse handling is via the buttons ButtonUI delegate.

The following example basically creates a custom ButtonUI which presents a round button, but will only be triggered in the user presses within inside the shape of the circle itself. Of course, the user can still trigger the button via the keyboard, but that's another issue entirely (but is actually solved through the same mechanisms)

enter image description here

It has default icon support!

enter image description here

enter image description here

Please note, the red rectangle shows the physical bounds of the button itself and are only for demonstration purpose

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.LinearGradientPaint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.plaf.basic.BasicButtonUI;

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

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridBagLayout());
            setBackground(Color.RED);

            JButton circleButton = new JButton("Hello");
            circleButton.setUI(new CircleButtonUI());
            circleButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.out.println("I've been triggered");
                }
            });

            add(circleButton);
        }
    }

    public class CircleButtonUI extends BasicButtonUI {

        private Shape circleShape;

        @Override
        public void installUI(JComponent c) {
            super.installUI(c);
            c.setOpaque(false);
        }

        @Override
        public boolean contains(JComponent c, int x, int y) {
            if (circleShape == null) {
                return c.contains(x, y);
            }
            return circleShape.contains(x, y);
        }

        @Override
        public void paint(Graphics g, JComponent c) {
            int width = c.getWidth() - 2;
            int height = c.getHeight() - 2;
            int size = Math.min(width, height);
            int x = ((width - size) / 2) + 1;
            int y = ((height - size) / 2) + 1;
            circleShape = new Ellipse2D.Double(x, y, size, size);
            AbstractButton b = (AbstractButton) c;
            paintContent(g, b);
            super.paint(g, c);
        }

        protected void paintContent(Graphics g, AbstractButton b) {
            if (circleShape == null) {
                return;
            }
            Graphics2D g2d = (Graphics2D) g.create();
            // paint the interior of the button
            g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
            g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

            ButtonModel model = b.getModel();
            Color highlight = b.getBackground();
            if (model.isArmed() && model.isPressed()) {
                highlight = highlight.darker();
            }
            Color darklight = highlight.darker();

            LinearGradientPaint lgp = new LinearGradientPaint(
                    circleShape.getBounds().getLocation(),
                    new Point((int) circleShape.getBounds().getMaxX(), (int) circleShape.getBounds().getMaxY()),
                    new float[]{0, 1f},
                    new Color[]{highlight, darklight});

            g2d.setPaint(lgp);
            g2d.fill(circleShape);

            // draw the perimeter of the button
            g2d.setColor(b.getBackground().darker().darker().darker());
            g2d.draw(circleShape);
            g2d.dispose();
        }

        @Override
        protected void paintFocus(Graphics g, AbstractButton b, Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
            // Paint focus highlight, if you want to
        }

        public Dimension getMinimumSize(JComponent c) {
            Dimension size = super.getMinimumSize(c);
            int maxSize = Math.max(size.width, size.height);
            return new Dimension(maxSize, maxSize);
        }

        public Dimension getPreferredSize(JComponent c) {
            Dimension size = super.getPreferredSize(c);
            int maxSize = Math.max(size.width, size.height);
            return new Dimension(maxSize, maxSize);
        }

        public Dimension getMaximumSize(JComponent c) {
            Dimension size = super.getPreferredSize(c);
            int maxSize = Math.max(size.width, size.height);
            return new Dimension(maxSize, maxSize);
        }
    }
}

More then one way to approach the problem...

As with most things, there's always more then one way to approach the problem.

This example just extends a JButton. The critical area is in overriding the contains method.

Personally, I like the ButtonUI approach, as it's relatively easy to introduce into an existing code base (arguably) and you don't end up with some weird result because the installed look and feel is doing something "different", just saying.

public class CircleButton extends JButton {

    private Shape circleShape;

    public CircleButton() {
        configureDefaults();
    }

    public CircleButton(Icon icon) {
        super(icon);
        configureDefaults();
    }

    public CircleButton(String text) {
        super(text);
        configureDefaults();
    }

    public CircleButton(Action a) {
        super(a);
        configureDefaults();
    }

    public CircleButton(String text, Icon icon) {
        super(text, icon);
        configureDefaults();
    }
    
    protected void configureDefaults() {
        setBorderPainted(false);
        setFocusPainted(false);
        setOpaque(false);
    }

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

    @Override
    public boolean contains(int x, int y) {
        if (circleShape == null) {
            return super.contains(x, y);
        }
        return circleShape.contains(x, y);
    }

    public Dimension getMinimumSize() {
        Dimension size = super.getMinimumSize();
        int maxSize = Math.max(size.width, size.height);
        return new Dimension(maxSize, maxSize);
    }

    public Dimension getPreferredSize() {
        Dimension size = super.getPreferredSize();
        int maxSize = Math.max(size.width, size.height);
        return new Dimension(maxSize, maxSize);
    }

    public Dimension getMaximumSize() {
        Dimension size = super.getPreferredSize();
        int maxSize = Math.max(size.width, size.height);
        return new Dimension(maxSize, maxSize);
    }

    @Override
    protected void paintComponent(Graphics g) {
        if (circleShape == null) {
            int width = getWidth() - 2;
            int height = getHeight() - 2;
            int size = Math.min(width, height);
            int x = ((width - size) / 2) + 1;
            int y = ((height - size) / 2) + 1;
            circleShape = new Ellipse2D.Double(x, y, size, size);
        }
        Graphics2D g2d = (Graphics2D) g.create();
        // paint the interior of the button
        g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

        ButtonModel model = getModel();
        Color highlight = getBackground();
        if (model.isArmed() && model.isPressed()) {
            highlight = highlight.darker();
        }
        Color darklight = highlight.darker();

        LinearGradientPaint lgp = new LinearGradientPaint(
                circleShape.getBounds().getLocation(),
                new Point((int) circleShape.getBounds().getMaxX(), (int) circleShape.getBounds().getMaxY()),
                new float[]{0, 1f},
                new Color[]{highlight, darklight});

        g2d.setPaint(lgp);
        g2d.fill(circleShape);

        // draw the perimeter of the button
        g2d.setColor(getBackground().darker().darker().darker());
        g2d.draw(circleShape);
        g2d.dispose();
        super.paintComponent(g);
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • 1
    the question is lacking, but .. *cough .. this looks like too much and not good enough at the same time: there are two aspects of the (assumed) problem - a) the graphics and b) the mouse interaction. To a) the graphics here are nice, I assume (didn't try, though) it can be handled in the component's paintComponent. To b) the handler isn't complete (f.i. hover effects are triggered inside the whole button area, leading to incorrect state changes in the buttonModel). Afair, there is no reason for a custom handler - overriding contains(x,y) should do the trick. – kleopatra May 02 '23 at 11:12
  • @kleopatra Yes, you "could" do the painting in the component's `paintComponent` method, but I wanted to try and dig a little deeper. I also agree with the criticism, but then I don't really have a tight set of requirements to work against , so I was more focused on the "idea" rather then the end result. I kind of like the UI delegate approach as it means it can be applied to any button, you don't need to specifically make use of the "extended" version, but this is just "one" way it might be achieved – MadProgrammer May 02 '23 at 13:29
  • yeah, I'm aware of the question quality (and took it into account :). Nevertheless, this question - at least partly, B - re-invents the wheel as an octagon: contains is meant to be overridden for that _exact_ use-case. And we both are aware that implementing additional functionality (assuming that's really a requirement or needed) in custom ui-delegates is a whole lot of more work then just extending BasicXX (SwingX has an elaborate mechanism .. those where the times and solutions :) – kleopatra May 02 '23 at 13:45
  • @kleopatra extension vs composition - this is after all just "one" way to do it. Personally, I think it works well as I don't need to mess to much with all the "other" properties of the button (borders, focus painting, etc for example) and since Swing supports "delegation", it seems like a nice fit, I'm just wanted to see if I "could" do it – MadProgrammer May 02 '23 at 13:48
  • @MadProgrammer When I run your first example, it I see this: https://i.stack.imgur.com/w2aFs.gif. Is this meant to happen? –  May 02 '23 at 15:27
  • @MadProgrammer But your comment [above](https://stackoverflow.com/questions/76151080/how-do-i-extend-jbutton-but-modify-actionperformed/76151919#comment134295518_76151080) gave me some useful examples! Thanks! –  May 02 '23 at 15:50
  • @Ben :/ Works fine for me - try setting the background color to something else (like `Color.RED` and see if that makes a difference – MadProgrammer May 02 '23 at 21:48
  • @MadProgrammer Yes, now it works! Add the code to your answer, please! –  May 03 '23 at 16:16
  • @Ben because it's a platform specific problem :P – MadProgrammer May 03 '23 at 22:08