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;
}
}
paintComponent()This method is needed to draw something on JPanel other than drawing the background color. This method already exists in a JPanel class so that we need to use the super declaration to add something to this method and takes Graphics objects as parameters.
The invocation of super. paintComponent(g) passes the graphics context off to the component's UI delegate, which paints the panel's background.
It looks like the paint() method actually draws the component, including the border and children. If you only want to customize the component's appearance excluding the border and children, you use paintComponent() . Save this answer.
The paintComponent() method can also be called explicitly by the repaint() method defined in Component class. The effect of calling repaint() is that Swing automatically clears the graphic on the panel and executes the paintComponent method to redraw the graphics on this panel.
There are two problems with the clipping code:
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With