QT5的软键盘输入法实现

来源:互联网 发布:淘宝刷几单封号 编辑:程序博客网 时间:2024/06/06 01:47

目录(?)[-]

  1. 一为什么要用输入法的方式实现
  2. 二QT5输入法插件的结构
  3. 三具体实现
  4. 四测试
  5. 五说明

一、为什么要用输入法的方式实现

要实现点击一个编辑框就跳出来一个软键盘方法很多,为什么要用输入法的方式呢?输入法的方式可以用在任一个QT程序上,而应用程序本身不需要去关心如何去输入,交给输入法就可以了。输入法与程序是独立的,两个程序通过通信的方式进行对话。就比如我们在手机上写个程序,从来就没关心过软键盘怎么去实现,只需要做应用这部分就可以了。

二、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,需要重新实现的有以下几个函数:

    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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里就是面板的显示隐藏,设置焦点等函数,QT是如何知道这个插件的名字呢?就是根据json文件了,json文件的内容如下:

{    "Keys": [ "vkim" ]}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

其中vkim就是输入法的名字。

三、具体实现

1.输入法界面 
输入法界面要在dbus上注册一个服务和对象,这个对象就是输入法界面,dbus上有了这个服务,应用程序才能通过dbus和输入法界面对话。注册服务和对象的代码如下: 
InputService类:

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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

主函数:

#include "inputservice.h"#include <QDBusConnection>int main(int argc, char *argv[]){    QApplication a(argc, argv);    Dialog keyboard;    InputService inputService(&keyboard);    return a.exec();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里把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)
  • 1
  • 2
  • 1
  • 2

上图中的local.keyboard.Dialog就变成了com.mh.input.vkim,不过这样有一个问题,这样就会把自己及父类所有的信号和槽都导出了,结果变成了3个com.mh.input.vkim,调用槽函数的时候还没啥问题,但是响应信号就有问题了,可能有3个一样的接口,系统不知道该找哪个了,就会提示没有该信号,这里我也没弄明白该如何自定义接口名字。 
下面就是添加一些信号和槽,只有定义成槽函数,才能被插件通过dbus调用。代码如下: 
dialog.h

public slots:    void showKeyboard(QPoint pt, QRect focusWidget);    void hideKeyboard();    bool isVisible() const;signals:    void commit(QString str);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

dialog.cpp

void Dialog::showKeyboard(QPoint pt, QRect focusWidget){    QWidget::show();}void Dialog::hideKeyboard(){    QWidget::hide();}bool Dialog::isVisible() const{    return QWidget::isVisible();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里省略了一些代码,代码太多影响阅读,commit信号是在点击确定或者键盘上按回车的时候给应用程序发送信号,把软键盘上的字符发给插件,插件响应这个信号,再把字符串发送给焦点编辑框。输入法界面的工作完成了,然后就看插件怎么去调用这些函数。 
2.插件的实现 
先看下插件的创建,代码如下: 
vkimplatforminputcontextplugin.h

#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);};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

其中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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

VkImPlatformInputContextPlugin继承于QPlatformInputContextPlugin,QApplication初始化的时候,由QPlatformInputContextFactory创建,factory根据环境变量QT_IM_MODULE查找对应的插件并调用create函数创建插件,而插件返回的就是我们的输入法VkImPlatformInputContext,继承于QPlatformInputContextQPlatformInputContext中的显示隐藏此类函数都是空的,需要我们自己去实现。代码如下: 
vkimplatforminputcontext.h

class VkImPlatformInputContext : public QPlatformInputContext{    Q_OBJECTpublic:    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;};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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");    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

其中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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

其中pressKey是输入法界面导出的槽函数,这里插件把按键传给了输入法界面,当然这里看自己的需求了,我这里字符串不是直接发给焦点控件的,是需要再输入法界面确认之后再发给焦点控件的,如果要直接发送给焦点控件,就不需要处理键盘事件了。输入法界面点了确定按钮之后,会发出一个commit信号,插件响应这个信号,把最终提交的字符串发送给焦点控件,这里要用到QInputMethodEvent,代码如下:

void VkImPlatformInputContext::keyboardCommit(QString str){    if (focusObject == NULL)    {        return;    }    QInputMethodEvent event;    event.setCommitString(str);    QGuiApplication::sendEvent(focusObject, &event);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

其中str是由输入法界面发送信号的时候传过来的。 
filterEvent又是什么时候调用的呢?QApplication在处理事件循环的时候,通知平台(xcb或者fb等)处理按键事件,平台优先把按键事件告诉输入法,输入法就调用filterEvent函数过滤掉一些事件,这些事件就不会再传给焦点控件了,比如上面的filterEvent函数,其中Qt::Key_Left这些方向键是没有过滤的,return false了,仍然会传递给焦点控件。

四、测试

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

五、说明

  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/
原创粉丝点击