9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

QtAdvent Calendar 2020

Day 5

Qtのイベント周りをざっくり見てみよう

Posted at

はじめに

事前に予約しておいても投稿が遅いhermit4です。毎度毎度ごめんなさい。

昨日は、 @kanryu さんによる「QMLを他のフレームワークと比較する(Cordova, Xamarin, React Native, Flutter)」でした。フレームワークという括りですと、表題としてはQt/QtQuickの方が良いような気もしますが、ともあれ他のクロスプラットフォーム向けフレームワークとの比較ということで興味深い記事でしたね。

KDE Free Qt Foundationについてで述べた通り、若干きな臭い話もありますし、企業が中心でメンテナンスされているオープンソースは、常に他の選択肢が有ることも重要ですので、各フレームワークとも切磋琢磨しながらより良い未来を築いてほしいものです。

本日の題材

先日、「QCoreApplication系を見直してみよう」ということで、QCoreApplication系を見直した際、イベント処理周りが色々でてきて十分な検証不足のまま記載してしまいました。そのため、本日は少しイベント周りを掘り下げて調べて見たいと思います。

一部は、「Qtでスレッドを使う前に知っておこう」で書いた内容とかぶりますが、イベントループということで大事なところですし、ご容赦ください。

イベント駆動

一般的にGUIアプリケーションは、イベント駆動型プログラミングが中心となります。QtはCUI向けに利用することももちろん可能なのですが、Qt/Qt Quickは、GUI向けのフレームワークとして発展してきたものですから、一般的なGUIフレームワークと同じくイベント駆動型のプログラミングを行えるように設計されています。

イベント駆動型プログラミングとは、プログラムが起動後にイベントを待ち合わせ、イベントが発生する毎にその処理を行った後、またイベントを待ち合わせるという処理をループさせる、発生するイベントに対する受動的に処理を行うプログラミングパラダイムです。

まずは、簡単なチュートリアルのコードをみてみましょう。

main.cpp
#include <QtWidgets>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QWidget window;
    window.resize(320, 240);
    window.setWindowTitle
          (QApplication::translate("childwidget", "Child widget"));
    window.show();

    QPushButton *button = new QPushButton(
        QApplication::translate("childwidget", "Press me"), &window);
    QObject::connect(button, SIGNAL(clicked(bool)), &window, SLOT(close()));
    button->move(100, 100);
    button->show();
    return app.exec();
}

このコードをビルドし実行すると、画面上にPress meというボタンが1つだけあるウィジェットが表示されます。

このプログラムは、起動しただけではウィジェットが表示されるだけで何もしません。例えば、Press meのボタンの上で”マウスがクリックされた”というイベントが発生してはじめて、QPushButtonクラスがイベントを処理し、clicked(bool)シグナルを発行し、それをQWidgetクラスが受信して、closeというスロット処理を実行することで、全てのウィンドウが閉じた事に伴いQApplicationのquitが呼び出されて終了するという動作をします。

Qtのイベント機構

先日の記事では、QCoreApplication系が、メインイベントループを作るとだけ説明しました。そこでまずはイベントループとは何かから見ていきます。

イベントループ

イベント駆動において、イベントを待ち合わせ、イベントがきたら処理し、またイベントを待ち合わせるというループ処理をイベントループと呼びます。サンプルコードではapp.exec()がイベントループを生成することになります。

このイベントループは、概念的には以下のようなものです(wikiから抜粋)。

    while (is_active) {
        while (!event_queue_is_empty)
            dispatch_next_event();

        wait_for_more_events();
    }

まずは、QCoreApplicationのソースコードを見てみましょう。

qcoreapplication.cpp
int QCoreApplication::exec()
{
    :
    QEventLoop eventLoop;
    :
    int returnCode = eventLoop.exec();
    :
    return returnCode;
}

将来的な分岐のためか、QApplication, QGuiApplicationともexec()が用意されていますが、現状、ほぼ親のexec()を呼び出しているだけです。
同様の処理は、マルチスレッドで利用するQThread::exec()にもあります。

qthread.cpp
int QThread::exec()
{
    :
    QEventLoop eventLoop;
    int returnCode = eventLoop.exec();
    :
    return returnCode;
}

メインイベントループを作るQCoreApplication::exec()も、スレッド用のイベントループを作るQThread::exec()も、共にQEventLoop::exec()が呼び出します。

そこでは、processEventsが呼び出され、最終的にイベントディスパッチャのprocessEventsがループで呼び出されて居ることがわかります。

qeventloop.cpp
int QEventLoop::exec(ProcessEventsFlags flags)
{
    :
    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);
    :
}


bool QEventLoop::processEvents(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    auto threadData = d->threadData.loadRelaxed();
    if (!threadData->hasEventDispatcher())
        return false;
    return threadData->eventDispatcher.loadRelaxed()->processEvents(flags);
}

上記のthreadDataは、Q_Dマクロを使った、d-pointerで実装されています。d-pointerの詳細は @task_jp さんの「d-pointer 化で開発効率を向上させよう!」をご覧下さい。

