2

I would like to know if there is anyway to make a JformattedTextField or jtextField behave like an atm money input. With that I mean you enter from the right to left, say you enter 10 you need to press 2 more 0's so that it will be 10.00 . The program enters the decimal point automatically as he types from right to left? If the 2 0's are not entered it will just be .10 . Is this possible? How would that be returned to me if I want to use that string to do calculations on then? I tried the abstract formatter but this doesn't work so nicely. I want to use this for input for the amount of money received by a customer. But make it idiot proof.

Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
LL.
  • 129
  • 1
  • 12
  • 2
    I think you can attach listener to the textfield and do whatever functionality you would like to. – kosa Jun 12 '13 at 20:31

4 Answers4

4

This forces the user to always enter text from the right no matter where the caret is positioned. All previous characters are shifted left as a new character is inserted. Formatting will be applied based on your formatter:

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

public class ABMTextField extends JTextField
{
    private DecimalFormat format;
    private String decimal;

    public ABMTextField(DecimalFormat format)
    {
        this.format = format;

        decimal = Character.toString( format.getDecimalFormatSymbols().getDecimalSeparator() );

        setColumns( format.toPattern().length() );
        setHorizontalAlignment(JFormattedTextField.TRAILING);

        setText( format.format(0.0) );

        AbstractDocument doc = (AbstractDocument)getDocument();
        doc.setDocumentFilter( new ABMFilter() );
    }

    @Override
    public void setText(String text)
    {
        Number number = format.parse(text, new ParsePosition(0));

        if (number != null)
            super.setText( text );
    }

    public class ABMFilter extends DocumentFilter
    {
        public void insertString(FilterBypass fb, int offs, String str, AttributeSet a)
            throws BadLocationException
        {
            replace(fb, offs, 0, str, a);
        }

        public void replace(FilterBypass fb, int offs, int length, String str, AttributeSet a)
            throws BadLocationException
        {
            if (".0123456789".contains(str))
            {
                Document doc = fb.getDocument();
                StringBuilder sb = new StringBuilder( doc.getText(0, doc.getLength()) );

                int decimalOffset = sb.indexOf( decimal );

                if (decimalOffset != -1)
                {
                    sb.deleteCharAt(decimalOffset);
                    sb.insert(decimalOffset + 1, decimal);
                }

                sb.append(str);

                try
                {
                    String text = format.format( format.parse( sb.toString() ) );
                    super.replace(fb, 0, doc.getLength(), text, a);
                }
                catch(ParseException e) {}
            }
            else
                Toolkit.getDefaultToolkit().beep();
        }

        public void remove(DocumentFilter.FilterBypass fb, int offset, int length)
            throws BadLocationException
        {
            Document doc = fb.getDocument();
            StringBuilder sb = new StringBuilder( doc.getText(0, doc.getLength()) );

            int decimalOffset = sb.indexOf( decimal );

            if (decimalOffset != -1)
            {
                sb.deleteCharAt(decimalOffset);
                sb.insert(decimalOffset - 1, decimal);
            }

            sb.deleteCharAt( sb.length() - 1) ;

            try
            {
                String text = format.format( format.parse( sb.toString() ) );
                super.replace(fb, 0, doc.getLength(), text, null);
            }
            catch(ParseException e) {}
        }
    }

    private static void createAndShowUI()
    {
        DecimalFormat format = new DecimalFormat("###,##0.00");
        ABMTextField abm = new ABMTextField( format );

        JPanel panel = new JPanel();
        panel.add( abm );

        JFrame frame = new JFrame("ABMTextField");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add( panel );
        frame.setSize(200, 200);
        frame.setLocationByPlatform( true );
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowUI();
            }
        });
    }
}

How would that be returned to me if I want to use that string to do calculations on then?

You would need to create a method, maybe getValue() that would use the format.parse(...) method to return an actual number.

camickr
  • 321,443
  • 19
  • 166
  • 288
2

