基本文本组件(二)

15.3 JTextField类

JTextField组件是用于单行输入的文本组件。JTextField的数据模型是Document接口的PlainDocument实现。PlainDocument模型将输入限制为单属性文本,意味着他必须是单一字体与单一颜色。

当在JTextField输入Enter键时,他自动通知所Actionlistener实现。

15.3.1 创建JTextField

JTextField组件有五个构造函数:

public JTextField()
JTextField textField = new JTextField();
public JTextField(String text)
JTextField textField = new JTextField("Initial Text");
public JTextField(int columnWidth)
JTextField textField = new JTextField(14);
public JTextField(String text, int columnWidth)
JTextField textField = new JTextField("Initial Text", 14);
public JTextField(Document model, String text, int columnWidth)
JTextField textField = new JTextField(aModel, null, 14);

默认情况下,我们会获得一个空的文本域,零列宽,带有默认模型的JTextFiled。我们可以指定JTextField的初始文本以及我们希望组件有多宽。宽度被以当前字体适应组件的m字符数。在可以输入的字符数上并没有限制。如果我们在构造函数中指定Document数据模型,也许我们将会希望指定了一个null初始数据参数。否则,当前文档的内容会被文本域的初始文本所替换。

15.3.2 使用JLabel热键

在第4章热键的讨论中,我们了解到各种按钮类可以有一个按钮组件被选中的键盘快捷键。特殊的热键字符通常以下划线来进行可视化标识。如果用户按下热键字符,以平台特定的热键激活键,例如对于Windows与Unix的Alt,按钮就会被激活/选中。我们可以借助JLabel为JTextField以及其他的文本组件提供类似的功能。

我们可以为标签设置热键显示,但是当热键被按下时并没有选中标签,而是会使得相关联的组件获得输入焦点。显示热键是通过public void setDisplayedMnemonic(character)方法来设置的,其中character可是一个int或是char。当修改热键设置时使用KeyEvent常量可以简化初始化操作。

下面的源代码显示了如何连接一个特定的JLabel与JTextField。

JLabel label = new JLabel("Name: ");
label.setDisplayedMnemonic(KeyEvent.VK_N);
JTextField textField = new JTextField();
label.setLabelFor(textField);

除了调用setDisplayedMnemonic()方法以外,我们必须同时调用JLabel的public void setLabelFor(Component component)方法。这会配置当特定的热键值被按下时,JLable会将输入焦点移动文本域。

图15-3显示了示例程序的样子。完整的程序源码显示在列表15-2中。


package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
 
public class LabelSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame =  new JFrame("Label Focus Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				JPanel panel = new JPanel(new BorderLayout());
				JLabel label = new JLabel("Name: ");
				label.setDisplayedMnemonic(KeyEvent.VK_N);
				JTextField textField = new JTextField();
				label.setLabelFor(textField);
				panel.add(label, BorderLayout.WEST);
				panel.add(textField, BorderLayout.CENTER);
				frame.add(panel, BorderLayout.NORTH);
				frame.add(new JButton("Somewhere Else"), BorderLayout.SOUTH);
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}

15.3.3 JTextField属性

表15-2列出了JTextField的14个属性。


在horizontalVisibility与scrollOffset属性之间有一个简单的关联。用于JTextField的horizontalVisibility属性的BoundedRangeModel表示显示文本域内容所需要的宽度。如果没有足够的空间来显示内容,scrollOffset设置反映已经滚动到距离左边文本多远处。当用户在JTextField的文本中浏览时,scrollOffset值会被自动更新。例如,图15-4中的文本包含26个字母以及10个数字: ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890。并不是所有的字符都能适应文本域;所以,字符A到J已经滚动偏离左边。


通过修改scrollOffset设置,我们可以控制文本域的哪一个部分是可见的。要保证文本域内容开始处是可见的,将scrollOffset设置为零。要使得内容的结束处是可见的,我们需要向horizontalVisibility属性查询BoundedRangeModel的extent是什么,来确定范围的宽度,然后将scrollOffset设置为extent设置,如下所示:

BoundedRangeModel model = textField.getHorizontalVisibility();
int extent = model.getExtent();
textField.setScrollOffset(extent);

通过修改horizontalAlignment属性设置,我们可以将一个JTextField的内容右对齐,左对齐或是居中对齐。默认情况下,文本对齐是左对齐。public void setHorizontalAlignment(int alignment)方法需要一个参数:JTextField.LEFT,JTextField.CENTER,JTextField.RIGHT,JTextField.LEADING(默认),或是JTextField.TRAILING来指定内容对齐。图15-5显示了对齐设置如何影响内容。


注意,我们可以将由JTextComponent继承来的document属性设置为Document接口的任意实现。如果我们为JTextField使用StyledDocument,UI委托就会忽略所有的格式属性。我们会在第16章中讨论StyledDocument接口。

15.3.4 JTextField中的JTextComponent操作

我们是否在寻找一种简单的方法来载入或是保存文本组件中的内容呢?Swing文本组件提供了这种方法。另外,Swing文本组件对访问系统剪切板用于剪切,复制与粘贴操作的内建支持。这些操作对于所有的JTextComponent子类都是可用的。在这里特别为JTextField显示这些操作,因为为了真正的演示他们需要特定的实现。我们可以使用JPasswordField,JTextArea,JEditorPane与JTextPane执行相同的任务。

载入与保存内容

使用JTextComponent的public void read(Reader in, Object description)与public void write(Writer out)方法(两个方法都抛出IOException),我们可以简单的由任意的文本组件载入与保存内容。使用read()方法,description参数被添加为Document数据模型的一个属性。这可以使得我们保存关于数据来自于哪里的信息。下面的示例演示了如何读取文件名的内容并且存放在textComponent中。文件名自动保存为描述。

FileReader reader = null;
try {
  reader = new FileReader(filename);
  textComponent.read(reader, filename);
}  catch (IOException exception) {
  System.err.println("Load oops");
}  finally {
  if (reader != null) {
    try {
      reader.close();
    } catch (IOException exception) {
      System.err.println("Error closing reader");
      exception.printStackTrace();
    }
  }
}

如果我们稍后希望由数据模型获取描述,在这种情况下恰好为文件中,我们只需要简单的查询,如下所示:

Document document = textComponent.getDocument();
String filename = (String)document.getProperty(Document.StreamDescriptionProperty);

