JTextField에서 숫자만 입력하는지 검사하기
현재(역주:2001년 11월 20일) 베타판이 나와있는 Java 2 SDK 표준에디션에는 양식에 맞는 텍스트 입력을 위해 JFormattedTextField 라는 컴포넌트가 추가되었다. 이 컴포넌트는 텍스트 필드에 입력된 것을 검사할 수 있다. 그러나 지금 당장 그것이 필요한데 정식판이 나올 때까지 기다릴 수 없다면 어쩌겠나? 텍스트 필드에 입력된 것을 검사할 수 있는 적어도 세가지의 다른 방법이 있다. 그 세가지는 키를 누르는 수준에서, 포커스 수준에서 그리고 데이터 모델 수준에서 검사할 수 있다. 이 팁은 숫자 입력만 받아들이는 텍스트 필드를 만들기 위해 이 기술들을 어떻게 사용하는 지를 보여줄 것이다.
AWT의 TextField에도 있는 것처럼, 스윙의 JTextField 컴포넌트에 KeyListener를 등록할 수 있다. 리스너가 등록되면, 키가 눌려지는 것을 알아낼 수 있다. 틀린 것을 수정하기 위해 backspace 키나 delete키 빼고는 숫자 키가 아니면 이것을 거부할 수 있다. 거부하는 것은 KeyEvent의 consume() 메소드를 호출함으로 처리된다. 이 메소드는 컴포넌트에게 그 키보드 입력은 입력 리스너에 의해 처리되었으니 화면으로 출력하지마라고 지시한다.
리스너를 사용해서 입력 검사하는 코드는 아래와 비슷할 것이다.
keyText.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (!((Character.isDigit(c) ||
(c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
getToolkit().beep();
e.consume();
}
}
});

입력기(역주:한글입력기 같이 시스템에 설치된 IME)가 설치해야 되는 환경에서 동작한다면, 키 리스너를 사용하는 데 특별히 고려해야 할 것이 있다. 이 경우 입력기가 키 리스너가 키를 누르는 것을 감지 못하게 할 것이다. 이것은 보통 문자가 키보드키와 1:1로 메핑되지 않는 경우 생긴다. 예를들면 한자나 일본어를 입력할 경우에 그렇다.
KeyListener 대신에 FocusListener를 사용하면 약간 다른 동작을 한다. KeyListener는 키보드 키가 눌러지는 매번 확인하지만, FocusListener는 입력필드가 포커스를 잃을 때 검사한다. 필드값 전체를 검사하기 때문에 이 방법은 간단히 Integer의 parseInt() 메소드를 사용한다. 사실 입력값은 중요하지 않다. 중요한 것은 입력된 것을 파싱할 수 있다는 것이다.
FocusListner를 이용한 입력값 검사코드는 다음과 비슷할 것이다.
focusText.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent e) {
JTextField textField = (JTextField)e.getSource();
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(content);
} catch (NumberFormatException nfe) {
getToolkit().beep();
textField.requestFocus();
}
}
}
});

불행히도, 입력 화면에 메뉴가 있거나 팝업 대화상자가 있으면, 포커스 수준에서의 리스너를 사용하는 데는 문제가 있다. 메뉴를 사용할 때, 리스너는 top-level 메뉴가 입력 포커스를 받을 때, 실제 호출된다. 그리니까 선택하려는 메뉴를 찾아 이동할 때 실제 호출이 된다. 위의 리스너는 잘못된 입력에 대해 비프음을 낸다. 그러나 오류 메세지를 나타내기 위해 대화상자가 팝업되었다면 무슨 일이 일어날지를 상상해보라.
스윙 컴포넌트의 포커스 수준에서의 검사하는 두 번째 방법이 있다. InputVerifier를 컴포넌트에 붙일 수 있다. 이 추상 클래스는 boolean verify(JComponent) 메소드 하나를 가지고 있는데 입력값에 대해 검사를 수행하게 구현하면 된다. 이 메소드는 입력이 옳바르면 true를 반환하고 그렇지 않으면 false를 반환해야된다. FocusListener 기법에서처럼, true인지 false인지를 검사하기 위해 parseInt()를 사용해도 된다. 이 검사기를 붙이려면, setInputVerifier()를 호출하면된다. 사용자가 해당 필드를 벗어나 입력 포커스를 옮길 때 검사기는 입력값을 검사한다.
FocusListener의 경우에서처럼, InputVerifier는 각각의 입력이 유효한가를 결정하는 게 아니라 입력값 전체에 대해서 검사를 한다. 예를 들면, 이것은 입력값이 특정 범위 내에서 이뤄져야 하는 경우 중요하다. ??? [원문보기] As with the FocusListener, the InputVerifier permits validation on the whole field, versus trying to determine if part of the input is valid. This is important, for instance, if you want the input to be within a certain range.
InputVerifier에 잘못된 입력에 대해 어떻게 반응하는지를 기술하는 두번째 메소드 boolean shouldYieldFocus(JComponent)가 있다. 이 메소드는 기본적으로 verify()의 결과를 반환하게 구현되어 있다. 만약 잘못된 입력에 대해 비프음을 내고 싶다면, 그 값을 반환하기 전에 검사해야 된다.
InputVerifier를 사용해서 잘못된 입력에 대해 비프음을 내는 예제를 보자.
inputText.setInputVerifier(new InputVerifier() {
public boolean verify(JComponent comp) {
boolean returnValue = true;
JTextField textField = (JTextField)comp;
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(textField.getText());
} catch (NumberFormatException e) {
returnValue = false;
}
}
return returnValue;
}

