Qt 输入法浅谈

什么是输入法

为了回答这个问题,让我们看看维基百科是怎么说的:

注意:输入法编辑器(英语:input method editor,缩写为IME),是指键盘击键或鼠标移动翻译成字符来输入的操作系统组件。

因此,输入法允许您将中文、日语、韩语或印度字符输入到应用程序的文本输入字段中,即使计算机仅连接了拉丁键盘。 这是通过分析输入为拉丁语的文本来完成的,例如打开一个弹出菜单,其中包含与该拉丁语输入相关的预先选择的中文字符。用户现在可以选择其中一个中文字符,然后替换文本字段中的拉丁语输入。

由于输入法支持现在是UI工具包的重要要求,因此我们最喜欢的一个(又名Qt)也提供了它也就不足为奇了。但由于Qt是跨平台的,它不仅支持单一的输入法,还支持各种输入法,具体取决于目标平台。

在这篇博文中,我们将仔细研究它是如何实现的,涉及哪些类以及它可以在哪里扩展或调整。

Qt 中的输入法

为了概述Qt中的输入法处理,让我们仔细看看 首先涉及的组件:

QPA plugin:从4.8版本开始,Qt包含一个抽象层(Qt Platform Abstraction),以简化Qt的UI部分移植到新的窗口系统。该层由一组抽象接口类 (QPlatform*) 组成,这些抽象接口类为目标窗口系统重新实现,并捆绑为可在运行时加载的插件。中央接口是 QPlatformIntegration,它在启动时由 QGuiApplication 从插件实例化,并提供对所有其他QPA接口的访问。

QPlatformInputContext:QPlatformInputContext 是用于不同窗口系统上可用的各种输入法系统的抽象接口。对于每个受支持的系统,都有一个 QPlatformInputContext 的子类,它实现与实际输入法后端的通信(例如 IBusQIBusPlatformInputContext)。这些子类可以作为独立的插件提供,可以在运行时加载(Qt 5.4附带了其中两个,“ibus”和“compose”),或者编译到QPA插件中(例如QNx的 QQnxInputContext 和 Windows 的 QWindowsInputContext)。QPlatformInputContext是Qt私有API的一部分,因此作为应用程序开发人员,您不会直接访问它,而是使用公共类 QInputMethod 的全局实例。此全局实例由 QGuiApplication::inputMethod() 返回,它只是将对其方法的调用转发到全局 QPlatformInputContext 实例。后者由加载的 QPA 插件提供(参见 QPlatformIntegration::inputContext()),它返回上述子类之一。

QGuiApplication:QGui应用程序继承了QCoreApplication,因此包含应用程序的主事件循环,负责转发传入的事件 从本机窗口系统到应用程序中的适当对象。虽然QCoreApplication只知道与UI无关的事件,但QGuiApplication有 有关 UI 相关状态的知识,例如哪个小组件具有活动键盘焦点,什么 当前的输入法是等。

Text input widgets:文本输入小部件(例如 QLineEditQTextEdit)的目的是以文本表示形式可视化用户的键盘输入。由于来自本机窗口系统的关键事件不能始终一对一地映射到输出字符,因此文本输入小部件需要输入法系统的支持才能进行转换。

深入剖析

虽然我们现在知道游戏中的主要参与者和他们的角色,但在下一步中,我们将看到他们如何相互交互。为此,我们在整个系统中跟随键盘上的击键,直到它在QLineEdit中显示为字符。

在我们的应用程序中,我们需要的第一件事是一个对键盘输入敏感的输入小部件。这可以通过实例化QLineEdit轻松完成。如果用户现在想要输入中文字符,他的第一个操作是通过用鼠标指针单击它或通过选项卡链导航到它来为这个 QLineEdit 提供输入焦点。一旦QLineEdit收到焦点,QGuiApplication中的一个私有插槽就会被称为(_q_updateFocusObject),它执行以下步骤:

  1. 检查 QPlatformIntegration 是否提供 QPlatformInputContext 对象
  2. 如果是,请检查具有输入焦点的对象是要使用输入法系统还是自行处理键事件
  3. 通知 QPlatformInputContext 有关新焦点对象的信息

第一次检查很容易完成,因为 QGuiApplication 本身已经加载了 QPA 插件,因此它可以访问 QPlatformIntegration 实例,并且可以简单地调用 QPlatformIntegration::inputContext() 方法来检查是否返回了有效的 QPlatformInputContext 对象。

第二个检查更高级一些。为了将QGuiApplication 与 QWidget 接口分离(例如,还支持 QQuickItem 的焦点处理),它不能只调用焦点对象上的方法来查询其属性,因为这需要QGuiApplication知道他们的API。相反,QGuiApplication 将同步事件(通过 QGuiApplication::sendEvent(QEvent*) 的阻塞调用)发送到焦点对象。然后焦点对象用请求的信息填充事件对象,当阻塞调用返回时,QGui应用程序从事件中提取信息。简而言之:同步事件发送用于将QGuiApplication与焦点对象的公共接口解耦。