Document属性只是简单的另一个键/值查询表。在这个特定的情况下键为类常量Document.StreamDescriptionProperty。如果我们不希望存储描述,我们可以传递null作为read()方法的descritption参数。(Document接口将会在本章稍后进行详细讨论。)

在我们将一个文件读取到文本组件之前,我们需要创建要读取的文件。这可以在Java程序之外完成,或者是我们可以使用JTextComponent的write()方法来创建文件。下面的代码演示了如何使用write()方法来写入内容。为了简单起见,他并没有处理由Document中获取的文件名,因为这可以不初始化设置。

FileWriter writer = null;
try {
  writer = new FileWriter(filename);
  textComponent.write(writer);
}  catch (IOException exception) {
  System.err.println("Save oops");
}  finally {
  if (writer != null) {
    try {
      writer.close();
    } catch (IOException exception) {
      System.err.println("Error closing writer");
      exception.printStackTrace();
    }
  }
}

图15-6为示了使用了载入与保存功能,并通过按钮来实现这些选项(尽管载入与保存选项在File菜单中更常见)的示例程序。Clear按钮清除文本域中的内容。


列表15-3中的源码将所有这些代码段组合在一起来演示载入与保存流。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
 
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.text.JTextComponent;
 
public class LoadSave {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				final String filename = "text.out";
				JFrame frame = new JFrame("Loading/Saving Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				final JTextField textField = new JTextField();
				frame.add(textField, BorderLayout.NORTH);
 
				JPanel panel = new JPanel();
 
				// Setup actions
				Action loadAction = new AbstractAction() {
					{
						putValue(Action.NAME, "Load");
					}
					public void actionPerformed(ActionEvent e) {
						doLoadCommand(textField, filename);
					}
				};
				JButton loadButton = new JButton(loadAction);
				panel.add(loadButton);
 
				Action saveAction = new AbstractAction() {
					{
						putValue(Action.NAME, "Save");
					}
					public void actionPerformed(ActionEvent e) {
						doSaveCommand(textField, filename);
					}
				};
				JButton saveButton = new JButton(saveAction);
				panel.add(saveButton);
 
				Action clearAction = new AbstractAction() {
					{
						putValue(Action.NAME, "Clear");
					}
					public void actionPerformed(ActionEvent e) {
						textField.setText("");
					}
				};
				JButton clearButton = new JButton(clearAction);
				panel.add(clearButton);
 
				frame.add(panel, BorderLayout.SOUTH);
 
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
	public static void doSaveCommand(JTextComponent textComponent, String filename) {
		FileWriter writer = null;
		try {
			writer = new FileWriter(filename);
			textComponent.write(writer);
		}
		catch (IOException exception) {
			System.err.println("Save oops");
			exception.printStackTrace();
		}
		finally {
			if(writer != null) {
				try {
					writer.close();
				}
				catch(IOException exception) {
					System.err.println("Error closing writer");
					exception.printStackTrace();
				}
			}
		}
	}
 
	public static void doLoadCommand(JTextComponent textComponent, String filename) {
		FileReader reader = null;
		try {
			reader = new FileReader(filename);
			textComponent.read(reader, filename);
		}
		catch(IOException exception) {
			System.err.println("Load oops");
			exception.printStackTrace();
		}
		finally {
			if(reader != null) {
				try {
					reader.close();
				}
				catch(IOException exception) {
					System.err.println("Error closing reader");
					exception.printStackTrace();
				}
			}
		}
	}
 
}

注意,默认情况下,文件读取与写入只处理普通文本。如果一个文本组件的内容是格式化的,格式化属性并不会被保存。EditorKit类可以自定义这种载入与保存的行为。我们将会在第16章探讨这个类。

访问剪切板

要使用系统剪切板用于剪切、复制与粘贴操作,我们并不需要手动编写一个Transferable剪切板对象。相反,我们只需要调用JTextComponent类的三个方法中的一个:public void cut(), public void copy()或是public void paste()。

我们可以由与按钮或是菜单项相相关联的ActionListener实现中直接调用这些方法,如下所示:

ActionListener cutListener = new ActionListener() {
  public void actionPerformed(ActionEvent actionEvent) {
    aTextComponent.cut();
  }
};

然而有一种不需要我们手动创建ActionListener实现的简单方法。这种方法是通过向文本组件查询已存在的剪切操作。如果我们看一下表15-1中的JTextComponent属性集合,我们就会注意到一个名为actions的属性,他是一个Action对象数组。这个属性包含一个我们可以直接将其作为ActionListener关联到任意按钮或是菜单项的预定义的Action实现集合。一旦我们获取当前文本组件的actions,我们就可以在数组中遍历直到我们到相应的实现。因为动作是被命名的,我们只需要知道名字的文本字符串。DefaultEditorKit类具有大约40个键作为公共常量。下面是获取剪切动作的示例:

Action actions[] = textField.getActions();
Action cutAction = TextUtilities.findAction(actions, DefaultEditorKit.cutAction);

文本组件集合中的所有动作都是TextAction类型的,他是AbstractAction类的一个扩展。关于TextAction我们需要了解的一件事就是他作用在最后一个获得焦点的文本组件上。(TextAction类以及DefaultEditorKit类将会在第16章中进行详细的讨论。)所以,尽管前面的代码片段获取了一个文本域中的剪切操作,相同的剪切动作也同样适用于同一屏幕上的其他文本组件。当特定的cutAction被激活时,最的一个获得输入焦点的文本组件的内容将会被剪切。

为了有助于我们理解这一行为,图15-7显示了一个屏幕,其中在顶部是一个JTextField,中间是一个JTextArea,而底部是用于剪切,复制与粘贴操作的按钮(尽管这些操作通常是通过编辑菜单获得的)。如果我们运行这个程序,我们就会注意到剪切,复制与粘贴操作作用在最后一个获得输入焦点的文本组件上。

列表15-4是用于查找actions属性数组中的Action并且使用剪切,复制与粘贴操作完整示例的源码。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
 
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.text.DefaultEditorKit;
 
public class CutPasteSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("Cupt/Paste Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				JTextField textField = new JTextField();
				JTextArea textArea = new JTextArea();
				JScrollPane scrollPane = new JScrollPane(textArea);
 
				frame.add(textField, BorderLayout.NORTH);
				frame.add(scrollPane, BorderLayout.CENTER);
 
				Action actions[] = textField.getActions();
 
				Action cutAction = TextUtilities.findAction(actions, DefaultEditorKit.cutAction);
				Action copyAction = TextUtilities.findAction(actions, DefaultEditorKit.copyAction);
				Action pasteAction = TextUtilities.findAction(actions, DefaultEditorKit.pasteAction);
 
				JPanel panel = new JPanel();
				frame.add(panel, BorderLayout.SOUTH);
 
				JButton cutButton = new JButton(cutAction);
				cutButton.setText("Cut");
				panel.add(cutButton);
 
				JButton copyButton = new JButton(copyAction);
				copyButton.setText("Copy");
				panel.add(copyButton);
 
				JButton pasteButton = new JButton(pasteAction);
				pasteButton.setText("Paste");
				panel.add(pasteButton);
 
				frame.setSize(250, 250);
				frame.setVisible(true);
			}
		};
 
		EventQueue.invokeLater(runner);
	}
 
}

