• 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;
            }
        }
    }
  • 相关阅读:
    UML基本关系
    C++关键字简述
    Install opencv on Centos
    C++编程规范
    YCbCr to RGB and RGB toYCbCr
    Linux目录结构(二)
    Dubbo工作流程
    spring bean的作用域和生命周期
    spring aop原理和实现
    静态代理、jdk动态代理、cglib动态代理
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/12603955.html
Copyright © 2020-2023  润新知