那么活动是什么样的,可以查询哪些信息呢?QInputMethodQueryEvent 是用于查询信息的实际事件,它允许 QGuiApplication 从焦点对象查询信息,例如:

  • 它是否接受输入法输入(Qt::ImEnabled)
  • 当前输入区域周围的文本(Qt::ImSurroundingText)
  • 首选输入语言(Qt::ImPreferredLanguage) 还有很多很多(参见 Qt::InputMethodQuery)。

在我们的例子中,QGuiApplication 向 QLineEdit 发送一个 QInputMethodQueryEvent 并请求 Qt::ImEnabled 标志。QLineEdit 在重新实现的 QWidget::inputMethodQuery() 方法中响应该事件,方法是检查是否设置了 Qt::WA_InputMethodEnabled widget 属性。此属性需要由任何想要使用输入法系统的小部件设置,并且默认情况下在Qt中的文本输入类(QLineEdit,QTextEdit等)上设置。

_q_updateFocusObject() 执行的最后一个步骤是通知 QPlatformInputContext 对象有关新焦点对象的信息,以便它可以在以后输入过程中根据需要从焦点对象查询更多信息。这可以通过调用 QPlatformInputContext::setFocusObject(QObject *object) 来完成。

现在 QLineEdit 具有键盘焦点,用户可以按键盘上的键,这将触发操作系统中的输入事件,该事件将被转发到窗口系统,并从那里调用当前加载的 QPA 插件中的某个函数。此时,QPA 插件会将本机密钥事件转换为 一个 QKeyEvent,并通过调用 QWindowSystemInterface::handleKeyEvent()QWindowSystemInterface::handleExtendedKeyEvent() 将其注入应用程序事件队列。但是,如果输入法系统处于活动状态(在这种情况下(QPlatformIntegration::inputContext() 返回有效的 QPlatformInputContext 对象),它将把原始输入数据发送到 QPlatformInputContext 对象。在这种情况下,如何将原始输入数据发送到 QPlatformInputContext 不是由任何 API 定义的,因此每个 QPA 实现都可以自由选择如何执行此操作。例如,XCB QPA插件期望QPlatformInputContext提供一个公共插槽'x11FilterEvent',它使用 QMetaObject::invokeMethod() 调用该插槽,并将xcb_keysym*数据作为参数传递。这种动态调用允许我们在 XCB 系统上使用不同的 QPlatformInputContext 实现,而无需让 XCB QPA 插件知道确切的类接口。另一方面,QNX QPA 插件编译了 QQnxInputContext,因此它有一个指向该实例的指针,只需调用一个方法将原始输入数据转发给它。

QPlatformInputContext 子类现在已经获得了原始输入数据,可以将它们转发到实际平台特定的输入法后端(例如,通过 DBus 发送到 IBus 服务器)。此时,输入法后端可能需要其他信息来处理当前的原始输入数据。示例信息可以是:

  • 当前输入区域周围的文本,用于解释上下文中的新输入数据
  • 光标的位置,用于打开它旁边的字符选择弹出菜单

为了查询此信息,QPlatformInputContext 再次将同步 QInputMethodQueryEvent 发送到当前焦点对象,就像 QGuiApplication 之前所做的那样。一旦它从焦点对象检索此信息,并将其转发到输入法后端,后端将撰写一个最终字符(或字符序列),该字符应设置为 QLineEdit 上的新文本。撰写要么以编程方式完成(通过遵循某些书写系统特定的规则),要么输入法系统打开一个弹出菜单,其中包含预先选择的可能字符,用户从中选择适当的字符。

那么,撰写的文本如何返回QLineEdit呢?为此,将完成另一个同步事件调用。QPlatformInputContext 创建 QInputMethodEvent 的实例,并在其上调用 setCommitString(),并将新组合的文本作为参数。之后,它将事件发送到焦点对象。

在焦点对象上,在我们的例子中是QLineEdit,重新实现的方法 QWidget::inputMethodEvent(QInputMethodEvent*) 被调用。该方法将使用组合文本更新 QLineEdit 的当前文本,重新定位文本光标,并可能更新当前选择。

此时,按键事件已到达其目标,用户将按键盘上的下一个键。

除了提交字符串之外,QPlatformInputContext 还可以使用 预编辑字符串 创建 QInputMethodEvent,并在撰写开始之前或进行撰写时将其发送到焦点对象。然后,该预编辑字符串作为中间结果显示在 QLineEdit 中。该文本的视觉外观可以通过在 QInputMethodEvent 上设置某些 属性 来影响。

在下一篇博文中,我们将学习如何实现进程外虚拟键盘,该键盘使用Qt的输入法框架与基于Qt的应用程序进行通信。敬请期待!