要實現點擊一個編輯框就跳出來一個軟鍵盤方法不少,爲何要用輸入法的方式呢?輸入法的方式能夠用在任一個QT程序上,而應用程序自己不須要去關心如何去輸入,交給輸入法就能夠了。輸入法與程序是獨立的,兩個程序經過通訊的方式進行對話。就好比咱們在手機上寫個程序,歷來就沒關心過軟鍵盤怎麼去實現,只須要作應用這部分就能夠了。git
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就是輸入法的名字。框架
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能夠看到註冊的對象,以及對象的槽,信號。如圖所示:
其中接口的名字也能夠本身定義,也就是把這句代碼改爲下面這樣子:測試
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 ¶mList); };
其中Q_PLUGIN_METADATA這些是必需要寫的,這是插件的規範,能夠在QT官網上找到,IID必需要寫成QPlatformInputContextFactoryInterface_iid
這個看也在父類QPlatformInputContextPlugin
的頭文件裏看到定義,FILE定義的就是前面說過的json文件。
VkImPlatformInputContext* VkImPlatformInputContextPlugin::create(const QString &key, const QStringList ¶mList) { 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
,繼承於QPlatformInputContext
,QPlatformInputContext
中的顯示隱藏此類函數都是空的,須要咱們本身去實現。代碼以下:
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了,仍然會傳遞給焦點控件。
行者不止 herbert@ih-tech.net 2016.02.01