Reputation: 584
I'm trying to build a panel with components laid out horizontally, with auto-wrap if not enough place and a vertical scrollbar.
Something like this:
+-----------------+
|[1][2][3][4][5] |
| |
+-----------------+
reducing the width:
+-----------+
|[1][2][3] |
|[4][5] |
+-----------+
reducing the width again, the scrollbar appears:
+---------+
|[1][2] ^|
|[3][4] v|
+---------+
I'm not far from a solution:
public class TestFlow extends JFrame {
public TestFlow() {
getContentPane().setLayout(new BorderLayout());
JPanel panel = new JPanel(new FlowLayout());
JScrollPane scroll = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
getContentPane().add(scroll, BorderLayout.CENTER);
panel.add(new MyComponent("A"));
panel.add(new MyComponent("B"));
panel.add(new MyComponent("C"));
panel.add(new MyComponent("D"));
panel.add(new MyComponent("E"));
panel.add(new MyComponent("F"));
panel.add(new MyComponent("G"));
panel.add(new MyComponent("H"));
panel.add(new MyComponent("I"));
panel.add(new MyComponent("J"));
panel.add(new MyComponent("K"));
panel.add(new MyComponent("L"));
panel.add(new MyComponent("M"));
panel.add(new MyComponent("N"));
panel.add(new MyComponent("O"));
scroll.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
Dimension max=((JScrollPane)e.getComponent()).getViewport().getExtentSize();
// panel.setMaximumSize(new Dimension(max.width,Integer.MAX_VALUE));
panel.setPreferredSize(new Dimension(max.width,Integer.MAX_VALUE));
// panel.setPreferredSize(max);
panel.revalidate();
// panel.repaint();
// System.out.println(panel.getSize().width+"--"+max.width);
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(500, 200);
setLocationRelativeTo(null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new TestFlow().setVisible(true));
}
private static class MyComponent extends JLabel {
public MyComponent(String text) {
super(String.join("", Collections.nCopies((int)(Math.round(Math.random()*4)+4), text)));
setOpaque(true);
setBackground(Color.YELLOW);
}
}
}
, but have still strange behaviours:
With the solution panel.setPreferredSize(new Dimension(max.width,Integer.MAX_VALUE));
With the solution panel.setPreferredSize(max);
Any suggestion in that code ?
[EDIT] I've complicated the original code, and applied the suggestions that were provided until now.
For design purpose, I would like to use a MigLayout on top of my Panel. At start, everything is well laid out. When enlarging the window, it works too. But not on reducing the window. The addComponentListener
does not bring any addedvalue.
public class TestMigFlow extends JFrame {
public TestMigFlow() {
getContentPane().setLayout(new BorderLayout());
JPanel panel = new JPanel(new MigLayout("debug, fill, flowy", "[fill]"));
JScrollPane scroll = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
getContentPane().add(scroll, BorderLayout.CENTER);
panel.add(new JLabel("A title as spearator "), "growy 0");
JPanel sub = new JPanel(new WrapLayout());
// panel.add(sub, "growx 0"); // Works well on shrink but not on grow
panel.add(sub); // Works well on grow but not on shrink
sub.add(new MyComponent("A"));
sub.add(new MyComponent("B"));
sub.add(new MyComponent("C"));
sub.add(new MyComponent("D"));
sub.add(new MyComponent("E"));
sub.add(new MyComponent("F"));
sub.add(new MyComponent("G"));
sub.add(new MyComponent("H"));
sub.add(new MyComponent("I"));
sub.add(new MyComponent("J"));
sub.add(new MyComponent("K"));
sub.add(new MyComponent("L"));
sub.add(new MyComponent("M"));
sub.add(new MyComponent("N"));
sub.add(new MyComponent("O"));
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
Dimension max = new Dimension(scroll.getWidth(), Short.MAX_VALUE);
panel.setMaximumSize(max);
panel.repaint();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(200, 500);
setLocationRelativeTo(null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new TestMigFlow().setVisible(true));
}
private static class MyComponent extends JLabel {
public MyComponent(String text) {
super(String.join("", Collections.nCopies((int) (Math.round(Math.random() * 4) + 4), text)));
setOpaque(true);
setBackground(Color.YELLOW);
}
}
Upvotes: 5
Views: 547
Reputation: 584
The solution is
addComponentListener
at the panel level and not the frame levelpreferredLayoutSize
the compute the adequate size of the panelThe negative-side of the solution is a bit of flickering when resizing the window.
public class TestMigFlow2 extends JFrame {
public TestMigFlow2() {
getContentPane().setLayout(new BorderLayout());
JPanel panel = new JPanel(new MigLayout("fillx, flowy", "[fill]"));
JScrollPane scroll
= new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
getContentPane().add(scroll, BorderLayout.CENTER);
panel.add(new MySeparator("sep1"), "growy 0, shrinky 100");
JPanel sub = new JPanel(new WrapLayout());
panel.add(sub, "shrinky 100"); // Works well on grow but not on shrink
sub.add(new MyComponent("A"));
sub.add(new MyComponent("B"));
sub.add(new MyComponent("C"));
sub.add(new MyComponent("D"));
sub.add(new MyComponent("E"));
sub.add(new MyComponent("F"));
sub.add(new MyComponent("G"));
sub.add(new MyComponent("H"));
sub.add(new MyComponent("I"));
sub.add(new MyComponent("J"));
sub.add(new MyComponent("K"));
sub.add(new MyComponent("L"));
sub.add(new MyComponent("M"));
sub.add(new MyComponent("N"));
sub.add(new MyComponent("O"));
panel.add(new MySeparator("sep2"), "growy 0, shrinky 100");
panel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
WrapLayout wl = (WrapLayout) sub.getLayout();
Dimension prefdim = wl.preferredLayoutSize(sub);
sub.setPreferredSize(prefdim);
panel.revalidate();
panel.repaint();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(200, 500);
setLocationRelativeTo(null);
}
The same also works with a BoxLayout instead of a MigLayout with 2 additions:
giving the panel a max height along to pref height to prevent the BoxLayout to share the extra space between the panel and the Vertical glue.
public TestBoxLayout() {
getContentPane().setLayout(new BorderLayout());
JPanel panel = new JPanel();
BoxLayout layout = new BoxLayout(panel, BoxLayout.PAGE_AXIS);
panel.setLayout(layout);
JScrollPane scroll
= new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
getContentPane().add(scroll, BorderLayout.CENTER);
JLabel separator;
separator = new MySeparator("Sep1");
panel.add(separator);
JPanel sub = new MyPanel(new WrapLayout());
sub.setAlignmentX(0f);
panel.add(sub);
sub.add(new MyComponent("A"));
sub.add(new MyComponent("B"));
sub.add(new MyComponent("C"));
sub.add(new MyComponent("D"));
sub.add(new MyComponent("E"));
sub.add(new MyComponent("F"));
sub.add(new MyComponent("G"));
sub.add(new MyComponent("H"));
sub.add(new MyComponent("I"));
sub.add(new MyComponent("J"));
sub.add(new MyComponent("K"));
sub.add(new MyComponent("L"));
sub.add(new MyComponent("M"));
sub.add(new MyComponent("N"));
sub.add(new MyComponent("O"));
separator = new MySeparator("Sep2");
panel.add(separator);
// -- Un filler --
panel.add(Box.createVerticalGlue());
panel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
WrapLayout wl=(WrapLayout) sub.getLayout();
Dimension prefdim=wl.preferredLayoutSize(sub);
sub.setPreferredSize(prefdim);
// Force the max height = pref height to prevent the BoxLayout dispatching the remaining height between the panel and the glue.
Dimension maxdim=new Dimension(Short.MAX_VALUE,prefdim.height);
sub.setMaximumSize(maxdim);
panel.revalidate();
panel.repaint();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(200, 500);
setLocationRelativeTo(null);
}
Upvotes: 1
Reputation: 11327
Probably I misunderstood your question, but when I add revalidate
call, all works fine also when the panel is shrinking.
Dimension max = new Dimension(scroll.getWidth(), Short.MAX_VALUE);
panel.setMaximumSize(max);
// trigger layout recalculation
panel.revalidate();
panel.repaint();
Here is a little bit sophisticated improvement (to provide better size consideration for scroll bar and layout), when it's required ;)
public class TestMigFlow extends JFrame {
private Integer layoutDecr;
public TestMigFlow() {
getContentPane().setLayout(new BorderLayout());
JPanel panel = new JPanel(new MigLayout("debug, fill, flowy", "[fill]"));
JScrollPane scroll =
new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
getContentPane().add(scroll, BorderLayout.CENTER);
panel.add(new JLabel("A title as spearator "), "growy 0");
JPanel sub = new JPanel(new WrapLayout());
// panel.add(sub, "growx 0"); // Works well on shrink but not on grow
panel.add(sub); // Works well on grow but not on shrink
sub.add(new MyComponent("A"));
sub.add(new MyComponent("B"));
sub.add(new MyComponent("C"));
sub.add(new MyComponent("D"));
sub.add(new MyComponent("E"));
sub.add(new MyComponent("F"));
sub.add(new MyComponent("G"));
sub.add(new MyComponent("H"));
sub.add(new MyComponent("I"));
sub.add(new MyComponent("J"));
sub.add(new MyComponent("K"));
sub.add(new MyComponent("L"));
sub.add(new MyComponent("M"));
sub.add(new MyComponent("N"));
sub.add(new MyComponent("O"));
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (layoutDecr == null && panel.getWidth() > 0) {
layoutDecr = panel.getWidth() - sub.getWidth();
}
JScrollBar vBar = scroll.getVerticalScrollBar();
int decr = vBar.isVisible() ? vBar.getPreferredSize().width : 0;
decr += layoutDecr == null ? 0 : layoutDecr;
Dimension max = new Dimension(scroll.getWidth() - decr, Short.MAX_VALUE);
sub.setMaximumSize(max);
sub.revalidate();
sub.repaint();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(200, 500);
setLocationRelativeTo(null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new TestMigFlow().setVisible(true));
}
private static class MyComponent extends JLabel {
public MyComponent(String text) {
super(String.join("", Collections.nCopies((int) (Math.round(Math.random() * 4) + 4), text)));
setOpaque(true);
setBackground(Color.YELLOW);
}
}
}
Upvotes: 0
Reputation: 17955
The code below works for me.
I have split the code into a component that does all the layout and the test code to verify that it works (in the static main
) method.
Instead of hooking to resize events on the JScrollPane, I do that on the whole TestFlow (TF) component - I believe this is equivalent, but it seems safer. Whenever the TF is resized, I update the preferred size of the panel, just as you did - but there are two steps that you were not taking:
Diving into how Swing handles layout and painting, and which of the several similar-looking methods get called when by whom always makes me think of dark magic.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
public class TestFlow extends JPanel {
JPanel panel;
JScrollPane scroll;
public TestFlow() {
panel = new JPanel(new FlowLayout());
scroll = new JScrollPane(panel,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
setLayout(new BorderLayout());
add(scroll, BorderLayout.CENTER);
addComponentListener(new ComponentAdapter(){
@Override
public void componentResized(ComponentEvent e){
Rectangle lastChildBounds = panel
.getComponent(panel.getComponentCount() - 1).getBounds();
Dimension preferred = new Dimension(
scroll.getWidth(),
lastChildBounds.y + lastChildBounds.height);
if (scroll.getVerticalScrollBar().isVisible()) {
preferred.width -= scroll.getVerticalScrollBar().getWidth();
}
// System.err.println("setting inner panel size to " + preferred);
panel.setPreferredSize(preferred);
panel.repaint();
}
});
}
public JPanel getInnerPanel() { return panel; }
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// get a TestFlow instance and fill it with random labels
TestFlow tf = new TestFlow();
Random r = new Random();
for (String s : "A B C D E F G H I J K L M N O".split(" ")) {
String labelText = String.join("", Collections.nCopies(4+r.nextInt(3)*4, s));
JLabel label = new JLabel(labelText);
label.setOpaque(true);
label.setBackground(Color.YELLOW);
tf.getInnerPanel().add(label);
}
// display it in a window
JFrame jf = new JFrame("test");
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.add(tf, BorderLayout.CENTER);
jf.setSize(500, 200);
jf.setLocationRelativeTo(null);
jf.setVisible(true);
});
}
}
Upvotes: 0