在任何一款桌面应用中,都难免会遇到让用户输入文字或者特殊字符的情况发生,所以输入法的支持与文本框组件的存在就变得必不可少。
由于Java具有桌面应用开发能力,它的图形组件中也当然配备有文本框,因而无论是继承自TextComponent的Text系组件抑或继承自JTextComponent的JText系组件都提供了让用户输入数据的功能。
现在的疑问是,虽然TextComponent与JTextComponent相类似,但两者的父类却并不同级。TextComponent直接继承自Component,但Component已经是所有Java图形组件的公共父类,JTextComponent的父类JComponent却继承自Container,而Container的父类才是Component。
为什么会这样呢?如果JTextComponent直接继承TextComponent难道不好吗?没错,不好,或者说不能。除了Swing与AWT运行原理造成的差异与组件关系的统一性需求外,造成这样情况的理由中还有一点至关重要,那就是不光JTextComponent不能,即便我们想在java.awt包外重载TextComponent也不能。原因在于,虽然TextComponent类并非final,但它的唯一构造函数却是default的,这意味着即便不同包中的类继承了它,也不能构造,根本无法重载。
更何况,就算可以重载的JTextComponent,也与TextComponent一样存在着一些很麻烦的默认配置问题(就更不要说重载JTextField抑或TextField了)。最主要的是,用它们制作标准文本框固然游刃有余,但如果我们需要的文本框不那么标准,甚至需要某些“奇形怪状”到只要求输入文字,但根本就算不上文本框的组件时,那么它们势必更加捉襟见肘。
那么,我们要怎样才能满足这种近乎于“变态”的要求呢?
很简单,自己“画”个文本框出来就好了,因为是“画”的,所以想它怎样,便是怎样,因为是凭空绘制,也没有利用现成Swing组件绘制时的不便。
所以能这样做,就在于Java获得输入法支持的关键点不在TextComponent与JTextComponent,而是java.awt.im包下的相关组件,更具体地说,只要你实现了InputMethodListener与InputMethodRequests两尊大神,那么所有Component都可以支持输入法,又何必专情于TextComponent与JTextComponent?
闲话少说,现在我就直接用Canvas来“画”个文本框,给大家瞧瞧。
TextCanvas.java
package org.test;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputMethodEvent;
import java.awt.event.InputMethodListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.awt.im.InputMethodRequests;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.AttributedCharacterIterator.Attribute;
import sun.awt.InputMethodSupport;
/**
*
* Copyright 2009
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* @project loonframework
* @author chenpeng
* @email:ceponline@yahoo.com.cn
* @version 0.1
*/
public class TextCanvas extends Canvas implements KeyListener, FocusListener,
InputMethodListener, InputMethodRequests {
/**
*
*/
private static final long serialVersionUID = 1L;
// 空的字符信息迭代器
private static final AttributedCharacterIterator EMPTY_TEXT = (new AttributedString(
"")).getIterator();
// 空的输入法信息
private static final Attribute[] IM_ATTRIBUTES = { TextAttribute.INPUT_METHOD_HIGHLIGHT };
// 判定当前组件是否具有焦点
private transient boolean haveFocus;
// 用户已输入数据缓存
private StringBuffer messageText = new StringBuffer();
// 文本布局器,用以限定输入后显示的具体位置
private transient TextLayout textLayout = null;
// 判定是否验证输入法布局器
private transient boolean validTextLayout = false;
// 文本显示位置的坐标修正值
private static final int textOffset = 5;
// 已输入文本定位器
private Point textOrigin = new Point(0, 0);
// 已输入信息的字符串构成数据
private AttributedString composedTextString;
// 已输入信息的字符构成数据
private AttributedCharacterIterator composedText;
// 插入符定位器,用以记载游标所在位置
private TextHitInfo caret = null;
/**
* 构造一个TextCanvas,用以通过纯绘制的方式输入文本信息
*
*/
public TextCanvas() {
super();
this.setForeground(Color.black);
this.setBackground(Color.white);
this.setFontSize(12);
this.setVisible(true);
this.setEnabled(true);
this.addInputMethodListener(this);
this.addKeyListener(this);
this.addFocusListener(this);
try {
Toolkit toolkit = Toolkit.getDefaultToolkit();
boolean shouldEnable = false;
// 验证当前环境是否支持输入法调用
if (toolkit instanceof InputMethodSupport) {
shouldEnable = ((InputMethodSupport) toolkit)
.enableInputMethodsForTextComponent();
}
enableInputMethods(shouldEnable);
} catch (Exception e) {
}
}
/**
* 重载Component的输入法调用接口,因为此类为InputMethodRequests的实现,所以返回this即可。
*/
public InputMethodRequests getInputMethodRequests() {
return this;
}
/**
* 调正当前显示的字体大小
*
* @param size
*/
public void setFontSize(int size) {
setFont(new Font("Dialog", Font.PLAIN, size));
textOrigin.x = 5;
textOrigin.y = (textOffset + size);
if (composedTextString != null) {
composedTextString.addAttribute(TextAttribute.FONT, getFont());
}
}
/**
* 返回用于显示的输入信息集
*
* @return
*/
public AttributedCharacterIterator getDisplayText() {
if (composedText == null) {
return getDisplayTextToAttributedCharacterIterator();
} else {
return EMPTY_TEXT;
}
}
/**
* 返回当前插入符所在位置
*
* @return
*/
public TextHitInfo getCaret() {
if (composedText == null) {
return TextHitInfo.trailing(messageText.length() - 1);
} else if (caret == null) {
return null;
} else {
return caret.getOffsetHit(getCommittedTextLength());
}
}
/**
* 触发输入法变更事件
*/
public void inputMethodTextChanged(InputMethodEvent e) {
int committedCharacterCount = e.getCommittedCharacterCount();
AttributedCharacterIterator text = e.getText();
composedText = null;
char c;
if (text != null) {
// 需要复制的字符长度
int toCopy = committedCharacterCount;
c = text.first();
while (toCopy-- > 0) {
insertCharacter(c);
c = text.next();
}
if (text.getEndIndex()
- (text.getBeginIndex() + committedCharacterCount) > 0) {
composedTextString = new AttributedString(text, text
.getBeginIndex()
+ committedCharacterCount, text.getEndIndex(),
IM_ATTRIBUTES);
composedTextString.addAttribute(TextAttribute.FONT, getFont());
composedText = composedTextString.getIterator();
}
}
e.consume();
invalidateTextLayout();
caret = e.getCaret();
repaint();
}
/**
* 修改插入符所在位置
*/
public void caretPositionChanged(InputMethodEvent event) {
caret = event.getCaret();
event.consume();
repaint();
}
/**
* 获得指定定位符对应的文本显示位置
*/
public Rectangle getTextLocation(TextHitInfo offset) {
Rectangle rectangle;
if (offset == null) {
rectangle = getCaretRectangle();
} else {
TextHitInfo globalOffset = offset
.getOffsetHit(getCommittedTextLength());
rectangle = getCaretRectangle(globalOffset);
}
Point location = getLocationOnScreen();
rectangle.translate(location.x, location.y);
return rectangle;
}
/**
* 获得偏移指定坐标的插入符信息
*/
public TextHitInfo getLocationOffset(int x, int y) {
Point location = getLocationOnScreen();
Point textOrigin = getTextOrigin();
x -= location.x + textOrigin.x;
y -= location.y + textOrigin.y;
TextLayout textLayout = getTextLayout();
if (textLayout != null && textLayout.getBounds().contains(x, y)) {
return textLayout.hitTestChar(x, y).getOffsetHit(
-getCommittedTextLength());
} else {
return null;
}
}
public int getInsertPositionOffset() {
return getCommittedTextLength();
}
/**
* 返回指定范围内的字符信息迭代器
*/
public AttributedCharacterIterator getCommittedText(int beginIndex,
int endIndex, Attribute[] attributes) {
return getMessageText(beginIndex, endIndex);
}
public AttributedCharacterIterator cancelLatestCommittedText(
Attribute[] attributes) {
return null;
}
public AttributedCharacterIterator getSelectedText(Attribute[] attributes) {
return EMPTY_TEXT;
}
public synchronized void update(Graphics g) {
paint(g);
}
/**
* 绘制目标界面
*/
public synchronized void paint(Graphics g) {
g.setColor(getBackground());
Dimension size = getSize();
g.fillRect(0, 0, size.width, size.height);
g.setColor(Color.black);
g.drawRect(0, 0, size.width - 1, size.height - 1);
if (haveFocus) {
g.drawRect(1, 1, size.width - 3, size.height - 3);
}
g.setColor(getForeground());
TextLayout textLayout = getTextLayout();
if (textLayout != null) {
textLayout.draw((Graphics2D) g, textOrigin.x, textOrigin.y);
}
Rectangle rectangle = getCaretRectangle();
if (haveFocus && rectangle != null) {
g.setXORMode(getBackground());
g.fillRect(rectangle.x, rectangle.y, 1, rectangle.height);
g.setPaintMode();
}
}
/**
* 将messageText转化为指定范围内的字符信息迭代器
*
* @param beginIndex
* @param endIndex
* @return
*/
public AttributedCharacterIterator getMessageText(int beginIndex,
int endIndex) {
AttributedString string = new AttributedString(messageText.toString());
return string.getIterator(null, beginIndex, endIndex);
}
/**
* 返回已输入的字符串信息长度
*/
public int getCommittedTextLength() {
return messageText.length();
}
public AttributedCharacterIterator getDisplayTextToAttributedCharacterIterator() {
AttributedString string = new AttributedString(messageText.toString());
if (messageText.length() > 0) {
string.addAttribute(TextAttribute.FONT, getFont());
}
return string.getIterator();
}
/**
* 返回当前文本布局器
*
* @return
*/
public synchronized TextLayout getTextLayout() {
if (!validTextLayout) {
textLayout = null;
AttributedCharacterIterator text = getDisplayText();
if (text.getEndIndex() > text.getBeginIndex()) {
FontRenderContext context = ((Graphics2D) getGraphics())
.getFontRenderContext();
textLayout = new TextLayout(text, context);
}
}
validTextLayout = true;
return textLayout;
}
/**
* 强制文本布局器验证无效化
*
*/
public synchronized void invalidateTextLayout() {
validTextLayout = false;
}
/**
* 返回文本绘制点
*
* @return
*/
public Point getTextOrigin() {
return textOrigin;
}
/**
* 返回对应插入点的矩形选框
*
* @return
*/
public Rectangle getCaretRectangle() {
TextHitInfo caret = getCaret();
if (caret == null) {
return null;
}
return getCaretRectangle(caret);
}
/**
* 返回对应插入点的矩形选框
*
* @param caret
* @return
*/
public Rectangle getCaretRectangle(TextHitInfo caret) {
TextLayout textLayout = getTextLayout();
int caretLocation;
if (textLayout != null) {
caretLocation = Math.round(textLayout.getCaretInfo(caret)[0]);
} else {
caretLocation = 0;
}
FontMetrics metrics = getGraphics().getFontMetrics();
return new Rectangle(textOrigin.x + caretLocation, textOrigin.y
- metrics.getAscent(), 0, metrics.getAscent()
+ metrics.getDescent());
}
/**
* 插入指定字符串
*
* @param c
*/
public void insertCharacter(char c) {
messageText.append(c);
invalidateTextLayout();
}
/**
* 用户输入
*/
public void keyTyped(KeyEvent event) {
char keyChar = event.getKeyChar();
// 处理文字删除
if (keyChar == '/b') {
int len = messageText.length();
if (len > 0) {
messageText.setLength(len - 1);
invalidateTextLayout();
}
} else {
insertCharacter(keyChar);
}
event.consume();
repaint();
}
public void keyPressed(KeyEvent event) {
}
public void keyReleased(KeyEvent event) {
}
public void focusGained(FocusEvent event) {
haveFocus = true;
repaint();
}
public void focusLost(FocusEvent event) {
haveFocus = false;
repaint();
}
public static void main(String[] args) {
Frame frame = new Frame("绘制一个输入框");
TextCanvas text = new TextCanvas();
frame.add(text);
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.pack();
frame.setSize(300, 300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
运行效果如下图:
怎么样?这时你在TextCanvas中进行输入操作,是不是与JTextField或TextField里相差无几呢?——什么?你说就算“重复发明轮子”也应该有个限度,已经有JTextField与TextField了,你再写一个有什么用?
嗯,您很聪明,单纯的绘制文本框确实没有任何意义,但是,如果有一系列直接通过AWT绘制的组件与其相呼应呢?——比如,偶在LGame-Simple中制作的那一系列UI组件……
那么事情,就会变成如下这个样子。
怎么样呢?如上图所示,这是一个纯绘制的界面,无论文本框的字体,大小,颜色乃至透明度,贴图都可以随性切换(甚至逆天的将两个文本框叠在一起也可以), 而这样一个纯绘制出的文本框能够获得输入法支持,意味着什么呢?这意味着,一个相对于Swing能耗更少,效率更高的类Swing体系已经搭建成型了!(当然,相对的功能也更少,不过事无两利嘛……)
PS:如上所述,LGame-Simple-0.2.5版Text系组件将获得输入法支持,中文或其它语言的输入已经没有任何问题。(此版预计同Android版LGame一道于12月中下旬发布……不过,那是理想状态,事实上偶欠着的事情挺多,尽力看看……)
嗯,其实PS中的话才是最主要的……