列表15-4中的示例使用列表15-5中所示的TextUtilities支持类。并没有直接的方法来确定一个特定按键的一个特定动作是否存在于acions属性数组中。相反,我们必须手动进行查找。public static Action findAction(Action actions[], String key)方法为我们进行相应的查找。

package swingstudy.ch15;
 
import java.util.Hashtable;
 
import javax.swing.Action;
 
public final class TextUtilities {
 
	private TextUtilities() {
 
	}
 
	public static Action findAction(Action actions[], String key) {
		Hashtable<Object, Action> commands = new Hashtable<Object, Action>();
		for(int i=0; i<actions.length; i++) {
			Action action = actions[i];
			commands.put(action.getValue(Action.NAME), action);
		}
		return commands.get(key);
	}
}


注意,出于安全的原因,JPasswordField类的cut()与copy()方法并没有将当前的内容放在系统剪切板中。然而我们仍然可以使用paste()方法将剪切板中的内容粘贴到JPasswordField中。

15.3.5 Document接口

Document接口定义了不同的文本组件的数据模型。这个接口的实现用来存储实际的内容以及标记内容的信息(粗体,斜体或是颜色)。尽管所有的内容都将是文本,然而文本组件显示内容的方式会导致非文本的输出,例如HTML渲染器。

数据模型是与文本组件分开存储的。所以,如果我们对监视文本组件的内容感兴趣,我们必须监视Document本身,而不是文本组件。如果修改到达文本组件,这就太迟了,模型已经发生了改变。要监听变化,向模型关联一个DocumentListener。然而,限制输入更可能的方式是提供一个自定义的模型或是向AbstractDocument关联一个DocumentFilter。我们也可以向文本组件关联一个InputVerifier。然而,直到输入焦点将离开组件时他才会起作用。

注意,除了通过Document接口访问文本内容以外,还定义了一个框架用于支持undo/redo功能。我们将会在第21章中探讨这一框架。

现在我们来了解一下标记Document的片段。首先我们来看一下基本的接口定义:

public interface Document {
  // Constants
  public final static String StreamDescriptionProperty;
  public final static String TitleProperty;
  // Listeners
  public void addDocumentListener(DocumentListener listener);
  public void removeDocumentListener(DocumentListener listener);
  public void addUndoableEditListener(UndoableEditListener listener);
  public void removeUndoableEditListener(UndoableEditListener listener);
  // Properties
  public Element getDefaultRootElement();
  public Position getEndPosition();
  public int getLength();
  public Element[ ] getRootElements();
  public Position getStartPosition();
  // Other methods
  public Position createPosition(int offset) throws BadLocationException;
  public Object getProperty(Object key);
  public String getText(int offset, int length) throws BadLocationException;
  public void getText(int offset, int length, Segment txt)
    throws BadLocationException;
  public void insertString(int offset, String str, AttributeSet a)
    throws BadLocationException;
  public void putProperty(Object key, Object value);
  public void remove(int offset, int len) throws BadLocationException;
  public void render(Runnable r);
}

Document中的内容是通过一系列的元素来描述的,其中每一个元素实现了Element接口。在每一个元素中,我们可以存储属性,从而可以将选中的内容修改为粗体,斜体或是颜色化。元素不存储内容;他们仅存储属性。所以,一个Document可以不同的Element集合进行不同的渲染。

下面的代码是一个带有标题与内容列表的基本的HTML文档。

<html>
<head>
<title>Cards</title>
</head>
<body>
<h1>Suits:</h1>
<ul>
    <li>Clubs</li>
    <li>Diamonds</li>
    <li>Hearts</li>
    <li>Spades</li>
</ul>
</body>
</html>

仔细查看一下这个HTML文档中的元素结构,我们可以得到图15-8所示的层次结构。

尽管这个特定的文档并不真实,但是多个元素的层次结构是可能的。每一个存储不同的属性,因为一个特定的文本组件会具有另一个内容渲染。相对应的,不同的格式化页表可以用来渲染相同的HTML标记。

AbstractDocument类

AbstractDocument类提供了Document接口的基本实现。他定义了监听器列表的管理,提供了一个读写锁机制来保证内容不会被破坏,并且提供了一个Dictionary用于存储文档属性。

表15-3列出了AbstractDocument类的11个属性,其中5个是由Document接口本身定义的。


对于属性中的大部分,我们不会直接访问这些属性,也许除了documentFilter。在documentProperties属性的情况下,我们通过public Object getProperty(Object key)与public void putProperty(Object key, Object value)方法获取与设置单个属性。对于length属性,在大多数情况下,我们可以简单的查询文本组件中的文本,然后使用textComponent.getText().length()方法获取其长度。

bidiRootElement属性用于双向根元素,他也许位于一定的Unicode字符集中。我们通常只需要使用defaultRootElement。然而,两者都很少被访问。

PlainDocument类

PlainDocument类是AbstractDocument类的一个特定实现。他并不为内容存储任何字符级的属性。相反,元素描述内容在哪里以及内容开始处的每一行。

列表15-6中的程序在一个PlainDocument中遍历Element树。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.ElementIterator;
 
public class ElementSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("Element Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				final JTextArea textArea = new JTextArea();
				JScrollPane scrollPane = new JScrollPane(textArea);
 
				JButton button =  new JButton("Show Elements");
				ActionListener actionListener =  new ActionListener() {
					public void actionPerformed(ActionEvent event) {
						Document document = textArea.getDocument();
						ElementIterator iterator = new ElementIterator(document);
						Element element = iterator.first();
						while(element != null) {
							System.out.println(element.getStartOffset());
							element = iterator.next();
						}
					}
				};
				button.addActionListener(actionListener);
 
				frame.add(scrollPane, BorderLayout.CENTER);
				frame.add(button, BorderLayout.SOUTH);
 
				frame.setSize(250, 250);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}

