游戏中经常要输入汉字,但当我们游戏没有自己实现输入法窗口时,windows会使用用户安装的输入法,但这个输入法窗口只会显示在游戏窗口外头,而且当我们游戏全屏时(真全屏,不是那种窗口式的假全屏),屏幕上无法显示其他程序的窗口,也就看不到任何输入法窗口了。此时玩家输入文字,无法看到候选词列表,用户输入将会变的举步维艰。所以一般网络游戏都会有一个游戏内置的输入法窗口。由于这个输入法窗口式游戏程序自己渲染的,不管全屏与否都会正常显示。如何实现游戏内置输入法窗口呢?
通过查阅相关资料,以及个人实践,我完成了一个教学版的输入法窗口实现。实现游戏内置输入法窗口并不难,和游戏相关的逻辑其实比较简单,难点主要在熟悉Windows对于输入法的消息管理,以及相关模块的API。首先让我们看一下Windows对于输入法的一下消息及相关API。
GCS_RESULTSTR
此消息是用户通过输入法生成了某些字符,这些字符需要输入到对应的编辑框中。另外再说一点,CEGUI支持中文显示之后,并不能通过输入法输入中文字符,这是因为CEGUI没有监听这个消息。所以在这个消息中,我们将把IME生成中文字符注入到CEGUI中( CEGUI::System::getSingleton().injectChar() )。获取生成字符也是通过ImmGetCompositionStringW()获取。
WM_IME_NOTIFY
IME相关的Windows只是我们大体已经了解了。现在介绍一下如果使用CEGUI实现这个教学班的输入法窗口。让我们分析一下:
基本上就这些了。下面是IME控件类的实现代码:
#ifndef _CEGUIIME_h_ #define _CEGUIIME_h_ #include "../CEGUIBase.h" #include "../CEGUIWindow.h" #include "CEGUIListbox.h" #if defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4251) #endif namespace CEGUI { class CEGUIEXPORT IME : public Listbox { public: static const String EventNamespace; //!< Namespace for global events static const String WidgetTypeName; //!< Window factory name static const String EventIMEOpen; static const String EventIMEClose; static const String EventIMEChanged; static const uint CompositionStringItemID; public: static IME* GetIME(); public: IME(const String& type, const String& name); ~IME(); virtual void setLookNFeel(const String& look); void SetCandidateStringList( const std::vector< String >& vctCandidateStringList ); void SetCompositionString( const String& strCompositionString ); protected: ListboxItem* _GetCompositionStringItem() const; //pConnectedEditbox: the edit box connected to this IME. if it set to NULL, // IME will try to use the edit box set by void SetConnectedEditbox(). void _AdjustSizeAndPosition(); //you can add your edit box window class to this function. bool _IsEditbox( CEGUI::Window* pWindow ) const; bool _OnEditboxActivated( const CEGUI::EventArgs& e ); bool _OnEditboxDeactivated( const CEGUI::EventArgs& e ); virtual void onShown(WindowEventArgs& e); virtual void onHidden(WindowEventArgs& e); protected: Window* m_pConnectedEditbox; //the size of frame of listbox Size m_FrameSize; }; } #endif
#include "elements/CEGUIIME.h" #include "CEGUIGlobalEventSet.h" #include "elements/CEGUIListboxTextItem.h" #include "elements/CEGUIMultiLineEditbox.h" #include "elements/CEGUIEditbox.h" #include "CEGUIWindowManager.h" #include "CEGUIPropertyHelper.h" namespace CEGUI { const String IME::EventNamespace = "IME"; const String IME::WidgetTypeName = "CEGUI/IME"; const String IME::EventIMEOpen = "IMEOpen"; const String IME::EventIMEClose = "IMEClose"; const String IME::EventIMEChanged = "IMEChanged"; const uint IME::CompositionStringItemID = 123456789; IME* IME::GetIME() { const char* szIMEName = "__IME__"; if( false == WindowManager::getSingleton().isWindowPresent( szIMEName ) ) { return dynamic_cast< IME* >( WindowManager::getSingleton().createWindow( "TaharezLook/IME", szIMEName ) ); } return dynamic_cast< IME* >( WindowManager::getSingleton().getWindow( szIMEName ) ); } IME::IME(const String& type, const String& name) : Listbox( type, name ), m_pConnectedEditbox( NULL ), m_FrameSize( 0, 0 ) { setAlwaysOnTop( true ); setClippedByParent( false ); CEGUI::GlobalEventSet::getSingleton().subscribeEvent( CEGUI::Window::EventNamespace + "/" + CEGUI::Window::EventActivated, CEGUI::Event::Subscriber( &IME::_OnEditboxActivated, this ) ); CEGUI::GlobalEventSet::getSingleton().subscribeEvent( CEGUI::Window::EventNamespace + "/" + CEGUI::Window::EventDeactivated, CEGUI::Event::Subscriber( &IME::_OnEditboxDeactivated, this ) ); } IME::~IME() { } void IME::setLookNFeel(const String& look) { Listbox::setLookNFeel( look ); //when looknfell is set, the window's size is zero. //then getListRenderArea() will return the frame size, but negative. m_FrameSize.d_height = -getListRenderArea().getHeight(); m_FrameSize.d_width = -getListRenderArea().getWidth(); addItem( new ListboxTextItem( "", CompositionStringItemID ) ); } void IME::SetCandidateStringList( const std::vector< String >& vctCandidateStringList ) { ListboxItem* pCompositionStringItem = _GetCompositionStringItem(); if( NULL == pCompositionStringItem ) { return; } //if current number item is less than we need, add some items. for( uint i = getItemCount(); i < vctCandidateStringList.size() + 1; ++i ) { insertItem( new ListboxTextItem( "" ), pCompositionStringItem ); } //if current number item is more than we need, delete some items. uint nNumItem = getItemCount(); for( uint i = vctCandidateStringList.size() + 1; i < nNumItem; ++i ) { removeItem( getListboxItemFromIndex( 0 ) ); } //set string for( uint i = 0; i < vctCandidateStringList.size(); ++i ) { ListboxItem* pItem = getListboxItemFromIndex( i ); if( pItem ) { String strPrefix = PropertyHelper::uintToString( i + 1 ) + ": "; pItem->setText( strPrefix + vctCandidateStringList[ i ] ); } } _AdjustSizeAndPosition(); } void IME::SetCompositionString( const String& strCompositionString ) { ListboxItem* pItem = _GetCompositionStringItem(); if( NULL == pItem ) { return; } pItem->setText( strCompositionString ); } ListboxItem* IME::_GetCompositionStringItem() const { uint nNumItem = getItemCount(); for( uint i = 0; i < nNumItem; ++i ) { ListboxItem* pItem = getListboxItemFromIndex( i ); if( pItem && CompositionStringItemID == pItem->getID() ) { return pItem; } } return NULL; } void IME::_AdjustSizeAndPosition() { if( NULL == m_pConnectedEditbox ) { return; } UVector2 ItemSize( cegui_absdim( getWidestItemWidth() ), cegui_absdim( getTotalItemsHeight() ) ); UVector2 FrameSize( cegui_absdim( m_FrameSize.d_width ), cegui_absdim( m_FrameSize.d_height ) ); setSize( ItemSize + FrameSize ); UVector2 Offset( cegui_absdim( 0 ), cegui_absdim( 0 )- getHeight() ); setPosition( m_pConnectedEditbox->getPosition() + Offset ); } bool IME::_IsEditbox( Window* pWindow ) const { return dynamic_cast< MultiLineEditbox* >( pWindow ) || dynamic_cast< Editbox* >( pWindow ); } bool IME::_OnEditboxActivated( const CEGUI::EventArgs& e ) { const CEGUI::ActivationEventArgs* pActivationEventArg = dynamic_cast< const CEGUI::ActivationEventArgs* >( &e ); if( NULL == pActivationEventArg || NULL == pActivationEventArg->window || ( false == _IsEditbox( pActivationEventArg->window ) ) ) { return false; } if( getParent() ) { getParent()->removeChildWindow( this ); } //insert IME to edit box's parent window if( pActivationEventArg->window->getParent() ) { pActivationEventArg->window->getParent()->addChildWindow( this ); m_pConnectedEditbox = pActivationEventArg->window; } return true; } bool IME::_OnEditboxDeactivated( const CEGUI::EventArgs& e ) { const CEGUI::ActivationEventArgs* pActivationEventArg = dynamic_cast< const CEGUI::ActivationEventArgs* >( &e ); if( NULL == pActivationEventArg || NULL == pActivationEventArg->window || false == _IsEditbox( pActivationEventArg->window ) ) { return false; } hide(); return true; } void IME::onShown(WindowEventArgs& e) { Window::onShown( e ); fireEvent( EventIMEOpen, e, EventNamespace ); } void IME::onHidden(WindowEventArgs& e) { Window::onHidden( e ); fireEvent( EventIMEClose, e, EventNamespace ); } }
void SetCandidateStringList( const std::vector< String >& vctCandidateStringList ); void SetCompositionString( const String& strCompositionString );
IME控件创建完成,我们接下来看一下,如何根据Windows的IME消息来使用这个控件。
case WM_INPUTLANGCHANGE: { HKL hKL = GetKeyboardLayout( 0 ); HIMC hIMC = ImmGetContext( hWnd ); std::string str( 64, 0 ); if( 0 == ImmEscape( hKL,hIMC, IME_ESC_IME_NAME, &str[ 0 ] ) ) { CEGUI::IME::GetIME()->hide(); } OutputDebugStringA( str.c_str() ); OutputDebugStringA( "\n" ); } break; case WM_IME_COMPOSITION: { if( lParam & GCS_RESULTSTR ) { //get IME string std::wstring strBuffer; HIMC hIMC = ImmGetContext( hWnd ); strBuffer.resize( ImmGetCompositionStringW( hIMC, GCS_RESULTSTR, NULL, 0 ) / 2 ); ImmGetCompositionStringW( hIMC, GCS_RESULTSTR, &strBuffer[ 0 ], strBuffer.length() * 2 ); ImmReleaseContext( 0, hIMC ); hIMC = NULL; for( unsigned i = 0; i < strBuffer.length(); ++i ) { CEGUI::System::getSingleton().injectChar( ( CEGUI::utf32 )strBuffer[ i ] ); } } else if( lParam & GCS_COMPSTR ) { std::wstring strBuffer; HIMC hIMC = ImmGetContext( hWnd ); strBuffer.resize( ImmGetCompositionStringW( hIMC, GCS_COMPSTR, NULL, 0 ) / 2 ); ImmGetCompositionStringW( hIMC, GCS_COMPSTR, &strBuffer[ 0 ], strBuffer.length() * 2 ); ImmReleaseContext( hWnd, hIMC ); CEGUI::IME::GetIME()->SetCompositionString( strBuffer.c_str() ); strBuffer += L"\n"; OutputDebugStringW( strBuffer.c_str() ); } } break; case WM_IME_NOTIFY: { if( IMN_OPENCANDIDATE == wParam || IMN_CHANGECANDIDATE == wParam ) { std::string strBuffer; HIMC hIMC = ImmGetContext( hWnd ); strBuffer.resize( ImmGetCandidateListW( hIMC, 0, NULL, 0 ) ); ImmGetCandidateListW( hIMC, 0, ( LPCANDIDATELIST )&strBuffer[ 0 ], strBuffer.size() ); ImmReleaseContext( hWnd, hIMC ); LPCANDIDATELIST pCandidateList = ( LPCANDIDATELIST )&strBuffer[ 0 ]; CEGUI::IME* pIMEWindow = CEGUI::IME::GetIME(); std::vector< CEGUI::String > vctCandidateStringList; for( DWORD i = pCandidateList->dwPageStart; i < pCandidateList->dwPageStart + pCandidateList->dwPageSize; ++i ) { //PageSize index wchar_t* pwcsCandidateString = ( wchar_t* )&strBuffer[ pCandidateList->dwOffset[ i ] ]; vctCandidateStringList.push_back( CEGUI::String( pwcsCandidateString ) ); } pIMEWindow->SetCandidateStringList( vctCandidateStringList ); pIMEWindow->show(); } else if( IMN_CLOSECANDIDATE == wParam ) { CEGUI::Window* pIMEWindow = CEGUI::WindowManager::getSingleton().getWindow( "__IME__" ); pIMEWindow->hide(); } } break;
这样之后我们程序中已经可以使用的IME窗口。但是还存在一个问题,就是当我们使用输入法时,一些按键会通过 WM_KEYDOWN 消息进入CEGUI,干扰IME控件的使用。如:当我们输入nihaoa,IME控件显示出了候选词列表,但是我们想删除最后一个 a 字母拼音,我们按【Backspace】,结果不光候选词改变,我们之前在编辑框输入的一个汉字也被删除了。所以我们在显示候选词列表的时候需要屏蔽这些按键消息。方法如下:在【CEGUISystem.cpp】的【bool System::injectKeyDown(uint key_code)】函数中添加三行判断代码:
bool System::injectKeyDown(uint key_code) { if( IME::GetIME()->isVisible() ) { return true; } // update system keys d_sysKeys |= keyCodeToSyskey((Key::Scan)key_code, true); KeyEventArgs args(getKeyboardTargetWindow()); // if there's no destination window, input can't be handled. if (!args.window) return false; args.scancode = (Key::Scan)key_code; args.sysKeys = d_sysKeys; args.window->onKeyDown(args); return args.handled != 0; }
这样教学版IME窗口基本完成。下面是我的使用截图:
如何疑问,敬请咨询。