ここで参照しているthreadDataは、QEventLoopPrivateの親クラスであるQObjectPrivateで定義されています。

qobject_p.h
   QAtomicPointer<QThreadData> threadData;

このポインタは、全てのQObjectクラスが有しており、QObjectのコンストラクト時やmoveToThread時に設定されます。QObjectの親が居る場合は、親の、いない場合は生成された現在のスレッドのQThreadDataのポインタになるよう実装されています。

qobject.cpp
QObject::QObject(QObjectPrivate &dd, QObject *parent)
    : d_ptr(&dd)
{
    :
    auto threadData = (parent && !parent->thread()) ? parent->d_func()->threadData.loadRelaxed() : QThreadData::current();
    threadData->ref();
    d->threadData.storeRelaxed(threadData);
    :

このQThreadData::current()は、WindowsとUNIXとでコードが分かれていますが、基本的にスレッド毎にQThreadDataが生成・保持されるように実装されています。QThreadDataは、以下のようにイベントディスパッチャのポインタを保持しています。

qthread_p.h
   QAtomicPointer<QAbstractEventDispatcher> eventDispatcher;

長々と解説しましたが、QEventLoopの提供するイベントループはスレッド毎のイベントディスパッチャでprocessEventが行います。

threadsandobjects.png
*この画像は、Qtの公式ドキュメントのものです。

イベントディスパッチャ

Dispatcherとは、運行管理者、作業管理者、工程管理者などを意味する単語です。必要なリソースを必要な場所に割り振る担当者であり、イベントディスパッチャは、イベントを適切な受け入れ先へ配送する役割を担います。

マウス操作、キーボード入力などユーザー側のアクションはまずOSにより検出され必要なプログラムに配信されます。そのため、イベントディスパッチャはプラットフォーム毎に異なる実行が行われます。

QAbstractEventDispatcherを継承しているクラスは以下のようなものがあります。

alt

イベントの伝わり方とイベントハンドラ

例に出したので、QPushButtonクラスのイベントを題材にします。

alt

ボタンを押すと以下のような処理が上から順に実施されていきます。

  1. [プラットフォーム依存] DispatcherのprocessEvents()
  2. [プラットフォーム依存] DispatcherのprocessPostedEvent()
  3. QWindowSystemInterface::sendWindowSystemEvents()
  4. QGuiApplicationPrivate::processWindowSystemEvent(event);
  5. QGuiApplicationPrivate::processMouseEvent(event);
  6. QGuiApplication::sendSpontaneousEvent(window, &ev);
  7. QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event);
  8. QApplication::notify(QObject *receiver, QEvent *event);
  9. QApplicationPrivate::notify_helper(QObject *receiver, QEvent * e)
  10. QWidgetWindow::event(QEvent * event)
  11. QWidgetWindow::handleMouseEvent(QMouseEvent *event)
  12. QApplicationPrivate::sendMouseEvent(...)
  13. QApplication::sendSpontaneousEvent(receiver, event)
  14. QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event);
  15. QApplication::notify(QObject *receiver, QEvent *event);
  16. QApplicationPrivate::notify_helper(QObject *receiver, QEvent * e)
  17. QPushButton::event(QEvent *event)
  18. QAbstractButton::event(QEvent *event)
  19. QWidget::event(QEvent *event)
  20. QAbstractButton::mouseReleaseEvent(QMouseEvent *e)
  21. QAbstractButtonPrivate::click()
  22. emit clicked();

簡単に解説すると、プラットフォーム依存のイベント処理で例えばWindowSystemのイベントが起こった場合、Window Systemのイベントを送付するクラスを経由して、適切なQEvent系クラス(この例では、QMouseEvent)を作成し、QCoreApplicationのイベント配送のための関数を通じ、QObject::event() へと渡されます。配送先は、上記の例ですとQGuiApplicationのprocessMouseEventが受け取るべき対象(この場合は、QWidgetWindow)を見つけて、マウスイベントを配信しています。

QWidgetWindowもまた、自分の子でイベントを受信すべき対象を見つけて配送し、イベントは親から子へ、子から孫へと受信すべき相手が見つかるまで受け渡されて行きます。

また、このとき配信されるイベントに応じて適切な処理を行うイベントハンドラが呼び出されます。上記の例では、マウスボタンをリリースするイベントであったため、最終的にはQAbstractButtonのmouseReleaseEventというハンドラ呼び出しが行われています。

ボタンが押される(ボタンがクリックされる)という挙動は、正確にはボタンと認識される画像の座標範囲内で、マウスが押された後、同じ範囲内で、マウスが放されるというユーザーのアクションで実行されます。上位の方では、WindowSystemのイベント向けの処理ですが、先に進むにつれて、それがボタンと認識されているウィジェットの上でマウスボタンがリリースされたというイベントに変わって言っている事が上記の流れでわかるかと思います。

イベントフィルタ

