游戏中经常要输入汉字,但当我们游戏没有自己实现输入法窗口时,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控件工厂中注册。可以参考我的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. c++实现输入法窗口自定义的代码

    #pragma once #include <Windows.h> #include <imm.h> #include <string> #pragma comme ...

  2. Android窗口管理服务WindowManagerService对输入法窗口(Input Method Window)的管理分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8526644 在Android系统中,输入法窗口 ...

  3. Android 浮窗开发之窗口层级

    很多人都知道如何去实现一个简单的浮窗,但是却很少有人去深入的研究背后的流程机制,由于项目中浮窗交互比较复杂,遇到了些坑查看了很多资料,故总结浮窗涉及到的知识点: 窗口层级关系(浮窗是如何"浮 ...

  4. Android应用程序窗口(Activity)与WindowManagerService服务的连接过程分析

    在前两文中,我们分析了Activity组件的窗口对象和视图对象的创建过程.Activity组件在其窗口对象和视图对象创建完成之后,就会请求与WindowManagerService建立一个连接,即请求 ...

  5. 图解Android - Android GUI 系统 (2) - 窗口管理 (View, Canvas, Window Manager)

    Android 的窗口管理系统 (View, Canvas, WindowManager) 在图解Android - Zygote 和 System Server 启动分析一 文里,我们已经知道And ...

  6. Android源码剖析之Framework层进阶版(Wms窗口管理)

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 上一篇我们主要讲了Ams,篇幅有限,本篇再讲讲Wms,即WindowManagerService,管 ...

  7. WindowManager和WindowManager.LayoutParams的使用以及实现悬浮窗口的方法

    写Android程序的时候一般用WindowManager就是去获得屏幕的宽和高,来布局一些小的东西.基本上没有怎么看他的其他的接口. 这两天想写一个简单的类似于Toast的东西,自定义布局,突然发现 ...

  8. Android EditText 不弹出输入法

    当第一次进入一个activity的时候  一般是第一个edittext是默认选中的,但是该死的软键盘也一起弹出来了 那是相当的不美观哈!(#‵′)凸.为此, 本大人就去寻找在刚进入这个activity ...

  9. Android InputMethodManager输入法简介

    正文 一.结构 public final class InputMethodManager extends Object Java.lang.Object android.view.inputmeth ...

随机推荐

  1. Qt中addStretch的有趣应用

    今天在使用addStretch,布局的时候,发现addStretch竟然是可以平均分配的,有意思.比如: QVBoxLayout *buttonLayout = new QVBoxLayout; bu ...

  2. 【操作系统】:Main features of the X86-64

    The combination of the new hardware supplied by Intel and AMD ,and the new versions of GCC targeting ...

  3. ios7下二维码功能的实现

    苹果公司升级到IOS7后自己的PassBook自带二维码扫描功能,所以现在使用二维码功能不需要在借助第三方库了 使用前请先导入AVFoundation.frameWork // //  YHQView ...

  4. 跟我一起学extjs5(25--模块Form的自己定义的设计[3])

    跟我一起学extjs5(25--模块Form的自己定义的设计[3])         自己定义的Form已经能够执行了,以下改一下配置,把Form里面的FieldSet放在Tab之下.改动一下Modu ...

  5. Web API 设计摘要

    近期读了一本微电子书 Brian Mulloy 所著<Web API Design>感觉颇多收获,特对其内容做了个整理摘要以便回想其观点精华以指导日常工作中的设计思路. 本文主要讲述 We ...

  6. 随机生成A~Z的字母CharDemo

  7. android 实现蓝牙自动配对连接

    BluetoothConnectActivityReceiver.java:监听蓝牙配对的广播 代码: package com.imte.Broadcast; import com.imte.util ...

  8. 【linux】 linux gpio操作

    欢迎转载,转载时需保留作者信息,谢谢. 邮箱:tangzhongp@163.com 博客园地址:http://www.cnblogs.com/embedded-tzp Csdn博客地址:http:// ...

  9. java-finalize

    finalize()来自Object的protected 方法,同clone()需要用户具体实现.一.源码中的介绍1.    Called by the garbage collector on an ...

  10. (Problem 14)Longest Collatz sequence

    The following iterative sequence is defined for the set of positive integers: n n/2 (n is even) n 3n ...