欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

WPF在自定义文本框中实现输入法跟随光标

程序员文章站 2024-04-01 08:59:16
本文告诉大家在 wpf 写一个自定义的文本框,如何实现让输入法跟随光标本文非小白向,本文适合想开发自定义的文本框,从底层开始开发的文本库的伙伴。在开始之前,期望了解了文本库开发的基础知识本文实现的效果...

本文告诉大家在 wpf 写一个自定义的文本框,如何实现让输入法跟随光标

本文非小白向,本文适合想开发自定义的文本框,从底层开始开发的文本库的伙伴。在开始之前,期望了解了文本库开发的基础知识

本文实现的效果如下

WPF在自定义文本框中实现输入法跟随光标

实现

本文的方法参考了 wpf 官方仓库的逻辑,可以在wpf仓库的wpf\src\microsoft.dotnet.wpf\src\presentationframework\system\windows\documents\immcomposition.cs文件看到官方是如何让textbox控件获取输入法焦点,和在输入光标变更时,修改输入法的输入框坐标

先了解一下输入法的相关知识。在 windows 编程开发里,输入法框架有三套,其中用的最多的是第二套。第二套是采用 imm 进行对接的。所谓 imm 就是 input method manager 也就是 输入法管理器

相关的另一个缩写词 ime 则是 input method editor 或者是 input method engine 的缩写,含义是输入法编辑器或输入法引擎

应用程序可以通过 imm 对接输入法。所用的 win32 的 api 重点是如下几个

  • immgetcontext 获取输入法上下文,用于后续所有的其他函数调用
  • immassociatecontext 关联输入法和对应的窗口,让输入法了解在哪个窗口输入
  • immsetcompositionwindow 用来设置输入法的窗口的坐标,也是本文最重要的函数

本文接下来将告诉大家如何一步步实现封装对 ime 输入法调用,在本文最后将会给出所有的源代码

这部分对输入法的逻辑可以封装为一个类,这样上层就可以不关注细节逻辑。如例子代码,放在 imesupporter 类型里

为了方便文本框的接入,咱再定义一个接口,用于设置文本框需要实现一些方法,用来提供参数给 imesupporter 使用才能进行接入

对于如微软拼音等输入法,是支持设置输入法的文本大小和字体。因此就需要文本框提供 getfontfamilyname 和 getfontsize 方法

而 getcaretlefttop 自然就是用来让输入法跟随的。为了让文本框可以做更多的定制,也需要 gettexteditorlefttop 方法,这个方法的返回值对大部分自定义的文本框控件来说,都应该是 0,0 点

在 imesupporter 类型构造函数,期望传入文本框控件,如此可以解决初始化值和监听的锅

为了同时约束传入的文本框控件继承 uielement 和 iimetexteditor 接口,用了泛形

在文本框控件 editor 获取焦点的时候,将需要唤起输入法进行输入。在 editor 失去焦点的时候,就应该告诉输入法当前不进行输入

根据 wpf 的约定,对自定义的支持输入法的控件,需要设置 isinputmethodsuspendedproperty 附加属性,如下面代码

在 editor_gotkeyboardfocus 需要实现的逻辑是调起输入法和设置初始的输入框的坐标。如上文,开始之前,需要先拿到输入法上下文。在拿到输入法上下文之前,可以先获取默认的 ime 类窗口句柄。先获取默认的 ime 类窗口句柄是为了在多进程嵌入窗口时,让微软拼音输入法的输入框跟随输入光标而不是在左上角

以上的 _defaultimewnd 是一个字段,在 imesupporter 里定义如下字段和属性

这里有一个细节是 immgetdefaultimewnd 也许会返回 0x00 空值。什么时候会返回空值?如打开一个 win32dialog 窗口,如 openfiledialog 或 savefiledialog 等,之后关闭,那么此时也许 immgetdefaultimewnd 将会返回空值