Take a look at How to use Formatted Text Fields, in particular Using MaskFormatter.

Something like...

MaskFormatter formatter = new MaskFormatter("##.##");
JFormattedTextField field = JFormattedTextField(formatter);

for example may help.

Simple example

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.HeadlessException;
import java.text.ParseException;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.MaskFormatter;

public class TestFormattedTextField {

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

    public TestFormattedTextField() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                try {
                    JFormattedTextField field = new JFormattedTextField();
                    MaskFormatter formatter = new MaskFormatter("##.##");
                    formatter.setPlaceholderCharacter('0');
                    field.setFormatterFactory(new DefaultFormatterFactory(formatter));

                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.setLayout(new GridBagLayout());
                    frame.add(field);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (ParseException exp) {
                    exp.printStackTrace();
                }
            }
        });
    }        
}

Additional Example

Now, I realise that the previous example doesn't meet your exact needs (as you described them), it is a simple solution, I've also added a DocumentFilter example...

enter image description here

Which will output...

Value = 0.1
Value = $0.10

enter image description here

Which will output

Value = 10.0
Value = $10.00

Code...

import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.NumberFormat;
import java.text.ParseException;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.MaskFormatter;

public class TestFormattedTextField {

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

    public TestFormattedTextField() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                MoneyField field = new MoneyField();
                field.addActionListener(new ActionListener() {
                    @Override
                    @SuppressWarnings("empty-statement")
                    public void actionPerformed(ActionEvent e) {
                        MoneyField field = (MoneyField) e.getSource();
                        double value = field.getValue();
                        System.out.println("Value = " + value);
                        System.out.println("Value = " + NumberFormat.getCurrencyInstance().format(value));
                    }
                });

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridBagLayout());
                frame.add(field);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class MoneyField extends JTextField {

        public MoneyField() {
            setColumns(5);
            setHorizontalAlignment(RIGHT);
            ((AbstractDocument) getDocument()).setDocumentFilter(new Filter());
        }

        public double getValue() {

            String text = getText();
            if (!text.contains(".")) {
                text = "0." + text;
            }

            return Double.parseDouble(text);

        }

        protected class Filter extends DocumentFilter {

            protected String getNumbers(String text) {
                StringBuilder sb = new StringBuilder(text.length());
                for (char c : text.toCharArray()) {
                    if (Character.isDigit(c)) {
                        sb.append(c);
                    }
                }
                return sb.toString();
            }

            @Override
            public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
                if (length > 0) {
                    fb.remove(offset, length);
                }
                insertString(fb, offset, text, attrs);
            }

            @Override
            public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
                text = getNumbers(text);
                if (text.length() > 0) {
                    int docLength = fb.getDocument().getLength();
                    if (docLength == 2) {
                        text = "." + text;
                    }
                    if (docLength + text.length() < 6) {
                        super.insertString(fb, offset, text, attr);
                    }
                }
            }

            @Override
            public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
                if (offset == 3) {
                    offset = 2;
                    length = 2;
                }
                super.remove(fb, offset, length);
            }
        }
    }
}

Check out DocumentFilter examples for more details

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • +1, I see we were both working on a DocumentFilter approach which I think is the way to go. – camickr Jun 13 '13 at 02:00
  • @camickr I Still think the `JFormattedTextField` is simpler approach, but the `DocumentFilter` will more closely meet the OP requirements... – MadProgrammer Jun 13 '13 at 09:34
1

Use DocumentFilter for the JTextField and override the appropriate method to handle the number formatting. Also it will be nice if you can post what you have tried and 'doesn't work'.

