Виртуальная клавиатура, или новые функции контекста ввода Qt

 

День добрый. Как мы знаем в последних версиях, вектор развития библиотеки Qt был направлен на поддержку разработки для мобильных устройств. В связи с этим, Qt обзавелась парой важных портов - на Symbian и MeeGo. Кроме того, Qt/Embedded вполне успешно используется многими компаниями при разработке приложений для планшетных компьютеров, которые всё быстрее набирают популярность и входят в нашу жизнь. Эти обстоятельства побуждают разработчиков к разработке специфических вспомогательных классов и средств, используемых в собственных приложениях. Одним из таких специфических средств при разработке приложений под планшеты и мобильные телефоны, не имеющие аппаратной клавиатуры является разработка собственной виртуальной клавиатуры, позволяющей вводить данные в поля ввода программы.

 

Задача осложняется тем, что в случае Qt, где вроде бы можно глобально перехватывать все события приложения через функцию eventFilter(), существуют не просто поля вроде QInputText, QTextEdit и QPlaintTextEdit для которых относительно просто получить событие focusIn, но существуют также QWebView и QDeclarativeView, внутри которых умещаются либо веб-страница с html-полями ввода, либо набор QML-компонентов, а может и компонент QML-компонент браузера, со вставленными уже в него html-полями... В общем иерархия получается та ещё. На практике можно достучаться до каждого из этих элементов, обрабатывать различные события происходящие в них и можно даже написать вполне себе работоспособную клавиатурку... Вот только это будет громоздко, глючно и вообще состоять из чёткой системы костылей и подпорок =)

 

Что же можно поставить в противовес? В Qt 4, изначально весь ввод приложения был реализован через так называемый “контекст ввода” (Input Context). ЭФактически, это абстракция, позволяющая поставить между приложением и устройством ввода собственную прослойку, которая и будет преобразовывать сигналы ввода с “устройства” в события Qt-приложения. Начиная с версии Qt 4.6 и началом мобильной эры Qt, разработчики позаботились о возможности создания виртуальной клавиатуры (http://labs.qt.nokia.com/2009/08/31/new-api-for-input-panel-virtual-keyboards/)... Как устройства ввода со специфическим контекстом ввода! В Qt появились два новых события RequestSoftwareInputPanel и CloseSoftwareInputPanel, обрабатывая которые в самописаном плагине, реализующем специфический контекст ввода, можно с лёгкостью сделать виртаульную клавиатуру, имеющую прямой доступ к любым доступным полям ввода на форме приложения, так как для самого приложения она остаётся по сути чёрным ящиком и представляется как та же самая аппаратная клавиатура. Кроме того, все виджеты Qt в зависимости от того необходима им клавиатура или нет, могут самостоятельно высылать оба события, то есть кроме указания приложению нового контекста ввода нет необходимости дополнительно следить за открытием/закрытием клавиатуры при необходимости.

 

Итак. Опираясь на пример http://doc.trolltech.com/4.7/tools-inputpanel.html, изменив его и прокомментировав, постараемся создать собственную небольшую виртуальную клавиатуру.

 

Начнём с написания плагина. Он будет состоять из трёх классов:

SoftwareInputPanel - виджет на котором будут размещены кнопки нашей клавиатуры

SoftwareInputContext - собственно контекст ввода. позволяющий приложению вызывать виртуальную клавиатуру при необходимости

SoftwareInputContextPlugin - и наконец Qt-плагин, позволяющий вписать созданный контекст ввода в общую структуру плагинов Qt.

 

Первое. Виджет ввода.

softwareinputpanel.h
#ifndef SOFTWAREINPUTPANEL_H
#define SOFTWAREINPUTPANEL_H
#include <QWidget>
#include <QSignalMapper>
namespace Ui {
class SoftwareInputPanel;
}
class SoftwareInputPanel : public QWidget
{
Q_OBJECT
public:
explicit SoftwareInputPanel(QWidget *parent = 0);
~SoftwareInputPanel();
signals:
void characterGenerated(QChar character);
protected:
bool event(QEvent *e);
private slots:
void saveFocusWidget(QWidget *oldFocus, QWidget *newFocus);
void buttonClicked(QWidget *w);
private:
QWidget *lastFocusedWidget;
QSignalMapper signalMapper;
Ui::SoftwareInputPanel *ui;
};
#endif // SOFTWAREINPUTPANEL_H

Здесь всё просто. В данном классе для нас главное:

1 - возвращать потерянный приложение фокус ввода на тот виджет в который необходимо вводить даные.

2 - собственно ни коим образом не ловить фокус ввода нашей панелью

3 - всегда оставлять панель поверх других окон

4 - и наконец сделать маппинг всех кнопок находящихся на форме на функцию, которая будет обрабатывать их и отправлять сигнал, оповещающий контекст ввода о нажатиях на кнопки виртуальной клавиатуры.


softwareinputpanel.cpp
#include "softwareinputpanel.h"
#include "ui_softwareinputpanel.h"
SoftwareInputPanel::SoftwareInputPanel(QWidget *parent) :

Оставляем нашу панель поверх других окон

QWidget(0, Qt::Tool | Qt::WindowStaysOnTopHint),
lastFocusedWidget(0),
ui(new Ui::SoftwareInputPanel)
{
ui->setupUi(this);
 

Запрещаем ловить фокус и возвращаем его на предыдущий виджет.

connect(qApp, SIGNAL(focusChanged(QWidget*,QWidget*)),
        this, SLOT(saveFocusWidget(QWidget*,QWidget*)));

Регистрируем все события нажатий на клавиши...
signalMapper.setMapping(ui->panelButton_1, ui->panelButton_1);
signalMapper.setMapping(ui->panelButton_2, ui->panelButton_2);
signalMapper.setMapping(ui->panelButton_3, ui->panelButton_3);
signalMapper.setMapping(ui->panelButton_4, ui->panelButton_4);
signalMapper.setMapping(ui->panelButton_5, ui->panelButton_5);
signalMapper.setMapping(ui->panelButton_6, ui->panelButton_6);
signalMapper.setMapping(ui->panelButton_7, ui->panelButton_7);
signalMapper.setMapping(ui->panelButton_8, ui->panelButton_8);
signalMapper.setMapping(ui->panelButton_9, ui->panelButton_9);
signalMapper.setMapping(ui->panelButton_star, ui->panelButton_star);
signalMapper.setMapping(ui->panelButton_0, ui->panelButton_0);
signalMapper.setMapping(ui->panelButton_hash, ui->panelButton_hash);
connect(ui->panelButton_1, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_2, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_3, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_4, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_5, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_6, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_7, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_8, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_9, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_star, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_0, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));
connect(ui->panelButton_hash, SIGNAL(clicked()),
        &signalMapper, SLOT(map()));

…и отправляем их все в функцию buttonClicked();
connect(&signalMapper, SIGNAL(mapped(QWidget*)),
        this, SLOT(buttonClicked(QWidget*)));
}

