How to evenly space Strings in String.format that's a DefaultListModel within a JList?

2.7k views Asked by At

I already asked here, so please have a read through here. This question is partially solved. How can I add \t spacing in a String that belongs to a DefaultListModel which belongs to a JLIst?

Now I have another problem:

Please look at this example first

// Declarations
String string1 = "Eggs";
String string2 = "Whole Chicken";
int quantity1 = 100;
int quantity2 = 25;

// HTML and PRE tags are added before and after string
StringBuilder builder1 = new Stringbuilder;
builder.append("<html><pre>");
builder.append(String.format("%s \t %d", string1, quantity1));
builder.append("</pre></html>");

StringBuilder builder2 = new Stringbuilder;
builder.append("<html><pre>");
builder.append(String.format("%s \t %d", string2, quantity2));
builder.append("</pre></html>");

// JList is defined, and the builder is added to the list.
JList<String> list = new JList<String>();
list.addElement(builder1.toString());
list.addElement(builder2.toString());

This is how it will display all the items

Eggs    100
Whole Chicken    25

What I would like to happen is that all the quantity variables are aligned rather than spaced based on the previous variable. Any ideas how I could make this happen?

Thanks!

==========================

Another suggestion would be if it's possible to define the minimum length of a string within a String.format()?

3

There are 3 answers

1
bracco23 On BEST ANSWER

As @GhostCat said, using the width in the format string will solve your issue.

For more info, look at the documentation of the format string.

However, what you're trying to do makes me think that maybe, instead of using a JList, you could use a JTable?

import javax.swing.*;

public class ssce{
    public static void main(String... args){
        // Declarations
        String string1 = "Eggs";
        String string2 = "Whole Chicken";
        int quantity1 = 100;
        int quantity2 = 25;

        JFrame f = new JFrame("prova");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);


        String[] col = {"Ingredient", "Quantity"};
        Object[][] data = {
            {string1, new Integer(quantity1)},
            {string2, new Integer(quantity2)}
        };
        JTable tb = new JTable(data, col);
        f.add(tb);
        f.setSize(300, 400);
        f.setVisible(true);
    }
}
1
GhostCat On

You can simply use

%20s

for example - that will put 20 spaces into the formatted output.

So the trick is basically to first scan all the strings in your list, to then compute the reasonable number you need in your formatting pattern. In other words: you first want to determine the maximum length of the strings to display; to then compute appropriate indenting for example.

EDIT: there are many different ways to influence the formatting, see this question, or enter link description here, or best, study the javadoc that specifies all formatting patterns that exist!

0
René Link On

The most simplest solution is to use a monospaced font and string format.

public class Main {

  public static void main(String[] args) {
    JFrame frame = new JFrame();
    frame.setSize(320, 100);
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    Container contentPane = frame.getContentPane();

    DefaultListModel<String> defaultListModel = new DefaultListModel<>();
    defaultListModel.addElement(String.format("%-20s %d", "Eggs", 100));
    defaultListModel.addElement(String.format("%-20s %d", "Whole Chicken", 25));

    JList<String> jList = new JList<>(defaultListModel);
    Font defaultListFont = jList.getFont();
    jList.setFont(new Font("monospaced", defaultListFont.getStyle(), defaultListFont.getSize()));

    contentPane.add(jList);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }
}

which will result in

With monospaced font

A solution without using a monospaced font

If you want to align the column width depending on the ListModel I would listen to ListModel changes in order to calculate the longest string in the model and use a custom renderer.

public static void main(String[] args) {
  JFrame frame = new JFrame();
  frame.setSize(320, 150);
  frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

  Container contentPane = frame.getContentPane();

  DefaultListModel<String[]> defaultListModel = new DefaultListModel<>();
  defaultListModel.addElement(new String[]{"Eggs", "100"});
  defaultListModel.addElement(new String[]{"The longest Entry in the list", "12125"});
  defaultListModel.addElement(new String[]{"Whole Chicken", "25"});

  JList<String[]> jList = new JList<>(defaultListModel);

  ListModelAwareColumnWidthProvider listModelAwareColumnWidthProvider = new ListModelAwareColumnWidthProvider();
  listModelAwareColumnWidthProvider.setListModel(defaultListModel);

  ColumnCellRenderer columnCellRenderer = new ColumnCellRenderer(listModelAwareColumnWidthProvider);
  jList.setCellRenderer(columnCellRenderer);

  contentPane.add(jList);
  frame.setLocationRelativeTo(null);
  frame.setVisible(true);
}

