한국어
Qt
 

이 글은 KDAB에 소개된 글과 예제소스코드를 기반으로 작성되었다.

 

가상키보드는 실제 물리 키보드가 없는 환경에서 입력장치로 유용하게 사용될 수 있다. 

 

Qt는 이미 훌륭한 Virtual keyboard를 제공하고 있지만 이 글에서는 dbus, QPA기반에서 키입력 인터페이스를 사용하는 방법과 가상키보드 구현 기본 개념을 살펴본다.

 

가상키보드를 구현하는 방법에는 크게 두가지 방법이 있다. in-process로 구현하거나 또는 하나의 독립 프로세스로(out-of-process) 동작하도록 구현하는 것이다.

 

in-process로 구현하는 접근 방식의 장점은 키보드와 응용프로그램간 즉, 프로세스 간 통신이 필요 없기 때문에 구현이 매우 쉽다는 것이다. 단점은 모든 응용프로그램이 가상키보드의 인스턴스를 가지고 있어야 한다는 것이다. 이 문제는 모든 인스턴스 (가시성, 활동 등)의 상태를 응용프로그램간 동기화를 어렵게 한다는 것을 의미한다.

 

반면 out-of-process 방식은 다른 모든 응용프로그램간에 가상 키보드의 단일 인스턴스(singleton)를 공유 할 수 있으므로 여러 가상키보드 인스턴스간에 동기화 문제가 발생하지 않는다는 것이다. 다만 이를 위해서는 전역 가상키보드 인스턴스를 사용하려는 모든 응용 프로그램 사이의 프로세스 간 통신 메커니즘이 필요하다.

 

이 글에서는 기존 리눅스 Input Method 시스템인 IBus에서 이미 작동하는 것으로 입증되었기 때문에 아웃 프로세스 (out-of-process) 방식을 사용하고 dbus를 IPC메커니즘으로 사용한다.

 

dbus에 관해서 더 자세한 내용은 다음 글을 참고하길 바란다. https://makersweb.net/linux/2027

 
Keyboard Application
 
키보드 프로그램은 처음 QDBus API를 통해 DBus 세션버스에 서비스 및 인스턴스를 등록한다.
#include <QApplication>
#include <QDBusConnection>

#include "keyboard.h"

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    if (!QDBusConnection::sessionBus().registerService("com.kdab.inputmethod")) {
        qFatal("Unable to register at DBus");
        return 1;
    }

    Keyboard keyboard;

    if (!QDBusConnection::sessionBus().registerObject("/VirtualKeyboard", &keyboard, QDBusConnection::ExportAllSignals | QDBusConnection::ExportAllSlots)) {
        qFatal("Unable to register object at DBus");
        return 1;
    }

    return app.exec();
}
 
키보드 클래스는 윈도우에 키보드 배열을 그리기 위해 QWidget을 상속받고 있으며 기본적인 메서드와 키입력 시그널을 제공한다.
#ifndef KEYBOARD_H
#define KEYBOARD_H

#include <QWidget>

class Keyboard : public QWidget
{
    Q_OBJECT

public:
    explicit Keyboard(QWidget *parent = Q_NULLPTR);

public slots:
    void showKeyboard(int globalX, int globalY);
    void hideKeyboard();
    bool keyboardVisible() const;

signals:
    void specialKeyClicked(int key);
    void keyClicked(const QString &text);

private slots:
    void buttonClicked(int key);
};

#endif
 
키보드 생성자에서는 키배열 레이아웃을 만들며 모든 버튼클릭은 중앙에서 처리되어야 하므로 QPushButton 중 하나를 클릭 할 때마다 QSignalMapper를 사용하여 private buttonClicked(int key) 슬롯과 연결한다.

Keyboard::Keyboard(QWidget *parent)
    : QWidget(parent)
{
    setWindowFlags(Qt::WindowDoesNotAcceptFocus | Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::BypassWindowManagerHint);

    QGridLayout *gridLayout = new QGridLayout(this);

    QSignalMapper *mapper = new QSignalMapper(this);
    connect(mapper, SIGNAL(mapped(int)), SLOT(buttonClicked(int)));

    int row = 0;
    int column = 0;

    for (int i = 0; i < layoutSize; ++i) {
        if (keyboardLayout[i].key == NEXT_ROW_MARKER) {
            row++;
            column = 0;
            continue;
        }

        QPushButton *button = new QPushButton;
        button->setFixedWidth(40);
        button->setText(QString::fromLatin1(keyboardLayout[i].label));

        mapper->setMapping(button, keyboardLayout[i].key);
        connect(button, SIGNAL(clicked()), mapper, SLOT(map()));

        gridLayout->addWidget(button, row, column);
        column++;
    }
}
 
