QT5的軟鍵盤輸入法實現

1、爲何要用輸入法的方式實現

要實現點擊一個編輯框就跳出來一個軟鍵盤方法不少,爲何要用輸入法的方式呢?輸入法的方式能夠用在任一個QT程序上,而應用程序自己不須要去關心如何去輸入,交給輸入法就能夠了。輸入法與程序是獨立的,兩個程序經過通訊的方式進行對話。就好比咱們在手機上寫個程序,歷來就沒關心過軟鍵盤怎麼去實現,只須要作應用這部分就能夠了。git

2、QT5輸入法插件的結構

QT5與QT4的輸入法框架是不同的,QT4已是過去式了,就不研究了。QT5的輸入法是經過插件的方式加載的,QT根據環境變量QT_IM_MODULE來加載不一樣的輸入法插件。輸入法插件所在目錄是QT安裝目錄/plugins/platforminputcontext,這個目錄裏能夠看到有ibus等輸入法插件,若是QT_IM_MODULE=ibus,那qt就會在插件目錄下找libibusplatforminputcontextplugin.so這個插件,插件的名字是有規範的。
不只是名字,咱們所要實現的插件類的定義也是有規範的,否則就不叫插件了,輸入法插件繼承於QPlatformInputContextPlugin,這個插件只有一個create方法,返回輸入上下文QPlatformInputContext,咱們要實現的context就繼承於QPlatFormInputContext,須要從新實現的有如下幾個函數:json

bool isValid() const Q_DECL_OVERRIDE;
    void setFocusObject(QObject *object) Q_DECL_OVERRIDE;

    void showInputPanel() Q_DECL_OVERRIDE;
    void hideInputPanel() Q_DECL_OVERRIDE;
    bool isInputPanelVisible() const Q_DECL_OVERRIDE;
    bool filterEvent(const QEvent *event) Q_DECL_OVERRIDE;

這裏就是面板的顯示隱藏,設置焦點等函數,QT是如何知道這個插件的名字呢?就是根據json文件了,json文件的內容以下:session

{
    "Keys": [ "vkim" ]
}

其中vkim就是輸入法的名字。框架

3、具體實現

1.輸入法界面
輸入法界面要在dbus上註冊一個服務和對象,這個對象就是輸入法界面,dbus上有了這個服務,應用程序才能經過dbus和輸入法界面對話。註冊服務和對象的代碼以下:
InputService類:ide

InputService::InputService(QObject *object)
{
    QDBusConnection connect = QDBusConnection::sessionBus();
    if (!connect.registerService("com.mh.input")) {
        qFatal("Unable to register at DBus");
        return;
    }
    qDebug() << "register servece" << endl;

    if (!connect.registerObject("/input/vkim", object,
        QDBusConnection::ExportAllSignals | QDBusConnection::ExportAllSlots))
    {
        qFatal("Unable to register object at DBus");
        return;
    }

    qDebug() << "register object" << endl;
}

InputService::~InputService()
{
    QDBusConnection connect = QDBusConnection::sessionBus();
    connect.unregisterObject("/input/vkim");
    qDebug() << "unregister object" << endl;

    connect.unregisterService("com.mh.input");
    qDebug() << "unregister servece" << endl;
}

主函數:函數

#include "inputservice.h"
#include <QDBusConnection>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    Dialog keyboard;
    InputService inputService(&keyboard);

    return a.exec();
}

這裏把dbus的註冊放到類裏面有一個好處就是能夠本身析構,服務和對象就能夠刪掉了。其中keyboard就是鍵盤界面,這樣就把鍵盤對象註冊到了dbus上,其中com.mh.input是服務名,/input/vkim是該服務下的路徑,另外還會生成一個接口的名字叫local.keyboard.Dialog,其中keyboard是程序的名字,Dialog是註冊對象的類型,用qt下面的qdbusviewer能夠看到註冊的對象,以及對象的槽,信號。如圖所示:
dbus上的對象
其中接口的名字也能夠本身定義,也就是把這句代碼改爲下面這樣子:測試

connect.registerObject("/input/vkim", 「com.mh.input.vkim", object,
        QDBusConnection::ExportAllSignals | QDBusConnection::ExportAllSlots)

