I have a JButton in which I overrided the paintComponent(Graphics)
function
with a child JLabel (I realize this sounds stupid, I promise it's not)
I have mouseEntered(MouseEvent)
& mouseExited(MouseEvent)
functions which change the visiblity of the label as well as set a boolean telling paintComponent
to draw a translucent overlay over the button
The expected behaviour is that the JLabel
draw over the button overlay. Without the overlay (override of paintComponent
) this works perfectly.
(I'm assuming this isn't only limited to buttons, though I haven't tested that theory)
Button Class:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class HoverButton extends JButton {
private final JLabel label;
private final String title;
private boolean entered = false;
public HoverButton(String title) {
label = new JLabel(title);
this.title = title;
int startChar = title.indexOf(']') + 1;
String regex = new StringBuilder("\\[[a-zA-Z0-9]+\\]").append("| \\[[A-Za-z0-9]+ [A-Za-z0-9]+\\]")
.append("| \\(decen\\)").append("| \\(eng, decen\\)").append("| \\(eng\\)")
.append("|\\{.+\\}").toString();
String text = String.format("<html><p><b>%s</b></p></html>",
title.substring(startChar).replaceAll(regex, "").trim());
label.setVisible(false);
add(label);
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
entered = true;
label.setVisible(true);
}
@Override
public void mouseExited(MouseEvent e) {
super.mouseExited(e);
entered = false;
label.setVisible(false);
}
});
}
public String getTitle() {
return title;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (g instanceof Graphics2D g2d) {
getIcon().paintIcon(this, g, getInsets().left, getInsets().top);
int xMax = getWidth() - getInsets().right - getInsets().left;
int yMax = getHeight() - getInsets().top - getInsets().bottom;
if (entered) {
g2d.setColor(new Color(0x88000000, true));
g2d.fillRect(getInsets().left, getInsets().top, xMax, yMax);
}
g2d.dispose();
}
}
@Override
public void setPreferredSize(Dimension d) {
super.setPreferredSize(new Dimension((int) (d.getWidth() + getInsets().right + getInsets().left),
(int) (d.getHeight() + getInsets().top + getInsets().bottom)));
label.setMinimumSize(d);
}
}
the setPreferredSize(Dimension)
at the bottom is there to ensure the label is doesn't resize the button
button.setIcon(icon);
button.setPreferredSize(new Dimension(icon.getWidth(), icon.getHeight()));
should be in the calling class
You can use a JLayer
for this type of overlay painting, where the events will pass through the label.
The JLayer
has two components:
JLayer
. The JLayer
will forward its events to the view. For example in this case we would like the events to pass to the button.JPanel
and you can use it like any other. Events will pass through it and its descendants. For example in this case we could add the label to the glass pane (JLabel
s can accept an Icon
along with text, or an Icon
by itself).This way you don't have to do the following:
ButtonModel
).Icon
. The label will do that for you.JLayer
will handle this.import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.ButtonModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class MainWithJLayer {
/**
* Changes the alpha component of the given {@code Color}.
* @param c
* @param alpha
* @return
*/
public static Color withAlpha(final Color c,
final int alpha) {
return new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
}
/** A {@code JPanel} which always draws its background color (dishonoring opaque property). */
private static class AlwaysDrawBackgroundPanel extends JPanel {
@Override
protected void paintComponent(final Graphics g) {
final Color originalColor = g.getColor();
try {
final Rectangle clipBounds = g.getClipBounds();
g.setColor(getBackground());
g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height);
}
finally {
g.setColor(originalColor);
super.paintComponent(g);
}
}
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> {
// try {
// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// }
// catch (final ClassNotFoundException | IllegalAccessException | InstantiationException | UnsupportedLookAndFeelException exception) {
// System.err.println("Failed to set system L&F.");
// }
final JButton button = new JButton("always clickable... ...always clickable");
button.addActionListener(e -> System.out.println("Clicked!"));
final JLabel label = new JLabel("Label overlay!", JLabel.CENTER);
label.setForeground(Color.RED);
final JPanel glassPane = new AlwaysDrawBackgroundPanel();
glassPane.setLayout(new BorderLayout());
glassPane.setBackground(withAlpha(Color.BLACK, 0x88)); //new Color(0x88000000,true)
glassPane.add(label, BorderLayout.CENTER);
final JLayer<JButton> layer = new JLayer<>(button);
layer.setGlassPane(glassPane);
glassPane.setOpaque(false); //This is mandatory in order to show the button under the label.
final JPanel contents = new JPanel(new BorderLayout());
contents.setBorder(BorderFactory.createEmptyBorder(100, 100, 100, 100));
contents.add(layer, BorderLayout.CENTER);
//Change glass pane visibility when hovering the button:
final ButtonModel buttonModel = button.getModel();
buttonModel.addChangeListener(e -> glassPane.setVisible(buttonModel.isRollover()));
final JFrame frame = new JFrame("Button overlay label");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(contents);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
Notice that the glass pane (which has the label) is only visible when we hover the button and that the button receives events normally (with or without the label being visible).
There are also several properties of the button and the label (such as margin, border, alignment, text position, icon-text-gap) to help solve icon (and/or text) placement issues between the two components.
Note, the main possible contribution here (assuming it fits your needs) is the suggestion to use a JLayer
, since using the ButtonModel
's rollover property was already suggested by @Holger.