Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

paintComponent() is drawing on other components

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?

Image

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;
    }
}
like image 677
Spitz Avatar asked Feb 04 '16 01:02

Spitz


People also ask

What is paintComponent method?

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.

What does a call to super paintComponent () accomplish?

The invocation of super. paintComponent(g) passes the graphics context off to the component's UI delegate, which paints the panel's background.

What is the difference between the paintComponent and repaint methods?

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.

How is paintComponent called?

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.


2 Answers

There are two problems with the clipping code:

  1. You don't start with the original clip when subtracting out the bubble (causing the component to be painted outside the scrollpane)
  2. 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);
}
like image 55
camickr Avatar answered Oct 03 '22 12:10

camickr


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!

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...

like image 44
MadProgrammer Avatar answered Oct 03 '22 14:10

MadProgrammer