public boolean shouldYieldFocus(JComponent input) {
boolean valid = super.shouldYieldFocus(input);
if (!valid) {
getToolkit().beep();
}
return valid;
}
});

이 세번째 방법은 입력 포커스를 다시 가져오기 위해 requestFocus()를 호출하지 않아도 되기 때문에 좀 깔끔하게 보이지만, 이것 역시 FocusListener와 같은 문제를 안고 있다.
이 팁에서 다루는 마지막 검증방법은 스윙의 모델-뷰-컨트롤러(MVC) 구조에 대한 이해를 수반한다. 모든 JTextComponent (JTextField 같은)은 이면에 데이터를 가진 텍스트 컴포넌트 모델이다. JTextField는 그 모델에 대해 단 한개의 뷰만 있다. 그 모델에 넣을 수 있는 것을 제한함으로써, JTextField에서 보여지는 것을 제한 할 수 있다.
데이터 모델에 입력 검사하는 것을 넣음으로써, 전에 언급된 메뉴를 선택되었을 때 무엇을 해야 하는 가하는 문제나 입력기(역주:IME) 리스너가 있을 때 입력을 검사하는 방법에 관한 문제는 피하게 된다. 이 마지막 검사 모델은 가장 복잡하지만 잘 동작한다.
JTextField의 기본 모델은 javax.swing.text.PlainDocument 클래스이다. 이 클래스는 사용자가 컴포넌트에 텍스트를 입력하거나 제거할 때 호출되는 insertString()와 remove() 메소드를 제공한다. 보통 한 번에 한 문자에 대해 행해진다. 그러나 오려내기나 복사하기가 수행될 때를 반드시 고려해야 된다. 각 메소드가 하는 것인 모델이 그 모델에 데이터가 추가되나 삭제되었을 때, 그것이 유효한가를 확인하는 것이다. 확인과정을 통과했다고 가정하면, super.insertString()이나 super.remove()를 호출해서 슈퍼클래스로 그 데이터를 전달한다.
insertString()의 핵심 부분은 대충 이렇하다. 입력된 것을 검정하기 위해서 어떤 것이 새 문자열이 될 것인가를 결정한다. 그 모델이 원래 비었다면 새 문자열은 입력된 값이다. 그렇지 않으면 새 값은 원래 있던 내용의 중간에 삽입된다. 새 값을 얻은 후, 그것을 Integer로 parseInt() 검사하고, 아무 문제 없으면, super.insertString()를 호출한다. 알아둘 것은 유효하지 않는 입력은 super.insertString()를 호출하지 않음으로써 그 입력이 거부되었음을 알린다. 문자열을 집어 넣지 않으면 아무것도 할 필요가 없다. 그러나 이 코드는 입력이 실패하면 비프음을 울린다.
String newValue;
int length = getLength();

if (length == 0) {
newValue = string;
} else {
String currentContent = getText(0, length);
StringBuffer currentBuffer = new StringBuffer(currentContent);
currentBuffer.insert(offset, string);
newValue = currentBuffer.toString();
}

try {
Integer.parseInt(newValue);
super.insertString(offset, string, attributes);
} catch (NumberFormatException exception) {
Toolkit.getDefaultToolkit().beep();
}

정수형 입력만 받아 들이는 모델의 경우, remove() 메소드의 기본 동작을 오버라이드할 필요는 없다. 정수형 텍스트 문자열에서 데이터(숫자)를 제거하고 숫자가 아닌 부분을 반환하는 것은 불가능하다. [원문보기] For the case of a model that only accepts integer input, it isn't necessary to override the default behavior of the remove() method. It is impossible to remove data from an integer text string and get back a non-integer.
완전한 모델을 정의한 후에, 그 모델을 텍스트 필드와 연결하기 위해 setDocument() 메소드를 사용해라.
modelText.setDocument(new IntegerDocument());

여기에 모든 네가지를 보여주는 완전한 예제가 있다. 이 안에 IntegerDocument 클래스의 정의도 볼 수 있을 것이다.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;

