在UI上的InputField中, 中文输入法的备选框不会跟随在光标旁边, 造成输入不方便.
看到有一个相似的, 可是是WebGL的 : https://blog.csdn.net/Rowley123456/article/details/103726927/
它通过添加Html的Input控件的方式来修改备选框位置, 直接跟平台相关了, 不具有泛用性.
按照这个思路, 直接找Windows的输入控制模块:
[DllImport"imm32.dll")] public static extern IntPtr ImmGetContextIntPtr hWnd); [DllImport"imm32.dll")] public static extern int ImmReleaseContextIntPtr hWnd, IntPtr hIMC); [DllImport"imm32.dll")] public static extern bool ImmSetCompositionWindowIntPtr hIMC, ref COMPOSITIONFORM lpCompForm); [System.Runtime.InteropServices.DllImport"user32.dll")] private static extern System.IntPtr GetActiveWindow);
然后获取窗口句柄, 设置位置的返回都是正确的, 可是结果并没有改变备选框位置:
void SetInputPos) { IntPtr hImc = ImmGetContextGetWindowHandle)); COMPOSITIONFORM cf = new COMPOSITIONFORM); cf.dwStyle = 2; cf.ptCurrentPos.X = 500; cf.ptCurrentPos.Y = 500; bool setcom = ImmSetCompositionWindowhImc, ref cf); // setcom == true ImmReleaseContextGetWindowHandle), 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:
[RequireComponenttypeofInputField))] public class IME_InputFollower : BaseInput { public InputField inputField; public override Vector2 compositionCursorPos { get { return base.compositionCursorPos; } set { base.compositionCursorPos = new Vector2200,200); // test } } private static void SetCurrentInputFollowerIME_InputFollower target) { var inputModule = EventSystem.current.currentInputModule; ifinputModule) { var field = inputModule.GetType).GetField"m_InputOverride", BindingFlags.Instance | BindingFlags.NonPublic); iffield != null) { field.SetValueinputModule, target); iftarget) { target.inputField.OnPointerDownnew PointerEventDataEventSystem.current)); int caretPosition = string.IsNullOrEmptytarget.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 Vector2Screen.width, Screen.height); Vector3[] coners = new Vector3[4]; inputField.transform as RectTransform).GetWorldCornersconers); Vector2 leftBottom = coners[0]; var compositionCursorPos = new Vector2leftBottom.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 { [RequireComponenttypeofInputField))] 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; SetCurrentInputFollowervalue); } } } public InputField inputField; public Vector2 imeOffset = new Vector2-20f, -20f); private Common.Determinator m_determin = new Common.DeterminatorCommon.Determinator.Logic.All, false); public override Vector2 compositionCursorPos { get { return base.compositionCursorPos; } set { #if UNITY_STANDALONE var size = new Vector2Screen.width, Screen.height); Vector3[] coners = new Vector3[4]; inputField.transform as RectTransform).GetWorldCornersconers); Vector2 leftBottom = coners[0]; Vector2 leftBottomOffset = leftBottom + imeOffset; var compositionCursorPos = new Vector2leftBottomOffset.x, size.y - leftBottomOffset.y); base.compositionCursorPos = compositionCursorPos; #else base.compositionCursorPos = value; #endif } } protected override void Awake) { base.Awake); ifinputField == 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) { ifthis == activeFollower) { activeFollower = null; } } private static void SetCurrentInputFollowerIME_InputFollower target) { var inputModule = EventSystem.current.currentInputModule; ifinputModule) { var field = inputModule.GetType).GetField"m_InputOverride", BindingFlags.Instance | BindingFlags.NonPublic); iffield != null) { field.SetValueinputModule, target); iftarget) { target.inputField.OnPointerDownnew PointerEventDataEventSystem.current)); int caretPosition = string.IsNullOrEmptytarget.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) { ApplyChangednewResult); } return newResult; } set { ifvalue != _lastResult) { ApplyChangedvalue); } } } public string FailedReason { get; private set; } public string SuccessedReason { get; private set; } public DeterminatorLogic logic, bool defaultVal) { this.logic = logic; _defaultValue = defaultVal; _lastResult = _defaultValue; } public void AddDeterminestring name, System.Func<bool> func) { m_determines[name] = func; } public void DeleteDeterminestring name) { m_determines.Removename); } public bool GetResult) { ifm_determines.Count > 0) { switchlogic) { case Logic.All: { foreachvar func in m_determines) { iffunc.Value.Invoke) == false) { FailedReason = func.Key; return false; } } FailedReason = null; return true; } break; case Logic.One: { foreachvar func in m_determines) { iffunc.Value.Invoke)) { SuccessedReason = func.Key; return true; } } SuccessedReason = null; return false; } break; default: return _defaultValue; } } else { return _defaultValue; } } private void ApplyChangedbool newResult) { var tempLast = _lastResult; _lastResult = newResult; ifchanged != null) { changed.InvoketempLast, newResult); } } public bool Tick) { return Result; } } }