Так как панель по сути является отдельным окном, прежде чем вернуть фокус на предыдущий виджет, нам необходимо вернуть фокус на окно. в котором собтсенно и расположен виджет, в который необходимо производить ввод.

bool SoftwareInputPanel::event(QEvent *e)
{
switch (e->type()) {
case QEvent::WindowActivate:
    if (lastFocusedWidget)
        lastFocusedWidget->activateWindow();
    break;
default:
    break;
}
return QWidget::event(e);
}
void SoftwareInputPanel::saveFocusWidget(QWidget * /*oldFocus*/, QWidget *newFocus)
{
if (newFocus != 0 && !this->isAncestorOf(newFocus)) {
    lastFocusedWidget = newFocus;
}
}
void SoftwareInputPanel::buttonClicked(QWidget *w)
{

По сути здесь нам необходимы две вещи - получить символ из виджета и сгенерировать сигнал с даннм кодом. Обо всём остальном позаботится наш контекст ввода.

QChar chr = qvariant_cast<QChar>(w->property("buttonValue"));
emit characterGenerated(chr);
}
SoftwareInputPanel::~SoftwareInputPanel()
{
delete ui;
}

Кроме того, существует ui-файл с нашей клавиатурой, на которой для примера размещены цифровые клавиши. При необходимости любой разоаботчик сможет самостоятельно добавить остальные клавиши и сделать полноценную клавиатуру.


softwareinputpanel.ui
...

Так. С одним разобрались. Далее, основной класс - класс реализующий новый контекст ввода для приложения. Мы наследуем свой собственный класс от класса QInputContext и далее переопределям ряд его методов, о работе которых станет ясно при рассмотрении файла реализации.

softwareinputcontext.h
#ifndef SOFTWAREINPUTCONTEXT_H
#define SOFTWAREINPUTCONTEXT_H
#include <QInputContext>
class SoftwareInputPanel;
class SoftwareInputContext : public QInputContext
{
Q_OBJECT
public:
explicit SoftwareInputContext(QObject *parent = 0);
~SoftwareInputContext();
bool filterEvent(const QEvent* event);
QString identifierName();
QString language();
bool isComposing() const;
void reset();
private slots:
void sendCharacter(QChar character);
private:
void updatePosition();
private:
SoftwareInputPanel *inputPanel;
};
#endif // SOFTWAREINPUTCONTEXT_H