假定JTextArea的内容如下:

Hello, World

Welcome Home

Adios

程序将会报告Element对象开始于0,0,13与26。第一个0表示内容的开始处;第二个表示第一行的开始处。

我们将会在第16章中了解到关于Element的更多内容。

过滤文档模型

在AWT世界中,如果我们要限制文本域的输入-例如限制为字母数字字符或是某一个范围的值-我们关联KeyListener并处理我们不希望出现在组件中的按键。使用Swing文本组件,我们可以创建一个新的Document实现并且自定义在Document中接受些什么,或者是关联一个DocumentFilter并由他来过滤输入。

虽然我们可以创建一个Document的自定义子类,但是更为面向对象的方法是创建一个过滤器,因为我们不希望修改Document;我们只是希望限制模型的输入。然后我们可以通过调用AbstractDocument的setDocumentFilter()方法来将新创建的过滤器关联到文档。过滤器适用于PlainDocument与StyledDocument子类。

DocumentFilter是一个类,而不是接口,所以我们必须创建一个该类的子类来过滤文本组件的文档的输入。如果我们创建一个DocumentFilter的子类,重写下面三个方法可以使得我们自定义输入:

• public void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attributes): 当一个文本字符串被插入到文档中时调用。 

• public void remove(DocumentFilter.FilterBypass fb, int offset, int length): 当某些内容被选中时调用。 

• public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs): 当某些内容被插入到当前被选中的文本中时调用。

要限制输入,只需要覆盖每一个方法并且检测新内容是否合法。如果内容不合法,则拒绝。

例如,创建一个DocumentFilter子类来限制数字范围,我们需要覆盖insertString(),remove()与replace()方法。因为我们要保证输入是数字并且位于是一个合法的范围中,我们需要验证输入并且确定他是否可以接受。如果可以接受,那么我们可以通过调用DocumentFilter的insertString(),remove()或是replace()方法来修改文档模型。当输入不可接受时,我们抛出一个BadLocationException。抛出这个异常保证输入方法模型理解用户的输入不合法。这通常会触发系统发出声响。列表15-7显示了一个限制整数范围的文档过滤器。

package swingstudy.ch15;
 
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
 
public class IntegerRangeDocumentFilter extends DocumentFilter {
 
	int minimum, maximum;
	int currentValue = 0;
 
	public IntegerRangeDocumentFilter(int minimum, int maximum) {
		this.minimum = minimum;
		this.maximum = maximum;
	}
 
	public void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
		if(string == null) {
			return;
		}
		else {
			String newValue;
			Document doc = fb.getDocument();
			int length = doc.getLength();
			if(length == 0) {
				newValue = string;
			}
			else {
				String currentContent = doc.getText(0, length);
				StringBuffer currentBuffer = new StringBuffer(currentContent);
				currentBuffer.insert(offset, string);
				newValue = currentBuffer.toString();
			}
			currentValue = checkInput(newValue, offset);
			fb.insertString(offset, string, attr);
		}
	}
 
	public void remove(DocumentFilter.FilterBypass fb, int offset, int length)
	    throws BadLocationException {
		Document doc = fb.getDocument();
		int currentLength = doc.getLength();
		String currentContent = doc.getText(0, currentLength);
		String before = currentContent.substring(0, offset);
		String after = currentContent.substring(length+offset, currentLength);
		String newValue = before+after;
		currentValue = checkInput(newValue, offset);
		fb.remove(offset, length);
	}
 
	public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
	    throws BadLocationException {
		Document doc = fb.getDocument();
		int currentLength = doc.getLength();
		String currentContent = doc.getText(0, currentLength);
		String before = currentContent.substring(0, offset);
		String after = currentContent.substring(length+offset, currentLength);
		String newValue = before+(text==null?"":text)+after;
		currentValue = checkInput(newValue, offset);
		fb.replace(offset, length, text, attrs);
	}
 
	public int checkInput(String proposedValue, int offset)
	    throws BadLocationException {
		int newValue = 0;
		if(proposedValue.length()>0) {
			try {
				newValue = Integer.parseInt(proposedValue);
			}
			catch(NumberFormatException e) {
				throw new BadLocationException(proposedValue, offset);
			}
		}
		if((minimum<=newValue) && (newValue<=maximum)) {
			return newValue;
		}
		else {
			throw new BadLocationException(proposedValue, offset);
		}
	}
}


图15-9显示了使用中的数字范围过滤器。


列表15-8显示了使用新的IntegerRangeDocumentFilter的示例程序。

package swingstudy.ch15;
 
import java.awt.EventQueue;
import java.awt.GridLayout;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
 
public class RangeSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame =  new JFrame("Range Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				frame.setLayout(new GridLayout(3,2));
 
				frame.add(new JLabel("Range: 0-255"));
				JTextField textFieldOne = new JTextField();
				Document textDocOne = textFieldOne.getDocument();
				DocumentFilter filterOne = new IntegerRangeDocumentFilter(0, 255);
				((AbstractDocument)textDocOne).setDocumentFilter(filterOne);
				frame.add(textFieldOne);
 
				frame.add(new JLabel("Range: -100-100"));
				JTextField textFieldTwo = new JTextField();
				Document textDocTwo = textFieldTwo.getDocument();
				DocumentFilter filterTwo = new IntegerRangeDocumentFilter(-100,100);
				((AbstractDocument)textDocTwo).setDocumentFilter(filterTwo);
				frame.add(textFieldTwo);
 
				frame.add(new JLabel("Range: 1000-2000"));
				JTextField textFieldThree = new JTextField();
				Document textDocThree = textFieldThree.getDocument();
				DocumentFilter filterThree = new IntegerRangeDocumentFilter(1000, 2000);
				((AbstractDocument)textDocThree).setDocumentFilter(filterThree);
				frame.add(textFieldThree);
 
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}

如果我们尝试这个程序,我们就会注意到一些有趣的问题。第一个文本域,范围为0到255,可以正常工作。只要内容是在这个范围之内,我们就可以随时输入或是删除字符。

