Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

 XNA中的中文输入(二)

仅供个人学习使用,请勿转载,勿用于任何商业用途。

 

         全屏模式下,由于GDIDirectX会发生冲突,我们需要自己渲染IME窗口。很多人都觉得渲染IME窗口是件很复杂的事情,但仔细观察一下CustomUI或者WOW中的IME窗口,其实就是一个简单的text或者label控件而已!


      只要得到IME中的字符信息,接下来就很简单了.为此,我们需要处理以下4个消息:

         第一个是WM_IME_STARTCOMPOSITION,这个消息在按下第一个字符,开始一次新的字符组合时触发,可以把类似清除字符串buffer的工作放在这里。

         接下来,WM_IME_COMPOSITION是一个很重要的消息。SDK文档里说“The IMM sends a WM_IME_COMPOSITION message to the application when the user enters a keystroke to change the composition string.lParam参数说明IME发生了什么样的变化,这里我们只需关心GCS_COMPSTR或者GCS_COMPREADSTR,对于中文来说,这两个标志所表示的内容都是一致的,表示输入字符发生了变化。可以用ImmGetCompositionString获得此时的IME输入字符。

         IME窗口发生变化时,向程序发送WM_IME_NOTIFY消息,wParam参数说明发生了什么变化。一般来说只需要关心IMN_CHANGECANDIDATE,它表示IME窗口中的候选字符发生了变化。此时需要用ImmGetCandidateList获得候选字符。

         最后,当完成输入时,处理WM_IME_ENDCOMPOSITION消息,同样使用ImmGetCompositionString获得最终生成的中文字符。

         基本的步骤就那么简单,下面是一些实现细节,主要是P/Invoke时可能遇到的问题。首先是两个函数的声明。

 [DllImport("imm32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
 
public static extern int ImmGetCompositionString(IntPtr hIMC, int CompositionStringFlag, StringBuilder buffer, int bufferLength);

 [DllImport(
"imm32.dll", CharSet = CharSet.Unicode, EntryPoint = "ImmGetCandidateList")]
 
public static extern uint ImmGetCandidateList(IntPtr hIMC, uint deIndex, IntPtr candidateList, uint dwBufLen);

 

         注意,必须显式指定CharSet属性,否则导入的总是Ansi版本的API

         先看ImmGetCompositionString函数,它的第一个参数是input context句柄,第二个参数表示我们希望取得IME中的哪种字符串。一般来说在WM_IME_COMPOSITION时,应该使用GCS_COMPSTRGCS_COMPREADSTR,在WM_IME_ENDCOMPOSITION时使用GCS_RESULTSTR。函数将把得到的字符串放在第三个参数中。最后则是字符串长度,如果这个值为0,则返回字符串的长度,因此你可能先查询字符串长度,然后在取字符串。

         ImmGetCandidateListImmGetCompositionString的用法非常相似。这里比较有技术性的地方在于返回的CandidateList参数。首先需要为这个结构做以下声明:

CandidateList
[StructLayoutAttribute(LayoutKind.Sequential)]
public struct CandidateList
{
    
public uint dwSize;
    
public uint dwStyle;
    
public uint dwCount;
    
public uint dwSelection;
    
public uint dwPageStart;
    
public uint dwPageSize;
    
/// DWORD[1]
    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType =UnmanagedType.U4)]
    
public uint[] dwOffset;
}

          
        CandidateList保存了IME中的候选字符串,但仔细看CandidateList的成员,似乎没有任何一个与字符串相关。文档中的说明也只会让你更迷糊。这是一个非常特殊的结构,以下是当有4个候选字符串时,CandidateList在内存中的布局:

 swSize|dwStyle|…….|dwPageSize|dwOffset|Offset1|Offset2|Offset3|string0|string1|string2|string3

         可以看到,所有字符串信息实际上附加在dwOffset后面,dwOffset记录了第一个字符串相对于CandidateList的偏移值,如果有n个字符串,则后面会有n-132位的值,分别表示第n个字符串相对于对CandidateList的偏移值。在这些32位的值之后,则是每个字符串实际的值。也就是说dwOffset记录了string0的偏移值,offset3记录了string3的便宜值。注意,当swSize1时,CandidateList又是另外一种布局,具体可参考sdk文档。显然,只有直接访问物理地址,才能得到每个字符串。如何在C#里访问物理地址呢?以下是解析CandidateList的代码:

if(m.Msg == WindowMessage.ImeNotify)
{
    
if(m.WParam.ToInt32() == IMN_CHANGECANDIDATE )
    {
        CandidateList candidate;
        IntPtr ptr;
        
uint size = IMM.ImmGetCandidateList(imeContext, 0, IntPtr.Zero, 0);
        
if(size > 0)
        {
            ptr 
= Marshal.AllocHGlobal((int)size);
            size 
= IMM.ImmGetCandidateList(imeContext, 0, ptr, size);
            candidate 
= (CandidateList)Marshal.PtrToStructure(ptr, typeof(CandidateList));
            
if(candidate.dwCount > 1)
            {
                
for (int i = 0; i < candidate.dwCount; i++)
                {
                    
int stringOffset = Marshal.ReadInt32(ptr, 24 + 4 * i);
                    IntPtr addr 
= (IntPtr)(ptr.ToInt32() + stringOffset);
                    
string str = Marshal.PtrToStringUni(addr );
                    Console.WriteLine(str);
                 }
            }
            
else......
            Marshal.FreeHGlobal(ptr);
        }
    }
}

 

       现在已经可以成功调用IME,并且获得IME中的字符了。下一次,我们将讨论如何在XNA的Game框架下使用IME。

 

更新12.28.09:

   在Vista和Windows 7下,ms用Text Service Framework取代了IMM,虽然大部分IMM API仍然可用,但由于TSF内部的原因,可能有一些兼容性问题。更好的方法是直接使用TSF。

 

 

posted on 2009-12-18 00:58  clayman  阅读(6167)  评论(1编辑  收藏  举报