拿到空值,需要重新绑定输入法,告诉输入法当前的窗口获取输入焦点,可以使用如下代码,通过修改附加属性的值,通过附加属性变更调用到 wpf 框架的逻辑,从而修复此问题

除了给 immgetdefaultimewnd 传入 intptr.zero 可以获取之外,还可以传入当前的 editor 所在的 hwndsource 进行获取,这里的 hwndsource 就相当于或者说大多数时候是等于 editor 所在的窗口

如果继续获取不到,那么可以尝试使用 getforegroundwindow 获取。使用 getforegroundwindow 获取到的也许不是正确的,但是能进入此分支,也好过没有输入法

接下来通过 _defaultimewnd 获取输入法上下文,如下面代码

如果从 _defaultimewnd 拿不到,则使用 _hwndsource.handle 获取

获取上下文之后,将输入法上下文和当前窗口关联起来。对于只实现第二套输入法框架的输入法,应用程序调用 immassociatecontext 关联,即可调起此输入法在关联的窗口输入

输入法在输入过程中,将会通过 windows 消息和当前窗口进行通讯,如获取输入框所需的坐标和输入文本等。因此咱需要加上 hook 消息,用于告诉输入法坐标。但不需要处理输入的文本的逻辑,因为输入文本的逻辑等在 wpf 已有处理

关于 wndproc 的函数逻辑,咱放在后面

在 wpf 框架里,会对第三套输入法有进行支持,于是就需要调用 itfthreadmgr 这个 com 组件进行关联焦点,如下面代码

初始化的过程还需要给输入法的输入框一个初始化的坐标,可使用 win32 的 immsetcompositionwindow 进行设置。在进行设置之前,需要获取到文本框的输入光标相对于窗口的坐标,用于给输入法使用

下面代码从文本框获取文本框实现接口的获取光标和输入框左上角

接下来使用如下代码将坐标转换为相对于窗口的

对 surface 设备来说,需要进行更多的处理

获取到的坐标传入到 immsetcompositionwindow 方法

以上注释的 _issoftwarepinyinoverwin7 的逻辑是判断在系统版本大于 win7 的系统,如 win10 系统上,使用微软拼音输入法,微软拼音输入法在几个版本,需要修改 y 坐标,加上输入的行高才可以。但是在一些 win10 版本,通过补丁又修了这个问题

以上就完成了输入法的初始化逻辑

接下来就是需要处理 windows 消息了,如在收到 wm_inputlangchange 消息时,需要重新获取输入法上下文

以上获取输入法上下文 createcontext 方法是获取 _currentcontext 的逻辑

在收到 wm_ime_composition 消息,需要更新输入法的输入框的坐标

以上的 updatecompositionwindow 方法是调用 immsetcompositionwindow 方法设置坐标的方法

关于此 imesupporter 类型的所有代码,可以从下文获取

接下来是对接 imesupporter 和具体的文本框

先在自定义的文本框 texteditor 控件上继承 iimetexteditor 接口。为了方便调试,咱先写测试逻辑,获取的输入光标就是上次鼠标点击的点以及固定的字体字号

在 onmousedown 方法里面,需要调用 focus 获取焦点,同时更新一下模拟的光标。模拟的光标是在 onrender 方法里面,使用画出一个矩形模拟的,没有做闪烁

为了让控件能接收键盘消息,需要设置 focusableproperty 属性。为了接收 tab 键,而不是被切到其他控件,需要设置 keyboardnavigation 的 istabstopproperty 和 tabnavigationproperty 附加属性。因为这是作用在所有的自定义文本框 texteditor 控件上的,因此可以在 texteditor 的静态构造函数,进行更改默认值,代码如下

完成 texteditor 控件的配置,就可以对接 imesupporter 类,对接方法是创建即可

这样就完成了文本框让输入法跟随输入的功能

代码

本文所有代码放在github 和 gitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源

获取代码之后,打开 lighttexteditorplus.sln 文件

到此这篇关于wpf在自定义文本框中实现输入法跟随光标的文章就介绍到这了,更多相关wpf输入法跟随光标内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!