在UI上的InputField中, 中文输入法的备选框不会跟随在光标旁边, 造成输入不方便.
看到有一个相似的, 可是是WebGL的 : https://blog.csdn.net/Rowley123456/article/details/103726927/
它通过添加Html的Input控件的方式来修改备选框位置, 直接跟平台相关了, 不具有泛用性.
按照这个思路, 直接找Windows的输入控制模块:
[DllImport("imm32.dll")] public static extern IntPtr ImmGetContext(IntPtr hWnd); [DllImport("imm32.dll")] public static extern int ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); [DllImport("imm32.dll")] public static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM lpCompForm); [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern System.IntPtr GetActiveWindow();
然后获取窗口句柄, 设置位置的返回都是正确的, 可是结果并没有改变备选框位置:
void SetInputPos() { IntPtr hImc = ImmGetContext(GetWindowHandle()); COMPOSITIONFORM cf = new COMPOSITIONFORM(); cf.dwStyle = 2; cf.ptCurrentPos.X = 500; cf.ptCurrentPos.Y = 500; bool setcom = ImmSetCompositionWindow(hImc, ref cf); // setcom == true ImmReleaseContext(GetWindowHandle(), hImc); }// 结构体略
这就比较尴尬了, 设置没有反应没有报错......
考虑到Unity应该有各个平台的底层接口的, 以实现标准化的输入(IME接口), 所以在BaseInputModule里面去找一找, 发现它下面有个BaseInput组件:
//StandaloneInputModule : PointerInputModule //PointerInputModule : BaseInputModule public abstract class BaseInputModule : UIBehaviour { protected BaseInput m_InputOverride; // // 摘要: // The current BaseInput being used by the input module. public BaseInput input { get; } ...... }
这个跟输入貌似有关系, 看到里面的变量跟Windows的API有点像:
public class BaseInput : UIBehaviour { public BaseInput(); // // 摘要: // Interface to Input.imeCompositionMode. Can be overridden to provide custom input // instead of using the Input class. public virtual IMECompositionMode imeCompositionMode { get; set; } // // 摘要: // Interface to Input.compositionCursorPos. Can be overridden to provide custom // input instead of using the Input class. public virtual Vector2 compositionCursorPos { get; set; } ...... }
估计只要继承它自己设置compositionCursorPos就能达到效果了, 直接创建一个继承类型, 然后通过反射的方式给StandaloneInputModule设定BaseInput:
[RequireComponent(typeof(InputField))] public class IME_InputFollower : BaseInput { public InputField inputField; public override Vector2 compositionCursorPos { get { return base.compositionCursorPos; } set { base.compositionCursorPos = new Vector2(200,200); // test } } private static void SetCurrentInputFollower(IME_InputFollower target) { var inputModule = EventSystem.current.currentInputModule; if(inputModule) { var field = inputModule.GetType().GetField("m_InputOverride", BindingFlags.Instance | BindingFlags.NonPublic); if(field != null) { field.SetValue(inputModule, target); if(target) { target.inputField.OnPointerDown(new PointerEventData(EventSystem.current)); int caretPosition = string.IsNullOrEmpty(target.inputField.text) == false ? target.inputField.text.Length : 0; target.inputField.caretPosition = caretPosition; } } } } }
当InputField被focus的时候, SetCurrentInputFollower使用反射的方式设定BaseInput到当前的InputModule中, 然后手动触发一下OnPointerDown和设定光标位置, 这样就能刷新输入法备选框了, 不会因为切换InputField而窗口不跟随. 还有就是在编辑器下窗口的大小为Game窗口的大小, 而不是渲染部分的大小, 所以在编辑器下窗口大小与渲染不同的时候计算位置是不对的.
PS : 在测试时发现在Windows下compositionCursorPos的计算方法是窗口坐标, 并且起始坐标为窗口坐上角(0, 0), 不知道是不是DX平台的特点.
填满窗口看看原始的输入法备选框在哪:
已经超出界面范围了, 现在添加IME_InputFollower组件, 来计算一下位置让备选框出现在输入框的左下角:
public override Vector2 compositionCursorPos { get { return base.compositionCursorPos; } set { #if UNITY_STANDALONE var size = new Vector2(Screen.width, Screen.height); Vector3[] coners = new Vector3[4]; (inputField.transform as RectTransform).GetWorldCorners(coners); Vector2 leftBottom = coners[0]; var compositionCursorPos = new Vector2(leftBottom.x, size.y - leftBottom.y); base.compositionCursorPos = compositionCursorPos; #else base.compositionCursorPos = value; #endif } }
证明确实可行, 这样这个逻辑应该就是可以在全部平台中跑了, 只要添加compositionCursorPos的set逻辑就行了, 而平台的差异只要在计算坐标中注意即可(不过除了Windows也没其他需要的平台了).
全部代码贴一下:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Reflection; namespace UIModules.UITools { [RequireComponent(typeof(InputField))] public class IME_InputFollower : BaseInput { private static IME_InputFollower _activeFollower = null; private static IME_InputFollower activeFollower { get { return _activeFollower; } set { if(_activeFollower != value) { _activeFollower = value; SetCurrentInputFollower(value); } } } public InputField inputField; public Vector2 imeOffset = new Vector2(-20f, -20f); private Common.Determinator m_determin = new Common.Determinator(Common.Determinator.Logic.All, false); public override Vector2 compositionCursorPos { get { return base.compositionCursorPos; } set { #if UNITY_STANDALONE var size = new Vector2(Screen.width, Screen.height); Vector3[] coners = new Vector3[4]; (inputField.transform as RectTransform).GetWorldCorners(coners); Vector2 leftBottom = coners[0]; Vector2 leftBottomOffset = leftBottom + imeOffset; var compositionCursorPos = new Vector2(leftBottomOffset.x, size.y - leftBottomOffset.y); base.compositionCursorPos = compositionCursorPos; #else base.compositionCursorPos = value; #endif } } protected override void Awake() { base.Awake(); if(inputField == false) { inputField = GetComponent<InputField>(); } m_determin.AddDetermine("Selected", () => { return inputField && inputField.isFocused; }); m_determin.changed += (_from, _to) => { if(_to) { activeFollower = this; } else { CancelSelection(); } }; } protected override void OnDisable() { base.OnDisable(); CancelSelection(); } void Update() { m_determin.Tick(); } private void CancelSelection() { if(this == activeFollower) { activeFollower = null; } } private static void SetCurrentInputFollower(IME_InputFollower target) { var inputModule = EventSystem.current.currentInputModule; if(inputModule) { var field = inputModule.GetType().GetField("m_InputOverride", BindingFlags.Instance | BindingFlags.NonPublic); if(field != null) { field.SetValue(inputModule, target); if(target) { target.inputField.OnPointerDown(new PointerEventData(EventSystem.current)); int caretPosition = string.IsNullOrEmpty(target.inputField.text) == false ? target.inputField.text.Length : 0; target.inputField.caretPosition = caretPosition; } } } } } }
Determinator 就是一个简单决策器:
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Common { public class Determinator { public enum Logic { All, One, } private bool _defaultValue; private bool _lastResult; public Logic logic { get; private set; } private Dictionary<string, System.Func<bool>> m_determines = new Dictionary<string, System.Func<bool>>(); public System.Action<bool, bool> changed = null; public bool Result { get { var newResult = GetResult(); if(_lastResult != newResult) { ApplyChanged(newResult); } return newResult; } set { if(value != _lastResult) { ApplyChanged(value); } } } public string FailedReason { get; private set; } public string SuccessedReason { get; private set; } public Determinator(Logic logic, bool defaultVal) { this.logic = logic; _defaultValue = defaultVal; _lastResult = _defaultValue; } public void AddDetermine(string name, System.Func<bool> func) { m_determines[name] = func; } public void DeleteDetermine(string name) { m_determines.Remove(name); } public bool GetResult() { if(m_determines.Count > 0) { switch(logic) { case Logic.All: { foreach(var func in m_determines) { if(func.Value.Invoke() == false) { FailedReason = func.Key; return false; } } FailedReason = null; return true; } break; case Logic.One: { foreach(var func in m_determines) { if(func.Value.Invoke()) { SuccessedReason = func.Key; return true; } } SuccessedReason = null; return false; } break; default: return _defaultValue; } } else { return _defaultValue; } } private void ApplyChanged(bool newResult) { var tempLast = _lastResult; _lastResult = newResult; if(changed != null) { changed.Invoke(tempLast, newResult); } } public bool Tick() { return Result; } } }