Edited at
QtDay 3

QtとReactive Extensions

More than 1 year has passed since last update.

Qt Advent Calendar 2016 3日目の記事です。


要約

QtのシグナルとイベントをObservableに変換するライブラリRxCppで作りました。


Reactive Extensions

Reactive Extensions(Rx)の解説はすでに無数にあるので省略します。

ここでは、RxのC++実装であるRxCppを使用します。


Observableとシグナル

ObservableとQtのシグナル/スロットは、どちらもObserverパターンである点で同じですが、RxではObservableから新たなObservableを作る関数が豊富に用意されているという利点があります。また、言語を超えて同じRxのAPIが使えるというのも(Rxを知っている人には)学習コストが低くなり嬉しいです。

そこで、シグナルをObservableへ変換します。

auto lineedit = new QLineEdit;

rxcpp::observable<QString> textChangedStream = rxcpp::observable<>::create<QString>(
[lineedit](rxcpp::subscriber<QString> s){
QObject::connect(
lineedit,
&QLineEdit::textChanged,
[s](const QString& v){ s.on_next(v); }
);
}
);

RxCppでobservable<T>を作るファクトリー関数の1つにcreate関数があります。create関数はsubscriber<T>を受け取る関数を引数に取ります。渡した関数の中で引数のsubscriber<T>on_next関数に渡した値がobservable<T>に流れます。

上の例では、s.on_next("Hello Observable");とすることで"Hello Observable"が流れます。

あとはシグナルが来た時にon_nextを呼べばよく、シグナルをon_nextを呼ぶラムダ式へconnectすることでobservable<T>に変換できます。


Observableとイベント

Qtにはシグナルの他にイベントもあります。イベントもObservableに変換できれば、シグナルとイベントを意識せずにObservableとして扱うことができて便利そうです。

Qtでイベントハンドリングをする方法は、イベントハンドラーをオーバーライドする方法の他に、外部からイベントフィルターを設定してハンドリングする方法があります。

次のようなイベントフィルターを作っておき、

class EventFilter: public QObject {

public:
EventFilter(QObject* parent, QEvent::Type type, rxcpp::subscriber<QEvent*> s)
: QObject(parent), type(type), s(s) {}
~EventFilter(){
s.on_completed();
}

bool eventFilter(QObject* obj, QEvent* event){
if(event->type() == type){
s.on_next(event);
}
return QObject::eventFilter(obj, event);
}
private:
QEvent::Type type;
rxcpp::subscriber<QEvent*> s;
};

目的のQObjectにインストールすることでイベントをObservableに変換できます。

auto lineedit = new QLineEdit;

rxcpp::observable<QEvent*> keyPressStream = rxcpp::observable<>::create<QEvent*>(
[](rxcpp::subscriber<QEvent*> s){
lineedit->installEventFilter(new EventFilter(lineedit, QEvent::KeyPress, s));
}
);


RxQt

シグナルとイベントをObservableに変換する処理をその都度書くのは大変なのでライブラリ化しました。


注意

RxCppが動けば大丈夫だと思いますが、まだVisual C++ 2015でしか試していません。


API


from_signal

template<class Q, class Ret>

rxcpp::observable<long>
rxqt::from_signal(QObject* object, Ret(Q::*)(void))

template<class Q, class Ret, class Arg>
rxcpp::observable<std::remove_const<std::remove_reference<Arg>::type>::type
rxqt::from_signal(QObject* object, Ret(Q::*)(Arg))

template<class Q, class Ret, class ...Args>
rxcpp::observable<std::tuple<std::remove_const<std::remove_reference<Args>::type>::type...>
rxqt::from_signal(QObject* object, Ret(Q::*)(Args...))

シグナルをObservableに変換します。observableの型はシグナルの引数の型から推論されます。ただし、引数についていた参照とconstは外れます。


  • 0引数の場合

    rxcppでobservable<void>は作れないため、observable<long>になります(値はシグナルが届いた回数)。

  • 1引数の場合

    observable<(引数の型)> になります。

  • n引数の場合(n>1)

    observable<std::tuple<(引数の型)>> になります。


example

auto lineedit = new QLineEdit;

// returnPressedシグナルの型は void (QLineEdit::*)(void) なので、
// observable<long> になる
rxcpp::observable<long> returnPressedStream
= rxqt::from_signal(lineedit, &QLineEdit::returnPressed)

// textChangedシグナルの型は void (QLineEdit::*)(const QString&) なので、
// observable<QString> になる
rxcpp::observable<QString> textChangedStream
= rxqt::from_signal(lineedit, &QLineEdit::textChanged);

// cursorPositionChangedシグナルの型は void (QLineEdit::*)(int, int) なので、
// observable<std::tuple<int, int>> になる
rxcpp::observable<std::tuple<int, int>> cursorPositionChangedStream
= rxqt::from_signal(lineedit, &QLineEdit::cursorPositionChanged);



from_event

rxcpp::observable<QEvent*>

rxqt::from_event(QObject* object, QEvent::Type type)

イベントをObservableに変換します。


example

auto lineedit = new QLineEdit;

rxqt::from_event(lineedit, QEvent::KeyPress)
.subscribe([](const QEvent* e){
auto ke = static_cast<const QKeyEvent*>(e);
qDebug() << ke->key();
});



サンプル

2つのボタンを2つともクリックしたら「両方押した」と出力するサンプルです。


example

auto button1 = new QPushButton("A");

auto button2 = new QPushButton("B");

rxqt::from_signal(button1, static_cast<void(QPushButton::*)(bool)>(&QPushButton::clicked))
.zip(rxqt::from_signal(button2, static_cast<void(QPushButton::*)(bool)>(&QPushButton::clicked)))
.subscribe([](auto&&){ qDebug() << "両方押した"; });


(オーバーロードされているとメンバー関数ポインタに対してstatic_castが必要になるのが少々残念)


まとめ

シグナルとイベントをObservableに変換することでQtでRxが使えます。