Поскольку этот вопрос появляется довольно часто, я прилагаю больше усилий в этом ответе, чем обычно.
Мой голос идет за JFormattedTextField
. ИМО каждый разработчик Swing должен иметь улучшенную версию этого класса в своем наборе инструментов, поскольку он позволяет проверять практически все, что вы можете придумать, путем правильного выбора Format
. Примеры, для которых я уже использовал это:
- Строковый ввод, где
String
не может быть пустым
- Координатный ввод
- Ввод даты
- Редактор
JSpinner
- Карта масштабов
- Цифры
- ...
Он также допускает визуальную обратную связь, когда ввод неверен, что, например, не относится к InputVerifier
. Он по-прежнему позволяет пользователю вводить что угодно, но это значение просто не принимается, когда оно недопустимо, и это значение никогда не покидает пользовательский интерфейс. Я думаю (но опять же, это мое мнение), что лучше позволить пользователю вводить неверный ввод, просто удаляя его автоматически, например, с помощью. DocumentFilter
. Я подозреваю ошибку при вводе символа в текстовое поле, и он не появляется.
Позвольте мне проиллюстрировать это с помощью некоторого кода (фактически, некоторого кода). Сначала небольшое демонстрационное приложение. Это приложение просто показывает JFormattedTextField
для чисел. Простое использование другого формата позволяет повторно использовать этот компонент для совершенно разных проверок.
import be.pcl.swing.ImprovedFormattedTextField;
import javax.swing.*;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
/**
* See http://stackoverflow.com/q/1313390/1076463
*/
public class FormattedTextFieldDemo {
public static void main( String[] args ) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame testFrame = new JFrame( "FormattedTextFieldDemo" );
NumberFormat integerNumberInstance = NumberFormat.getIntegerInstance();
ImprovedFormattedTextField integerFormattedTextField = new ImprovedFormattedTextField( integerNumberInstance, 100 );
integerFormattedTextField.setColumns( 20 );
testFrame.add( createButtonPanel( integerFormattedTextField ), BorderLayout.NORTH );
final JTextArea textArea = new JTextArea(50, 50);
PropertyChangeListener updateTextAreaListener = new PropertyChangeListener() {
@Override
public void propertyChange( PropertyChangeEvent evt ) {
textArea.append( "New value: " + evt.getNewValue() + "\n" );
}
};
integerFormattedTextField.addPropertyChangeListener( "value", updateTextAreaListener );
testFrame.add( new JScrollPane( textArea ), BorderLayout.CENTER );
testFrame.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE );
testFrame.pack();
testFrame.setVisible( true );
}
} );
}
private static JPanel createButtonPanel( final JFormattedTextField aTextField ){
JPanel panel = new JPanel( new BorderLayout( ) );
panel.add( aTextField, BorderLayout.WEST );
Action action = new AbstractAction() {
{
aTextField.addPropertyChangeListener( "editValid", new PropertyChangeListener() {
@Override
public void propertyChange( PropertyChangeEvent evt ) {
setEnabled( ( ( Boolean ) evt.getNewValue() ) );
}
} );
putValue( Action.NAME, "Show current value" );
}
@Override
public void actionPerformed( ActionEvent e ) {
JOptionPane.showMessageDialog( null, "The current value is [" + aTextField.getValue() + "] of class [" + aTextField.getValue().getClass() + "]" );
}
};
panel.add( new JButton( action ), BorderLayout.EAST );
return panel;
}
}
, который просто показывает ImprovedFormattedTextField
и JButton
, который включается только при правильном вводе (ага, есть это решение DocumentFilter
). Он также показывает JTextArea
, в котором значение печатается каждый раз, когда встречается новое действительное значение. При нажатии на кнопку отображается значение.
Код для ImprovedFormattedTextField
можно найти ниже вместе с ParseAllFormat
, от которого он зависит
package be.pcl.swing;
import javax.swing.JFormattedTextField;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.Color;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.text.Format;
import java.text.ParseException;
/**
* <p>Extension of {@code JFormattedTextField} which solves some of the usability issues</p>
*/
public class ImprovedFormattedTextField extends JFormattedTextField {
private static final Color ERROR_BACKGROUND_COLOR = new Color( 255, 215, 215 );
private static final Color ERROR_FOREGROUND_COLOR = null;
private Color fBackground, fForeground;
/**
* Create a new {@code ImprovedFormattedTextField} instance which will use {@code aFormat} for the
* validation of the user input.
*
* @param aFormat The format. May not be {@code null}
*/
public ImprovedFormattedTextField( Format aFormat ) {
//use a ParseAllFormat as we do not want to accept user input which is partially valid
super( new ParseAllFormat( aFormat ) );
setFocusLostBehavior( JFormattedTextField.COMMIT_OR_REVERT );
updateBackgroundOnEachUpdate();
//improve the caret behavior
//see also http://tips4java.wordpress.com/2010/02/21/formatted-text-field-tips/
addFocusListener( new MousePositionCorrectorListener() );
}
/**
* Create a new {@code ImprovedFormattedTextField} instance which will use {@code aFormat} for the
* validation of the user input. The field will be initialized with {@code aValue}.
*
* @param aFormat The format. May not be {@code null}
* @param aValue The initial value
*/
public ImprovedFormattedTextField( Format aFormat, Object aValue ) {
this( aFormat );
setValue( aValue );
}
private void updateBackgroundOnEachUpdate() {
getDocument().addDocumentListener( new DocumentListener() {
@Override
public void insertUpdate( DocumentEvent e ) {
updateBackground();
}
@Override
public void removeUpdate( DocumentEvent e ) {
updateBackground();
}
@Override
public void changedUpdate( DocumentEvent e ) {
updateBackground();
}
} );
}
/**
* Update the background color depending on the valid state of the current input. This provides
* visual feedback to the user
*/
private void updateBackground() {
boolean valid = validContent();
if ( ERROR_BACKGROUND_COLOR != null ) {
setBackground( valid ? fBackground : ERROR_BACKGROUND_COLOR );
}
if ( ERROR_FOREGROUND_COLOR != null ) {
setForeground( valid ? fForeground : ERROR_FOREGROUND_COLOR );
}
}
@Override
public void updateUI() {
super.updateUI();
fBackground = getBackground();
fForeground = getForeground();
}
private boolean validContent() {
AbstractFormatter formatter = getFormatter();
if ( formatter != null ) {
try {
formatter.stringToValue( getText() );
return true;
} catch ( ParseException e ) {
return false;
}
}
return true;
}
@Override
public void setValue( Object value ) {
boolean validValue = true;
//before setting the value, parse it by using the format
try {
AbstractFormatter formatter = getFormatter();
if ( formatter != null ) {
formatter.valueToString( value );
}
} catch ( ParseException e ) {
validValue = false;
updateBackground();
}
//only set the value when valid
if ( validValue ) {
int old_caret_position = getCaretPosition();
super.setValue( value );
setCaretPosition( Math.min( old_caret_position, getText().length() ) );
}
}
@Override
protected boolean processKeyBinding( KeyStroke ks, KeyEvent e, int condition, boolean pressed ) {
//do not let the formatted text field consume the enters. This allows to trigger an OK button by
//pressing enter from within the formatted text field
if ( validContent() ) {
return super.processKeyBinding( ks, e,
condition, pressed ) && ks != KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 );
}
else {
return super.processKeyBinding( ks, e,
condition, pressed );
}
}
private static class MousePositionCorrectorListener extends FocusAdapter {
@Override
public void focusGained( FocusEvent e ) {
/* After a formatted text field gains focus, it replaces its text with its
* current value, formatted appropriately of course. It does this after
* any focus listeners are notified. We want to make sure that the caret
* is placed in the correct position rather than the dumb default that is
* before the 1st character ! */
final JTextField field = ( JTextField ) e.getSource();
final int dot = field.getCaret().getDot();
final int mark = field.getCaret().getMark();
if ( field.isEnabled() && field.isEditable() ) {
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
// Only set the caret if the textfield hasn't got a selection on it
if ( dot == mark ) {
field.getCaret().setDot( dot );
}
}
} );
}
}
}
}
Класс ParseAllFormat
:
package be.pcl.swing;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
/**
* <p>Decorator for a {@link Format Format} which only accepts values which can be completely parsed
* by the delegate format. If the value can only be partially parsed, the decorator will refuse to
* parse the value.</p>
*/
public class ParseAllFormat extends Format {
private final Format fDelegate;
/**
* Decorate <code>aDelegate</code> to make sure if parser everything or nothing
*
* @param aDelegate The delegate format
*/
public ParseAllFormat( Format aDelegate ) {
fDelegate = aDelegate;
}
@Override
public StringBuffer format( Object obj, StringBuffer toAppendTo, FieldPosition pos ) {
return fDelegate.format( obj, toAppendTo, pos );
}
@Override
public AttributedCharacterIterator formatToCharacterIterator( Object obj ) {
return fDelegate.formatToCharacterIterator( obj );
}
@Override
public Object parseObject( String source, ParsePosition pos ) {
int initialIndex = pos.getIndex();
Object result = fDelegate.parseObject( source, pos );
if ( result != null && pos.getIndex() < source.length() ) {
int errorIndex = pos.getIndex();
pos.setIndex( initialIndex );
pos.setErrorIndex( errorIndex );
return null;
}
return result;
}
@Override
public Object parseObject( String source ) throws ParseException {
//no need to delegate the call, super will call the parseObject( source, pos ) method
return super.parseObject( source );
}
}
Возможные улучшения:
-
setBackground
не соблюдается всеми взглядами. Иногда вы можете использовать setForeground
вместо этого, но даже это не гарантируется для всех L & F. Поэтому для визуальной обратной связи лучше использовать восклицательный знак, расположенный рядом с полем. Недостатком является то, что это может испортить макет, если вы вдруг добавите / удалите значок
- обратная связь указывает только на то, что ввод действителен / недействителен. Там нет ничего, что указывает на то, что ожидаемый формат. Возможное решение состоит в том, чтобы использовать самостоятельно созданное расширение
Format
, которое включает описание / пример допустимого ввода, и поместить его в качестве всплывающей подсказки на JFormattedTextField
.