昨天在博客里提到了Panda3D对于中文输入的问题,没有收到什么解答。昨晚从8点到凌晨4点,除去中间零零碎碎的其他事情,大约花了5、6个小时的时间搞了一个简单的解决方法。在这里记录一下整个思考过程,并把自己实现的控件源码提供出来,希望有人可以给出更加优美完善的解决方案。
首先,说明一下Panda3D输入中文时存在的问题,使用DirectEntry控件,切换到搜狗等输入法尝试输入,会发现没有什么效果,而且再切换回英文键盘也无法继续输入,只能响应删除字符等控制操作。
Mentor说了,在游戏中,一般实现中文输入有两种方法,第一是使用操作系统的输入法,第二是自己实现一套输入法。以我现在做Mini项目的进度,第二个明显hold不住,于是只能尝试使用第一种方法。我在DirectEntry中切换到微软输入法,惊奇地发现可以输入字符,但是显示的结果是乱码。
如果可以输入,那么显示的乱码和输入的中文肯定有一点的关系!基于这一思考,我输入“中文”,然后用repr()将DirectEntry中的内容在控制台打印出来,结果是:u'\xd6\xd0\xce\xc4',这明显不是utf-8格式的字符,出于好奇,我拿这个去google搜索了一下,发现'\xd6\xd0\xce\xc4'是“中文”的gb2312格式编码,这就可以解释为什么会显示为乱码的原因,'\xd6\xd0\xce\xc4'应该是一个长度为2个宽字符的字串,而u'\xd6\xd0\xce\xc4'的长度为4,Panda3D将输入法的输入识别为unicode,但是没有做相应的合并。我不清楚这是我的设置问题还是Panda3D的一个bug,问题的根源找到了,那么就可以想办法解决了。只需要将u'\xd6\xd0\xce\xc4'变为s='\xd6\xd0\xce\xc4',然后使用s.decode('gb2312')就可以转化为可供显示的中文了。
想要解决这个问题,需要查阅DirectEntry底层的实现,而DirectEntry.py提供给的信息太少了,我猜测底层的键盘响应应该是用C++封装的,只给python提供了一些接口,那么从根源拦截字符输入就比较困难。另外一种折中方案是在输入之后显示之前修改内容,这样就需要能够监听按键输入的事件。在分析DirectEntry之后,找到了如下的方法:
self.accept(self.guiItem.getTypeEvent(), self._handleTpye)
self.accept(self.guiItem.getOverflowEvent(), self._handleOverFlow)
self.accept(self.guiItem.getEraseEvent(), self._handleEease)
第一个是Type事件,第二个是控件的输入超出长度的事件,第三个是擦除事件。经过测试发现,后面三个函数调用的时间都是在底层已经更改完self.guiItem的内容之后才会触发的,也就是说完全截获输入去修改的想法不太容易实现。在我将事件响应函数的参数guiEvent打印出来之后发现,每次输入一个字符就会调用一个_handleType函数,并且guiEvent.getKeycode()为对应输入的字符。
对于英文字符和符号,ascii码的范围是0~127,大于等于128的我可以判定为unicode的一半。基于这一假设,我自己维护一个输入内容self.showingContent,然后对于小于128的字符直接处理,对于大于127的字符首先存放到self.tempchar中,累积为2个的时候,取出来合并为一个gb2312编码的unicode字符,刷新输入内容即可。
最后的结果就是以继承自DirectEntry的方式实现,因为时间关系,一些初始化代码直接从父类拷贝过来,没有精简,具体代码如下,考虑了光标的移动等内容处理,还有长度溢出半个字符的情况等。
# -*- coding: utf-8 -*- # Author: Daivd # Date : 2012-09-02 # E-Mail: [email protected] # If you have any questions or better solution, let me know. # Thanks. # State: This class is not the perfect solution for Chinese input in Panda3D. # You have to use Microsoft Pinyin or Smart ABC as IME. # You'd better not use the Shift key to switch Chinese input to English input, # which may make the type listener out of operation. # Panda3D is Open Source and free for any purpose, so is this file. # Enjoy it! __all__ = ['ChiDirectEntry'] from direct.gui.DirectGui import DirectEntry from pandac.PandaModules import * import direct.gui.DirectGuiGlobals as DGG from direct.gui.DirectFrame import * from direct.gui.OnscreenText import OnscreenText from direct.showbase.DirectObject import DirectObject # ChiDirectEntry States: ENTRY_FOCUS_STATE = PGEntry.SFocus # 0 ENTRY_NO_FOCUS_STATE = PGEntry.SNoFocus # 1 ENTRY_INACTIVE_STATE = PGEntry.SInactive # 2 class ChiDirectEntry(DirectEntry): """ ChiDirectEntry(parent) - Create a DirectGuiWidget which responds to keyboard buttons """ directWtext = ConfigVariableBool('direct-wtext', 1) def __init__(self, parent = None, **kw): # Inherits from DirectFrame # A Direct Frame can have: # - A background texture (pass in path to image, or Texture Card) # - A midground geometry item (pass in geometry) # - A foreground text Node (pass in text string or Onscreen Text) # For a direct entry: # Each button has 3 states (focus, noFocus, disabled) # The same image/geom/text can be used for all three states or each # state can have a different text/geom/image # State transitions happen automatically based upon mouse interaction optiondefs = ( # Define type of DirectGuiWidget ('pgFunc', PGEntry, None), ('numStates', 3, None), ('state', DGG.NORMAL, None), ('entryFont', None, DGG.INITOPT), ('width', 10, self.setup), ('numLines', 1, self.setup), ('focus', 0, self.setFocus), ('cursorKeys', 1, self.setCursorKeysActive), ('obscured', 0, self.setObscureMode), # Setting backgroundFocus allows the entry box to get keyboard # events that are not handled by other things (i.e. events that # fall through to the background): ('backgroundFocus', 0, self.setBackgroundFocus), # Text used for the PGEntry text node # NOTE: This overrides the DirectFrame text option ('initialText', '', DGG.INITOPT), # Command to be called on hitting Enter ('command', None, None), ('extraArgs', [], None), # Command to be called when enter is hit but we fail to submit ('failedCommand', None, None), ('failedExtraArgs',[], None), # commands to be called when focus is gained or lost ('focusInCommand', None, None), ('focusInExtraArgs', [], None), ('focusOutCommand', None, None), ('focusOutExtraArgs', [], None), # Sounds to be used for button events ('rolloverSound', DGG.getDefaultRolloverSound(), self.setRolloverSound), ('clickSound', DGG.getDefaultClickSound(), self.setClickSound), ('autoCapitalize', 0, self.autoCapitalizeFunc), ('autoCapitalizeAllowPrefixes', ChiDirectEntry.AllowCapNamePrefixes, None), ('autoCapitalizeForcePrefixes', ChiDirectEntry.ForceCapNamePrefixes, None), ) # Merge keyword options with default options self.defineoptions(kw, optiondefs) # Initialize superclasses DirectEntry.__init__(self, parent) if self['entryFont'] == None: font = DGG.getDefaultFont() else: font = self['entryFont'] # Create Text Node Component self.onscreenText = self.createcomponent( 'text', (), None, OnscreenText, (), parent = hidden, # Pass in empty text to avoid extra work, since its really # The PGEntry which will use the TextNode to generate geometry text = '', align = TextNode.ALeft, font = font, scale = 1, # Don't get rid of the text node mayChange = 1) # We can get rid of the node path since we're just using the # onscreenText as an easy way to access a text node as a # component self.onscreenText.removeNode() # Bind command function self.bind(DGG.ACCEPT, self.commandFunc) self.bind(DGG.ACCEPTFAILED, self.failedCommandFunc) self.accept(self.guiItem.getFocusInEvent(), self.focusInCommandFunc) self.accept(self.guiItem.getFocusOutEvent(), self.focusOutCommandFunc) # listen for auto-capitalize events on a separate object to prevent # clashing with other parts of the system self._autoCapListener = DirectObject() # Call option initialization functions self.initialiseoptions(ChiDirectEntry) if not hasattr(self, 'autoCapitalizeAllowPrefixes'): self.autoCapitalizeAllowPrefixes = ChiDirectEntry.AllowCapNamePrefixes if not hasattr(self, 'autoCapitalizeForcePrefixes'): self.autoCapitalizeForcePrefixes = ChiDirectEntry.ForceCapNamePrefixes # Update TextNodes for each state for i in range(self['numStates']): self.guiItem.setTextDef(i, self.onscreenText.textNode) # Now we should call setup() again to make sure it has the # right font def. self.setup() # Update initial text self.unicodeText = 0 if self['initialText']: self.enterText(self['initialText']) self.accept(self.guiItem.getTypeEvent(), self._handleTpye) self.accept(self.guiItem.getOverflowEvent(), self._handleOverFlow) self.accept(self.guiItem.getEraseEvent(), self._handleEease) if self['initialText']: self.showingContent = unicode(self['initialText'], 'utf-8') else: self.showingContent = u'' self.tempchar = None def _handleOverFlow(self, guiEvent): self.tempchar = None def _handleTpye(self, guiEvent): pos = self.guiItem.getCursorPosition() addition = '' if guiEvent.getKeycode() <= 0: self.tempchar = None return if guiEvent.getKeycode() < 128: addition = unichr(guiEvent.getKeycode()) elif self.tempchar == None: self.tempchar = guiEvent.getKeycode() else: s = '\\x%x\\x%x'%(self.tempchar, guiEvent.getKeycode()) tempC = '' try : exec("tempC = unicode('%s', 'gb2312')"%s) except: pass addition = tempC self.tempchar = None if len(self.get()) != len(self.showingContent): diff = len(self.get()) - len(self.showingContent) pos -= diff if addition != '': self.showingContent = self.showingContent[:pos] + addition + self.showingContent[pos:] pos += 1 self.setCursorPosition(pos) self.set(self.showingContent) def _handleEease(self, guiEvent): self.tempchar = None pos = self.guiItem.getCursorPosition() self.showingContent = self.showingContent[:pos] + self.showingContent[pos+1:] def set(self, text): self.showingContent = text DirectEntry.set(self, self.showingContent) def get(self): return DirectEntry.get(self)
时间关系,实现得较为简陋。而且经过我自己的测试,在微软输入法下,按Shift键切换中英文输入,也会导致输入框无法再响应输入的问题,我猜测这和搜狗输入法一样,对于Panda3D底层的按键响应事件产生了影响。上一张实现结果。。。
上述的思考过程和实现结果仅供参考,如果有人可以提供更好的办法或者在此基础上的改进,烦劳知会,谢谢~
E-mail:[email protected]
源码见附件。