Unity輸入法相關(IME)


  在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;
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM