Jak umożliwić wpisywanie tylko cyfr w komponenty

Koziołek

Standardowa metoda pobierania liczb z komponentów takich jak JTextField wygląda zazwyczaj tak:

private static void przypadek1() {
	JFrame frame = new JFrame("Metoda 1");
	final JTextField textField = new JTextField();
	JButton button = new JButton("Parsuj do liczby");

	frame.setLayout(new GridLayout(2, 1));
	frame.addWindowListener(new WindowAdapter() {
		public void windowClosing(WindowEvent e) {
			przypadek2();
		}
	});

	frame.setSize(200, 75);
	frame.setResizable(false);

	frame.add(textField);
	frame.add(button);

	button.addActionListener(new ActionListener() {

		public void actionPerformed(ActionEvent e) {
			String stringToParse = textField.getText();
			Integer parsedNumber = null;
			try {
				parsedNumber = Integer.parseInt(stringToParse);
				JOptionPane.showMessageDialog(null, "Liczba: "
					+ parsedNumber.intValue(), "Wynik parsowania",
					JOptionPane.INFORMATION_MESSAGE);
			} catch (Exception exp) {
				JOptionPane
					.showMessageDialog(null, exp.getStackTrace(),
					"Wynik parsowania",
					JOptionPane.INFORMATION_MESSAGE);
			}
		}
	});

	frame.setVisible(true);
}

W tym przypadku blok try/catch służy do sprawdzenia czy operacja parsowania przebiegła prawidłowo. Jest to mało wydajne rozwiązanie oparte o programowanie sterowane wyjątkami. Taki sposób rozwiązywania problemów jest zły ponieważ przepływ sterowania jest zarządzany przez kombinację wyrzucanych wyjątków i operacji ich obsługi. Powoduje to ogromy narzut na czas wykonywania operacji. Operacja try/catch wykonuje się w sprzyjających warunkach (program jedno wątkowy, niewielka ilość operacji w bloku try) aż 10-krotnie wolniej niż normalny kod.

Znacznie lepszym rozwiązaniem jest przerzucenie odpowiedzialności za przyjmowane znaki na obiekt textField. Najprostszym sposobem na dokonanie tego jest dodanie odpowiedniego obiektu nasłuchującego (ang. listener) do naszego pola:

private static void przypadek2() {
	JFrame frame = new JFrame("Metoda 2");
	final JTextField textField = new JTextField();
	JButton button = new JButton("Parsuj do liczby");

	frame.setLayout(new GridLayout(2, 1));
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.setSize(200, 75);
	frame.setResizable(false);

	frame.add(textField);
	frame.add(button);

	textField.addKeyListener(new KeyAdapter() {
		public void keyTyped(KeyEvent e) {
			if (e.getKeyChar() == '-'
				&& textField.getText().indexOf('-') > -1) {
				e.consume();
				return;
			}
			if (!(e.getKeyChar() >= '0' && e.getKeyChar() <= '9'))
				e.consume();
		}
	});

	button.addActionListener(new ActionListener() {

		public void actionPerformed(ActionEvent e) {
		String stringToParse = textField.getText();
		Integer parsedNumber = null;
		parsedNumber = Integer.parseInt(stringToParse);
		JOptionPane.showMessageDialog(null, "Liczba: "
			+ parsedNumber.intValue(), "Wynik parsowania",
			JOptionPane.INFORMATION_MESSAGE);

		}
	});

	frame.setVisible(true);
}

W tym przypadku w obiekcieKeyAdapter dokonywane jest sprawdzenie czy wpisany znak jest cyfrą lub minusem jako pierwszy znak, a jeżeli tak nie jest to zdarzenie zostaje skonsumowane (wywołanie w.consume()). Dzięki temu mamy pewność, że tekst pobrany z pola tekstowego i przekazany do parsowania nie zawiera znaków innych niż cyfry.

Dopisane - bogdans
Nie zgadzam się z krytycznym podejściem do korzystania w tej sytuacji z wyjątków. Ponadto powyższy kod jest błędny: pozwala wkleić inne znaki niż cyfry, nie pozwala wpisać minusa, pozwala przekroczyć zakres typu int.
Zatem parsowanie może wygenerować nieobsługiwany wyjątek. Poniższy kod jest pozbawiony pierwszych dwóch błędów, trudniej jest tez zrobić trzeci błąd (długość wpisywanego tekstu jest ograniczona do 11 znaków).

MaskFormatter mask = new MaskFormatter("***********"); 
//jest 11 gwiazdek, bo liczba typu int może mieć najwyżej 11 znaków
mask.setValidCharacters("-1234567890 ");
JFormattedTextField tf = new JFormattedTextField(mask);
tf.setColumns(11);