public class TextInput extends JFrame {
JPanel contentPane;
JPanel jPanel1 = new JPanel();
FlowLayout flowLayout1 = new FlowLayout();
GridLayout gridLayout1 = new GridLayout();
JLabel keyLabel = new JLabel();
JTextField keyText = new JTextField();
JLabel focusLabel = new JLabel();
JTextField focusText = new JTextField();
JLabel inputLabel = new JLabel();
JTextField inputText = new JTextField();
JLabel modelLabel = new JLabel();
JTextField modelText = new JTextField();
IntegerDocument integerDocument1 =
new IntegerDocument();

public TextInput() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
contentPane = (JPanel)getContentPane();
contentPane.setLayout(flowLayout1);
this.setSize(new Dimension(400, 300));
this.setTitle("Input Validation");
jPanel1.setLayout(gridLayout1);
gridLayout1.setRows(4);
gridLayout1.setColumns(2);
gridLayout1.setHgap(20);
keyLabel.setText("Key Listener");
modelLabel.setText("Model");
focusLabel.setText("Focus Listener");
inputLabel.setText("Input Verifier");

keyText.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (!(Character.isDigit(c) ||
(c == KeyEvent.VK_BACK_SPACE) ||
(c == KeyEvent.VK_DELETE))) {
getToolkit().beep();
e.consume();
}
}
});

focusText.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent e) {
JTextField textField = (JTextField)e.getSource();
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(content);
} catch (NumberFormatException nfe) {
getToolkit().beep();
textField.requestFocus();
}
}
}
});

inputText.setInputVerifier(new InputVerifier() {
public boolean verify(JComponent comp) {
boolean returnValue = true;
JTextField textField = (JTextField)comp;
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(textField.getText());
} catch (NumberFormatException e) {
getToolkit().beep();
returnValue = false;
}
}
return returnValue;
}
});

modelText.setDocument(integerDocument1);

contentPane.add(jPanel1);
jPanel1.add(keyLabel);
jPanel1.add(keyText);
jPanel1.add(focusLabel);
jPanel1.add(focusText);
jPanel1.add(inputLabel);
jPanel1.add(inputText);
jPanel1.add(modelLabel);
jPanel1.add(modelText);
}

public static void main(String args[]) {
TextInput frame = new TextInput();
frame.pack();
frame.show();
}

static class IntegerDocument extends PlainDocument {

public void insertString(int offset, String string, AttributeSet attributes)
throws BadLocationException {

if (string == null) {
return;
} else {
String newValue;
int length = getLength();
if (length == 0) {
newValue = string;
} else {
String currentContent = getText(0, length);
StringBuffer currentBuffer =
new StringBuffer(currentContent);
currentBuffer.insert(offset, string);
newValue = currentBuffer.toString();
}
try {
Integer.parseInt(newValue);
super.insertString(offset, string, attributes);
} catch (NumberFormatException exception) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}
}

네 개의 텍스트 필드 전부에 대해서 복사해서 붙이기를 해서 어떻게 되나 봐라. 예를 들면, 첫번째 KeyListener 기법을 사용해서 검사하는 텍스트 필드는 숫자가 아닌 것을 붙이기 하면 필드에 입력이 되어버린다. 이런 현상을 처리하려면 붙이기가 안되게 해야될 것이다. 반면에, "Model"이라고 되어 있는 IntegerDocument로 검사하는 텍스트 필드는 제대로 동작을 해서 붙이기가 되지 않는다.
프로그램을 AWT의 TextField로 바꾸려고 한다면, JTextField에서 지원되지 않지만 TextField에는 가능한 한가지는 컨트롤에 TextListener를 붙일 수 있다는 것이다. (역주: TextField의 슈퍼클래스인 java.awt.TextComponent에서 제공하는 메소드이다. javax.swing.text.JTextComponent에는 없다.) 그러나, 사용자 정의 Document를 붙이는 데이터 모델을 사용함으로써 이것을 쉽게 대체할 수 있다. Document는 값이 바뀌었을 때 통보되는 것에 (혹은, 적어도 바꾸고 깊어하는 것에) 직접적으로 연결해 사용한다. (역주:이 마지막 문장은 좀 잘 모르겠더라구요. 고민 끝에 이렇게 해석했는데, 이상하다고 생각되시면, 원문을 한 번 참조해 보세요..^^) [원문보기] If you are transitioning a program with an AWT TextField component to a Swing JTextField, note that one TextField behavior that is not supported on the JTextField is the ability to attach a TextListener to the control. However, you can you can easily replace this behavior by using the data model approach of attaching a custom Document. The Document usage directly maps to being notified when the value of the text has changed (or, at least, wants to change).
스윙 텍스트 컴포넌트에 대해 더 공부하고 싶으면 자바 튜토리얼의 Creating a GUI with JFC/Swing 뒷부분에 스윙 컴포넌트 사용하기 강좌를 참조해라.


내 남자의 길~! 블로그를 구독하고 싶으시면 Click --->


1 ··· 577 578 579 580 581 582 583 584 585 ··· 717 

글 보관함

카운터

Total : 1,676,590 / Today : 139 / Yesterday : 230
get rsstistory!