让绘制的Java文本框响应输入法事件

在任何一款桌面应用中,都难免会遇到让用户输入文字或者特殊字符的情况发生,所以输入法的支持与文本框组件的存在就变得必不可少。

由于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:[email protected] * @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); } }

运行效果如下图:

让绘制的Java文本框响应输入法事件

怎么样?这时你在TextCanvas中进行输入操作,是不是与JTextField或TextField里相差无几呢?——什么?你说就算“重复发明*”也应该有个限度,已经有JTextField与TextField了,你再写一个有什么用?

嗯,您很聪明,单纯的绘制文本框确实没有任何意义,但是,如果有一系列直接通过AWT绘制的组件与其相呼应呢?——比如,偶在LGame-Simple中制作的那一系列UI组件……

那么事情,就会变成如下这个样子。

让绘制的Java文本框响应输入法事件

怎么样呢?如上图所示,这是一个纯绘制的界面,无论文本框的字体,大小,颜色乃至透明度,贴图都可以随性切换(甚至逆天的将两个文本框叠在一起也可以), 而这样一个纯绘制出的文本框能够获得输入法支持,意味着什么呢?这意味着,一个相对于Swing能耗更少,效率更高的类Swing体系已经搭建成型了!(当然,相对的功能也更少,不过事无两利嘛……)

PS:如上所述,LGame-Simple-0.2.5版Text系组件将获得输入法支持,中文或其它语言的输入已经没有任何问题。(此版预计同Android版LGame一道于12月中下旬发布……不过,那是理想状态,事实上偶欠着的事情挺多,尽力看看……)

嗯,其实PS中的话才是最主要的……