1

I have a JFrame that consists of a potentially arbitrarily-large set of "headers" which can be clicked on to expose panels, one per header (conceptually similar to a vertically-arranged toolbar, with the headers being tabs that expose various tool panels). I want the headers to have a fixed amount of vertical space assigned to them, but to grow horizontally to fill the width of the frame. I want the panels exposed by the headers to consume all excess vertical space in the frame -- but only if they're visible; when invisible they should take zero space.

Unfortunately, no matter how I try to tweak the layout, I can't keep it from assigning extra space to the headers and to invisible panels. In the demo app, simply make the window larger. The desired behavior is that the blue "Hello 0" panel should grow taller while all other components stay "compact"; in practice I see empty space around the two "Compact label" labels and below the two lower "Click me" headers.

Thanks for your time!

public class Demo {
   public static void main(String[] args) {
      final JFrame frame = new JFrame("Inspector");
      frame.setLayout(new MigLayout("debug, fill, flowy, insets 0, gap 0"));
      frame.add(new JLabel("Compact label 1"));
      frame.add(new JLabel("Compact label 2"));
      for (int i = 0; i < 3; ++i) {
         JPanel wrapper = new JPanel(
               new MigLayout("debug, fill, flowy, insets 0, gap 0",
                  "", "0[]0[grow]0"));
         // Fixed-height header should grow horizontally only
         JPanel header = new JPanel(
               new MigLayout("flowx, fillx, insets 0, gap 0"));
         header.add(new JLabel("Click me!"), "growx, height 40!");
         header.setBackground(Color.RED);
         // Variably-sized body should fill any extra space.
         final JPanel body = new JPanel(new MigLayout("fill"));
         body.add(new JLabel("Hello, " + i), "grow");
         body.setBackground(Color.BLUE);
         body.setVisible(i == 0);
         wrapper.add(header, "growx");
         wrapper.add(body, "growy, hidemode 2");
         header.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
               boolean shouldShow = !body.isVisible();
               body.setVisible(shouldShow);
               frame.pack();
            }
         });
         frame.add(wrapper, "grow");
      }
      frame.pack();
      frame.setVisible(true);
   }
}
chris
  • 171
  • 10

3 Answers3

1

It might be best to use a JTabbedPane to get what you want.

Otherwise you can put percentage width and height constraints on your components. So for your headers you can tell them to use 100% of the width with:

wrapper.add(header, "growx , w 100%");

For the body you can put "h 100%" (you can use h or height)

wrapper.add(body, "growy, hidemode 2, h 100%");

It's not perfect, but hopefully this helps.

Matt Hubbard
  • 101
  • 3
  • `JTabbedPane` doesn't do what I want here, which is to have multiple different panels that can independently be shown/hidden by clicking on their header bars. I also have special rendering requirements for the headers, which I didn't include in my example. Your height trick works in that no extra space is allocated to invisible bodies or to headers, with the weird side-effect that when a header is clicked, the corresponding body appears with its minimum size -- preferable would be for all of the bodies to have the same height apportioned between them. – chris Feb 17 '16 at 16:45
  • Also, if you have two bodies visible, and repeatedly click on one header, then the other header will grow repeatedly. I've tried manually setting the size of each body (or of the respective wrappers), but the layout manage ignores my size settings, presumably because they're not in the form of layout constraints. – chris Feb 17 '16 at 17:26
  • Have a look at [JOutlookBar](http://www.informit.com/guides/content.aspx?g=java&seqNum=236). I'm sure you could edit this to get a bit closer to what you want. – Matt Hubbard Feb 18 '16 at 08:17
0

Ultimately I had to resort to removing and re-adding components to the layout manager, using different constraints depending on whether or not a given body was visible. This isn't ideal, but it does result in the desired behavior. MigLayout does have a setComponentConstraints() method which you'd think would work for this, but, like Matt Hubbard's suggestion, it doesn't recalculate sizes when the bodies are shown/hidden, resulting in an uneven distribution of space to the visible bodies.

Here's the updated demo program:

public class Demo {
   public static void main(String[] args) {
      final JFrame frame = new JFrame("Inspector");
      final JPanel contents = new JPanel(
            new MigLayout("debug, fill, flowy, insets 0, gap 0"));
      contents.setBackground(Color.GRAY);
      frame.add(contents);
      final ArrayList<Component> components = new ArrayList<Component>();
      components.add(new JLabel("I should not grow"));
      components.add(new JLabel("I shouldn't grow either"));
      for (int i = 0; i < 3; ++i) {
         // Fixed-height header should grow horizontally only
         final JPanel header = new JPanel(
               new MigLayout("flowx, fillx, insets 0, gap 0"));
         header.add(new JLabel("Click me!"), "growx, height 40!");
         header.setBackground(Color.RED);
         components.add(header);
         // Variably-sized body should fill any extra space.
         final JPanel body = new JPanel(new MigLayout("fill"));
         body.add(new JLabel("Hello, " + i), "grow");
         body.setBackground(Color.BLUE);
         body.setVisible(i == 0);
         components.add(body);
         header.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
               boolean shouldShow = !body.isVisible();
               body.setVisible(shouldShow);
               refill(frame, contents, components);
            }
         });
      }
      refill(frame, contents, components);
      frame.setVisible(true); 
   } 
   private static void refill(JFrame frame, JPanel contents,
         ArrayList<Component> components) {
      contents.removeAll();
      for (int i = 0; i < components.size(); ++i) {
         Component component = components.get(i);
         // First two components don't grow at all.
         if (i < 2) {
            contents.add(component, "gap 0, grow 0, pushy 0");
         }
         // Even-numbered components are headers.
         else if (i % 2 == 0) {
            contents.add(component, "gap 0, growx, growy 0, pushy 0");
         }
         // Odd-numbered components are bodies, only if visible.
         else if (component.isVisible()) {
            contents.add(component, "gap 0, grow, pushy 100");
         }
      }
      Dimension curSize = frame.getSize();
      frame.pack();
      frame.setSize(curSize);
   }
}
chris
  • 171
  • 10
0

I think what you're looking for is the hidemode command. This will prevent MigLayout from assigning space to a component that is not yet visible.

I'll leave this SO question here which explains how to use this option.

Jasperan
  • 2,154
  • 1
  • 16
  • 40