Search code examples
javaswinguser-interfacejframejtabbedpane

AbstractBorder paints over JTabbedPane


So I'm using Swing and trying to use a JTabbedPane to encompass several JPanels with JScrollPanes to scroll through a list of JPanels. The issue that arises is whenever I change tabs, among other things, the custom border function I have in the JPanels inside the scroll pane results in the components painting over the top of the JTabbedPane header and I have no idea of how to fix this problem: AbstractBorder paints over JTabbedPane header. I have provided code that mimics what I'm trying to do below. To replicate the issue, scroll down some on one tab until one of the outlined panels is halfway visible at the top. Then, switch to a different tab and then back. Then, scroll some more and the issue will present itself at the top of the JTabbedPane

public class Example extends JFrame {
private final JTabbedPane tabbedPane;
public Example() {
    setTitle("MIN Example");
    setSize(600, 700);
    setLayout(new BorderLayout());
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setBackground(Color.BLACK);
    setExtendedState(JFrame.MAXIMIZED_BOTH);

    tabbedPane = new JTabbedPane();
    tabbedPane.setBorder(BorderFactory.createEmptyBorder());
    tabbedPane.setFocusable(false);

    add(tabbedPane, BorderLayout.CENTER);
    createTabs();

    setVisible(true);
}

private void createTabs() {
    // Get the list of guilds
    List<String> tabs = new ArrayList<>(Arrays.asList("Tab 1", "Tab 2", "Tab 3"));

    // Iterate through the tabs and create the tab
    for (String tab : tabs) {
        // Initialize the list JPanel and formatting
        JPanel panelList = new JPanel();
        panelList.setLayout(new GridBagLayout());

        // House the list panel inside a JScrollPane
        JScrollPane listScroll = new JScrollPane(panelList);
        listScroll.setBorder(BorderFactory.createEmptyBorder());

        // Create tab and formatting
        tabbedPane.addTab(tab, listScroll);
        populateList(panelList);
    }
}

private void populateList(JPanel tab) {
    // Create GBC for formatting
    GridBagConstraints gbc = new GridBagConstraints();
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.weightx = 1;
    gbc.weighty = 1;

    // Remove all components from the JPanel
    tab.removeAll();

    // Add filler JPanel
    JPanel filler = new JPanel();
    filler.setOpaque(false);
    tab.add(filler, gbc);

    // Update GBC constraints
    gbc.insets = new Insets(10, 10, 0, 10);
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.weighty = GridBagConstraints.RELATIVE;

    // Go through the tabs and add JPanels
    for (int i = 10; i >= 0; i--) {
        JPanel tempPanel = new JPanel();
        tempPanel.setBorder(new RoundedBorder(Color.BLACK, 6, 16));
        tab.add(tempPanel, gbc, 0);
        tempPanel.setPreferredSize(new Dimension(0, 100));
    }

    // Refresh the console to display updated lists
    validate();
    repaint();
}
}

public class RoundedBorder extends AbstractBorder {
private final Color color;
private final int thickness;
private final int radii;
private final Insets insets;
private final BasicStroke stroke;
private final int strokePad;
RenderingHints hints;

/**
 * Creates the rounded border
 *
 * @param color The color of the border outline
 * @param thickness The thickness of the border outline
 * @param radii The radius of the rounded border
 */
public RoundedBorder(Color color, int thickness, int radii) {
    this.thickness = thickness;
    this.radii = radii;
    this.color = color;

    stroke = new BasicStroke(thickness);
    strokePad = thickness / 2;

    hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    int pad = radii + strokePad;
    int bottomPad = pad + strokePad;
    insets = new Insets(pad, pad, bottomPad, pad);
}

@Override
public Insets getBorderInsets(Component c) {
    return insets;
}

@Override
public Insets getBorderInsets(Component c, Insets insets) {
    return getBorderInsets(c);
}

@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
    Graphics2D g2 = (Graphics2D) g;

    int bottomLineY = height - thickness;

    RoundRectangle2D.Double bubble = new RoundRectangle2D.Double(strokePad, strokePad, width - thickness, bottomLineY, radii, radii);

    Area area = new Area(bubble);

    g2.setRenderingHints(hints);

    // Paint the background color of the parent
    Component parent  = c.getParent();
    if (parent != null) {
        Color background = parent.getBackground();
        Rectangle rect = new Rectangle(0,0,width, height);
        Area borderRegion = new Area(rect);
        borderRegion.subtract(area);
        g2.setClip(borderRegion);
        g2.setColor(background);
        g2.fillRect(0, 0, width, height);
        g2.setClip(null);
    }

    g2.setColor(color);
    g2.setStroke(stroke);
    g2.draw(area);
}
}

The only things I have tried to do to fix this issue was changing the order of adding the JTabbedPane (Add before the tabs were populated with panels) to no avail. Help would be greatly appreciated, thank you.


Solution

  • In the future, an MRE should include:

    1. the import statements
    2. the main() method

    In the original code the "clip" of the Graphics is being set to null so the entire Rectangle of the panel appears to be painted.

    I modified the painting of the parent to use a separate Graphics object so the original "clip" area is not affected:

    // Paint the background color of the parent
    Component parent  = c.getParent();
    if (parent != null) {
        Graphics2D g2d = (Graphics2D)g2.create();
        Color background = parent.getBackground();
        //Rectangle rect = new Rectangle(0, 0, width, height);
        Rectangle rect = g2d.getClip().getBounds();
        Area borderRegion = new Area(rect);
        borderRegion.subtract(area);
        g2d.setClip(borderRegion);
        g2d.setColor(background);
        g2d.fillRect(0, 0, width, height);
        g2d.dispose();
    }