and you have to implement something like that

A renderer that supports columns

class ColumnCellRenderer implements ListCellRenderer<String[]> {

  private ColumnWidthProvider columnWidthProvider;

  public ColumnCellRenderer(ColumnWidthProvider columnWidthProvider) {
    this.columnWidthProvider = columnWidthProvider;
  }


  @Override
  public Component getListCellRendererComponent(JList<? extends String[]> list, String[] value, int index, boolean isSelected, boolean cellHasFocus) {
    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));

    for (int i = 0; i < value.length; i++) {
      String valueElement = value[i];

      JLabel label = new JLabel(valueElement);

      align(label, i);
      panel.add(label);
    }

    applyColors(list, panel, isSelected);

    return panel;
  }

  private void applyColors(JList<?> list, JComponent component, boolean isSelected) {
    if (isSelected) {
      component.setForeground(list.getSelectionForeground());
      component.setBackground(list.getSelectionBackground());
    } else {
      component.setForeground(list.getForeground());
      component.setBackground(list.getBackground());
    }
  }

  private void align(JComponent component, int columnIndex) {
    Font font = component.getFont();
    FontMetrics fontMetrics = component.getFontMetrics(font);
    int fontHeight = fontMetrics.getHeight();

    int columnWidth = columnWidthProvider.getColumnWidth(fontMetrics, columnIndex);
    component.setPreferredSize(new Dimension(columnWidth, fontHeight));
  }

}

an interface for the renderer to get the column width

interface ColumnWidthProvider {
  public int getColumnWidth(FontMetrics fontMetrics, int columnIndex);
}

and an implementation of the ColumnWidthProvider that observes the ListModel to content changes

class ListModelAwareColumnWidthProvider implements ColumnWidthProvider {

  private class ListModelChangeAdapter implements ListDataListener {
    @Override
    public void intervalAdded(ListDataEvent e) {
      initializeColumnWidths();
    }

    @Override
    public void intervalRemoved(ListDataEvent e) {
      initializeColumnWidths();
    }

    @Override
    public void contentsChanged(ListDataEvent e) {
      initializeColumnWidths();
    }
  }

  private int defaultColumnWidth = 100;

  private ListModelChangeAdapter listModelChangeAdapter = new ListModelChangeAdapter();
  private ListModel<String[]> listModel;
  private java.util.List<String> longestColumnStrings = new ArrayList<>();

  public void setListModel(ListModel<String[]> listModel) {
    if (this.listModel != null) {
      this.listModel.removeListDataListener(listModelChangeAdapter);
    }

    this.listModel = listModel;
    initializeColumnWidths();

    if (this.listModel != null) {
      this.listModel.addListDataListener(listModelChangeAdapter);
    }
  }

  private void initializeColumnWidths() {
    longestColumnStrings.clear();

    if (listModel != null) {
      int size = listModel.getSize();
      for (int i = 0; i < size; i++) {
        String[] elementAt = listModel.getElementAt(i);

        for (int columnIndex = 0; columnIndex < elementAt.length; columnIndex++) {
          String columnValue = elementAt[columnIndex];

          while (columnIndex >= longestColumnStrings.size()) {
            longestColumnStrings.add(null);
          }

          String lastLongestColumnString = longestColumnStrings.get(columnIndex);
          if (lastLongestColumnString == null) {
            longestColumnStrings.set(columnIndex, columnValue);
          } else if (columnValue.length() > lastLongestColumnString.length()) {
            longestColumnStrings.set(columnIndex, columnValue);
          }
        }
      }
    }
  }

  public void setDefaultColumnWidth(int defaultColumnWidth) {
    this.defaultColumnWidth = defaultColumnWidth;
  }

  @Override
  public int getColumnWidth(FontMetrics fontMetrics, int columnIndex) {
    if (columnIndex < longestColumnStrings.size()) {
      String longestColumnString = longestColumnStrings.get(columnIndex);
      return fontMetrics.stringWidth(longestColumnString);
    }
    return defaultColumnWidth;
  }
}

which will result in

With renderer