이제 사용자가 QPushButton 중 하나를 클릭하면 buttonClicked() 슬롯이 호출된다.
void Keyboard::buttonClicked(int key)
{
    if ((key == Qt::Key_Enter) || (key == Qt::Key_Backspace))
        emit specialKeyClicked(key);
    else
        emit keyClicked(keyToCharacter(key));
}
 
누른 키가 Backspace 또는 Enter이면 specialKeyClicked() 신호가 내보내지며, 그렇지 않으면 키 코드가 문자에 매핑되고 keyClicked() 신호가 해당 문자와 함께 매개 변수로 생성된다.
static QString keyToCharacter(int key)
{
    for (int i = 0; i < layoutSize; ++i) {
        if (keyboardLayout[i].key == key)
            return QString::fromLatin1(keyboardLayout[i].label);
    }

    return QString();
}
 
Qt Input Method plugin
 
Input Method 시스템은 공용 프론트 엔드 클래스 QInputMethod와 개인용 백엔드 클래스 QPlatformInputContext를 통해 수행된다.
 
1. QPlatformInputContext를 상속받는 서브 클래스 구현
2. QPlatformInputContextPlugin을 상속받는 서브 클래스 구현
 
먼저 QPlatformInputContextPlugin을 상속받은 새로운 클래스 QVkImPlatformInputContextPlugin을 선언하고 가상 메소드 create()를 다시 구현한다.
class QVkImPlatformInputContextPlugin : public QPlatformInputContextPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QPlatformInputContextFactoryInterface_iid FILE "vkim.json")

public:
    QVkImPlatformInputContext *create(const QString&, const QStringList&) Q_DECL_OVERRIDE;
};

QVkImPlatformInputContext *QVkImPlatformInputContextPlugin::create(const QString& system, const QStringList& paramList)
{
    Q_UNUSED(paramList);

    if (system == QLatin1String("vkim")) {
        return new QVkImPlatformInputContext;
    }

    return 0;
}
 
플러그인 메타 데이터는 외부 파일 'vkim.json'에 저장되며 다음 줄만 포함된다.
{
    "Keys": [ "vkim" ]
}
'vkim'이라는 용어는 'virtual keyboard Input method'의 약자로, 입력 메소드 플러그인의 식별자로 사용된다. 나중에 필요한 곳에서 다시 보게 될 것이다.
 
QVkImPlatformInputContextPlugin::create() 구현 시 요청한 객체가 해당 식별자를 가지고 있는지 확인하고 이 경우 사용자 정의 클래스인 QVkImPlatformInputContext의 새 인스턴스를 반환한다.
 
QPlatformInputContext의 다섯 가지 가상 메서드를 다시 구현한다.
 
isValid()는 QPlatformInputContextFactory (플러그인을로드하고 create() 메소드를 호출하는 클래스)에 의해 호출되어 입력 메소드가 사용할 준비가되었는지 확인.
setFocusObject()는 QGuiApplication에 의해 호출되어 QPlatformInputContext에 현재 입력 포커스가있는 객체를 알린다.
showInputPanel()은 텍스트 입력 필드에 포커스되면 QInputMethod::show()에 의해 호출되어 키보드를 호출한다.
hideInputPanel()은 QInputMethod::hide()에 의해 호출되어 현재 텍스트 입력 필드가 포커스를 잃으면 키보드를 숨긴다.
isInputPanelVisible()은 QInputMethod::isVisible()에 의해 호출되어 키보드의 visible 상태를 응용 프로그램에 알린다.
 
플러그인은 결과적으로 키보드가 필요한 응용프로그램이 사용하며 키보드프로그램으로부터 이벤트를 받을 수 있도록 dbus 연결을 만든다.
QVkImPlatformInputContext::QVkImPlatformInputContext()
    : m_focusObject(0)
{
    m_keyboardInterface = new QDBusInterface("com.kdab.inputmethod", "/VirtualKeyboard", "local.server.Keyboard", QDBusConnection::sessionBus(), this);

    connect(m_keyboardInterface, SIGNAL(keyClicked(QString)), SLOT(keyboardKeyClicked(QString)));
    connect(m_keyboardInterface, SIGNAL(specialKeyClicked(int)), SLOT(keyboardSpecialKeyClicked(int)));
}
 
