1

Recently I asked which was the best Swing component to bind to a BigDecimal variable (with some particular editing properties). It turns out that none of the standard Swing components suit me completely, nor did the third-party Swing component libraries I've found out there. So I’ve decided to create my own Swing component.

Component description:

I want to extend JTextField or JFormattedTextField, so my new component can be easily bound to a BigDecimal variable.

The component will have customizable scale and length properties.

Behavior:

When the component is drawn, it shows only the decimal point and space for scale digits to its right.

When the component receives focus the caret should be positioned left to the decimal point. As the user types numbers (any other character is ignored) they appear to the left of the caret, only lengthscale numbers are accepted, any other number typed is ignored as the integer portion is full. Any time the user types the decimal point the caret moves to the right side of the decimal point. The following numbers typed are shown in the decimal part, only scale numbers are considered any other number typed is ignored as the decimal portion is full. Additionally, thousand separators should appear as the user types numbers left to the decimal point.

I also want to be able to use the component as a Cell Editor in a JTable (without having to code it twice).

Invoking a getValue() method on the component should yield the BigDecimal representing the number just entered.


I’ve never created my own Swing component; I’ve barely used the standard ones. So I would appreciate any good tutorial/info/tip on creating the component described. This is the only thing I've got so far.

Thanks in advance.

Community
  • 1
  • 1
mmutilva
  • 18,688
  • 22
  • 59
  • 82
  • Cross posted: http://www.coderanch.com/t/488720/Swing-AWT-SWT-JFace/java/Advice-creating-my-own-Swing – camickr Mar 24 '10 at 23:54

2 Answers2

3

I like the Grouchnikov article you cited, but I'm not sure you would want to change the UI delegate. As this will be a view of an immutable object, I'd favor composition over inheritance. I tend to think of the component you describe in terms of being a renderer, as seen in this example. You can add an InputVerifier or DocumwntListener to obtain the validation you want.

Addendum: Here's an example that uses JFormattedTextField and a MaskFormatter. You'll need to adjust the format mask to match your scale and length.

public class TableGrid extends JPanel {

    private DecimalFormat df;
    private MaskFormatter mf;
    private JFormattedTextField tf;

    public TableGrid() {
        df = new DecimalFormat("0.00");
        try {
            mf = new MaskFormatter("#.##");
        } catch (ParseException ex) {
            ex.printStackTrace();
        }
        tf = new JFormattedTextField(mf);
        TableModel dataModel = new TableModel();
        JTable table = new JTable(dataModel);
        table.setCellSelectionEnabled(true);
        table.setRowHeight(32);
        table.setDefaultRenderer(BigDecimal.class, new DecRenderer(df));
        table.setDefaultEditor(BigDecimal.class, new DecEditor(tf, df));
        this.add(table);
    }

    private static class TableModel extends AbstractTableModel {

        private static final int SIZE = 4;
        private BigDecimal[][] matrix = new BigDecimal[SIZE][SIZE];

        public TableModel() {
            for (Object[] row : matrix) {
                Arrays.fill(row, BigDecimal.valueOf(0));
            }
        }

        @Override
        public int getRowCount() {
            return SIZE;
        }

        @Override
        public int getColumnCount() {
            return SIZE;
        }

        @Override
        public Object getValueAt(int row, int col) {
            return matrix[row][col];
        }

        @Override
        public void setValueAt(Object value, int row, int col) {
            matrix[row][col] = (BigDecimal) value;
        }

        @Override
        public Class<?> getColumnClass(int col) {
            return BigDecimal.class;
        }

        @Override
        public boolean isCellEditable(int row, int col) {
            return true;
        }
    }

    private static class DecRenderer extends DefaultTableCellRenderer {

        DecimalFormat df;

        public DecRenderer(DecimalFormat df) {
            this.df = df;
            this.setHorizontalAlignment(JLabel.CENTER);
            this.setBackground(Color.lightGray);
            this.df.setParseBigDecimal(true);
        }

        @Override
        protected void setValue(Object value) {
            setText((value == null) ? "" : df.format(value));
        }
    }

    private static class DecEditor extends DefaultCellEditor {

        private JFormattedTextField tf;
        private DecimalFormat df;

        public DecEditor(JFormattedTextField tf, DecimalFormat df) {
            super(tf);
            this.tf = tf;
            this.df = df;
            tf.setHorizontalAlignment(JFormattedTextField.CENTER);
        }

        @Override
        public Object getCellEditorValue() {
            try {
                return new BigDecimal(tf.getText());
            } catch (NumberFormatException e) {
                return BigDecimal.valueOf(0);
            }
        }

        @Override
        public Component getTableCellEditorComponent(JTable table,
            Object value, boolean isSelected, int row, int column) {
            tf.setText((value == null) ? "" : df.format((BigDecimal) value));
            if (isSelected) tf.selectAll();
            return tf;
        }
    }

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

            @Override
            public void run() {
                JFrame f = new JFrame("TableGrid");
                f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                f.add(new TableGrid());
                f.pack();
                f.setVisible(true);
            }
        });
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
2

Use whatever component you like and register a KeyListener to reject characters to match your behaviour.Add a getValue() & setValue to get/set easily a BiDecimal and some other methods and all the painting will be provided by any JTextComponent.

adrian.tarau
  • 3,124
  • 2
  • 26
  • 29
  • What do you mean by "all the painting" will be provided by any JTextComponent."? – mmutilva Mar 24 '10 at 21:26
  • 1
    Exactly as it sounds :) In the end you would like to have a component that edits "text" which contains only some characters and a certain inserting behaviour. So with a KeyListener and analysing the character under the cursor(or left & right) you will decide if the keystroke is accepted and if yes where to insert it. In the end, your requirements talks about how the component behaves when inserting data and nothing about painting. Let the JTextField to the painting and change the behaviour how it accepts keystrokes. – adrian.tarau Mar 24 '10 at 21:55
  • Ok. One more thing: are you sure is a KeyListener I have to register? How do I 'reject the characters' that don't match what I want? I think adding a DocumentListener to the component's document would be more appropiate, but you tell me... you seem to know more than I about this. – mmutilva Mar 24 '10 at 22:41
  • 1
    With DocumentListener the change is already in, and you want to accept only some input. It doesn't make sense to be notified after the document is changed and change the document back by removing characters. Yes, register a KeyListener and combined cu cursor position(see where you are in the document, etc) you can obtain the desired behavior. Do not forget to consume KeyEvents to eliminate an undesired keystroke. – adrian.tarau Mar 25 '10 at 00:29
  • But the document is modified after the keylistener executes keytyped, keypressed and keyreleased methods. How can I tell the document not to change from withing the keylistener? That's what I don't get. Maybe the answer is to extend PlainDocument, that way I can control the input WHILE it's happening (insertString method), not before (as with keylistener), nor after (as with DocumentListener). – mmutilva Mar 25 '10 at 14:11
  • Consume the event at KeyListener level and the document is NOT changed. A component has several hooks in place for consuming keystrokes(like special keys) and the last station is all registered KeyListeners. Ex: reject anything except digits JTextField text = new JTextField(); text.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (!Character.isDigit(e.getKeyChar())) { e.consume(); } } }); – adrian.tarau Mar 25 '10 at 16:28