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.

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
.