在第二个文本域中,合法的范围为-100到+100。尽管我们可以在这个文本域中输入任意的201个数字,但是如果我们希望一个负数,我们需要输入一个数字,例如3,左箭头,再输入负号。因为文本域会使用每一个键验证输入,负号本身是不合法的。我们需要在自定义的DocumentFilter的checkInput()方法将负号作为合法的输入接受,或者是强制用户以一种后退的方式输入负数。

第三个文本域展示了一种更为麻烦的情况。输入的合法范围为1000-2000。当我们按下每一个键来输入数字时,例如1500,他会被拒绝。我们不能构建1500的输入,因为1,5与0是非法输入。相反,要在这个文本域中输入数字,我们必须在其他位置输入这个数字,将其放入系统剪切板中,然后使用Ctrl-V将其粘贴到文本域中作为文本域的最终值。我们不能使用Backspace来修正错误,因为三位数是非法的。

虽然列表15-7中的IntegerRangeDocumentFilter类表示了一个对于任意的整数范围可用的DocumentFilter,他适用于由零开始的整数范围。如果我们并不介意看到文本域中的临时非法输入,也许更好的方法是仅关联一个InputVerifier在离开文本域的时候处理验证。

15.3.6 DocumentListener与DocumentEvent接口

如果我们对文本组件的内容何时发生变化感兴趣,我们可以向组件的Document模型关联一个DocumentListener接口的实现。

public interface DocumentListener implements EventListener {
  public void changedUpdate(DocumentEvent documentEvent);
  public void insertUpdate(DocumentEvent documentEvent);
  public void removeUpdate(DocumentEvent documentEvent);
}

通过上面的三个接口方法,我们可以确定内容是否被添加(insertUpdate()),移除(removeUpdate())或是格式化修改(changedUpdate())。注意,后者是属性变化而不是内容变化。

接口方法将会接收一个DocumentEvent的实例,从中我们可以确定在哪里发生了变化以及变化的类型,如下所示:

public interface DocumentEvent {
  public Document getDocument();
  public int getLength();
  public int getOffset();
  public DocumentEvent.EventType getType();
  public DocumentEvent.ElementChange getChange(Element element);
}

事件的offset属性是变化的起始处。事件的length属性报告发生变化的长度。事件的类型可以由被调用的三个DocumentListener方法中的一个导出。另外,DocumentEvent.EventType类有三个常量-CHANGE,INSERT与REMOVE-所以我们可以直接由type属性直接确定所发生的事件类型。

DocumentEvent的getChange()方法需要一个Element来返回DocumentEvent.ElementChange。我们通常使用Document的默认根元素,如下面的示例所示。

Document documentSource = documentEvent.getDocument();
Element rootElement = documentSource.getDefaultRootElement();
DocumentEvent.ElementChange change = documentEvent.getChange(rootElement);

一旦我们具有DocumentEvent.ElementChange实例,如果我们需要该级别的信息,我们可以确定添加与移除的元素。

public interface DocumentEvent.ElementChange {
  public Element[ ] getChildrenAdded();
  public Element[ ] getChildrenRemoved();
  public Element getElement();
  public int getIndex();
}

15.3.7 Caret与Highlighter接口

现在我们已经理解了文本组件的数据模型方面,我们可以了解通过Caret与Highlighter接口进行选中渲染的相关知识了。记住这些是文本组件的属性,而不是数据模型的属性。

Caret接口描述通常被作为光标引用的内容:在文档中我们可以插入文本的位置。Highlighter接口提供了如何绘制选中文本的基础。这两个接口,他们相关的接口以及他们的实现都很少被修改。文本组件简单的通过DefaultCaret与DefaultHighlighter类使用他们的默认实现。

尽管我们并不会修改一个文本组件的caret与highlighter的行为,但是我们应该了解有许多内部相关的类协同工作。对于Highlighter接口,预定义的实现被称之为DefaultHighlighter,他扩展了另一个名为LayeredHighlighter的实现。Highlighter同时管理一个Highlighter.Highlight对象的集合来指定被高亮的部分。

DefaultHighlighter创建一个DefaultHighlighter.HighlightPainter来绘制文本的高亮部分。HighlightPainter是Highlighter.HighlightPainter接口的实现,并且扩展了LayeredHighlighter.LayerPainter类。要绘制的每一个部分通过Highlighter.Highlight进行描述,其中Highlighter管理集合。实际的HighlightPainter是通过DefaultCaret实现来创建的。

Highlighter接口描述如何绘制文本组件中被选中的文本。如果我们不喜欢颜色,我们可以简单的将TextField.selectionBackground UI属性设置修改为另一个不同的颜色。

public interface Highlighter {
  // Properties
  public Highlighter.Highlight[ ] getHighlights();
  // Other methods
  public Object addHighlight(int p0, int p1, Highlighter.HighlightPainter p)
    throws BadLocationException;
  public void changeHighlight(Object tag, int p0, int p1)
    throws BadLocationException;
  public void deinstall(JTextComponent component);
  public void install(JTextComponent component) 
  public void paint(Graphics g);
  public void removeAllHighlights();
  public void removeHighlight(Object tag);
}

Caret接口描述当前的光标以及一些选中的属性。在Highlighter与Caret接口之间,后者是我们实际使用的,尽管并没有必要对其进行派生。
public interface Caret {
  // Properties
  public int getBlinkRate();
  public void setBlinkRate(int newValue);
  public int getDot();
  public void setDot(int newValue);
  public Point getMagicCaretPosition();
  public void setMagicCaretPosition(Point newValue);
  public int getMark();
  public boolean isSelectionVisible();
  public void setSelectionVisible(boolean newValue);
  public boolean isVisible();
  public void setVisible(boolean newValue);
  // Listeners
  public void addChangeListener(ChangeListener l);
  public void removeChangeListener(ChangeListener l);
  // Other methods
  public void deinstall(JTextComponent c);
  public void install(JTextComponent c);
  public void moveDot(int dot);
  public void paint(Graphics g);
}

表15-4列出了Caret的六个属性。

blinkRate是caret闪烁之间的毫秒延迟。dot属性是文本组件中当前光标的当前位置。要将光标移动另一个位置从而某些文本可以被高亮显示,添加moveDot(int newPosition)方法调用。这会将mark属性设置旧的dot位置并且设置新的dot设置为新位置。