Реализация контекста ввода.
softwareinputcontext.cpp
#include "softwareinputcontext.h"
#include "softwareinputpanel.h"
#include <QPointer>
#include <QApplication>

Для начала в конструкторе создаём собственно наш виджет, являющийся виртуальной клавиатурой и подключаем его сигнал сообщающий о нажатии некой кнопки к нашему обработчику событий, слоту sendCharacter().


SoftwareInputContext::SoftwareInputContext(QObject *parent) :
QInputContext(parent)
{
inputPanel = new SoftwareInputPanel;
connect(inputPanel, SIGNAL(characterGenerated(QChar)), SLOT(sendCharacter(QChar)));
}
SoftwareInputContext::~SoftwareInputContext()
{
delete inputPanel;
}

Через данный метод мы фактически управляем показом/скрытием нашей клавиатуры, основываясь на событиях, генерируемых конкретным виджетом, получающим в приложении фокус в какой-либо момент времени.

bool SoftwareInputContext::filterEvent(const QEvent* event)
{
if (event->type() == QEvent::RequestSoftwareInputPanel) {
    updatePosition();
    inputPanel->show();
    return true;
} else if (event->type() == QEvent::CloseSoftwareInputPanel) {
    inputPanel->hide();
    return true;
}
return false;
}

Просто идентификатор нашего метода ввода

QString SoftwareInputContext::identifierName()
{
return "SoftwareInputPanel";
}
void SoftwareInputContext::reset()
{
}
bool SoftwareInputContext::isComposing() const
{
return false;
}
QString SoftwareInputContext::language()
{
return "en_US";
}

А вот здесь начинается магия ;-). Как мы помним, данная функция вызывается в тот момент, когда у нас уже открыта панель ввода, фокус силами панели возвращён на виджет требущий ввода и была нажата какая-либо клавиша.


void SoftwareInputContext::sendCharacter(QChar character)
{
С легкой руки Qt, получаем фокус ввода. Это может быть любое поле ввода, собственно умеющий принимать события типа QKeyEvent. Будь то виджет на форме, QML-элемент или форма ввода на веб-странице.
QPointer<QWidget> w = focusWidget();
if (!w)
    return;

Получив виджет - мы создаём два события - нажатие не кнопку и отпускание кнопки. В купе, это даёт нам события клика. И соответственно виджет имеющий фокус может его правильно обработать.

QKeyEvent keyPress(QEvent::KeyPress, character.unicode(), Qt::NoModifier, QString(character));
QApplication::sendEvent(w, &keyPress);
if (!w)
    return;
QKeyEvent keyRelease(QEvent::KeyPress, character.unicode(), Qt::NoModifier, QString());
QApplication::sendEvent(w, &keyRelease);
}

И конечно куда же мы без вспомогательной функции. Здесь мы лишь получаем позицию нашего виджета и размещаем панель таким образом, чтобы она находилась рядом с ним.


void SoftwareInputContext::updatePosition()
{
QWidget *widget = focusWidget();
if (!widget)
    return;
QRect widgetRect = widget->rect();
QPoint panelPos = QPoint(widgetRect.left(), widgetRect.bottom() + 2);
panelPos = widget->mapToGlobal(panelPos);
inputPanel->move(panelPos);
}
 

На данный момент мы имеем вполне себе готовый контекст ввода с рабочей виртуальной клавиатурой. Остаётся научиться подключать всё это хозяйство к любому приложению. Разработчики Qt естественно позаботились об этом заранее, создав класс QInputContextPlugin, с помощью которого можно создать подключаемый модуль Qt, который вписывается в общую систему плагинов и представляющий собой динамически подключаемую библиотеку.


softwareinputcontextplugin.h
#ifndef SOFTWAREINPUTCONTEXTPLUGIN_H
#define SOFTWAREINPUTCONTEXTPLUGIN_H
#include <QtGui/QInputContextPlugin>
#include "softwareinputcontext.h"
class SoftwareInputContextPlugin : public QInputContextPlugin {
Q_OBJECT
public:
SoftwareInputContextPlugin(QObject *parent = 0);
SoftwareInputContext *create ( const QString & key );
QString description ( const QString & key );
QString displayName ( const QString & key );
QStringList keys () const;
QStringList languages ( const QString & key );
};
#endif // SOFTWAREINPUTCONTEXTPLUGIN_H

Всё донельзя просто - наследуем QInputContextPlugin и переопределяем его методы.