현재 포커스된 텍스트 입력 위젯이 있는 경우 입력 된 문자를 이벤트의 커밋 문자열로 설정하여 객체로 보낸다.
void QVkImPlatformInputContext::keyboardKeyClicked(const QString &characters)
{
    if (!m_focusObject)
        return;

    QInputMethodEvent event;
    event.setCommitString(characters);

    QGuiApplication::sendEvent(m_focusObject, &event);
}
 
프로젝트를 빌드하면 플러그인(libvkimplatforminputcontextplugin.so)과 키보드 프로그램(server)이 만들어 진다. libvkimplatforminputcontextplugin.so은 Qt가 설치된 곳의 plugins/platforminputcontexts 디렉토리에 deploy되어야 한다. 
 
그 다음 sever를 실행하면 dbus 세션버스에 다음과 같이 인터페이스가 노출된다.
vkim_dbus.png

 

이제 입력 필드를 가진 간단한 응용프로그램을 만든다.

widget_input.png

 

기본적으로 사용되는 QPA 플러그인은 시스템에 대한 입력 메서드를 사용하므로 우리가 만든 입력 메소드를 사용하려면 QT_IM_MODULE 환경변수를 입력 메소드의 식별자로 설정하여 명시적으로 Qt에 알려야 한다. 이전에 'vkim'이라는 식별자를 기억 할 것이다. 아래처럼 환경변수를 설정해준다.
vkim_env.png

 

응용프로그램을 실행하고 입력 필드를 클릭하면 키보드가 나타나고 키를 클릭하면 글자가 입력된다.

vkim.png

 

아래는 예제소스코드 저장소이다.

https://github.com/KDAB/virtual-keyboard-demo

번호 제목 글쓴이 날짜 조회 수
공지 Qt프로그래밍(QtQuick) Beginner를 위한 글 읽는 순서 운영자 2019.01.05 1488
66 QtCreator Design으로 GUI만들기 (QML로 만드는 Hello World -2) file makersweb 2019.05.26 4
65 QML에서 멀티 스레드(multithreading) 프로그래밍 file makersweb 2019.05.25 19
64 QtSerialPort를 사용한 시리얼(Serial)통신 makersweb 2019.05.21 36
63 Qt기반의 오픈소스 프로젝트들 makersweb 2019.05.15 51
62 Q_D매크로와 d-pointer file makersweb 2019.05.07 37
61 가상키보드(Qt Virtual Keyboard)를 사용하는 방법 file makersweb 2019.05.03 49
60 Windows에서 Qt D-Bus를 사용하여 프로세스간 통신(IPC) file makersweb 2019.05.02 57
59 QML과 QtQuick 모듈 개념과 기본 타입들 makersweb 2019.04.26 135
58 QML 전역 객체 (Global Object) file makersweb 2019.04.10 57
57 tslib의 ts_calibrate를 응용해서 Qt로 터치보정기능 구현 file makersweb 2019.04.06 46
56 GPU가 없는 장치에서 Qt Quick을 사용 makersweb 2019.04.02 73
55 QTextCodec클래스를 사용하여 유니코드와 EUC-KR 변환 makersweb 2019.03.25 41
54 qInstallMessageHandler를 이용한 디버그 메세지 출력 제어하기 makersweb 2019.02.25 73
» Qt5기반 독립 프로세스(out-of-process)로 동작하는 가상키보드(virtual keyboard) file makersweb 2019.02.24 151
52 Qml 기본 컴포넌트 강좌 (4) - 모델 리스팅(Listing) file 운영자 2019.02.23 109
51 Qt Bluetooth를 이용한 시리얼(Serial) 통신 file makersweb 2019.02.17 203
50 Qml 기본 컴포넌트 강좌 (3) - 배치(positioning) 컴포넌트 file 운영자 2019.02.10 105
49 QString 문자열 다루기 예제 운영자 2019.01.26 621
48 Qt SQL을 이용한 가벼운 데이터베이스 다루기 file 운영자 2019.01.23 96
47 구글 클라우드 Speech-To-Text API를 Qt기반(C++, Qml)테스트 file makersweb 2019.01.20 330