ところで、Qtにはイベントフィルタという機能がある。QObjectのeventFilter関数をオーバーライドし、イベントを到達前に受信したり、フィルタしたりする機能があります。

QObjectのイベントフィルタ

QObjectには、eventFilter()関数が用意されています。

qobect.h
virtual bool QObject::eventFilter(QObject *watched, QEvent *event);

ちなみに、QObjectの実装では、このクラスは何もしません。

qobject.cpp
bool QObject::eventFilter(QObject * /* watched */, QEvent * /* event */)
{
    return false;
}

イベントフィルタを使いたい場合、自身でQObject(あるいは、その子クラス)を継承するクラスを作り、eventFilterを実装したうえで、イベント配送前にキャッチしたいQObject系クラスのinstallEventFilter関数を使ってインストールする事になります。

void installEventFilter(QObject *filterObj);

ちなみに、QCoreApplicationもQObject系ですので、インストール対象とすることができますが、この場合は、特定のオブジェクトへのイベントではなく、全てのオブジェクトに対するイベントに対し、eventFilter関数が呼び出される事になります。

このフィルタの呼び出しは、イベントを他のイベントに送付するQCoreApplicationPrivateのnotify_helperで実装されています。

bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
{
    // Note: when adjusting the tracepoints in here
    // consider adjusting QApplicationPrivate::notify_helper too.
    Q_TRACE(QCoreApplication_notify_entry, receiver, event, event->type());
    bool consumed = false;
    bool filtered = false;
    Q_TRACE_EXIT(QCoreApplication_notify_exit, consumed, filtered);

    // send to all application event filters (only does anything in the main thread)
    if (QCoreApplication::self
            && receiver->d_func()->threadData->thread.loadAcquire() == mainThread()
            && QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
        filtered = true;
        return filtered;
    }
    // send to all receiver event filters
    if (sendThroughObjectEventFilters(receiver, event)) {
        filtered = true;
        return filtered;
    }

    // deliver the event
    consumed = receiver->event(event);
    return consumed;
}

メインスレッドに属する場合は、QCoreApplication系にinstallされたイベントフィルタを試した後、QObjectにインストールされたフィルタを試して、イベントフィルタがtrueを返した場合は、対象の受信者にイベントを送付せず終了している事がわかりますね。

ネイティブイベントフィルタ

QCoreApplicationには、もう一つネイティブのイベントフィルタがあることを先日の記事で紹介しました。

  void installNativeEventFilter(QAbstractNativeEventFilter * filterObj);
  void removeNativeEventFilter(QAbstractNativeEventFilter * filterObject);

これはそのまま、QAbstractEventDispatcherの同名の関数呼び出しが行われており、Event Dispatcherにインストールされ,
QAbstractEventDispatcher::filterNativeEventで呼び出されます。この関数は、各プラットフォーム向けのディスパッチャーの実装から呼び出されており、OSからイベントが上がった段階でフィルタされる事になります。

イベントの配送

イベントループの中では、アプリケーションの外部(OS)からのイベントを受け取り、イベントに変換して配送していますが、アプリケーション内部でもイベントを配送したい場合があります。このときに利用する関数もQCoreApplicationに用意されています。

static bool	sendEvent(QObject *receiver, QEvent *event);
static void	postEvent(QObject *receiver, QEvent *event, int priority = Qt::NormalEventPriority);
static void	removePostedEvents(QObject *receiver, int eventType = 0);
static void	sendPostedEvents(QObject *receiver = nullptr, int event_type = 0);

sendとpostの違いは、先日の記事でも紹介しましたが、sendが即時配送(配送が終わって初めて処理が戻る)であり、postがキューに積んで即時戻り、イベントループの中で適時配送されると言うことにあります。
post されたイベントはキューにある間は削除することも出来ますし、即時配信するよう指示することもできます。

なお、postされたイベントですが、優先度に応じて配信順序が制御され、イベントによっては圧縮されるケースがあります。例えばウィンドウ枠のサイズを拡大・縮小が行われている最中に大量のサイズ変更イベントがある場合、これらが圧縮されます。またペイントイベントも、描画よりも頻度の高いイベントには意味が無いため適度な圧縮が行われるそうです。

なお、postEvent()については、オブジェクトの初期化中にも呼び出しが可能で、オブジェクトの生成が完了した直後にディスパッチされる場合もあります。カスタムウィジェットを実装する場合には、イベントが予期せず早くにイベントを受け取る可能性にも考慮が必要になります。

まとめ

本日はQtのイベント周りについて駆け足で眺めてみた内容を書き付けてみました。
まぁ、普段あまり真面目に追うような場所ではありませんが、イベントがどこで作られ、どのように配信されていくのかを理解していると、謎な挙動をした際の調査に役立つかと思います。例によっていまいちまとまりのない記事で申し訳ありませんが、この記事が皆様の楽しいプログラミングライフの一助となれば幸いです。

明日は @shin1_okada さんが何か書いてくださるようなので期待しましょう。

9
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?