上圖中的local.keyboard.Dialog就變成了com.mh.input.vkim,不過這樣有一個問題,這樣就會把本身及父類全部的信號和槽都導出了,結果變成了3個com.mh.input.vkim,調用槽函數的時候還沒啥問題,可是響應信號就有問題了,可能有3個同樣的接口,系統不知道該找哪一個了,就會提示沒有該信號,這裏我也沒弄明白該如何自定義接口名字。
下面就是添加一些信號和槽,只有定義成槽函數,才能被插件經過dbus調用。代碼以下:
dialog.hui

public slots:
    void showKeyboard(QPoint pt, QRect focusWidget);
    void hideKeyboard();
    bool isVisible() const;

signals:
    void commit(QString str);

dialog.cppthis

void Dialog::showKeyboard(QPoint pt, QRect focusWidget)
{
    QWidget::show();
}

void Dialog::hideKeyboard()
{
    QWidget::hide();
}

bool Dialog::isVisible() const
{
    return QWidget::isVisible();
}

這裏省略了一些代碼,代碼太多影響閱讀,commit信號是在點擊肯定或者鍵盤上按回車的時候給應用程序發送信號,把軟鍵盤上的字符發給插件,插件響應這個信號,再把字符串發送給焦點編輯框。輸入法界面的工做完成了,而後就看插件怎麼去調用這些函數。
2.插件的實現
先看下插件的建立,代碼以下:
vkimplatforminputcontextplugin.h.net

#include <qpa/qplatforminputcontextplugin_p.h>
#include "vkimplatforminputcontext.h"

class VkImPlatformInputContextPlugin : public QPlatformInputContextPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QPlatformInputContextFactoryInterface_iid FILE "vkim.json")

public:
    virtual VkImPlatformInputContext *create(const QString &key, const QStringList &paramList);
};

其中Q_PLUGIN_METADATA這些是必需要寫的,這是插件的規範,能夠在QT官網上找到,IID必需要寫成QPlatformInputContextFactoryInterface_iid這個看也在父類QPlatformInputContextPlugin的頭文件裏看到定義,FILE定義的就是前面說過的json文件。

VkImPlatformInputContext* VkImPlatformInputContextPlugin::create(const QString &key, const QStringList &paramList)
{
    if (key == QLatin1String("vkim")) {
        qDebug() << "vkim input context plugin created" << endl;
        return new VkImPlatformInputContext;
    }

    return NULL;
}

VkImPlatformInputContextPlugin繼承於QPlatformInputContextPlugin,QApplication初始化的時候,由QPlatformInputContextFactory建立,factory根據環境變量QT_IM_MODULE查找對應的插件並調用create函數建立插件,而插件返回的就是咱們的輸入法VkImPlatformInputContext,繼承於QPlatformInputContextQPlatformInputContext中的顯示隱藏此類函數都是空的,須要咱們本身去實現。代碼以下:
vkimplatforminputcontext.h

class VkImPlatformInputContext : public QPlatformInputContext
{
    Q_OBJECT
public:
    VkImPlatformInputContext();
    ~VkImPlatformInputContext();

public:
    bool isValid() const Q_DECL_OVERRIDE;
    void setFocusObject(QObject *object) Q_DECL_OVERRIDE;

    void showInputPanel() Q_DECL_OVERRIDE;
    void hideInputPanel() Q_DECL_OVERRIDE;
    bool isInputPanelVisible() const Q_DECL_OVERRIDE;
    bool filterEvent(const QEvent *event) Q_DECL_OVERRIDE;

public slots:
    void keyboardCommit(QString str);

private:
    QObject *focusObject;
    QDBusInterface *dbusInterface;
};

vkimplatforminputcontext.cpp

VkImPlatformInputContext::VkImPlatformInputContext() :
    focusObject(NULL)
{
    dbusInterface = new QDBusInterface("com.mh.input", "/input/vkim", "local.keyboard.Dialog", QDBusConnection::sessionBus(), this);
     connect(dbusInterface, SIGNAL(commit(QString)), SLOT(keyboardCommit(QString)));
}

void VkImPlatformInputContext::showInputPanel()
{
    if (dbusInterface != NULL)
    {
        QWidget *w = qobject_cast<QWidget*>(focusObject);
        QPoint pt = w->pos();
        QRect rect = w->rect();
        pt = w->mapToGlobal(QPoint(0, 0));
        int x = pt.x();
        int y = pt.y();
        dbusInterface->call("showKeyboard", pt, rect);
    }
    else
    {
        qDebug() << "interface is null" << endl;
    }
}

