Qt使用QQuickWidget的输入法问题(Qt5.12及以前)

Qt使用QQuickWidget的输入法问题(Qt5.12及以前)

最近有网友表示,在使用QQuickWidget嵌入到QWidget时,QML内部的输入法会有问题。

主要表现是,当焦点从QWidget(比如QLineEdit)切换到QQuickWidget内(比如TextInput),不能切换输入法;而当切换到其他应用程序,在切换回Qt程序时,输入法状态可正常切换。

我个人只有 5.6.3、5.12.2、5.15.2 三个版本环境,经过测试,5.15.2 已经修复了这个bug。而 Qt5.6 到 5.12,一些接口和机制有变化,所以解决方案不一样。本文主要讨论一下这个bug形成的具体原因。

Qt的焦点与输入法

Qt窗口内的子控件,有的可以唤起输入法(比如QLineEdit),有的不能(比如QPushButton)。当焦点从一个QWidget切换到另一个QWidget,Qt通过会向焦点控件发送QInputMethodQueryEvent事件,将系统感兴趣的信息返回给系统。

而QQuickWidget内部的不同元素,有自己的焦点,有不同的输入法状态。

当混合使用QWidget与QQuickWidget(继承自QWidget)时,对于上层QApplication而言,只会感知到QWidget的焦点变化,QML内元素的输入法状态会经过QQuickWidget这一层来返回给系统。

Bug原因

所以bug本质原因是,鼠标点击QML内输入框时,QQuickWidget会获得焦点,随即触发QInputMethodQueryEvent事件,QQuickWidget处理该事件。而此时,QML的焦点状态不对,或者Qt内部Bug,导致没有正确将事件发送给焦点QQuickItem。

而当窗口由非激活切换为激活状态,Qt内部能正确将事件发送给焦点对象,输入法正常。

解决办法

不同Qt版本可能机制和接口有变化,建议通过相关事件、信号、焦点状态来找到合适的解决办法。

下面时5.6.3和5.12.2版本的解决方案:

  • Qt.5.6.3

    该版本里,QQuickWidget收到输入法事件时,将事件发送给了QQuickWindow,QQuickWindow虽然保存了当前QML的焦点元素,但实际源码什么都没做。

    Qt 源码:

    在这里插入图片描述

    所以,可以对QQuickWidget注册事件过滤器或者重写event方法,通过QQuickWidget::quickWindow拿到关联的QQuickWindow,再由QQuickWindow::activeFocusItem获取到焦点元素,将事件发送给它。

    bool Widget::eventFilter(QObject *watched, QEvent *event)
    {
        if(watched == quickWidget && event->type() == QEvent::InputMethodQuery)
        {
        	// 重新发送该消息。
            QApplication::sendEvent(quickWidget->quickWindow()->focusObject(), event);
            return true;
        }
        return false;
    }
    

    (Qt 5.6.3的QQuickWindow会始终保存焦点元素,即便焦点切换到外部,内部仍然保持了自己的焦点。)

  • Qt.5.12.2

    该版本里,Qt的逻辑有了变化,QQuickWidget 处理了输入法事件,向当前的焦点元素发送事件,但此时焦点元素不准确,实际焦点有些滞后。

    Qt源码:
    在这里插入图片描述

    但相比Qt5.6.3,QML的焦点能准确变换。

    所以,可以绑定QQuickWindow::focusObjectChanged信号,当QML的焦点变化时,更新输入法。

    connect(quickWidget->quickWindow(), &QQuickWindow::focusObjectChanged, this, [this](QObject *){
    	// 限定一下,判断当前焦点
        if(QApplication::focusWidget() == quickWidget)
            QGuiApplication::inputMethod()->update(Qt::ImQueryAll);
    });
    

    ( Qt 5.6.3里,QQuickWiget失去焦点并不会导致QQuickItem失去焦点,内部总是维持了焦点,所以再切回时,不会触发该信号…)

总结

输入法状态异常的解决办法,实际就是需要让正确的焦点对象处理 QInputMethodQueryEvent。如果上述两个解决办法不能解决其他版本问题,就需要看源码,从下面几个问题入手:

  • QQuickWidget是怎么处理输入法事件
  • 焦点切换时,QApplication::focusWidget、QQuiApplication::focusObject是什么
  • QQuickWindow::focusObjectChanged有没有正确触发
  • 窗口非激活到激活状态,是怎么触发输入法事件,焦点是谁