Best way to constrain user to enter a time in a JTextField
you could use a JFormattedTextField
have a look at the example below, this will create a JFormattedTextField
which will accept only numbers and put them in the form XXhXXminXXs then add a ActionListener
to the JFormattedTextField
and in that attempt to parse to a valid time object when ENTER for now I only show the use of the JFormattedTextField
and the ActionListener
I didn't attempt to convert or any converting and parsing for valid time:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.ParseException;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.text.MaskFormatter;
public class FormattedTextFieldExample {
public FormattedTextFieldExample() {
initComponents();
}
private void initComponents() {
JFrame frame = new JFrame("JFormattedTextField Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
MaskFormatter mask = null;
try {
mask = new MaskFormatter("##h##min##s");//the # is for numeric values
mask.setPlaceholderCharacter('#');
} catch (ParseException e) {
e.printStackTrace();
}
//
// Create a formatted text field that accept a valid time.
//
final JFormattedTextField timeField = new JFormattedTextField(mask);
//Add ActionListener for when enter is pressed
timeField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
Object source = ae.getSource();
if (source == timeField) {
//parse to a valid time here
System.out.println(timeField.getText());
}
}
});
frame.add(timeField);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new FormattedTextFieldExample();
}
});
}
}
- Multiple
JSpinner
instances allowing to select hours, minutes and seconds - Use a
JFormattedTextField
with some improvements to provide immediate user feedback (for example color the background red as soon as the input becomes invalid)
I did a prototype of a TimeField
some time ago, it still needs a little work, but the concept is pretty basic.
It basically presents two JTextFields
, one for the hour and one for the minutes and makes them appear to be a single field.
Through the use of some DocumentFilter
s, it restricts the users input.
I did this some time ago and it needs work, but the basic idea is there...if you have problems, you will need to try and figure it out yourself ;)
/**
*
* @author MadProgrammer
*/
public class TimeField extends javax.swing.JPanel {
// The time of day...
public enum TimeOfDay {
AM,
PM
}
private HourDocumentFilter hourDocumentFilter;
private MinuteDocumentFilter minDocumentFilter;
private HourKeyHandler hourKeyHandler;
private MinuteKeyHandler minuteKeyHandler;
private HourFocusHandler hourFocusHandler;
private MinuteFocusHandler minuteFocusHandler;
private boolean use24HourClock;
private ActionHandler actionHandler;
/**
* Creates new form TimeField
*/
public TimeField() {
initComponents();
pnlFields.setBorder(new CompoundBorder(UIManager.getBorder("TextField.border"),new EmptyBorder(0, 2, 0, 2)));
set24HourClock(false);
setTime(new Date());
fldHour.addKeyListener(new HourKeyHandler());
}
@Override
public void addNotify() {
super.addNotify();
// Add all the required functionality to make this thing work...
((AbstractDocument) fldHour.getDocument()).setDocumentFilter(getHourDocumentFilter());
((AbstractDocument) fldMin.getDocument()).setDocumentFilter(getMinuteDocumentFilter());
fldHour.addFocusListener(getHourFocusHandler());
fldMin.addFocusListener(getMinuteFocusHandler());
fldHour.addKeyListener(getHourKeyHandler());
fldMin.addKeyListener(getMinuteKeyHandler());
fldHour.addActionListener(getActionHandler());
fldMin.addActionListener(getActionHandler());
cmbTimeOfDay.addActionListener(getActionHandler());
}
@Override
public void removeNotify() {
// Clean up our listeners...
((AbstractDocument) fldHour.getDocument()).setDocumentFilter(null);
((AbstractDocument) fldMin.getDocument()).setDocumentFilter(null);
fldHour.removeFocusListener(getHourFocusHandler());
fldMin.removeFocusListener(getMinuteFocusHandler());
fldHour.removeKeyListener(getHourKeyHandler());
fldMin.removeKeyListener(getMinuteKeyHandler());
fldHour.removeActionListener(getActionHandler());
fldMin.removeActionListener(getActionHandler());
cmbTimeOfDay.removeActionListener(getActionHandler());
super.removeNotify();
}
/**
* Adds an action listener to the component. Actions are fired when the user
* presses the enter key
*
* @param listener
*/
public void addActionListener(ActionListener listener) {
listenerList.add(ActionListener.class, listener);
}
public void removeActionListener(ActionListener listener) {
listenerList.remove(ActionListener.class, listener);
}
/**
* Returns the field that is acting as the hour editor
*
* @return
*/
public JTextField getHourEditor() {
return fldHour;
}
/**
* Returns the field that is acting as the minute editor
*
* @return
*/
public JTextField getMinuteEditor() {
return fldMin;
}
/**
* Returns the combo box that provides the time of day selection
*
* @return
*/
public JComboBox getTimeOfDayEditor() {
return cmbTimeOfDay;
}
/**
* Returns the internal action handler. This handler monitors actions on the
* individual components and merges them into one.
*
* @return
*/
protected ActionHandler getActionHandler() {
if (actionHandler == null) {
actionHandler = new ActionHandler();
}
return actionHandler;
}
/**
* Returns the hour key listener
*
* @return
*/
protected HourKeyHandler getHourKeyHandler() {
if (hourKeyHandler == null) {
hourKeyHandler = new HourKeyHandler();
}
return hourKeyHandler;
}
/**
* Returns the minute key listener
*
* @return
*/
protected MinuteKeyHandler getMinuteKeyHandler() {
if (minuteKeyHandler == null) {
minuteKeyHandler = new MinuteKeyHandler();
}
return minuteKeyHandler;
}
/**
* Returns the document filter used to filter the hour field
*
* @return
*/
protected HourDocumentFilter getHourDocumentFilter() {
if (hourDocumentFilter == null) {
hourDocumentFilter = new HourDocumentFilter();
}
return hourDocumentFilter;
}
/**
* Returns the document filter user to filter the minute field
*
* @return
*/
protected MinuteDocumentFilter getMinuteDocumentFilter() {
if (minDocumentFilter == null) {
minDocumentFilter = new MinuteDocumentFilter();
}
return minDocumentFilter;
}
/**
* Returns the focus listener used to monitor the hour field
*
* @return
*/
protected HourFocusHandler getHourFocusHandler() {
if (hourFocusHandler == null) {
hourFocusHandler = new HourFocusHandler();
}
return hourFocusHandler;
}
/**
* Used the focus listener used to monitor the minute field
*
* @return
*/
protected MinuteFocusHandler getMinuteFocusHandler() {
if (minuteFocusHandler == null) {
minuteFocusHandler = new MinuteFocusHandler();
}
return minuteFocusHandler;
}
/**
* Sets the time based on the supplied date
*
* @param date
*/
public void setTime(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int hour = cal.get(Calendar.HOUR);
int min = cal.get(Calendar.MINUTE);
int dayPart = cal.get(Calendar.AM_PM);
TimeOfDay timeOfDay = TimeOfDay.AM;
switch (dayPart) {
case Calendar.PM:
timeOfDay = TimeOfDay.PM;
break;
}
setTime(hour, min, timeOfDay);
}
/**
* Sets the time based on a 24 hour clock. The field does not need to be in 24
* hour mode to use this method, the method will automatically correct the
* hour appropriately.
*
* @param hour
* @param min
*/
public void setTime(int hour, int min) {
hour = correctHour(hour);
min = correctMinute(min);
TimeOfDay timeOfDay = TimeOfDay.AM;
if (hour >= 12) {
timeOfDay = TimeOfDay.PM;
}
setTime(hour, min, timeOfDay);
}
/**
* Corrects the minute value to make sure it is within allowable ranges.
*
* For example, if you pass in 90 the method, it will automatically correct
* the value to 30, discard the overflow.
*
* This will not effect the hour value...although this might be worth
* consideration in the future
*
* @param min
* @return
*/
protected int correctMinute(int min) {
// Make sure the value is positive.
// If we were interested in altering the hour value as well, we wouldn't
// want to do this...
if (min < 0) {
min += (min * -2);
}
// Correct the minute value....
if (min > 59) {
// How many hours fit into this value
float part = min / 60f;
part = (float) (part - Math.floor(part)); // Get remainder
min = (int) (60 * part); // Calculate the number of minutes...
}
return min;
}
/**
* Basically, this method will attempt to correct the hour value and bring the
* it into range of a single day.
*
* We are basically going to try and figure out how many parts of the day that
* the hour falls in and make it equal to a single day...
*
* That is, if the hour is 35, it's actually 1.458... days, which is roughly 1
* day and 11 hours. We are only interested in the 11 hours, cause the date is
* irrelevant to us
*
* @param hour
* @return
*/
protected int correctHour(int hour) {
if (hour < 0) {
hour += (hour * -2);
}
if (hour > 23) {
float part = hour / 24f;
part = (float) (part - Math.floor(part));
hour = (int) (24 * part);
}
return hour;
}
/**
* Sets the time value for this field...
*
* @param hour
* @param min
* @param timeOfDay
*/
public void setTime(int hour, int min, TimeOfDay timeOfDay) {
hour = correctHour(hour);
min = correctMinute(min);
// Now that we have a correct hour value, we need to know if it will
// actually fit within the correct part of the day...
switch (timeOfDay) {
case AM:
cmbTimeOfDay.setSelectedIndex(0);
break;
case PM:
cmbTimeOfDay.setSelectedIndex(1);
break;
}
if (!is24HourClock()) {
if (hour > 12) {
hour -= 12;
}
} else {
if (hour < 12 && timeOfDay.equals(TimeOfDay.PM)) {
hour += 12;
}
}
fldHour.setText(pad(Integer.toString(hour), 2));
fldMin.setText(pad(Integer.toString(min), 2));
}
public int getHour() {
return Integer.parseInt(getHourEditor().getText());
}
public int getMinute() {
return Integer.parseInt(getMinuteEditor().getText());
}
public TimeOfDay getTimeOfDay() {
TimeOfDay tod = null;
switch (cmbTimeOfDay.getSelectedIndex()) {
case 0:
tod = TimeOfDay.AM;
break;
case 1:
tod = TimeOfDay.PM;
break;
}
return tod;
}
/**
* Sets if we should be using 24 or 12 hour clock. This basically configures
* the time of day field and the validation ranges of the various fields
*
* @param value
*/
public void set24HourClock(boolean value) {
if (value != use24HourClock) {
use24HourClock = value;
cmbTimeOfDay.setVisible(!use24HourClock);
if (cmbTimeOfDay.getSelectedIndex() == 1) {
setTime(getHour() + 12, getMinute(), getTimeOfDay());
}
invalidate();
firePropertyChange("24HourClock", !use24HourClock, value);
}
}
/**
* Returns if this is using a 24 or 12 hour clock
*
* @return
*/
public boolean is24HourClock() {
return use24HourClock;
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
cmbTimeOfDay = new javax.swing.JComboBox();
pnlFields = new javax.swing.JPanel();
lblSeperator = new javax.swing.JLabel();
fldHour = new javax.swing.JTextField();
fldMin = new javax.swing.JTextField();
addFocusListener(new java.awt.event.FocusAdapter() {
public void focusGained(java.awt.event.FocusEvent evt) {
doFocusGained(evt);
}
});
setLayout(new java.awt.GridBagLayout());
cmbTimeOfDay.setModel(new javax.swing.DefaultComboBoxModel(new String[]{"am", "pm"}));
cmbTimeOfDay.setBorder(null);
cmbTimeOfDay.setEditor(null);
cmbTimeOfDay.setOpaque(false);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 0;
gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 0);
add(cmbTimeOfDay, gridBagConstraints);
pnlFields.setBackground(new java.awt.Color(255, 255, 255));
pnlFields.setLayout(new java.awt.GridBagLayout());
lblSeperator.setText(":");
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 0;
gridBagConstraints.insets = new java.awt.Insets(0, 2, 0, 2);
pnlFields.add(lblSeperator, gridBagConstraints);
fldHour.setBorder(null);
fldHour.setColumns(2);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 0;
pnlFields.add(fldHour, gridBagConstraints);
fldMin.setBorder(null);
fldMin.setColumns(2);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 2;
gridBagConstraints.gridy = 0;
pnlFields.add(fldMin, gridBagConstraints);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 0;
add(pnlFields, gridBagConstraints);
}// </editor-fold>
private void doFocusGained(java.awt.event.FocusEvent evt) {
fldHour.requestFocus();
}
// Variables declaration - do not modify
private javax.swing.JComboBox cmbTimeOfDay;
private javax.swing.JTextField fldHour;
private javax.swing.JTextField fldMin;
private javax.swing.JLabel lblSeperator;
private javax.swing.JPanel pnlFields;
// End of variables declaration
/**
* Moves the focus forward to the next field.
*
* This is used to provide "automatic" focus movement
*/
protected void moveFocusForward() {
if (fldHour.hasFocus()) {
fldMin.requestFocus();
} else if (fldMin.hasFocus()) {
cmbTimeOfDay.requestFocus();
}
}
/**
* Moves the focus backwards to the previous field.
*
* This is used to provide "automatic" focus movement
*/
protected void moveFocusBackward() {
if (fldMin.hasFocus()) {
fldHour.requestFocus();
} else if (cmbTimeOfDay.hasFocus()) {
fldMin.requestFocus();
}
}
/**
* Fires the action performed event to all registered listeners
*
* @param evt
*/
protected void fireActionPerformed(ActionEvent evt) {
List<ActionListener> lstListeners = Arrays.asList(listenerList.getListeners(ActionListener.class));
if (!lstListeners.isEmpty()) {
Collections.reverse(lstListeners);
for (ActionListener listener : lstListeners) {
listener.actionPerformed(evt);
}
}
}
/**
* Hour key handler, used to monitor "special" keys for the hour field.
*
* This looks for the user pressing the ":" key and the right arrow key in
* order to perform special navigation
*/
protected class HourKeyHandler extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
boolean numLock = false;
try {
// Get the state of the nums lock
numLock = Toolkit.getDefaultToolkit().getLockingKeyState(KeyEvent.VK_NUM_LOCK);
} catch (Exception exp) {
}
// Move focus forward if the user presses the ":"
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON && e.isShiftDown()) {
moveFocusForward();
// Move focus forward if the user pressed the left arrow key
} else if ((e.getKeyCode() == KeyEvent.VK_NUMPAD6 && !numLock) || e.getKeyCode() == KeyEvent.VK_RIGHT) {
// If we are in the last edit position
if (fldHour.getCaretPosition() >= 2) {
moveFocusForward();
// Or we are in the first edit position and the field only contains a single character
} else if (fldHour.getText().trim().length() == 1 && fldHour.getCaretPosition() == 1) {
moveFocusForward();
}
}
}
}
/**
* Minute key handler, used to monitor "special" keys for the hour field.
*
* This looks for the user pressing the left arrow key in order to perform
* special navigation
*/
protected class MinuteKeyHandler extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
boolean numLock = false;
try {
numLock = Toolkit.getDefaultToolkit().getLockingKeyState(KeyEvent.VK_NUM_LOCK);
} catch (Exception exp) {
}
if ((e.getKeyCode() == KeyEvent.VK_NUMPAD4 && !numLock) || e.getKeyCode() == KeyEvent.VK_LEFT) {
// Only want to move backwards if we are at the first edit position
if (fldMin.getCaretPosition() == 0) {
moveFocusBackward();
}
}
}
}
/**
* Hour field focus handler. This watches for focus lost events a
* automatically pads the field with a leading "0" if the field is only 1
* character in length
*/
protected class HourFocusHandler extends FocusAdapter {
@Override
public void focusLost(FocusEvent e) {
String text = fldHour.getText();
if (text.length() < 2) {
text = pad(text, 2);
fldHour.setText(text);
}
}
}
/**
* Minute field focus handler, watches for focus lost events and automatically
* adds a "0" to the end of the field if it is only 1 character in length
*/
protected class MinuteFocusHandler extends FocusAdapter {
@Override
public void focusLost(FocusEvent e) {
String text = fldMin.getText();
if (text.length() < 2) {
fldMin.setText(text + "0");
}
}
}
/**
* The document filter used to filter the hour field.
*/
protected class HourDocumentFilter extends DocumentFilter {
@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
System.out.println("insert: offset = " + offset + "; text = " + text);
super.insertString(fb, offset, text, attr);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
try {
boolean isAcceptable = false;
boolean passOnFocus = false;
int strLength = text.length();
// We convert the value here to make sure it's a number...
int value = Integer.parseInt(text);
// If the length of the string been replaced is only 1 character
if (strLength == 1) {
// If we are at the start of the editing position
if (offset == 0) {
// What clock type are we using...
if (!is24HourClock()) {
// only accept 0 or 1...
if (value <= 1) {
isAcceptable = true;
}
} else if (value <= 2) {
isAcceptable = true;
}
// If we are at the second editing position
} else if (offset == 1) {
// Get the preceeding value, should be 0, 1 or 2
String upperPart = fb.getDocument().getText(0, 1);
// Convert the value to an int
int upperValue = Integer.parseInt(upperPart);
// The acceptable range of values for the given position
int lowerRange = 0;
int upperRange = 9;
// Which clock are we using
if (is24HourClock()) {
// If the first value is 2, we can only accept values from 0-3 (20-23)
if (upperValue == 2) {
upperRange = 3;
}
} else {
// 12 hour clock
// If the first value is 1, we can only accept values from 0-2 (10-12)
if (upperValue == 1) {
upperRange = 2;
}
}
// Is the value within accpetable range...
if (value >= lowerRange && value <= upperRange) {
isAcceptable = true;
}
// Pass on focus (only if the value is accepted)
passOnFocus = true;
}
} else {
// First, we need to trim the value down to a maximum of 2 characters
// Need to know at what offest...
// 2 - offset..
// offset == 0, length = 2 - offset = 2
// offset == 1, length = 2 - offset = 1
strLength = 2 - offset;
String timeText = text.substring(offset, strLength);
value = Integer.parseInt(timeText);
// this will only work if we are using a 24 hour clock
if (value >= 0 && value <= 23) {
while (value > 12 && is24HourClock()) {
value -= 12;
}
// Pad out the text if required
text = pad(value, 2);
isAcceptable = true;
}
}
if (isAcceptable) {
super.replace(fb, offset, length, text, attrs);
if (passOnFocus) {
moveFocusForward();
}
}
} catch (NumberFormatException exp) {
}
}
}
/**
* The document filter used to filter the minute field.
*/
protected class MinuteDocumentFilter extends DocumentFilter {
@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
System.out.println("insert: offset = " + offset + "; text = " + text);
super.insertString(fb, offset, text, attr);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
try {
boolean isAcceptable = false;
boolean passOnFocus = false;
// How long is the text been added
int strLength = text.length();
// Convert the value to an integer now and save us the hassel
int value = Integer.parseInt(text);
// If the length is only 1, probably a new character has been added
if (strLength == 1) {
// The valid range of values we can accept
int upperRange = 9;
int lowerRange = 0;
if (offset == 0) {
// If we are at the first edit position, we can only accept values
// from 0-5 (50 minutes that is)
upperRange = 5;
} else if (offset == 1) {
// Second edit position...
// Every thing is valid here...
// We want to pass on focus if the clock is in 12 hour mode
passOnFocus = !is24HourClock();
}
// Is the value acceptable..
if (value >= lowerRange && value <= upperRange) {
isAcceptable = true;
}
} else {
// Basically, we are going to trim the value down to at max 2 characters
// Need to know at what offest...
// 2 - offset..
// offset == 0, length = 2 - offset = 2
// offset == 1, length = 2 - offset = 1
strLength = 2 - offset;
String timeText = text.substring(offset, strLength);
value = Integer.parseInt(timeText);
if (value >= 0 && value <= 59) {
// Pad out the value as required
text = pad(value, 2);
isAcceptable = true;
}
}
if (isAcceptable) {
super.replace(fb, offset, length, text, attrs);
if (passOnFocus) {
moveFocusForward();
}
}
} catch (NumberFormatException exp) {
}
}
}
/**
* This is a simple "pass" on action handler...
*/
protected class ActionHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
ActionEvent evt = new ActionEvent(TimeField.this, e.getID(), e.getActionCommand(), e.getModifiers());
fireActionPerformed(evt);
}
}
public static String pad(long lValue, int iMinLength) {
return pad(Long.toString(lValue), 2);
}
public static String pad(int iValue, int iMinLength) {
return pad(Integer.toString(iValue), iMinLength);
}
public static String pad(String sValue, int iMinLength) {
StringBuilder sb = new StringBuilder(iMinLength);
sb.append(sValue);
while (sb.length() < iMinLength) {
sb.insert(0, "0");
}
return sb.toString();
}
}
why should someone chose this bulky implementation over above given DavidKroukamp's simple solution. Also, this has some bugs when manually entering numbers. I prefer DavidKroukamp's way..
Simple answer, validation. JFormattedTextField
does not valid the values, but only limits the fact that you can enter digits, you could just as easily enter 99:99
into a JFormattedTextField
. The intention of this implementation is to provide real time validation while providing simple data entry requirements