Home > Enterprise >  JTextField width according to max input, calculating string width
JTextField width according to max input, calculating string width

Time:10-19

I would like to have a TextField which shows by its width the maximum of allowed input characters.
I used Font.getStringBounds to calculate the width of the maximum length. But to my surprise the resulting width in the example looks like omitting a complete character!

  • Using FontMetrics.stringWidth supplies the same width value.
  • Creating the textField just using the JTextField(int columns) constructor gave a better result, but the field is still too small.

What is missing?

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import javax.swing.*;

public class InputWidthTextField extends JFrame {

  public InputWidthTextField() {
    setSize(250, 230);
    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
    setLayout(new FlowLayout(FlowLayout.CENTER, 60, 20));
    Font font= new Font(Font.MONOSPACED, Font.PLAIN, 12);
    int max= 4;
    JLabel lb= new JLabel("Enter up to " max " characters:");
    add(lb);
    MaxInputWidthTextField tf= new MaxInputWidthTextField(font, max);
    add(tf);
    setVisible(true);
  }


  public static void main(String... args) {
    EventQueue.invokeLater(InputWidthTextField::new);
  }


  class MaxInputWidthTextField extends JTextField {
    public MaxInputWidthTextField(Font font, int maxCount) {
      super();
      if (font==null)
        font= getFont();
      else
        setFont(font);
      FontMetrics fm= getFontMetrics(font);
      FontRenderContext frc= fm.getFontRenderContext();
      String buf= "8".repeat(maxCount);
      Rectangle2D rect= font.getStringBounds(buf, frc);
      Dimension dim= new Dimension((int)rect.getWidth(), (int)rect.getHeight());
      setPreferredSize(dim);
      System.out.println((int)rect.getWidth() ", " (int)rect.getHeight());
      System.out.println(fm.stringWidth(buf));
      System.out.println(getGraphics());
    }    
  }

}

CodePudding user response:

I'm not even going to try and figure out all the things that are wrong with your code, instead, I'm going to demonstrate what you should be doing instead.

Take a look at Example

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = GridBagConstraints.REMAINDER;

            add(new JTextField(8), gbc);
            add(new MaxInputWidthTextField(null, 8), gbc);
        }

    }

    class MaxInputWidthTextField extends JTextField {

        public MaxInputWidthTextField(Font font, int maxCount) {
            super();
            if (font == null) {
                font = getFont();
            } else {
                setFont(font);
            }
            FontMetrics fm = getFontMetrics(font);
            FontRenderContext frc = fm.getFontRenderContext();
            String buf = "8".repeat(maxCount);
            Rectangle2D rect = font.getStringBounds(buf, frc);
            Dimension dim = new Dimension((int) rect.getWidth(), (int) rect.getHeight());
            setPreferredSize(dim);
        }
    }
}

What I do recommend is having a look at the pre-existing implementation

The JTextField#getPreferredSize implementation looks like...

/**
 * Returns the column width.
 * The meaning of what a column is can be considered a fairly weak
 * notion for some fonts.  This method is used to define the width
 * of a column.  By default this is defined to be the width of the
 * character <em>m</em> for the font used.  This method can be
 * redefined to be some alternative amount
 *
 * @return the column width &gt;= 1
 */
protected int getColumnWidth() {
    if (columnWidth == 0) {
        FontMetrics metrics = getFontMetrics(getFont());
        columnWidth = metrics.charWidth('m');
    }
    return columnWidth;
}

/**
 * Returns the preferred size <code>Dimensions</code> needed for this
 * <code>TextField</code>.  If a non-zero number of columns has been
 * set, the width is set to the columns multiplied by
 * the column width.
 *
 * @return the dimension of this textfield
 */
public Dimension getPreferredSize() {
    Dimension size = super.getPreferredSize();
    if (columns != 0) {
        Insets insets = getInsets();
        size.width = columns * getColumnWidth()  
            insets.left   insets.right;
    }
    return size;
}

Instead of "setting" the preferred size, I would "consider" overriding the getPreferredSize and injecting your "custom" workflow into instead

CodePudding user response:

@Camickr
Rob, you seem to have been very strict yesterday. ;-)
But surely, not understanding is also on my side. For when I tell you that in a monospaced font both "W" and "i" have the same width, this will certainly be nothing new to you. So I don't understand why you are asking me for an MRE or why only "WWWW" didn't work for you. I can only imagine, you didn't use a monospace font, but this is a sine qua non when calculating the maximum possible width beforehand, given a maximum character count, for any string the user will enter.
I hope, we don't see different things on our screens due to resolution or scaling differences. So I send a picture of how it looks like at my site. Whenever I enter the last character in one of the textfields, the whole string is shifted to the left, so that the first character gets partly hidden. Hence in my example the zero is always cut. You may deem this to be acceptable, but I would prefer to have each character fully visible.
enter image description here

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

public class FieldWidthTest extends JFrame {

  public FieldWidthTest() {
    setSize(250, 230);
    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
    setLayout(new GridLayout(4, 1));
    Font font= new Font(Font.MONOSPACED, Font.PLAIN, 12);
    int max= 4;
    for (int i=0; i<4; i  ) {
      JPanel p= new JPanel();
      JTextField tf= new JTextField(max  );
      tf.setFont(font);
      p.add(tf);
      add(p);
    }
    setVisible(true);
  }


  public static void main(String... args) {
    EventQueue.invokeLater(FieldWidthTest::new);
  }

}

CodePudding user response:

@MadProgrammer
Overriding getPreferredSize(), as you suggested, worked well; but using setPreferredSize() also did the job and saves the overriding.
It was an error not to create and set a monospaced font in case the paramter was null. The solution, however, was to add 2 to the dimension's width, which worked for all font sizes. Camickr would call this justifiably "using magic numbers", but I don't know where to get this number from.
Thanks to all.

  class MaxInputWidthTextField extends JTextField {
    public MaxInputWidthTextField(Font monoFont, int maxCount) {
      super();
      if (monoFont==null) monoFont= new Font(Font.MONOSPACED, Font.PLAIN, 13);
      setFont(monoFont);
      FontMetrics fm= getFontMetrics(monoFont);
      FontRenderContext frc= fm.getFontRenderContext();
      String buf= "8".repeat(maxCount);
      Rectangle2D rect= monoFont.getStringBounds(buf, frc);
//      Insets insets= getMargin(); // Parameters are all 0.
      Insets insets= getInsets();
      int iWidth= insets.left   (int)rect.getWidth()   insets.right  2;
      int iHeight= insets.top   (int)rect.getHeight()   insets.bottom;
      Dimension dim= new Dimension(iWidth, iHeight);
      setPreferredSize(dim);
    }
  }
  • Related