4

I searched a little bit and did not find a good answer to my problem.

I am working on a gui that has to be resizable. It contains a status JTextArea that is inside a JScrollPane. And this is my problem. As long as I don't manually resize my JFrame, the "initial" layout is kept and everything looks fine. As soon as I manually resize (if the JTextArea is already in scrolled mode), the layout gets messed up.

Here is a SSCCE (I got rid of most of the parts while keeping the structure of the code. I hope it's more readable that way):

import java.awt.Color;
import java.awt.Font;

import javax.swing.BorderFactory;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.border.TitledBorder;

import net.miginfocom.swing.MigLayout;

public class Tab extends JFrame {
  private static final long serialVersionUID = 1L;
  private JTextArea messageTextArea;
  private JPanel optionPanel, messagePanel;
  private JTabbedPane plotTabPane;

  public static void main(String[] args) {
    final Tab tab = new Tab();
    tab.setSize(1000, 600);
    tab.setVisible(true);
    new Thread(new Runnable() {
      @Override
      public void run() {
        int count = 0;
        tab.printRawMessage("start");
        while (true) {
          try {
            Thread.sleep(200);
          } catch (InterruptedException e) {}
          tab.printRawMessage("\ntestMessage" + count++);
        }
      }
    }).start();
  }

  public Tab() {
    super();
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    initComponents();
  }

  private void initComponents() {
    JPanel mainPanel = new JPanel();
    mainPanel.setLayout(new MigLayout("insets 0", "", ""));
    mainPanel.add(getLeftTopPanel(), "shrinky, top, w 450!");
    mainPanel.add(getRightPanel(), "spany 5, wrap, grow, pushx, wmin 400");
    mainPanel.add(getMessagePanel(), "pushy, growy, w 450!");

    JScrollPane contentScrollPane = new JScrollPane(mainPanel);
    contentScrollPane.setBorder(BorderFactory.createEmptyBorder());
    setContentPane(contentScrollPane);
  }

  protected JPanel getLeftTopPanel() {
    if (optionPanel == null) {
      optionPanel = new JPanel();
      optionPanel.setBorder(BorderFactory.createTitledBorder(null, "Configuration", TitledBorder.LEFT, TitledBorder.TOP, new Font("null", Font.BOLD, 12), Color.BLUE));
      optionPanel.setLayout(new MigLayout("insets 0", "", "top, align 50%"));

      JLabel label = new JLabel("Choose");
      label.setHorizontalAlignment(JLabel.RIGHT);
      optionPanel.add(label, "w 65!");
      optionPanel.add(new JSeparator(JSeparator.VERTICAL), "spany 5, growy, w 2!");
      optionPanel.add(new JComboBox(new String[] {"option1", "option2", "option3"}), "span, growx, wrap");

      optionPanel.add(new JLabel("Type"), "right");
      optionPanel.add(new JTextField("3"), "w 65!, split 2");
      optionPanel.add(new JLabel("Unit"), "wrap");

      optionPanel.add(new JLabel("Slide"), "right");
      optionPanel.add(new JSlider(0, 100), "span, growx, wrap");
    }
    return optionPanel;
  }

  protected JTabbedPane getRightPanel() {
    if (plotTabPane == null) {
      plotTabPane = new JTabbedPane();
      plotTabPane.add("Tab1", new JPanel());
      plotTabPane.add("Tab2", new JPanel());
    }
    return plotTabPane;
  }

  protected JPanel getMessagePanel() {
    if (messagePanel == null) {
      messagePanel = new JPanel();
      messagePanel.setBorder(BorderFactory.createTitledBorder(null, "Status Console", TitledBorder.LEFT, TitledBorder.TOP, new Font("null", Font.BOLD, 12), Color.BLUE));
      messagePanel.setLayout(new MigLayout("insets 0", "", "top, align 50%"));
      messagePanel.add(new JScrollPane(getMessageTextArea()), "push, grow");
    }
    return messagePanel;
  }

  protected JTextArea getMessageTextArea() {
    if (messageTextArea == null) {
      messageTextArea = new JTextArea();
      messageTextArea.setEditable(false);
      messageTextArea.setFont(new Font(null, Font.PLAIN, 20));
      messageTextArea.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
    }
    return messageTextArea;
  }

  public void printRawMessage(String rawMessage) {
    getMessageTextArea().append(rawMessage);
    getMessageTextArea().setCaretPosition(getMessageTextArea().getText().length());
  }
}

The layout stuff basically happens in initComponents().

To see the problem:

  1. Start the Application (I used miglayout-4.0-swing.jar).
  2. Wait a bit (don't resize the window), until there are enough messages to create the scrollbar in the status text area.
  3. Now this is what I want. The JTextArea goes all the way to the bottom of the JFrame and is scrolled if neccessary.
  4. Now resize the window. As you can see, everything gets messed up. It will only be fine, if the window is maximized.

Here are two screenshots. The first one is how I want it to be: Before resizing: good! The second one is after resizing: After resizing: bad!

My question: Can somebody tell me, how I keep the layout the way it is before resizing? I want to have the JTextArea go all the way down to the bottom of the window. And if neccessary, the scrollbar should appear. The only way, the status panel can go below the bottom of the window is, if the window is too small (because the configuration panel has a fixed height).

I hope I made myself clear. If not, please ask. ;)

EDIT: You can see the behaviour I want, if you remove the top JScrollPanel (the one that holds all the components). Just change

JScrollPane contentScrollPane = new JScrollPane(mainPanel);
contentScrollPane.setBorder(BorderFactory.createEmptyBorder());
setContentPane(contentScrollPane);

to

setContentPane(mainPanel);

to see what I mean. Unfortunately, this way I loose the scrollbars if the window is very small.

brimborium
  • 9,362
  • 9
  • 48
  • 76

2 Answers2

3

Focusing on your status area and using nested layouts produces the result shown below. Note in particular,

  • Use invokeLater() to construct the GUI on the EDT.
  • Use javax.swing.Timer to update the GUI on the EDT.
  • Use pack() to make the window fit the preferred size and layouts of its subcomponents.
  • Use the update policy of DefaultCaret to control scrolling.
  • Avoid needless lazy instantiation in public accessors.
  • Avoid setXxxSize(); override getXxxSize() judiciously.
  • Critically examine the decision to extend JFrame.

image

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.Timer;
import javax.swing.border.TitledBorder;
import javax.swing.text.DefaultCaret;

public class Tab extends JFrame {

    private JTextArea messageTextArea;
    private JPanel optionPanel, messagePanel;
    private JTabbedPane plotTabPane;

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {

                final Tab tab = new Tab();
                tab.setVisible(true);
                Timer t = new Timer(200, new ActionListener() {

                    int count = 0;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        tab.printRawMessage("testMessage" + count++);
                    }
                });
                t.start();
            }
        });
    }

    public Tab() {
        initComponents();
    }

    private void initComponents() {
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel mainPanel = new JPanel(new GridLayout(1, 0));
        Box leftPanel = new Box(BoxLayout.Y_AXIS);
        leftPanel.add(getLeftTopPanel());
        leftPanel.add(getMessagePanel());
        mainPanel.add(leftPanel);
        mainPanel.add(getRightPanel());
        this.add(mainPanel);
        this.pack();
        this.setLocationRelativeTo(null);
    }

    protected JPanel getLeftTopPanel() {
        optionPanel = new JPanel();
        optionPanel.setBorder(BorderFactory.createTitledBorder(null,
            "Configuration", TitledBorder.LEFT, TitledBorder.TOP,
            new Font("null", Font.BOLD, 12), Color.BLUE));
        JLabel label = new JLabel("Choose");
        label.setHorizontalAlignment(JLabel.RIGHT);
        optionPanel.add(label);
        optionPanel.add(new JSeparator(JSeparator.VERTICAL));
        optionPanel.add(new JComboBox(
            new String[]{"option1", "option2", "option3"}));
        optionPanel.add(new JLabel("Type"));
        optionPanel.add(new JTextField("3"));
        return optionPanel;
    }

    protected JTabbedPane getRightPanel() {
        plotTabPane = new JTabbedPane();
        plotTabPane.add("Tab1", new JPanel());
        plotTabPane.add("Tab2", new JPanel());
        return plotTabPane;
    }

    protected JPanel getMessagePanel() {
        messagePanel = new JPanel(new GridLayout());
        messagePanel.setBorder(BorderFactory.createTitledBorder(null,
            "Status Console", TitledBorder.LEFT, TitledBorder.TOP,
            new Font("null", Font.BOLD, 12), Color.BLUE));
        final JScrollPane sp = new JScrollPane(getMessageTextArea());
        sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        messagePanel.add(sp);
        return messagePanel;
    }

    protected JTextArea getMessageTextArea() {
        messageTextArea = new JTextArea("", 10, 19);
        messageTextArea.setEditable(false);
        messageTextArea.setFont(new Font(null, Font.PLAIN, 20));
        messageTextArea.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
        DefaultCaret caret = (DefaultCaret) messageTextArea.getCaret();
        caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
        return messageTextArea;
    }

    public void printRawMessage(String rawMessage) {
        messageTextArea.append(rawMessage + "\n");
    }
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    Wow, thanks a lot for all the input. I never really "learned" how to properly code large-scale gui's, so this is of great help to me. :) – brimborium Jun 15 '12 at 08:03
  • Regarding "critically examine the decision to extend JFrame": In my application, I have several of these Tabs (extending JPanel, I just changed that for this SSCCE) that are in a single JFrame (well, an extension). I have a lot of methods in this extended JFrame like `addPlotData()`, `setSelectionList()` and stuff like that, that controls what the gui is showing. Would it be better to have some kind of `GuiManager` class that holds this JFrame and does all the operations? Or what is the preferred way to do this? – brimborium Jun 15 '12 at 08:07
  • Oh and what do you mean with "Avoid needless lazy instantiation in public accessors."? – brimborium Jun 15 '12 at 08:27
  • Your main class _has-a_ `JFrame`; unless it override's `JFrame` methods, it's probably not _is-a_ `JFrame`. You have several methods starting `if (xPanel == null)`; I don't understand why they are `protected`. – trashgod Jun 15 '12 at 09:36
  • I made them protected because I don't need them outside of this class. But I need them in subclasses (for instance to change the border). – brimborium Jun 15 '12 at 15:03
  • A good resource on this [Bloch](http://java.sun.com/docs/books/effective/), [chapter 4](http://java.sun.com/docs/books/effective/toc.html). Is this more how you wanted scrolling to work on resize? – trashgod Jun 15 '12 at 17:14
  • I cannot say that because I don't own this book. But you helped me a lot with your answer and comments. Thanks. – brimborium Jun 18 '12 at 10:23
  • Sorry, I confounded two separate issues: Bloch addresses accessibility, not scrolling. Glad you got it sorted. – trashgod Jun 18 '12 at 13:47
  • I'd discourage the use of getter methods which return new instances of UI components. Imagine later I wanted to getLeftTopPanel().setBackground(Color.RED) in response to some user action, it wouldn't work as expected. – hudsonb Nov 14 '13 at 17:39
  • @hudsonb: Good point; the factory methods should probably be private; see also [Bloch](http://www.oracle.com/technetwork/java/effectivejava-136174.html), Item 17: _Design and document for inheritance or else prohibit it_. – trashgod Nov 14 '13 at 21:12
0

Add size constraints to your mainPanel in the initComponents method. For instance :

    mainPanel.setMinimumSize(new Dimension(400, 400));
    mainPanel.setPreferredSize(new Dimension(400, 400));
    mainPanel.setMaximumSize(new Dimension(400, 400));
Yanflea
  • 3,876
  • 1
  • 14
  • 14
  • Hm, I could do that, but then I would have to create a listener for the window resizing and set the constraints to the available window size. Also, this would result in a mess for small windows (so that my fixed-size configuration panel would not fit entirely into the `JFrame`... I think there must be a way to achieve this solely over layout. But thanks for your contribution, I have thought about that as well, it just seemed very messy to me. – brimborium Jun 14 '12 at 12:12
  • I maybe do not understand right, but I don't think you need a listener. The idea was just to specify a size under which you want the scrollpane to appear. Here I took 400*400 but you can change it if you consider it too small/big. You can also set it with the sum of the sizes of you sub-components. – Yanflea Jun 14 '12 at 12:23
  • Yes, that would work with the minimal size, but my problem (that the status panel is expanding very far down on resizing as can be seen in the second screenshot) still remains. – brimborium Jun 14 '12 at 12:28