magicCaretPosition属性处理不同长度行的向上移动与向下移动。例如,假定在我们的屏幕上有下面三个文本行:

Friz Freleng

Mel Blanc

What's up Doc?

现在假定光标位于第一行的n与g之间。如果我们按下向下键两次,我们希望光标位于相同的水平位置,而不是较短的第二处的结束处。保存这个信息的就是magicCursorPosition属性,从而光标停在第三行的D与o之间。如果没有保存位置信息,光标将会停留在最后一行的p与空格之间。

使用caret的十分有用的实例就是响应按键来确定当前的屏幕位置。这样,我们就可以在当前的光标位置弹出一个菜单。这就是类似于JBuilder中的Code Insights或是Visual Studio中的IntelliSense,在其中通过弹出一个方法菜单来帮助我们完成方法调用。指定模型中的当前光标位置,使用JTextComponent(可以抛出BadLocationException)的public Rectangle modelToView(int position)方法将其映射到视图中的位置。然后使用作为位置返回的Rectangle弹出菜单,如图15-10所示。


列表15-9中的程序会在文本域中句点被按下的位置显示一个JPopupMenu。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
 
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.text.BadLocationException;
 
public class PopupSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("Popup Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				final JPopupMenu popup = new JPopupMenu();
				JMenuItem menuItem1 = new JMenuItem("Option 1");
				popup.add(menuItem1);
 
				JMenuItem menuItem2 = new JMenuItem("Option 2");
				popup.add(menuItem2);
 
				final JTextField textField = new JTextField();
				frame.add(textField, BorderLayout.NORTH);
 
				ActionListener actionListener = new ActionListener() {
					public void actionPerformed(ActionEvent event) {
						try {
							int dotPosition = textField.getCaretPosition();
							Rectangle popupLocation = textField.modelToView(dotPosition);
							popup.show(textField, popupLocation.x, popupLocation.y);
						}
						catch(BadLocationException badLocationException) {
							System.err.println("Oops");
						}
					}
				};
				KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, 0, false);
				textField.registerKeyboardAction(actionListener, keystroke, JComponent.WHEN_FOCUSED);
 
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}


15.3.8 CaretListener接口与CareEvent类

我们可以使用两种方法监听光标的移动:将ChangeListener关联到Caret或是将CaretListener关联到JTextComponent。尽管两种方法作用相同,直接JTextComponent则是更为简单的方法。

在CaretListener的情况下,接口只定义了一个方法:

public interface CaretListener implements EventListener {
  public void caretUpdate (CaretEvent caretEvent);
}

当监听器得到通知时,CaretEvent被发送,他会报告新位置并标记位置。
public abstract class CaretEvent extends EventObject {
  public CaretEvent(Object source);
  public abstract int getDot();
  public abstract int getMark();
}

为了进行演示,图15-11显示了一个将CaretListener关联到内联JTextArea的程序。当CaretEvent发生时,当前光标值会发送到顶部文本域,而当前标记设置会被发送到按钮。在这个示例中,光标位置在第二行的起始处,而标记则是在结束处。

列表15-10显示了与图15-11中的示例相关联的源码。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
 
public class CaretSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("Caret Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				JTextArea textArea = new JTextArea();
				JScrollPane scrollPane = new JScrollPane(textArea);
				frame.add(scrollPane, BorderLayout.CENTER);
 
				final JTextField dot = new JTextField();
				dot.setEditable(false);
				JPanel dotPanel = new JPanel(new BorderLayout());
				dotPanel.add(new JLabel("Dot: "), BorderLayout.WEST);
				dotPanel.add(dot, BorderLayout.CENTER);
				frame.add(dotPanel, BorderLayout.NORTH);
 
				final JTextField mark = new JTextField();
				mark.setEditable(false);
				JPanel markPanel = new JPanel(new BorderLayout());
				markPanel.add(new JLabel("Mark: "), BorderLayout.WEST);
				markPanel.add(mark, BorderLayout.CENTER);
				frame.add(markPanel, BorderLayout.SOUTH);
 
				CaretListener listener = new CaretListener() {
					public void caretUpdate(CaretEvent event) {
						dot.setText(Integer.toString(event.getDot()));
						mark.setText(Integer.toString(event.getMark()));
					}
				};
 
				textArea.addCaretListener(listener);
 
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}


15.3.9 NavigationFilter类

类似于将DocumentFilter关联到Document来限制文本组件的输入一样,我们可以将NavigationFilter关联到JTextComponent来限制光标可以达到哪里。这个类有三个方法:

public void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias)
public void moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias)
public int getNextVisualPositionFrom(JTextComponent text, int pos, 
  Position.Bias bias, int direction, Position.Bias[] biasRet)

要限制移动,通常我们要重写前两个方法,而保留后一个方法。例如,列表15-11显示的程序在JTextArea的起始处有一个保留区域(也就是报告标题)。如果我们尝试设置或是将光标移动到保留区域,过滤器会拒绝修改并将光标移动到区域之后。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
 
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.text.NavigationFilter;
import javax.swing.text.Position;
 
public class NavigationSample {
 
	private static final String START_STRING = "Start\n";
	private static final int START_STRING_LENGTH = START_STRING.length();
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("Navigation Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				JTextArea textArea = new JTextArea(START_STRING);
				textArea.setCaretPosition(START_STRING_LENGTH);
				JScrollPane scrolPane = new JScrollPane(textArea);
				frame.add(scrolPane, BorderLayout.CENTER);
 
				NavigationFilter filter = new NavigationFilter() {
					public void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) {
						if(dot<START_STRING_LENGTH) {
							fb.setDot(START_STRING_LENGTH, bias);
						}
						else {
							fb.setDot(dot, bias);
						}
					}
					public void moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) {
						if(dot<START_STRING_LENGTH) {
							fb.setDot(START_STRING_LENGTH, bias);
						}
						else {
							fb.setDot(dot, bias);
						}
					}
				};
				textArea.setNavigationFilter(filter);
 
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}

图15-2显示了在文本组件中输入一些信息之后的屏幕。

15.3.10 Keymap接口

以MVC的角度来看,文本组件的keymap属性是控制器部分。他通过Keymap接口将KeyStroke对象映射到单个的动作。(KeyStroke类在第2章中进行了讨论。)当我们使用registerKeyboardAction()方法向JTextComponent注册KeyStroke时,如本章前面的列表15-9中的PopupSample程序所示,文本组件在Keymap中存储由KeyStroke到Action的映射。例如,回退键被映射到删除前一个字符。如果我们要添加另一个绑定,我们只需要注册另一个按键。

