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
1 Answer 1
You can use a JLayer for this type of overlay painting, where the events will pass through the label.
The JLayer has two components:
- The view. This is the component which is wrapped by the
JLayer. TheJLayerwill forward its events to the view. For example in this case we would like the events to pass to the button. - The glass pane. This can be used as a painting area. It is a
JPaneland 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 (JLabels can accept anIconalong with text, or anIconby itself).
This way you don't have to do the following:
- Track mouse events. This is already implemented in the button, so you can directly listen for such events (via the
ButtonModel). - Paint the
Icon. The label will do that for you. - Override preferred size. The
JLayerwill 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.
g2d.dispose();-g2dwas not created by posted method, should not be discarded by it (I would also stop usingStringBuilderto concatenate strings, at least three reasons not do so here [no loop, only literals, no advantage after Java 9] )+for constant strings would produce a constant string in all versions whereas using+for nonconstant strings before Java 9 would produce the same compiled code as thisStringBuildercode.StringBuilderis not useful here. The use ofString.format("<html><p><b>%s</b></p></html>", ...)to basically concatenate three parts is also inferior to just using string concatenation but also inconsistent. It looks like an attempt to use as many different ways to concatenate strings as possible.JButtonhas built-in rollover support, so there’s no need to do all this stuff. For example:JButton b = new JButton("Some test text"); b.setRolloverEnabled(true); b.getModel().addChangeListener(ev -> b.setText(((ButtonModel)ev.getSource()).isRollover()? "<html><b>Some test text": "Some test text"));I wouldn’t mess around with the color but rather select a Look&Feel which provides good colors for roll-over highlighting. Otherwise, a custom icon would be enough to paint a translucent overlay.AbstractButtoninstalls anOverlayLayoutwhen you add the first child and no layout has been set so before. But there’s no sense in adding aJLabelanyway when you can simply set the text on the button in the first place, as both have the same text rendering features.