XNA中的中文输入(二)
仅供个人学习使用,请勿转载,勿用于任何商业用途。
全屏模式下,由于GDI和DirectX会发生冲突,我们需要自己渲染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时可能遇到的问题。首先是两个函数的声明。
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_COMPSTR和GCS_COMPREADSTR,在WM_IME_ENDCOMPOSITION时使用GCS_RESULTSTR。函数将把得到的字符串放在第三个参数中。最后则是字符串长度,如果这个值为0,则返回字符串的长度,因此你可能先查询字符串长度,然后在取字符串。
ImmGetCandidateList与ImmGetCompositionString的用法非常相似。这里比较有技术性的地方在于返回的CandidateList参数。首先需要为这个结构做以下声明:
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-1个32位的值,分别表示第n个字符串相对于对CandidateList的偏移值。在这些32位的值之后,则是每个字符串实际的值。也就是说dwOffset记录了string0的偏移值,offset3记录了string3的便宜值。注意,当swSize为1时,CandidateList又是另外一种布局,具体可参考sdk文档。显然,只有直接访问物理地址,才能得到每个字符串。如何在C#里访问物理地址呢?以下是解析CandidateList的代码:
{
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。