注意,事实上,Keymap只是ActionMap/InputMap对的前端。JTextComponent依据某些内部作用间接使用ActionMap/InputMap类。

我们也可以直接向Keymap中添加按键动作。这可以使得我们在多个文本组件之间共享同一个按键映射,只要他们共享相同的扩展行为。

public interface Keymap {
  // Properties
  public Action[ ] getBoundActions();
  public KeyStroke[ ] getBoundKeyStrokes();
  public Action getDefaultAction();
  public void setDefaultAction(Action action);
  public String getName();
  public Keymap getResolveParent();
  public void setResolveParent(Keymap parent);
  // Other methods
  public void addActionForKeyStroke(KeyStroke keystroke, Action action);
  public Action getAction(KeyStroke keystroke);
  public KeyStroke[ ] getKeyStrokesForAction(Action action);
  public boolean isLocallyDefined(KeyStroke keystroke);
  public void removeBindings();
  public void removeKeyStrokeBinding(KeyStroke keystroke);
}

对于某些程序,我们也许希望由按键映射中移动按键。例如,JTextField在键盘映射中有一个用于Enter键的实体,从而所注册的ActionListener对象都会得到通知。如果JTextField位于设计有默认按钮的屏幕上,按下Enter并不会选中预期的默认按钮。去掉这种默认行为仅是简单的请求由Keymap中移除KeyStroke,如下所示:
Keymap keymap = textField.getKeymap();
KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false);
keymap.removeKeyStrokeBinding(keystroke);

然后,当我们在文本域中按下Enter时,默认按钮就会被激活,如图15-13所示。


图15-13的示例程序源码显示在列表15-12中。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.text.Keymap;
 
public class DefaultSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("Default Example");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				JTextField textField = new JTextField();
				frame.add(textField, BorderLayout.NORTH);
 
				ActionListener actionListener = new ActionListener() {
					public void actionPerformed(ActionEvent event) {
						System.out.println(event.getActionCommand()+" selected");
					}
				};
 
				JPanel panel = new JPanel();
				JButton defaultButton = new JButton("Default Button");
				defaultButton.addActionListener(actionListener);
				panel.add(defaultButton);
 
				JButton otherButton =  new JButton("Other Button");
				otherButton.addActionListener(actionListener);
				panel.add(otherButton);
 
				frame.add(panel, BorderLayout.SOUTH);
 
				Keymap keymap = textField.getKeymap();
				KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false);
				keymap.removeKeyStrokeBinding(keystroke);
 
				frame.getRootPane().setDefaultButton(defaultButton);
 
				frame.setSize(250, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}


15.3.11 JTextComponent.KeyBinding类

JTextComponent类借且于JTextComponent.KeyBinding类来存储特定的按键绑定。当前的观感定义了文本组件按键绑定的默认集合,例如我们所熟悉的在Microsoft Windows平台上Ctrl-X用于剪切,Ctrl-C用于复制以及Ctrl-V用于粘贴。

15.3.12 处理JTextField事件

处理Swing文本组件中的事件完全不同于处理AWT文本组件中的事件。尽管我们仍然可以关联一个ActionListener来监听用户在文本域中输入Enter键的情况,关联KeyListener或是TextListsener不再有用。

要验证输入,关联InputVerifier要好于关联FocusListener。然而,输入验证最好是留给Document来实现或是当用户提交表单时实现。

使用ActionListener来监听JTextField事件

当用户在文本域中按下Enter后,JTextField会通知所注册的ActionListener对象。组件会向ActionListener对象发送一个ActionEvent。ActionEvent的部分是一个动作命令。默认情况下,事件的动作命令是组件的当前内容。对于Swing的JTextField,我们也可以将动作命令设置为不同于内容的某些东西。JTextField有一个actionCommand属性。当这个属性被设置为null时(默认设置),ActionEvent的动作命令会使用组件的内容。然而,如果我们为JTextField设置actionCommand属性,那么actionCommand就会成为ActionEvent的组成部分。

下面的代码显示了这种区域。有两个文本域。当在第一个文本域中按下Enter时,会使得所注册的ActionListener得到通知,并输出“Yo”。当在第二个文本域中按下Enter时,则内容会被输出。

JTextField nameTextField = new JTextField();
JTextField cityTextField = new JTextField();
ActionListener actionListener = new ActionListener() {
  public void actionPerformed(ActionEvent actionEvent) {
    System.out.println("Command: " + actionEvent.getActionCommand());
  }
};
nameTextField.setActionCommand("Yo");
nameTextField.addActionListener(actionListener);
cityTextField.addActionListener(actionListener);


使用KeyListener监听JTextField事件

对于Swing文本组件,我们通常并不使用KeyListener来监听键盘事件-至少不用来验证输入。运行下面的程序可以演示我们仍然可以确定一个按键何时被按下或释放,而不仅是何时输入。

KeyListener keyListener = new KeyListener() {
  public void keyPressed(KeyEvent keyEvent) {
    printIt("Pressed", keyEvent);
  }
  public void keyReleased(KeyEvent keyEvent) {
    printIt("Released", keyEvent);
  }
  public void keyTyped(KeyEvent keyEvent) {
    printIt("Typed", keyEvent);
  }
  private void printIt(String title, KeyEvent keyEvent) {
    int keyCode = keyEvent.getKeyCode();
    String keyText = KeyEvent.getKeyText(keyCode);
    System.out.println(title + " : " + keyText);
  }
};
nameTextField.addKeyListener(keyListener);
cityTextField.addKeyListener(keyListener);


使用InputVerifer监听JTextField事件

实现InputVerifier接口可以使得我们进行JTextField的域级别验证。在焦点移除一个文本组件之前,验证会运行。如果输入不合法,验证器就会拒绝修改并将焦点保持在指定的组件中。

在下面的示例中,如果我们尝试将输入焦点移除文本域之外,我们就会发现我们并不能办到,除非文本域的内容是空的,或者内容由字符串“Exit”组成。

