CEGUI 输入法窗口实现

游戏中经常要输入汉字,但当我们游戏没有自己实现输入法窗口时,windows会使用用户安装的输入法,但这个输入法窗口只会显示在游戏窗口外头,而且当我们游戏全屏时(真全屏,不是那种窗口式的假全屏),屏幕上无法显示其他程序的窗口,也就看不到任何输入法窗口了。此时玩家输入文字,无法看到候选词列表,用户输入将会变的举步维艰。所以一般网络游戏都会有一个游戏内置的输入法窗口。由于这个输入法窗口式游戏程序自己渲染的,不管全屏与否都会正常显示。如何实现游戏内置输入法窗口呢?

通过查阅相关资料,以及个人实践,我完成了一个教学版的输入法窗口实现。实现游戏内置输入法窗口并不难,和游戏相关的逻辑其实比较简单,难点主要在熟悉Windows对于输入法的消息管理,以及相关模块的API。首先让我们看一下Windows对于输入法的一下消息及相关API。

  1. WM_INPUTLANGCHANGE
    根据字面意思,就是输入语言切换消息。实际上就是当我们更换输入法就会发出这个消息。我们通过监听这个消息,来显示隐藏游戏内置输入法窗口。因为当我们切换成Windows的【EN】输入法时,我们是纯英文输入,无需任何输入法窗口,所以这时我们要隐藏我们的输入法窗口。如何判断是不是【EN】输入法呢?
    • ImmEscape()
      此API可以获取当前输入法的一些基本信息,如输入法的名称等。具体API的用法MSDN写的十分清楚了,这里就不讲解了。这里需要注意【EN】输入法是没有名称的,所以可以当使用 ImmEscape() 获取输入法名称失败时,我们就隐藏游戏内置输入法,因为之时表明用户切换到了【EN】输入法。


  2. WM_IME_COMPOSITION
    有IME( Input Method Edit )前缀的消息都是输入法消息。此消息表明输入法混合状态改变。什么是混合?什么是混合状态?混合是指通过多个键盘按键组合生成一些其他语言字符过程。所以混合状态可以认为是一些和混合相关记录信息,如输入的拼音改变,输入法候选词列表改变。此消息是个消息大类,包含了很多子消息。多个子消息可以组合在一起,存放在消息的 lParam 参数中。我们可以通过 ( lParam & 子消息 )来判断此消息中是否包含该子消息。下面看一下和我们相关的两个子消息。
    1. GCS_COMPSTR
      此消息是输入法“混合字符”改变的消息。什么是混合字符呢?当我们用搜狗等中文输入法的时候,我们打汉字都是有拼音,我们通过打拼音混合出汉字,拼字字符串就是混合字符。所以说 ni 是混合字符, nih 也是混合字符,由 ni 改变成 nih,表明用户添加或者删除了拼音。混合字符对我们输入法窗口是很有用的。你用搜狗输入法打个字会发现,第一行显示的就是混合字符。所以我们通过监听这个消息,绘画输入法窗口中用户输入的拼音。
      • ImmGetCompositionStringW()
        此API比较强大,通过传入一些Flag,可以获取输入法的混合字符,以及混合之后的生成字符等。API在MSDN中讲的比较清楚,而且后面我会附带源码,可以参考一下我是如何使用的(^-^)。
    2. GCS_RESULTSTR
      此消息是用户通过输入法生成了某些字符,这些字符需要输入到对应的编辑框中。另外再说一点,CEGUI支持中文显示之后,并不能通过输入法输入中文字符,这是因为CEGUI没有监听这个消息。所以在这个消息中,我们将把IME生成中文字符注入到CEGUI中( CEGUI::System::getSingleton().injectChar() )。获取生成字符也是通过ImmGetCompositionStringW()获取。


  3. WM_IME_NOTIFY

    此消息包括IME的各种通知消息,如候选词列表改变,打开关闭,圆角半角切换等。暂时,这里面我们比较关心的是候选词列表的打开,改变,关闭消息。候选词列表我们应该都知道,当我们输入 ni 的时候搜狗输入法会给出5个候选词。我们需要从中选择一个。候选词列表也是输入法窗口渲染中最重要的模块。其中用到的API是:
    • ImmGetCandidateListW()
      相关信息查阅MSDN,参考一下我的用法可能会理解的快一点。其中最主要是一个以char [1]数组结尾结构体,一般用于变长结构体。它将候选词的偏移量以及候选词字符串全都放在结构体最后。

IME相关的Windows只是我们大体已经了解了。现在介绍一下如果使用CEGUI实现这个教学班的输入法窗口。让我们分析一下:

  1. 首先,我们需要一个能显示多项候选词列表,以及混合字符串的控件。我们自定义控件当然可以,但是作为教学,我们尽可能的利用存在的控件。其实我觉得CEGUI::Listbox已经很合适了,我们将混合字符串放在列表框的最下层,然后将候选词列表从上到下排列。
  2. 但直接使用列表框,不能很好的提供便利函数,比如设置候选词列表,更改字符串,以及一些可能存在的输入法窗口状态。所以我觉得派生自列表框,服用列表框的LookNFeel,WindowRenderer。这样我们需要做的只是添加一个新的控件类。

基本上就这些了。下面是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 );
}

}

  • GetIME()静态函数是为了方便游戏中只有一个IME窗口。本想将IME控件弄成【单件模式】,但是这样之后和CEGUI的【工厂模式】不兼容。所以提供一个【单件接口】,创建和使用IME都使用这个接口。而不要使用CEGUI::WindowManager::createWindow()。
  • 重载SetLookFeel(),是为了获取列表框外框的一些尺寸,这样才能正确的设置整个列表框的大小。
  • CEGUI::Listbox的窗口不会根据候选词的宽度变化而变化,所以我们需要在设置候选词以及混合字符串的时候手动的更改窗口的大小。
  • IME控件在出现在编辑框,或者多行编辑框的左上角。具体方法是:IME控件会监听全局编辑框,多行编辑框激活,以及取消激活的消息,然后根据当前激活的编辑框设置IME窗口的位置。
  • 我们只另外提供两个公用接口。所以IME控件使用起来非常简单。
        void SetCandidateStringList( const std::vector< String >& vctCandidateStringList );
        void SetCompositionString( const String& strCompositionString );
  • 当然控件还需要在CEGUI控件工厂中注册。
    在 CEGUISystem.cpp 文件的 void System::addStandardWindowFactories() 函数内添加如下代码,并添加头文件包含:
    WindowFactoryManager::addFactory< TplWindowFactory<IME> >();

    如何添加自定义控件,可以参考我的CEGUI添加自定义控件

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;

  • 其中ImmGetContext()是获取输入法上下文句柄(即输入法相关各种信息的句柄),通过这个句柄才能拿到输入法各种信息。
  • 其中ImmReleaseContext()是释放上下文句柄。很多句柄都是要获取并释放(估计是将使用此句柄所做的更改进行保存之类的)。
  • 其他的API最好也在MSDN上查阅一下(英文阅读能让你拥有更多的知识来源),明白这些API,结合之前的讲解,应该能明白这些代码。


这样之后我们程序中已经可以使用的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窗口基本完成。下面是我的使用截图:

CEGUI 输入法窗口实现_第1张图片

本案例资源代码下载地址:CEGUI-0.7.4_输入法案例相关代码及资源

如何疑问,敬请咨询。

你可能感兴趣的:(CEGUI 输入法窗口实现)