Mubin
  • 4,192
  • 3
  • 26
  • 45
  • `DocumentListener` isn't an appropriate choice for "filtering" input into the document. Changes have already being made to the document by the time `DocumentListener` is called and any modifications within the listener will cause an exception to be thrown – MadProgrammer Jun 13 '13 at 02:04
  • @MadProgrammer - You are right. I was referring to DocumentFilter and typed DocumentListener :-) – Mubin Jun 13 '13 at 08:49
  • Don't you love it when your fingers do one thing and brain does another :P – MadProgrammer Jun 13 '13 at 09:36
  • I have tried to create a regex for it . I have tried the making an input mask, the input mask work until the number becomes bigger than 99.99 . With the Regular expression using the matcher I have found it was way to buggy when it had to check on the key pressed. – LL. Jun 13 '13 at 18:44
0

I am a few years late to replying to this question, however, I would like to suggest an improvement over the code written by camickr. His code is really good, but I have modified it a bit to have 3 additional methods:

setLong() - sets a value programmatically to the text field;
getLong() - returns a value programmatically from the text field;
clearLong() - programmatically sets value to 0.00;

JCurrencyField class:

import java.awt.Toolkit;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.ParsePosition;

import javax.swing.JFormattedTextField;
import javax.swing.JTextField;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;

/**
 * @apiNote This currency field class is limited by long's capabilities. It is
 *          best to use BigDecimals when dealing with money values due to higher
 *          precision and longer maximal value
 */
public class JCurrencyField extends JTextField {
    // ----------------------- CLASS RELATED LOGIC

    private static final long serialVersionUID = 1L;
    private DecimalFormat format = new DecimalFormat("###,##0.00");
    private String decimal;

    /**
     * This class is responsible for accepting long value input. It has an ATM like
     * behavior.
     * 
     * @apiNote
     *          <h1>WARNING:</h1> Do not use {@link #setText(String)}.<br>
     *          <br>
     *          Use:
     *          <ul>
     *          <li>{@link #setLong(long)};</li>
     *          <li>{@link #clearLong()};</li>
     *          <li>{@link #getLong()}</li>
     *          </ul>
     *          methods instead.
     */
    public JCurrencyField() {
        decimal = Character.toString(format.getDecimalFormatSymbols().getDecimalSeparator());

        setColumns(format.toPattern().length());
        setHorizontalAlignment(JFormattedTextField.TRAILING);

        setText(format.format(0.00));

        AbstractDocument doc = (AbstractDocument) getDocument();
        doc.setDocumentFilter(new ATMFilter());
    }
    
    /**
     * This class is responsible for accepting long value input. It has an ATM like
     * behavior.
     * 
     * @apiNote
     *          <h1>WARNING:</h1> Do not use {@link #setText(String)}.<br>
     *          <br>
     *          Use:
     *          <ul>
     *          <li>{@link #setLong(long)};</li>
     *          <li>{@link #clearLong()};</li>
     *          <li>{@link #getLong()}</li>
     *          </ul>
     *          methods instead.
     *
     * @param value the value
     */
    public JCurrencyField(long value) {
        decimal = Character.toString(format.getDecimalFormatSymbols().getDecimalSeparator());

        setColumns(format.toPattern().length());
        setHorizontalAlignment(JFormattedTextField.TRAILING);

        setText(format.format(0.00));

        AbstractDocument doc = (AbstractDocument) getDocument();
        doc.setDocumentFilter(new ATMFilter());
        
        setLong(value);
    }

    /**
     * @apiNote This method is overriden, it sets only one char/letter at a time.
     *
     * @apiNote DO NOT USE THIS METHOD TO SET A VALUE TO THIS JCurrencyField. Use
     *          {@link #setLong(long)}; {@link #clearLong()}; {@link #getLong()}
     *          methods instead.
     * @param text the new text
     */
    @Override
    public void setText(String text) {
        // Delete value
        if (text.equals("")) {
            super.setText(text);
        }

        // Add 1 char of number
        Number number = format.parse(text, new ParsePosition(0));

        if (number != null)
            super.setText(text);
    }

    /**
     * This class changes the behavior of {@link JTextField}.
     * <ul>
     * <li>When method {@link JTextField#setText(String)} is called it will append
     * only one char/letter at the end of the value</li>
     * <li>When backspace/delete is pressed, only the last char/letter at the of the
     * value will be deleted</li>
     * </ul>
     */
    public class ATMFilter extends DocumentFilter {

