I would like to format a float number as a percent-value with JFormattedTextField that allows inputs from 0 to 100 percent (converted to 0.0f-1.0f), always shows the percent
1) consider using JSpinner instead of JFormattedTextField
because there you can set SpinnerNumberModel for initial values
from API
Integer value = new Integer(50);
Integer min = new Integer(0);
Integer max = new Integer(100);
Integer step = new Integer(1);
and with simple hack for JSpinner
(with SpinnerNumberModel
) it doesn't allows another input as Digits, otherwise is there possible input any of Chars
2) for JFormattedTextField
you have to implements
and of both cases for JFormattedTextField
you have to write workaround for catch if value is less or more than required range ...
EDIT:
.
.
not true at all, :-) you are so far from ... simple wrong :-), there is small mistake with your result, please look at this code
import java.awt.BorderLayout;
import java.text.NumberFormat;
import javax.swing.*;
import javax.swing.text.*;
public class TestDigitsOnlySpinner {
public static void main(String... args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame("enter digit");
JSpinner jspinner = makeDigitsOnlySpinnerUsingDocumentFilter();
frame.getContentPane().add(jspinner, BorderLayout.CENTER);
frame.getContentPane().add(new JButton("just another widget"), BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
}
private JSpinner makeDigitsOnlySpinnerUsingDocumentFilter() {
JSpinner spinner = new JSpinner(new SpinnerNumberModel());
JSpinner.NumberEditor jsEditor = (JSpinner.NumberEditor) spinner.getEditor();
JFormattedTextField textField = jsEditor.getTextField();
final DocumentFilter digitOnlyFilter = new DocumentFilter() {
@Override
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
if (stringContainsOnlyDigits(string)) {
super.insertString(fb, offset, string, attr);
}
}
@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
super.remove(fb, offset, length);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
if (stringContainsOnlyDigits(text)) {
super.replace(fb, offset, length, text, attrs);
}
}
private boolean stringContainsOnlyDigits(String text) {
for (int i = 0; i < text.length(); i++) {
if (!Character.isDigit(text.charAt(i))) {
return false;
}
}
return true;
}
};
/*NumberFormat format = NumberFormat.getIntegerInstance();
format.setGroupingUsed(false);// or add the group chars to the filter
NumberFormat format = NumberFormat.getInstance();*/
NumberFormat format = NumberFormat.getPercentInstance();
format.setGroupingUsed(false);
format.setGroupingUsed(true);// or add the group chars to the filter
format.setMaximumIntegerDigits(10);
format.setMaximumFractionDigits(2);
format.setMinimumFractionDigits(5);
textField.setFormatterFactory(new DefaultFormatterFactory(new InternationalFormatter(format) {
private static final long serialVersionUID = 1L;
@Override
protected DocumentFilter getDocumentFilter() {
return digitOnlyFilter;
}
}));
return spinner;
}
});
}
}
Ok, I've made it. The solution is far from simple, but at least it does exactly what I want. Except for returning doubles instead of floats. One major limitation is that it does not allow fraction digits, but for now I can live with that.
import java.awt.BorderLayout;
import java.text.NumberFormat;
import java.text.ParseException;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.DocumentFilter;
import javax.swing.text.NavigationFilter;
import javax.swing.text.NumberFormatter;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Position.Bias;
public class JPercentField extends JComponent {
private static final double MIN_VALUE = 0.0d;
private static final double MAX_VALUE = 1.0d;
private static final double STEP_SIZE = 0.01d;
private static final long serialVersionUID = -779235114254706347L;
private JSpinner spinner;
public JPercentField() {
initComponents();
initLayout();
spinner.setValue(MIN_VALUE);
}
private void initComponents() {
SpinnerNumberModel model = new SpinnerNumberModel(MIN_VALUE, MIN_VALUE, MAX_VALUE, STEP_SIZE);
spinner = new JSpinner(model);
initSpinnerTextField();
}
private void initSpinnerTextField() {
DocumentFilter digitOnlyFilter = new PercentDocumentFilter(getMaximumDigits());
NavigationFilter navigationFilter = new BlockLastCharacterNavigationFilter(getTextField());
getTextField().setFormatterFactory(
new DefaultFormatterFactory(new PercentNumberFormatter(createPercentFormat(), navigationFilter,
digitOnlyFilter)));
getTextField().setColumns(6);
}
private int getMaximumDigits() {
return Integer.toString((int) MAX_VALUE * 100).length();
}
private JFormattedTextField getTextField() {
JSpinner.NumberEditor jsEditor = (JSpinner.NumberEditor) spinner.getEditor();
JFormattedTextField textField = jsEditor.getTextField();
return textField;
}
private NumberFormat createPercentFormat() {
NumberFormat format = NumberFormat.getPercentInstance();
format.setGroupingUsed(false);
format.setMaximumIntegerDigits(getMaximumDigits());
format.setMaximumFractionDigits(0);
return format;
}
private void initLayout() {
setLayout(new BorderLayout());
add(spinner, BorderLayout.CENTER);
}
public double getPercent() {
return (Double) spinner.getValue();
}
public void setPercent(double percent) {
spinner.setValue(percent);
}
private static class PercentNumberFormatter extends NumberFormatter {
private static final long serialVersionUID = -1172071312046039349L;
private final NavigationFilter navigationFilter;
private final DocumentFilter digitOnlyFilter;
private PercentNumberFormatter(NumberFormat format, NavigationFilter navigationFilter,
DocumentFilter digitOnlyFilter) {
super(format);
this.navigationFilter = navigationFilter;
this.digitOnlyFilter = digitOnlyFilter;
}
@Override
protected NavigationFilter getNavigationFilter() {
return navigationFilter;
}
@Override
protected DocumentFilter getDocumentFilter() {
return digitOnlyFilter;
}
@Override
public Class<?> getValueClass() {
return Double.class;
}
@Override
public Object stringToValue(String text) throws ParseException {
Double value = (Double) super.stringToValue(text);
return Math.max(MIN_VALUE, Math.min(MAX_VALUE, value));
}
}
/**
* NavigationFilter that avoids navigating beyond the percent sign.
*/
private static class BlockLastCharacterNavigationFilter extends NavigationFilter {
private JFormattedTextField textField;
private BlockLastCharacterNavigationFilter(JFormattedTextField textField) {
this.textField = textField;
}
@Override
public void setDot(FilterBypass fb, int dot, Bias bias) {
super.setDot(fb, correctDot(fb, dot), bias);
}
@Override
public void moveDot(FilterBypass fb, int dot, Bias bias) {
super.moveDot(fb, correctDot(fb, dot), bias);
}
private int correctDot(FilterBypass fb, int dot) {
// Avoid selecting the percent sign
int lastDot = Math.max(0, textField.getText().length() - 1);
return dot > lastDot ? lastDot : dot;
}
}
private static class PercentDocumentFilter extends DocumentFilter {
private int maxiumDigits;
public PercentDocumentFilter(int maxiumDigits) {
super();
this.maxiumDigits = maxiumDigits;
}
@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs)
throws BadLocationException {
// Mapping an insert as a replace without removing
replace(fb, offset, 0, text, attrs);
}
@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
// Mapping a remove as a replace without inserting
replace(fb, offset, length, "", SimpleAttributeSet.EMPTY);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
int replaceLength = correctReplaceLength(fb, offset, length);
String cleanInput = truncateInputString(fb, filterDigits(text), replaceLength);
super.replace(fb, offset, replaceLength, cleanInput, attrs);
}
/**
* Removes all non-digit characters
*/
private String filterDigits(String text) throws BadLocationException {
StringBuilder sb = new StringBuilder(text);
for (int i = 0, n = sb.length(); i < n; i++) {
if (!Character.isDigit(text.charAt(i))) {
sb.deleteCharAt(i);
}
}
return sb.toString();
}
/**
* Removes all characters with which the resulting text would exceed the maximum number of digits
*/
private String truncateInputString(FilterBypass fb, String filterDigits, int replaceLength) {
StringBuilder sb = new StringBuilder(filterDigits);
int currentTextLength = fb.getDocument().getLength() - replaceLength - 1;
for (int i = 0; i < sb.length() && currentTextLength + sb.length() > maxiumDigits; i++) {
sb.deleteCharAt(i);
}
return sb.toString();
}
private int correctReplaceLength(FilterBypass fb, int offset, int length) {
if (offset + length >= fb.getDocument().getLength()) {
// Don't delete the percent sign
return offset + length - fb.getDocument().getLength();
}
return length;
}
}
}
Imho https://docs.oracle.com/javase/tutorial/uiswing/components/formattedtextfield.html gives a pretty good example (see section "Specifying Formatters and Using Formatter Factories").
The key is to use a Percent Format to display values and a custom NumberFormatter to edit values. This approach also allows the use of fraction digits.
// create a format for displaying percentages (with %-sign)
NumberFormat percentDisplayFormat = NumberFormat.getPercentInstance();
// create a format for editing percentages (without %-sign)
NumberFormat percentEditFormat = NumberFormat.getNumberInstance();
// create a formatter for editing percentages - input will be transformed to percentages (eg. 50 -> 0.5)
NumberFormatter percentEditFormatter = new NumberFormatter(percentEditFormat) {
private static final long serialVersionUID = 1L;
@Override
public String valueToString(Object o) throws ParseException {
Number number = (Number) o;
if (number != null) {
double d = number.doubleValue() * 100.0;
number = new Double(d);
}
return super.valueToString(number);
}
@Override
public Object stringToValue(String s) throws ParseException {
Number number = (Number) super.stringToValue(s);
if (number != null) {
double d = number.doubleValue() / 100.0;
number = new Double(d);
}
return number;
}
};
// set allowed range
percentEditFormatter.setMinimum(0D);
percentEditFormatter.setMaximum(100D);
// create JFormattedTextField
JFormattedTextField field = new JFormattedTextField(
new DefaultFormatterFactory(
new NumberFormatter(percentDisplayFormat),
new NumberFormatter(percentDisplayFormat),
percentEditFormatter));