0

I am trying to solve the following problem: I have a program, where text fields are being added dynamically to a JPanel, but when too many fields are added, I want a scrollbar to be shown, so that the user doesn't have to resize the window in order to see all the fields. So far I can generate the fields without a problem, but adding the scrollbar seems not to be working... I have the following code:

import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.awt.FlowLayout;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import javax.swing.JScrollBar;

public class AddRuleFrame extends JFrame implements ActionListener {

JPanel panel;
JPanel buttonPanel;
JScrollPane scroll;
private JButton btnAddType;
private JButton btnDeleteField;
private JButton btnSaveRule;

public AddRuleFrame() {
    getContentPane().setLayout(new BorderLayout());       
    this.buttonPanel=new JPanel();
    getContentPane().add(buttonPanel, BorderLayout.SOUTH);
    buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));

    //Initializing the JScrollPane
    scroll = new JScrollPane(this.panel);
    scroll.setViewportView(this.panel);


    btnAddType = new JButton("Add type");
    btnAddType.addActionListener(this);
    buttonPanel.add(btnAddType);

    btnDeleteField = new JButton("Delete field");
    btnDeleteField.addActionListener(this);
    buttonPanel.add(btnDeleteField);

    btnSaveRule = new JButton("Save rule");
    buttonPanel.add(btnSaveRule);
    this.panel = new JPanel();     
    this.panel.setLayout(new FlowLayout());
    getContentPane().add(panel, BorderLayout.CENTER);

   setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(538, 487);
    setVisible(true);
  }

  public void actionPerformed(ActionEvent evt) {
    if(evt.getSource()==this.btnAddType){
        this.panel.add(new JTextField(20));
        this.panel.revalidate();
    }
    if(evt.getSource()==this.btnDeleteField){
        System.out.println("delete pressed");
    }
    if(evt.getSource()==this.btnSaveRule){
        System.out.println("");
    }

    validate();
  }

   public static void main(String[] args) {
    AddRuleFrame frame = new AddRuleFrame();
}
}

Thank you!

newbie
  • 31
  • 6

2 Answers2

4

The issue with the scroll pane simply was that you were adding it to nothing and setting its viewport before the panel was intialized.

However, I noticed a few other issues.

One issue is that FlowLayout adds components horizontally, so I've changed the layout of the panel to a BoxLayout and created a small subclass of JTextField to override the maximum size. (BoxLayout uses maximum sizes to size components, so without doing that the text fields get stretched to the height of the panel.)

I also used SwingUtilities.invokeLater to start the program on the Swing thread, as show in the Initial Threads tutorial.

Instead of calling setSize on a JFrame directly, I overrode getPreferredSize and calculated a size dynamically based on the screen dimensions, then called pack() to size the components automatically. In general, Swing isn't designed for explicitly setting pixel dimensions.

enter image description here

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class AddRuleFrame extends JFrame implements ActionListener {
    private JPanel panel;
    private JPanel buttonPanel;
    private JScrollPane scroll;
    private JButton btnAddType;
    private JButton btnDeleteField;
    private JButton btnSaveRule;

    public AddRuleFrame() {
        getContentPane().setLayout(new BorderLayout());       
        buttonPanel = new JPanel();
        getContentPane().add(buttonPanel, BorderLayout.SOUTH);
        buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));

        btnAddType = new JButton("Add type");
        btnAddType.addActionListener(this);
        buttonPanel.add(btnAddType);

        btnDeleteField = new JButton("Delete field");
        btnDeleteField.addActionListener(this);
        buttonPanel.add(btnDeleteField);

        btnSaveRule = new JButton("Save rule");
        buttonPanel.add(btnSaveRule);

        panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        scroll = new JScrollPane(panel,
                                 JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                                 JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setLocationRelativeTo(null); // this centers the window
        setVisible(true);
    }

    @Override
    public void actionPerformed(ActionEvent evt) {
        if (evt.getSource() == btnAddType) {
            panel.add(new BoxyTextField(20));
            panel.revalidate();
        }
        validate();
    }

    class BoxyTextField extends JTextField {
        BoxyTextField(int width) {
            super(width);
        }

        @Override
        public Dimension getMaximumSize() {
            Dimension size = super.getMaximumSize();
            size.height = getPreferredSize().height;
            return size;
        }
    }

    @Override
    public Dimension getPreferredSize() {
        // See my exchange with MadProgrammer in the comments for
        // a discussion of whether Toolkit#getScreenSize() is an
        // appropriate way to get the screen dimensions for sizing
        // a window.
//        Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
        // This is the correct way, as suggested in the documentation
        // for java.awt.GraphicsEnvironment#getMaximumWindowBounds():
        GraphicsConfiguration config = getGraphicsConfiguration();
        Insets  insets = Toolkit.getDefaultToolkit().getScreenInsets(config);
        Dimension size = config.getBounds().getSize();
        size.width  -= insets.left + insets.right;
        size.height -= insets.top  + insets.bottom;
        // Now we have the actual available space of the screen
        // so we can compute a relative size for the JFrame.
        size.width   = size.width / 3;
        size.height  = size.height * 2 / 3;
        return size;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                AddRuleFrame frame = new AddRuleFrame();
            }
        });
    }
}

For your comment, generally the correct way to add a gap between components with a BoxLayout is to use a filler component. This is discussed in the tutorial, which I already linked to.

So you might do something like this:

