Return-Path: <env_5222466841201067@hermes.sun.com>
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 <alexp@mit.edu>; 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" <body_5222466841201067@hermes.sun.com>
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