void VkImPlatformInputContext::hideInputPanel()
{
    if (dbusInterface != NULL)
    {
        dbusInterface->call("hideKeyboard");
    }
}

其中dbus的接口要跟輸入法界面註冊的接口一致,focusObject就是當前的焦點控件。插件經過dbus的call函數就能夠調用輸入法界面導出的槽函數了。那showInputPanel又是誰來調用的呢?這是由QGuiApplication::inputMethod()來調用的,在qlineedit.cpp文件中的mouseReleaseEvent函數中,能夠看到調用了handleSoftwareInputPanel,這個是qwidget_p.h中的函數,由於QLineEditPrivate繼承了QWidgetPriave,能夠看到handleSoftwareInputPanel函數調用了QGuiApplication::inputMethod()->show();而這個輸入法實際上只是一個接口,他調用的仍是插件裏的show函數,platform在初始化的時候建立了一個platform_integration,就叫他平臺集成吧,這平臺集成裏有各類各樣與平臺相關的東西,輸入法插件就是其中一個,輸入法就是從平臺集成中獲取到了inputContext也就是咱們建立的插件。
另一個就是鍵盤的處理,我但願按下鍵盤時,輸入法界面也能跳出來,就跟咱們日常打字同樣,按了鍵輸入法才跳出來,按了回車或者空格,界面就消失了,輸入法界面是沒有焦點的,不能接受鍵盤的輸入,只能經過應用程序把輸入的字符串在dbus上發給輸入法。這裏要實現filterEvent函數

bool VkImPlatformInputContext::filterEvent(const QEvent *event)
{
    const QKeyEvent *keyEvent = (const QKeyEvent *)event;
    int key = keyEvent->key();
    // should pass only the key presses
    if (keyEvent->type() != QEvent::KeyPress) {
        return false;
    }

    if (key == Qt::Key_Left || key == Qt::Key_Right ||
            key == Qt::Key_Up || key == Qt::Key_Down ||
            key == Qt::Key_Tab)
    {
        return false;
    }

    if (!isInputPanelVisible())
    {
        showInputPanel();
    }

    if (dbusInterface != NULL)
    {
        dbusInterface->call("pressKey", keyEvent->key());
    }

    return true;
}

其中pressKey是輸入法界面導出的槽函數,這裏插件把按鍵傳給了輸入法界面,固然這裏看本身的需求了,我這裏字符串不是直接發給焦點控件的,是須要再輸入法界面確認以後再發給焦點控件的,若是要直接發送給焦點控件,就不須要處理鍵盤事件了。輸入法界面點了肯定按鈕以後,會發出一個commit信號,插件響應這個信號,把最終提交的字符串發送給焦點控件,這裏要用到QInputMethodEvent,代碼以下:

void VkImPlatformInputContext::keyboardCommit(QString str)
{
    if (focusObject == NULL)
    {
        return;
    }

    QInputMethodEvent event;
    event.setCommitString(str);

    QGuiApplication::sendEvent(focusObject, &event);
}

其中str是由輸入法界面發送信號的時候傳過來的。
filterEvent又是何時調用的呢?QApplication在處理事件循環的時候,通知平臺(xcb或者fb等)處理按鍵事件,平臺優先把按鍵事件告訴輸入法,輸入法就調用filterEvent函數過濾掉一些事件,這些事件就不會再傳給焦點控件了,好比上面的filterEvent函數,其中Qt::Key_Left這些方向鍵是沒有過濾的,return false了,仍然會傳遞給焦點控件。

4、測試

  1. export QT_IM_MODULE=vkim
  2. ./keyboard &
  3. ./Dialog

5、說明

  1. 文章裏的代碼刪掉了一部分,便於閱讀,完整代碼能夠到這裏下載http://git.oschina.net/tracing/VkIm,或者直接用git clone,https://git.oschina.net/tracing/VkIm.git
  2. 參考的文章及代碼http://www.kdab.com/qt-input-method-depth/

行者不止 herbert@ih-tech.net 2016.02.01

相關文章
相關標籤/搜索