@Override
public void actionPerformed(ActionEvent evt) {
    if (evt.getSource() == btnAddType) {
        if (panel.getComponentCount() > 0) {
            panel.add(Box.createVerticalStrut(10));
        }
        panel.add(new BoxyTextField(20));
        panel.revalidate();
    }

However, this creates a bit of an issue if you're planning on removing stuff dynamically, because you need to remember to remove the filler component as well:

    if (evt.getSource() == btnDeleteField) {
        int lastZIndex = panel.getComponentCount() - 1;
        if (lastZIndex >= 0) {
            panel.remove(lastZIndex);
            if (lastZIndex > 0) {
                panel.remove(lastZIndex - 1);
            }
            panel.revalidate();
        }
    }
    validate();
    panel.repaint();
}

So I think the best option is that instead of extending JTextField and adding the the text field and filler to the panel directly, extend JPanel, and do something like this:

class BoxyTextFieldCell extends JPanel {
    JTextField jTextField;

    BoxyTextFieldCell(int width, int margin) {
        jTextField = new JTextField(width);
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        add(jTextField);
        add(Box.createVerticalStrut(margin));
    }

    @Override
    public Dimension getMaximumSize() {
        Dimension size = super.getMaximumSize();
        size.height = getPreferredSize().height;
        return size;
    }
}

@Override
public void actionPerformed(ActionEvent evt) {
    if (evt.getSource() == btnAddType) {
        panel.add(new BoxyTextFieldCell(20, 10));
        panel.revalidate();
    }
    if (evt.getSource() == btnDeleteField) {
        int lastZIndex = panel.getComponentCount() - 1;
        if (lastZIndex >= 0) {
            panel.remove(lastZIndex);
            panel.revalidate();
        }
    }
    validate();
    panel.repaint();
}

Doing something like that certainly leaves you with a lot of flexibility.

Otherwise, I think you could also use an editable JTable with a single column (which more or less behaves just like a stack of text fields) and use setRowMargin(int). I guess it might end up being easier to use a JTable if you aren't very comfortable with using layouts yet. See e.g. here for examples of adding and removing rows in a JTable.

Community
  • 1
  • 1
Radiodef
  • 37,180
  • 14
  • 90
  • 125
  • Thank you very much ! It works perfect! I just have one more question: How can I set the "vertical gap" between the text fields? – newbie May 02 '17 at 16:04
  • See my edit for some suggestions for that. You might also find that using a `JTable` is easier, which is something I didn't think of when originally writing my answer. – Radiodef May 02 '17 at 17:48
  • *"I overrode getPreferredSize and calculated a size dynamically based on the screen dimensions"* - Then `Toolkit#getScreenSize` isn't the best choice for the tasks, as it doesn't take into account the "viewable" area (ie the space excluding things like the taskbar/dock). It might be better to use the `Scrollable` interface and define the result for `getPreferredScrollableViewportSize` which affect the `JScrollPane`'s `preferredSize` – MadProgrammer May 02 '17 at 22:33
  • @MadProgrammer That's true. Actually I think the best way to do what I intended is as described [here](http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsEnvironment.html#getMaximumWindowBounds--) by using `jFrame.getGraphicsConfiguration().getBounds()` and then subtracting `Toolkit.getScreenInsets(...)`. It's kind of inconvenient to explain in a simple answer, though. Usually I just ignore the issue altogether. – Radiodef May 02 '17 at 23:09
  • 1
    @Radiodef Up until we get another question about why the window doesn't look "centered" or appears beneath those widgets :P - There's also `getScreenInsets` from `Toolkit` ;) – MadProgrammer May 03 '17 at 00:05
  • The delete event didn't work as expected,but I edited it: if (evt.getSource() == btnDeleteField) { int comps = panel.getComponentCount(); if (comps == 1) { panel.remove(comps-1); panel.revalidate(); } if(comps>1){ panel.remove(comps-1); panel.remove(comps-2); panel.revalidate(); } } validate(); panel.repaint(); – newbie May 04 '17 at 11:55
2

There are two problems :

1) You never add your JScrollPane to anything.

2) You set its viewport view to a Component that is null (not yet initialized).

This is a modified version of your constructor that fixes both problems (see comments in the code) :

public AddRuleFrame() {
    getContentPane().setLayout(new BorderLayout());
    buttonPanel = new JPanel();
    getContentPane().add(buttonPanel, BorderLayout.SOUTH);
    buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));

    //Initializing the JScrollPane

    btnAddType = new JButton("Add type");
    btnAddType.addActionListener(this);
    buttonPanel.add(btnAddType);

    btnDeleteField = new JButton("Delete field");
    btnDeleteField.addActionListener(this);
    buttonPanel.add(btnDeleteField);

    btnSaveRule = new JButton("Save rule");
    buttonPanel.add(btnSaveRule);
    panel = new JPanel();
    panel.setLayout(new FlowLayout());

    scroll = new JScrollPane(panel);
    scroll.setViewportView(panel);// Set the viewport view only when the panel has been initialized 
    getContentPane().add(scroll, BorderLayout.CENTER);// Add the scrollpane, not the panel

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(538, 487);
    setVisible(true);
}
Arnaud
  • 17,229
  • 3
  • 31
  • 44
  • There is a problem with the scroll direction. If you now add the TextFields they are added horizontally. – M. Haverbier May 02 '17 at 13:59
  • 1
    @M.Schwarzer-Haverbier : Sure, if there is an horizontal scrolling, there is no "max" width of the panel, so everything gets added "on the same line" by the `FlowLayout`. However, this behavior can change if the layout manager is different (e.g `panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));`). – Arnaud May 02 '17 at 14:12