Return-Path: Received: from pacific-carrier-annex.mit.edu by po10.mit.edu (8.9.2/4.7) id OAA08517; Tue, 20 Nov 2001 14:03:01 -0500 (EST) Received: from hermes.sun.com (hermes.sun.com [64.124.140.169]) by pacific-carrier-annex.mit.edu (8.9.2/8.9.2) with SMTP id OAA10127 for ; Tue, 20 Nov 2001 14:02:58 -0500 (EST) Message-Id: <200111201902.OAA10127@pacific-carrier-annex.mit.edu> Date: Tue, 20 Nov 2001 19:02:58 GMT+00:00 From: "JDC Tech Tips" To: alexp@mit.edu Subject: JDC Tech Tips November 20, 2001 Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit X-Mailer: Beyond Email J D C T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips, November 20, 2001. This issue covers: * Validating Numerical Input in a JTextField * Working with Fonts These tips were developed using Java(tm) 2 SDK, Standard Edition, v 1.3. This issue of the JDC Tech Tips is written by John Zukowski, president of JZ Ventures, Inc. (http://www.jzventures.com). You can view this issue of the Tech Tips on the Web at http://java.sun.com/jdc/JDCTechTips/2001/tt1120.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VALIDATING NUMERICAL INPUT IN A JTEXTFIELD The Java 2 SDK, Standard Edition, v 1.4 which is currently available as a Beta release, adds a JFormattedTextField component for formatted text input. Among other things, this gives you the ability to validate input to the field. But what do you do if you need to validate input now and can't wait for the new release? There are at least three different ways you can validate your text input fields today: keystroke level, focus level, and data model level. This tip shows you how to use these techniques to create an input field that only accepts numeric input. As is the case for the AWT TextField component, the Swing JTextField component supports registering a KeyListener with the component. When a listener is registered, you can watch for keys pressed. If the key pressed is not a numeric key, you can reject it, that is, with one exception: you have to permit backspace and delete to correct mistakes. Rejection is handled by calling the consume() method of the KeyEvent, which tells the component that the keystroke was dealt with by one of its input listeners and shouldn't be displayed. Here's what input verification using a listener like this might look like: keyText.addKeyListener(new KeyAdapter() { public void keyTyped(KeyEvent e) { char c = e.getKeyChar(); if (!((c >= '0') && (c <= '9') || (c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) { getToolkit().beep(); e.consume(); } } }); There's a special consideration in using a key listener if you are working in an environment where you need to install an input method listener. In this case, the input method listener will disable the ability to capture keystrokes with a key listener. This usually happens when there are not enough keyboard keys to map to input characters. An example of this is accepting Chinese or Japanese characters as input. Using a FocusListener instead of a KeyListener provides a slightly different behavior. Where the KeyListener verifies each keystroke, the FocusListener validates the input when the focus on the input field is lost. Because it verifies the whole field, this technique simply involves parsing the input with the parseInt() method of Integer. In fact, the input value doesn't matter. What does matter is that you can parse the input. Here's what the FocusListner version of input verification looks like: 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(); } } } }); Unfortunately, there is a problem with using focus level listeners when your input screen has a menu or pops up a dialog. Either of these events would trigger a call to the focus listener. With menus, the listener is actually called when each top-level menu gets input focus, that is, as you move to find the right menu item to select. The listener shown above beeps on invalid input, however, imagine what would happen if a dialog popped up to display an error message. There's a second way of doing focus-level verification of Swing components. You can attach an InputVerifier to the component. The abstract class has a single method, boolean verify(JComponent), that you implement to perform validation of the input. The method needs to return true if the input is valid, and false otherwise. As in the FocusListener technique, you can use parseInt() to check for true or false. To attach the verifier, you call the setInputVerifier() method. When a user tries to move the input focus beyond the associated field, the verifier takes action to validate the input. 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. There's a second method in InputVerifier for handling how to respond to invalid input: boolean shouldYieldFocus(JComponent). The default implementation of the method returns the value returned by verify(). If you want to beep on invalid input, you have to check the value before returning. Here's an example of input verification and beeping on invalid input using an 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; } }); While this third way might look a little cleaner, in that it doesn't require you to provide the requestFocus() call to return the input focus, it too suffers from the same problem as the FocusListener. The final way to validate input covered in this tip involves understanding Swing's Model-View-Controller (MVC) architecture. Behind every JTextComponent (such as a JTextField), is a model that holds the data in the text component. The JTextField is just one view into that model. By limiting what you can put in the model, you can limit what can be displayed in the JTextField. By adding the validation of the input to the data model, you avoid the previously mentioned problems of what to do when a menu is selected or how to validate the input when an input method listener is attached. While this last validation model is the most complex, it works well. The default model for the JTextField is the javax.swing.text.PlainDocument class. The class provides insertString() and remove() methods that are called when a user enters or removes text in the component. Normally, this would be done a character at a time. However, you must take into account when a cut or paste operation is performed. What each method does is make sure the model would be valid if the new data was added to the model or removed from the model. This task sounds more complicated then it really is. You just have to manually determine what the new content would be with (or without) the new data. Assuming the validation passes, you pass the data to the superclass by calling super.insertString() or super.remove(). Here's what the core part of the insertString() method looks like. To validate the input, you determine what the new string would be. If the model was originally empty, the new value is the input. Otherwise, you insert the new value in the middle of the existing contents. After you have the new value, you validate it with the parseInt() method of Integer. If the validation succeeds, you call super.insertString(). Notice that rejection is indicated simply by not calling super.insertString(). If you don't insert the string, you don't have to do anything. However, this code does beep if the input fails. 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 (Exception exception) { Toolkit.getDefaultToolkit().beep(); } 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. After you define the complete model, use the setDocument() method to associate the model with the text field: modelText.setDocument(new IntegerDocument()); Here's a complete example that demonstrates all four options. In it, you'll also find the definition of the IntegerDocument class: 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 (!((c >= '0') && (c <= '9') || (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 { checkInput(newValue); super.insertString(offset, string, attributes); } catch (Exception exception) { Toolkit.getDefaultToolkit().beep(); } } } public void remove(int offset, int length) throws BadLocationException { int currentLength = getLength(); String currentContent = getText(0, currentLength); String before = currentContent.substring( 0, offset); String after = currentContent.substring( length+offset, currentLength); String newValue = before + after; try { checkInput(newValue); super.remove(offset, length); } catch (Exception exception) { Toolkit.getDefaultToolkit().beep(); } } private int checkInput(String proposedValue) throws NumberFormatException { int newValue = 0; if (proposedValue.length() > 0) { newValue = Integer.parseInt( proposedValue); } return newValue; } } } Be sure to try out all four text fields with cut-and-paste to see what happens. For instance, the text field validated using the KeyListener technique allows you to paste non-numerical data into the field. To correct this behavior, you would have to disable pasting. By comparison, the text field that is validated with the IntegerDocument, that is, the one labeled "Model," works properly when pasting text data. 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). To learn more about the Swing text components, see the Using Swing Components lesson in the Creating a GUI with JFC/Swing trail found in The Java Tutorial at http://java.sun.com/docs/books/tutorial/uiswing/components/text.html. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WORKING WITH FONTS Drawing text in Java programs hasn't changed that much since the birth of the Java platform. Just set the font to the appropriate type and size then draw the string, as shown in the following simple program: import java.awt.*; import javax.swing.*; public class Text1 extends JFrame { public void paint(Graphics g) { g.drawString("Hello, JDC", 50, 100); } public static void main(String args[]) { JFrame frame = new Text1(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); frame.setSize(300, 150); Font f = new Font("Serif", Font.BOLD, 48); frame.setFont(f); frame.show(); } } The type Serif is one of five logical font names supported by the Java platform, the other four are Dialog, DialogInput, Monospaced, and SansSerif. The runtime platform maps these logical names to platform-specific font face names, such as Times Roman. If you want to use a specific font, such as Helvetica, you can pass that name to the Font constructor. However, there is no guarantee that the font is installed on the system. Instead, what you need to do is ask the system what fonts actually are installed, and find an appropriate one to use from that set. The GraphicsEnvironment class in the AWT package provides two ways to get the set of fonts installed in the local graphics environment. You can either ask for all the font family names with the getAvailableFontFamilyNames() method, or ask for the specific Font objects with the getAllFonts() method. To demonstrate, the following program asks for the names of fonts that are installed, and then displays ten of the names. Each name is displayed in the style of that font. import java.awt.*; import javax.swing.*; public class Fonts extends JFrame { Insets insets; GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); String fontList[] = ge.getAvailableFontFamilyNames(); Fonts() { setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(435, 150); show(); } public void paint(Graphics g) { super.paint(g); if (insets == null) { insets = getInsets(); } g.translate(insets.left, insets.top); Font theFont; FontMetrics fm; int fontHeight = 0; int count=Math.min(10, fontList.length); for (int i = 0; i < count; i+=2) { theFont = new Font(fontList[i], Font.PLAIN, 11); g.setFont(theFont); fm = g.getFontMetrics(theFont); fontHeight += fm.getHeight(); g.drawString(fontList[i], 10, fontHeight); if (i+1 != fontList.length) { theFont = new Font(fontList[i+1], Font.PLAIN, 11); g.setFont(theFont); g.drawString(fontList[i+1], 200, fontHeight); } } } public static void main(String args[]) { new Fonts(); } } If you're developing a program, the only sure way to know the specific font used by the running program is to load the font at runtime. You can, in fact, dynamically load a TrueType font. The ability to dynamically load a TrueType font was introduced in Java 2 SDK, Standard Edition version 1.3. To load the font, you get the font data in an InputStream and then call the createFont() method of Font. You can then use that Font to derive other font sizes and styles through the deriveFont() method. Here's the earlier basic drawing program, rewritten to use a TrueType font filename passed in as a command line argument. import java.awt.*; import javax.swing.*; import java.io.*; public class Text2 extends JFrame { public void paint(Graphics g) { g.drawString("Hello, JDC", 50, 100); } public static void main(String args[]) throws Exception { if (args.length != 0) { JFrame frame = new Text2(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); InputStream is = new FileInputStream(args[0]); Font font = Font.createFont( Font.TRUETYPE_FONT, is); frame.setFont(font.deriveFont(24f)); frame.setSize(400, 150); frame.show(); } else { System.err.println( "Pass in the .TTF filename"); } } } After compiling the program, you can execute it with a command similar to the following: java Text2 font.ttf Replace font.ttf with the name of an actual TrueType font file. If you don't have a TrueType font file available, you can look on the Web for one. For example, a good place to look is FontParty.com (http://www.fontparty.com/). Many fonts listed there are free for personal use. Some are even free for commercial use. You'll need to look at the licensing arrangements for the specific fonts that interest you. For additional information about drawing text and working with fonts, see the Using Fonts lesson in the 2D Text Tutorial located at http://java.sun.com/jdc/onlineTraining/Media/2DText/fonts.html. This lesson includes some exercises, too. . . . . . . . . . . . . . . . . . . . . . . . IMPORTANT: Please read our Terms of Use and Privacy policies: http://www.sun.com/share/text/termsofuse.html http://www.sun.com/privacy/ * FEEDBACK Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com * SUBSCRIBE/UNSUBSCRIBE - To subscribe, go to the subscriptions page, (http://developer.java.sun.com/subscription/), choose the newsletters you want to subscribe to and click "Update". - To unsubscribe, go to the subscriptions page, (http://developer.java.sun.com/subscription/), uncheck the appropriate checkbox, and click "Update". - To use our one-click unsubscribe facility, see the link at the end of this email: - ARCHIVES You'll find the JDC Tech Tips archives at: http://java.sun.com/jdc/TechTips/index.html - COPYRIGHT Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA. This document is protected by copyright. For more information, see: http://java.sun.com/jdc/copyright.html JDC Tech Tips November 20, 2001 Sun, Sun Microsystems, Java, and Java Developer Connection are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. To use our one-click unsubscribe facility, select the following URL: http://sunmail.sun.com/unsubscribe?5222466841201067