在UI上的InputField中, 中文輸入法的備選框不會跟随在光标旁邊, 造成輸入不友善.
看到有一個相似的, 可是是WebGL的 : https://blog.csdn.net/Rowley123456/article/details/103726927/
它通過添加Html的Input控件的方式來修改備選框位置, 直接跟平台相關了, 不具有泛用性.
按照這個思路, 直接找Windows的輸入控制子產品:
[DllImport("imm32.dll")]public static externIntPtr ImmGetContext(IntPtr hWnd);
[DllImport("imm32.dll")]public static extern intImmReleaseContext(IntPtr hWnd, IntPtr hIMC);
[DllImport("imm32.dll")]public static extern bool ImmSetCompositionWindow(IntPtr hIMC, refCOMPOSITIONFORM lpCompForm);
[System.Runtime.InteropServices.DllImport("user32.dll")]private static extern System.IntPtr GetActiveWindow();
然後擷取視窗句柄, 設定位置的傳回都是正确的, 可是結果并沒有改變備選框位置:
voidSetInputPos()
{
IntPtr hImc=ImmGetContext(GetWindowHandle());
COMPOSITIONFORM cf= newCOMPOSITIONFORM();
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 classBaseInputModule : UIBehaviour
{protectedBaseInput m_InputOverride;//
//摘要://The current BaseInput being used by the input module.
public BaseInput input { get; }
......
}
這個跟輸入貌似有關系, 看到裡面的變量跟Windows的API有點像:
public classBaseInput : UIBehaviour
{publicBaseInput();//
//摘要://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 classIME_InputFollower : BaseInput
{publicInputField inputField;public overrideVector2 compositionCursorPos
{get{return base.compositionCursorPos;
}set{base.compositionCursorPos = new Vector2(200,200); //test
}
}private static voidSetCurrentInputFollower(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(newPointerEventData(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平台的特點.
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5COkVWOkhTZjZTO2IGMmdjZiljMlRDZxgzY1cTZ0ADMl9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
填滿視窗看看原始的輸入法備選框在哪:
已經超出界面範圍了, 現在添加IME_InputFollower元件, 來計算一下位置讓備選框出現在輸入框的左下角:
public overrideVector2 compositionCursorPos
{get{return base.compositionCursorPos;
}set{#if UNITY_STANDALONE
var size = newVector2(Screen.width, Screen.height);
Vector3[] coners= new Vector3[4];
(inputField.transformasRectTransform).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也沒其他需要的平台了).
全部代碼貼一下:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.UI;usingUnityEngine.EventSystems;usingSystem.Reflection;namespaceUIModules.UITools
{
[RequireComponent(typeof(InputField))]public classIME_InputFollower : BaseInput
{private static IME_InputFollower _activeFollower = null;private staticIME_InputFollower activeFollower
{get{return_activeFollower;
}set{if(_activeFollower !=value)
{
_activeFollower=value;
SetCurrentInputFollower(value);
}
}
}publicInputField inputField;public Vector2 imeOffset = new Vector2(-20f, -20f);private Common.Determinator m_determin = new Common.Determinator(Common.Determinator.Logic.All, false);public overrideVector2 compositionCursorPos
{get{return base.compositionCursorPos;
}set{#if UNITY_STANDALONE
var size = newVector2(Screen.width, Screen.height);
Vector3[] coners= new Vector3[4];
(inputField.transformasRectTransform).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 voidAwake()
{base.Awake();if(inputField == false)
{
inputField= GetComponent();
}
m_determin.AddDetermine("Selected", () => { return inputField &&inputField.isFocused; });
m_determin.changed+= (_from, _to) =>{if(_to)
{
activeFollower= this;
}else{
CancelSelection();
}
};
}protected override voidOnDisable()
{base.OnDisable();
CancelSelection();
}voidUpdate()
{
m_determin.Tick();
}private voidCancelSelection()
{if(this ==activeFollower)
{
activeFollower= null;
}
}private static voidSetCurrentInputFollower(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(newPointerEventData(EventSystem.current));int caretPosition = string.IsNullOrEmpty(target.inputField.text) == false ? target.inputField.text.Length : 0;
target.inputField.caretPosition=caretPosition;
}
}
}
}
}
}
Determinator 就是一個簡單決策器:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceCommon
{public classDeterminator
{public enumLogic
{
All,
One,
}private bool_defaultValue;private bool_lastResult;public Logic logic { get; private set; }private Dictionary> m_determines = new Dictionary>();public System.Action changed = null;public boolResult
{get{var newResult =GetResult();if(_lastResult !=newResult)
{
ApplyChanged(newResult);
}returnnewResult;
}set{if(value !=_lastResult)
{
ApplyChanged(value);
}
}
}public string FailedReason { get; private set; }public string SuccessedReason { get; private set; }public Determinator(Logic logic, booldefaultVal)
{this.logic =logic;
_defaultValue=defaultVal;
_lastResult=_defaultValue;
}public void AddDetermine(string name, System.Funcfunc)
{
m_determines[name]=func;
}public void DeleteDetermine(stringname) { m_determines.Remove(name); }public boolGetResult()
{if(m_determines.Count > 0)
{switch(logic)
{caseLogic.All:
{foreach(var func inm_determines)
{if(func.Value.Invoke() == false)
{
FailedReason=func.Key;return false;
}
}
FailedReason= null;return true;
}break;caseLogic.One:
{foreach(var func inm_determines)
{if(func.Value.Invoke())
{
SuccessedReason=func.Key;return true;
}
}
SuccessedReason= null;return false;
}break;default:return_defaultValue;
}
}else{return_defaultValue;
}
}private void ApplyChanged(boolnewResult)
{var tempLast =_lastResult;
_lastResult=newResult;if(changed != null)
{
changed.Invoke(tempLast, newResult);
}
}public boolTick()
{returnResult;
}
}
}