I'm using a custom class, based on the code in this answer, to draw a background shaped like a speech bubble. Whenever I resize the window of my application enough to make a component poke out at the top or bottom, the outlines of the said component is drawn outside the JScrollPane
on top of other components; in this case the JPanel
.
In the left-side image, the border of the component at the bottom of the JScrollPane
is drawn, due to the component still being visible; while in the right-side image, the mentioned component is no longer visible and everything looks as intended.
I believe it has something to do with the fact that I'm using a JScrollPane
to contain the components and thus allowing the component to slide under the JPanel
. How do I prevent this?
Main:
public class Main {
public static void main(String[] args) {
JPanel panel = new JPanel(), panelbar = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panelbar.setLayout(new FlowLayout());
JScrollPane scroll = new JScrollPane(panel,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
JFrame frame = new JFrame("");
frame.setLayout(new BorderLayout());
frame.setSize(200, 223);
for (int i = 0; i < 6; i++) {
JLabel label = new JLabel("JLabel");
label.setBorder(new CustomBorder());
label.setOpaque(true);
label.setBackground(Color.ORANGE);
panel.add(label);
}
panelbar.add(new JLabel("JPanel"));
frame.add(scroll, BorderLayout.CENTER);
frame.add(panelbar, BorderLayout.SOUTH);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
Custom class:
public class CustomBorder extends AbstractBorder {
private static final long serialVersionUID = 1L;
Insets i;
CustomBorder() {
i = new Insets(10, 20, 10, 20);
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
super.paintBorder(c, g, x, y, width, height);
Polygon bubble = new Polygon();
bubble.addPoint(x + 10, y + 5);
bubble.addPoint(x + width - 10, y + 5);
bubble.addPoint(x + width - 10, y + height / 3);
bubble.addPoint(x + width, y + height / 2);
bubble.addPoint(x + width - 10, y + height * 2 / 3);
bubble.addPoint(x + width - 10, y - 5 + height);
bubble.addPoint(x + 10, y - 5 + height);
Graphics2D g2d = (Graphics2D) g;
Area rect = new Area(new Rectangle(x, y, width, height));
rect.subtract(new Area(bubble));
g2d.setClip(rect);
g2d.setColor(c.getParent().getBackground());
g2d.fillRect(0, 0, width, height);
g2d.setClip(null);
g2d.setColor(Color.BLACK);
g2d.draw(bubble);
}
@Override
public Insets getBorderInsets(Component c) {
return i;
}
@Override
public Insets getBorderInsets(Component c, Insets insets) {
return i;
}
}
There are two problems with the clipping code:
- You don't start with the original clip when subtracting out the bubble (causing the component to be painted outside the scrollpane)
- You don't restore the original clip before painting the bubble:
The changes would be:
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
super.paintBorder(c, g, x, y, width, height);
Polygon bubble = new Polygon();
bubble.addPoint(x + 10, y + 5);
bubble.addPoint(x + width - 10, y + 5);
bubble.addPoint(x + width - 10, y + height / 3);
bubble.addPoint(x + width, y + height / 2);
bubble.addPoint(x + width - 10, y + height * 2 / 3);
bubble.addPoint(x + width - 10, y - 5 + height);
bubble.addPoint(x + 10, y - 5 + height);
Graphics2D g2d = (Graphics2D) g;
//Area rect = new Area(new Rectangle(x, y, width, height));
Shape clip = g2d.getClip();
Area rect = new Area(clip);
rect.subtract(new Area(bubble));
g2d.setClip(rect);
g2d.setColor(c.getParent().getBackground());
g2d.fillRect(0, 0, width, height);
//g2d.setClip(null);
g2d.setClip(clip);
g2d.setColor(Color.BLACK);
g2d.draw(bubble);
}
Your basic problem is, you're changing the clipping area, which was set before the component was painted, to something, well, else, which is allowing you to paint beyond the bounds of the component...
As discussed here and here, borders aren't meant to be filled, nor do they effect the area filled by paintComponent
If you take a look at A Closer Look at the Paint Mechanism you will see the paintComponent
is called before paintBorder
...
javax.swing.JComponent extends this class and further factors the
paint method into three separate methods, which are invoked in the
following order:
- protected void paintComponent(Graphics g)
- protected void paintBorder(Graphics g)
- protected void paintChildren(Graphics g)
So, what's the solution? Fake it!
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Polygon;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;
public class BorderCheat {
public static void main(String[] args) {
new BorderCheat();
}
public BorderCheat() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JPanel panel = new JPanel(), panelbar = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panelbar.setLayout(new FlowLayout());
JScrollPane scroll = new JScrollPane(panel,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
for (int i = 0; i < 6; i++) {
BubblePane bp = new BubblePane();
bp.setBackground(Color.ORANGE);
JLabel label = new JLabel("JLabel");
bp.add(label);
panel.add(bp);
}
panelbar.add(new JLabel("JPanel"));
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(scroll);
frame.add(panelbar, BorderLayout.SOUTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class BubblePane extends JPanel {
public BubblePane() {
setLayout(new GridBagLayout());
setBorder(new EmptyBorder(10, 20, 10, 30));
setOpaque(false);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
Insets insets = getInsets();
int x = 0;
int y = 0;
int width = getWidth();
int height = getHeight();
Polygon bubble = new Polygon();
bubble.addPoint(x, y);
bubble.addPoint(x + width - insets.right + 10, y);
bubble.addPoint(x + width - insets.right + 10, y + height / 3);
bubble.addPoint(x + width, y + height / 2);
bubble.addPoint(x + width - insets.right + 10, y + height * 2 / 3);
bubble.addPoint(x + width - insets.right + 10, y + height);
bubble.addPoint(x, y + height);
g2d.setColor(getBackground());
g2d.fill(bubble);
g2d.setColor(Color.BLACK);
g2d.draw(bubble);
g2d.dispose();
}
}
}
Okay, "but there's no gap between them" you say. Okay, so use a CompoundBorder
or a layout which allows you to specify the vertical or horizontal spacing between components...