koniec dopisanego

Na koniec pełen program wraz z parsowaniem liczb zmiennoprzecinkowych Double (i rozwiązaniem problemu kropki i liczb ujemnych):

package eu.runelord.programmersjava.faq.jcomponentdigitsonly;

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

public class JComponentsDigitsOnly {

	private static final int HEIGHT = 75;
	private static final int WIDTH = 300;

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

	private static void przypadek1() {
		JFrame frame = new JFrame("Metoda 1 - nieprawidłowa");
		final JTextField textField = new JTextField();
		JButton button = new JButton("Parsuj do liczby");

		frame.setLayout(new GridLayout(2, 1));
		frame.addWindowListener(new WindowAdapter() {

			public void windowClosing(WindowEvent e) {
				przypadek2();
			}
		});

		frame.setSize(WIDTH, HEIGHT);
		frame.setResizable(false);

		frame.add(textField);
		frame.add(button);

		button.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				String stringToParse = textField.getText();
				Integer parsedNumber = null;
				try {
					parsedNumber = Integer.parseInt(stringToParse);
					JOptionPane.showMessageDialog(null, "Liczba: "
							+ parsedNumber.intValue(), "Wynik parsowania",
							JOptionPane.INFORMATION_MESSAGE);
				} catch (Exception exp) {
					JOptionPane
							.showMessageDialog(null, exp.getStackTrace(),
									"Wynik parsowania",
									JOptionPane.INFORMATION_MESSAGE);
				}
			}
		});

		frame.setVisible(true);
	}

	private static void przypadek2() {
		JFrame frame = new JFrame("Metoda 2 - prawidłowa liczby całkowite");
		final JTextField textField = new JTextField();
		JButton button = new JButton("Parsuj do liczby");

		frame.setLayout(new GridLayout(2, 1));
		frame.addWindowListener(new WindowAdapter() {

			public void windowClosing(WindowEvent e) {
				przypadek3();
			}
		});

		frame.setSize(WIDTH, HEIGHT);
		frame.setResizable(false);

		frame.add(textField);
		frame.add(button);

		textField.addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent e) {
				if (e.getKeyChar() == '-'
						&& textField.getText().indexOf('-') > -1) {
					e.consume();
					return;
				}
				if (!(e.getKeyChar() >= '0' && e.getKeyChar() <= '9'))
					e.consume();
			}
		});

		button.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				String stringToParse = textField.getText();
				Integer parsedNumber = null;
				parsedNumber = Integer.parseInt(stringToParse);
				JOptionPane.showMessageDialog(null, "Liczba: "
						+ parsedNumber.intValue(), "Wynik parsowania",
						JOptionPane.INFORMATION_MESSAGE);

			}
		});

		frame.setVisible(true);
	}

	private static void przypadek3() {
		JFrame frame = new JFrame("Metoda 3 - liczby zmiennoprzecinkowe");
		final JTextField textField = new JTextField();
		JButton button = new JButton("Parsuj do liczby");

		frame.setLayout(new GridLayout(2, 1));
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		frame.setSize(WIDTH, HEIGHT);
		frame.setResizable(false);

		frame.add(textField);
		frame.add(button);

		textField.addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent e) {
				if (e.getKeyChar() == '.'
						&& textField.getText().indexOf('.') > -1) {
					e.consume();
					return;
				}
				if (e.getKeyChar() == '-'
						&& textField.getText().indexOf('-') > -1) {
					e.consume();
					return;
				}
				if (!((e.getKeyChar() >= '0' && e.getKeyChar() <= '9') || e
						.getKeyChar() == '.'))
					e.consume();
			}
		});

		button.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				String stringToParse = textField.getText();
				Double parsedNumber = null;
				parsedNumber = Double.parseDouble(stringToParse);
				JOptionPane.showMessageDialog(null, "Liczba: "
						+ parsedNumber.doubleValue(), "Wynik parsowania",
						JOptionPane.INFORMATION_MESSAGE);

			}
		});

		frame.setVisible(true);
	}
}

3 komentarzy

Ostrzeżenie, kod jest błędny. Nie chroni przed wklejeniem ze schowka niepoprawnych znaków, nie chroni przed przekroczeniem zakresu typu int. W obu wypadkach mamy nieobsłużony wyjątek.

Heh. Już dawno nie widziałem tak złego kodu. Rozwlekły, a jednocześnie nieczytelny.

chyba najprostszym sposobem jest przesłonięcie metody insertString w obiekcie typu Document:

http://java.sun.com/j2se/1.4.2/docs/api/javax/swing/JTextField.html