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<(引数の型)>>
になります。
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に変換します。
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つともクリックしたら「両方押した」と出力するサンプルです。
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が使えます。