InputVerifier verifier = new InputVerifier() {
  public boolean verify(JComponent input) {
    final JTextComponent source = (JTextComponent)input;
    String text = source.getText();
    if ((text.length() != 0) && !(text.equals("Exit"))) {
      Runnable runnable = new Runnable() {
        public void run() {
          JOptionPane.showMessageDialog (source, "Can't leave.",
            "Error Dialog", JOptionPane.ERROR_MESSAGE);
        }
      };
      EventQueue.invokeLater(runnable);
      return false;
    }  else {
      return true;
    }
  }
};
nameTextField.setInputVerifier(verifier);
cityTextField.setInputVerifier(verifier);


使用DocumentListener监听JTextField事件

要确定文本组件的内容何时发生变化,我们需要向数据模型关联一个监听器。在这种情况下,数据模型为Document,而监听器为DocumentListener。下面的示例仅是告诉我们模型何时以及如何发生变化。记住changedUpdate()用于属性变化。不要使用DocumentListener进行输入验证。

DocumentListener documentListener = new DocumentListener() {
  public void changedUpdate(DocumentEvent documentEvent) {
    printIt(documentEvent);
  }
  public void insertUpdate(DocumentEvent documentEvent) {
    printIt(documentEvent);
  }
  public void removeUpdate(DocumentEvent documentEvent) {
    printIt(documentEvent);
  }
  private void printIt(DocumentEvent documentEvent) {
    DocumentEvent.EventType type = documentEvent.getType();
    String typeString = null;
    if (type.equals(DocumentEvent.EventType.CHANGE)) {
      typeString = "Change";
    }  else if (type.equals(DocumentEvent.EventType.INSERT)) {
      typeString = "Insert";
    }  else if (type.equals(DocumentEvent.EventType.REMOVE)) {
      typeString = "Remove";
    }
    System.out.print("Type  :   " + typeString + " / ");
    Document source = documentEvent.getDocument();
    int length = source.getLength();
    try {
      System.out.println("Contents: " + source.getText(0, length));
    }  catch (BadLocationException badLocationException) {
      System.out.println("Contents: Unknown");
    }
  }
};
nameTextField.getDocument().addDocumentListener(documentListener);
cityTextField.getDocument().addDocumentListener(documentListener);

将所有内容组合在一起

现在我们已经分别了解了监听器的使用,让我们将这些内容组合在一个示例中。图15-14显示了最终的结果。记住要离开组件的魔法单词是“Exit”。


图15-14后面程序的源码显示在列表15-13中。

package swingstudy.ch15;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
 
import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
 
public class JTextFieldSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("TextField Listener Sample");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				JPanel namePanel = new JPanel(new BorderLayout());
				JLabel nameLabel = new JLabel("Name: ");
				nameLabel.setDisplayedMnemonic(KeyEvent.VK_N);
				JTextField nameTextField = new JTextField();
				nameLabel.setLabelFor(nameTextField);
				namePanel.add(nameLabel, BorderLayout.WEST);
				namePanel.add(nameTextField, BorderLayout.CENTER);
				frame.add(namePanel, BorderLayout.NORTH);
 
				JPanel cityPanel = new JPanel(new BorderLayout());
				JLabel cityLabel = new JLabel("City: ");
				cityLabel.setDisplayedMnemonic(KeyEvent.VK_C);
				JTextField cityTextField = new JTextField();
				cityLabel.setLabelFor(cityTextField);
				cityPanel.add(cityLabel, BorderLayout.WEST);
				cityPanel.add(cityTextField, BorderLayout.CENTER);
				frame.add(cityPanel, BorderLayout.SOUTH);
 
				ActionListener actionListener = new ActionListener() {
					public void actionPerformed(ActionEvent event) {
						System.out.println("Command: "+event.getActionCommand());
					}
				};
				nameTextField.setActionCommand("Yo");
				nameTextField.addActionListener(actionListener);
				cityTextField.addActionListener(actionListener);
 
				KeyListener keyListener = new KeyListener() {
					public void keyPressed(KeyEvent event) {
						printIt("Pressed", event);
					}
					public void keyReleased(KeyEvent event) {
						printIt("Released", event);
					}
					public void keyTyped(KeyEvent event) {
						printIt("Typed", event);
					}
					private void printIt(String title, KeyEvent event) {
						int keyCode = event.getKeyCode();
						String keyText = event.getKeyText(keyCode);
						System.out.println(title+" : "+keyText+" / "+event.getKeyChar());
					}
				};
 
				nameTextField.addKeyListener(keyListener);
				cityTextField.addKeyListener(keyListener);
 
				InputVerifier verifier = new InputVerifier() {
					public boolean verify(JComponent input) {
						final JTextComponent source = (JTextComponent)input;
						String text = source.getText();
						if((text.length()!=0) && !(text.equals("Exit"))) {
							JOptionPane.showMessageDialog(source, "Can't leave.", "Error Dialog", JOptionPane.ERROR_MESSAGE);
							return false;
						}
						else {
							return true;
						}
					}
				};
				nameTextField.setInputVerifier(verifier);
				cityTextField.setInputVerifier(verifier);
 
				DocumentListener documentListener = new DocumentListener() {
					public void changedUpdate(DocumentEvent event) {
						printIt(event);
					}
					public void insertUpdate(DocumentEvent event) {
						printIt(event);
					}
					public void removeUpdate(DocumentEvent event) {
						printIt(event);
					}
					private void printIt(DocumentEvent event) {
						DocumentEvent.EventType type = event.getType();
						String typeString = null;
						if(type.equals(DocumentEvent.EventType.CHANGE)) {
							typeString = "Change";
						}
						else if(type.equals(DocumentEvent.EventType.INSERT)) {
							typeString = "Insert";
						}
						else if(type.equals(DocumentEvent.EventType.REMOVE)) {
							typeString = "Remove";
						}
						System.out.println("Type : "+typeString+" / ");
						Document source = event.getDocument();
						int length = source.getLength();
 
						try {
							System.out.println("Contents: "+source.getText(0, length));
						}
						catch(BadLocationException badLocationException) {
							System.out.println("Contents: Unknown");
						}
					}
				};
				nameTextField.getDocument().addDocumentListener(documentListener);
				cityTextField.getDocument().addDocumentListener(documentListener);
 
				frame.setSize(250, 100);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}


15.3.13 自定义JTextField观感

每一个可安装的Swing观感都提供了一个不同的JTextField外观以及默认的UIResource值集合。表15-5显示了JTextField的25 UIResource相关的属性。



图15-15显示了JTextField在预安装的观感类型集合Motif,Windows与Ocean下的外观。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值