softwareinputcontextplugin.cpp
#include "softwareinputcontextplugin.h"
SoftwareInputContextPlugin::SoftwareInputContextPlugin(QObject *parent) :
QInputContextPlugin(parent)
{
}

Создание нового контекста ввода
SoftwareInputContext *SoftwareInputContextPlugin::create ( const QString & key )
{
return new SoftwareInputContext(this);
}

Текстовое описание...
QString SoftwareInputContextPlugin::description ( const QString & key )
{
return "Example for Software Input Context";
}
...и имя показываемое... ну нупримерв даилоге выбора контекста ввода для приложения =)
QString SoftwareInputContextPlugin::displayName ( const QString & key )
{
return "Software Input Context";
}

О том какие контексты ввода предоставляет данный плагин...
QStringList SoftwareInputContextPlugin::keys () const
{
return QStringList() << "SoftwareInputPanel";
}

...и для каких языков... По сути, данный параметр не столь важен. потому как клавиатура сама может следить за тем какой язык показывать в определённый момент времени.
QStringList SoftwareInputContextPlugin::languages ( const QString & key )
{
return QStringList() << "en_US";
}
Q_EXPORT_PLUGIN2(SoftwareInputPanel, SoftwareInputContextPlugin)

Из всего этого мы сделаем Qt-проект, а именно подключаемую библиотеку. Смотрим:
#-------------------------------------------------
#
# Project created by QtCreator 2011-05-19T23:00:20
#
#-------------------------------------------------
QT       += core gui
TARGET = SoftwareInputPanel
TEMPLATE = lib
CONFIG += plugin
DESTDIR = $$[QT_INSTALL_PLUGINS]/inputmethods
SOURCES += softwareinputcontextplugin.cpp \
softwareinputcontext.cpp \
softwareinputpanel.cpp
HEADERS += softwareinputcontextplugin.h \
softwareinputcontext.h \
softwareinputpanel.h
unix:!symbian {
maemo5 {
    target.path = /opt/usr/lib
} else {
    target.path = /usr/local/lib
}
INSTALLS += target
}
FORMS += \
softwareinputpanel.ui

Как видно по DESTDIR - наш плагин сразу будет ставится в каталог по умолчанию для всех плагинов реализующих различные методы ввода.

Наконец стоит остаётся вопрос, как подключить данный плагин к своему приложению? Посмотрим на примере...

SoftwareInputExample.pro
SOURCES += main.cpp
HEADERS +=
FORMS   += mainform.ui
# install
target.path = $$[QT_INSTALL_EXAMPLES]/tools/inputpanel
sources.files = $$SOURCES $$HEADERS $$RESOURCES $$FORMS inputpanel.pro
sources.path = $$[QT_INSTALL_EXAMPLES]/tools/inputpanel
INSTALLS += target sources
symbian: include($$PWD/../../symbianpkgrules.pri)
maemo5: include($$PWD/../../maemo5pkgrules.pri)
symbian: warning(This example might not fully work on Symbian platform)
maemo5: warning(This example might not fully work on Maemo platform)
simulator: warning(This example might not fully work on Simulator platform)

По сути, в приложении всё сводится к двум строчкам кода:
#include <QInputContextFactory>
и
app.setInputContext(QInputContextFactory::create("SoftwareInputPanel",&app));

А если говорить человеческими словами, то мы всего лишь подключаем заголовок Factory-класса. позволяющего динамически создать новый контекст ввода из имющегося плагина, по его текстовому идентификатору. А стало быть нам нет нужды знать о внутреннем устройстве созданной динамической библиотеки и совсем не нужно таскать с приложением её заголовочные файлы.

main.cpp
#include <QtGui/QApplication>
#include <QtGui/QWidget>
#include "ui_mainform.h"
#include <QInputContextFactory>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
Подключаем новый контекст.
app.setInputContext(QInputContextFactory::create("SoftwareInputPanel",&app));
Инициализируем форму приложения...
QWidget widget;
Ui::MainForm form;
form.setupUi(&widget);
И показываем её...
widget.show();
return app.exec();
}
mainform.ui
...

В сухом остатке мы получаем работоспособную виртуальную клавиатуру, которая не использует каких-бы то ни было костылей для доступа к текущему виджету и работающую на любой платформе - MeeGo, Symbian, QWS и прочих системах где нет нормальной аппаратной клавиатуры...

Ссылка на полную версию статьи, с включённым кодом ui-форм: https://docs.google.com/document/pub?id=1YM7U2gXglyBAEKPZkCFexo3GNQzpsP6...