        /**
         * Inserts a string.
         *
         * @param fb   the fb
         * @param offs the offs
         * @param str  the str
         * @param a    the a
         * @throws BadLocationException the bad location exception
         */
        public void insertString(FilterBypass fb, int offs, String str, AttributeSet a) throws BadLocationException {
            replace(fb, offs, 0, str, a);
        }

        /**
         * Adds one char at the right end of this component.
         *
         * @param fb     the fb
         * @param offs   the offs
         * @param length the length
         * @param str    the str
         * @param a      the a
         * @throws BadLocationException the bad location exception
         */
        public void replace(FilterBypass fb, int offs, int length, String str, AttributeSet a)
                throws BadLocationException {
            Document doc = fb.getDocument();
            StringBuilder sb = new StringBuilder(doc.getText(0, doc.getLength()));

            // Dealing with single char input
            if (".0123456789".contains(str)) {
                int decimalOffset = sb.indexOf(decimal);

                if (decimalOffset != -1) {
                    sb.deleteCharAt(decimalOffset);
                    sb.insert(decimalOffset + 1, decimal);
                }

                sb.append(str);

                try {
                    String text = format.format(format.parse(sb.toString()));
                    super.replace(fb, 0, doc.getLength(), text, a);
                } catch (ParseException e) {
                }
            } else
                Toolkit.getDefaultToolkit().beep();
        }

        /**
         * Removes one char from the right end.
         *
         * @param fb     the fb
         * @param offset the offset
         * @param length the length
         * @throws BadLocationException the bad location exception
         */
        public void remove(DocumentFilter.FilterBypass fb, int offset, int length) throws BadLocationException {
            Document doc = fb.getDocument();
            StringBuilder sb = new StringBuilder(doc.getText(0, doc.getLength()));

            int decimalOffset = sb.indexOf(decimal);

            if (decimalOffset != -1) {
                sb.deleteCharAt(decimalOffset);
                sb.insert(decimalOffset - 1, decimal);
            }

            sb.deleteCharAt(sb.length() - 1);

            try {
                String text = format.format(format.parse(sb.toString()));
                super.replace(fb, 0, doc.getLength(), text, null);
            } catch (ParseException e) {
            }
        }
    }

    // ----------------------- METHOD TO BE USED BY PROGRAMMER TO MODIFY THE VALUE

    /**
     * Clears Value from this {@link JCurrencyField}.
     */
    public void clearLong() {
        try {
            String valueStr = String.valueOf(getLong());
            int length = valueStr.length();

            for (int iChar = 0; iChar < length; iChar++) {
                getDocument().remove(0, 0);
            }

        } catch (BadLocationException e) {
            e.printStackTrace();
        }
    }

    /**
     * Sets Value for this {@link JCurrencyField}.
     *
     * @param value the new long
     */
    public void setLong(long value) {
        // Clear old value
        clearLong();
        
        // Set new value
        String valueStr = String.valueOf(value);
        int length = valueStr.length();

        for (int iChar = 0; iChar < length; iChar++) {
            setText(valueStr.substring(iChar, iChar + 1));
        }
    }

    /**
     * Gets value from this {@link JCurrencyField}.
     *
     * @return the long
     */
    public long getLong() {
        // Remove white spaces of any kind, remove . and , -> return only numbers
        String s = getText().replace(",", "").replace(".", "").replace(" ", "").replace("\\s", "").replaceAll("\u00a0",
                "");
        s = s.trim();

        return Long.valueOf(s);
    }
}

Note: In my modified code I use long as the basis of the currency value instead of String. Basically I assume that money value is a long by doing so.

Corey
  • 94
  • 10
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 16 '22 at 08:02
  • I do apologies, but in my sincere opinion I think you consider my well documented answer as unclear just because you are a bot and not a real human being. – Corey Oct 18 '22 at 14:08