javaswinglayoutawtjscrollpane

Horizontal JScrollPane inside vertical JScrollPane


Good afternoon! It is necessary to make a list (vertical) from lists of events (horizontal). There are at least 2 problems:

  1. the area of the list of events (horizontal scrolling) expands beyond the borders of the panel. Apparently, the problem is with Layout, but I can not find the right combination;
  2. horizontal scrolling does not work (probably due to the problem described above) and the scrollbar must be different for each group of events.
import ivank.components.EventAdd;
 
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
public class WindowAddCameras extends JFrame {
    public static final List<JPanel> labels = new ArrayList<JPanel>();
 
    public WindowAddCameras() {
        super("Добавить камеру");
 
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        JPanel panelButton = new JPanel();
 
        JButton addButton = new JButton("+");
        addButton.setFocusable(false);
        panelButton.add(addButton);
 
        JButton remButton = new JButton("-");
        remButton.setFocusable(false);
        panelButton.add(remButton);
 
        JPanel externalPanel = new JPanel();
        externalPanel.setLayout(new BorderLayout(0, 0));
        JScrollPane scrollPaneGroupEvent = new JScrollPane(
                externalPanel,
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
        );
 
        JPanel internalPanel = new JPanel();
        internalPanel.setLayout(new GridLayout(0, 1, 0, 0));
 
        JScrollPane scrollPaneEvent = new JScrollPane(internalPanel);
        scrollPaneEvent.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
        scrollPaneEvent.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
 
        externalPanel.add(scrollPaneEvent, BorderLayout.NORTH);
        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                int number = labels.size() + 1;
                EventAdd eventAdd = new EventAdd();
                Dimension labelSize = new Dimension(80, 80);
 
                //add event to group event
                Random rand = new Random();
                for(int a = 0; a < 20; a++) {
                    //random color border event for TEST
                    Color randomColor = new Color(rand.nextFloat(), rand.nextFloat(), rand.nextFloat());
                    eventAdd.createEventLabel("Камера " + number, labelSize, randomColor);
                }
 
                labels.add(eventAdd);
                internalPanel.add(eventAdd, BorderLayout.NORTH);
                scrollPaneGroupEvent.revalidate();
            }
        });
 
        remButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if(labels.size() > 0) {
                    int index = labels.size() - 1;
                    JPanel panel = labels.remove(index);
                    internalPanel.remove(panel);
                    internalPanel.repaint();
                    scrollPaneGroupEvent.revalidate();
                }
            }
        });
 
        this.getContentPane().setLayout(new BorderLayout());
        this.getContentPane().add(panelButton, BorderLayout.NORTH);
        this.getContentPane().add(scrollPaneGroupEvent, BorderLayout.CENTER);
 
        this.setPreferredSize(new Dimension(600, 400));
        this.pack();
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }
}
package ivank.components;
 
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
 
public class EventAdd extends JPanel {
    public EventAdd() {
        super(new FlowLayout(FlowLayout.LEFT));
    }
 
    public JComponent createEventLabel(String name, Dimension labelSize, Color randomColor) {
        this.setBorder(BorderFactory.createTitledBorder(name));
        JLabel label = new JLabel();
        label.setPreferredSize(labelSize);
        label.setHorizontalAlignment(JLabel.CENTER);
 
        label.setBorder(BorderFactory.createLineBorder(randomColor, 5));
        this.add(label);
 
        return label;
    }
}

What I have: enter image description here What I want to get: enter image description here


Solution

  • A scrolling area inside another scrolling area is a user interface antipattern (anti-design?). It should be avoided.

    I would create a scrollable panel based on a vertical BoxLayout:

    public class WindowAddCameras extends JFrame {
        private static final long serialVersionUID = 1;
    
        public static final List<JPanel> labels = new ArrayList<JPanel>();
    
        private static class CameraListPanel
        extends JPanel
        implements Scrollable {
            private static final long serialVersionUID = 1;
    
            CameraListPanel() {
                setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
            }
    
            @Override
            public Dimension getPreferredScrollableViewportSize() {
                return getPreferredSize();
            }
    
            @Override
            public boolean getScrollableTracksViewportWidth() {
                return true;
            }
    
            @Override
            public boolean getScrollableTracksViewportHeight() {
                return false;
            }
    
            @Override
            public int getScrollableUnitIncrement(Rectangle visibleRect,
                                                  int orientation,
                                                  int direction) {
                return getScrollableIncrement(30,
                    visibleRect, orientation, direction);
            }
    
            @Override
            public int getScrollableBlockIncrement(Rectangle visibleRect,
                                                   int orientation,
                                                   int direction) {
                return getScrollableIncrement(
                    orientation == SwingConstants.HORIZONTAL ?
                        getWidth() : getHeight(),
                    visibleRect, orientation, direction);
            }
    
            private int getScrollableIncrement(int amount,
                                               Rectangle visibleRect,
                                               int orientation,
                                               int direction) {
                if (orientation == SwingConstants.HORIZONTAL) {
                    return Math.min(amount, direction < 0 ? visibleRect.x :
                        getWidth() - (visibleRect.x + visibleRect.width));
                } else {
                    return Math.min(amount, direction < 0 ? visibleRect.y :
                        getHeight() - (visibleRect.y + visibleRect.height));
                }
            }
        }
     
        public WindowAddCameras() {
            super("Добавить камеру");
     
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
     
            JPanel panelButton = new JPanel();
     
            JButton addButton = new JButton("+");
            addButton.setFocusable(false);
            panelButton.add(addButton);
     
            JButton remButton = new JButton("-");
            remButton.setFocusable(false);
            panelButton.add(remButton);
    
            JPanel camerasPanel = new CameraListPanel();
            JScrollPane scrollPaneGroupEvent = new JScrollPane(camerasPanel);
     
            addButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    int number = labels.size() + 1;
                    EventAdd eventAdd = new EventAdd();
                    Dimension labelSize = new Dimension(80, 80);
     
                    //add event to group event
                    Random rand = new Random();
                    for(int a = 0; a < 20; a++) {
                        //random color border event for TEST
                        Color randomColor = new Color(rand.nextFloat(), rand.nextFloat(), rand.nextFloat());
                        eventAdd.createEventLabel("Камера " + number, labelSize, randomColor);
                    }
     
                    labels.add(eventAdd);
                    camerasPanel.add(eventAdd);
                    scrollPaneGroupEvent.revalidate();
                }
            });
     
            remButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if(labels.size() > 0) {
                        int index = labels.size() - 1;
                        JPanel panel = labels.remove(index);
                        camerasPanel.remove(panel);
                        camerasPanel.repaint();
                        scrollPaneGroupEvent.revalidate();
                    }
                }
            });
     
            this.getContentPane().setLayout(new BorderLayout());
            this.getContentPane().add(panelButton, BorderLayout.NORTH);
            this.getContentPane().add(scrollPaneGroupEvent, BorderLayout.CENTER);
     
            this.setPreferredSize(new Dimension(600, 400));
            this.pack();
            this.setLocationRelativeTo(null);
            this.setVisible(true);
        }
    
        public static void main(String[] args) {
            EventQueue.invokeLater(() -> new WindowAddCameras());
        }
    }
    

    The CameraListPanel class is mostly a basic scrollable panel; the important part is that getScrollableTracksViewportWidth() returns true, which will cause the panel’s width to match the width of any JScrollPane viewport. This eliminates any need for a horizontal scrollbar.

    Of course, you will want to show all of your child components somehow. To do that, I would have the EventAdd class compute a height that can accommodate all of the children:

    public class EventAdd extends JPanel {
        private static final long serialVersionUID = 1;
    
        private final FlowLayout layout;
    
        public EventAdd() {
            layout = new FlowLayout(FlowLayout.LEFT);
            setLayout(layout);
        }
    
        @Override
        public Dimension getPreferredSize() {
            Rectangle childSize = new Rectangle();
            Component[] children = getComponents();
            for (Component child : children) {
                childSize.add(new Rectangle(child.getPreferredSize()));
            }
    
            Insets insets = getInsets();
    
            int hgap = layout.getHgap();
            int vgap = layout.getVgap();
            int childWidth = childSize.width + hgap;
    
            Dimension size;
            if (getParent() == null) {
                size = new Dimension(
                    children.length * (childWidth * hgap) + hgap,
                    childSize.height + vgap * 2);
            } else {
                int width = getParent().getWidth();
                width -= insets.left + insets.right;
                int childrenPerRow =
                    childWidth == 0 ? 0 : (width - hgap) / childWidth;
    
                int rows;
                if (childrenPerRow == 0) {
                    rows = 0;
                } else {
                    rows = children.length / childrenPerRow;
                    if (children.length % childrenPerRow > 0) {
                        rows++;
                    }
                }
                size = new Dimension(width,
                    vgap + rows * (childSize.height + vgap));
            }
    
            size.width += insets.left + insets.right;
            size.height += insets.top + insets.bottom;
    
            return size;
        }