シグナル&スロット

導入

GUIプログラミングでは、あるウィジェットを変更したときに、別のウィジェットに通知させたいことがよくある。より一般的には、あらゆる種類のオブジェクトが互いに通信できるようにしたい。例えば、ユーザがClose ボタンをクリックしたら、ウィンドウのclose() 関数を呼び出したいでしょう。

他のツールキットでは、コールバックを使ってこのような通信を実現しています。コールバックとは関数へのポインタのことで、処理関数に何らかのイベントを通知させたい場合は、別の関数(コールバック)へのポインタを処理関数に渡します。処理関数は、適切なときにコールバックを呼び出す。このメソッドを使ったフレームワークも存在するが、コールバックは直感的でないことがあり、コールバック引数の型が正しくないという問題がある。

シグナルとスロット

Qtでは、コールバックに代わる手法がある:シグナルとスロットを使います。シグナルは、特定のイベントが発生したときに発せられます。Qtのウィジェットには定義済みのシグナルがたくさんありますが、ウィジェットをサブクラス化して独自のシグナルを追加することもできます。スロットは、特定のシグナルに反応して呼び出される関数です。Qt のウィジェットには多くの定義済みスロットがありますが、ウィジェットをサブクラス化して独自のスロットを追加し、興味のあるシグナルを扱えるようにするのが一般的です。

シグナルとスロットのメカニズムはタイプセーフです:シグナルのシグネチャは、受信スロットのシグネチャと一致しなければなりません。(実際、スロットは余分な引数を無視できるので、受信するシグナルよりも短いシグネチャを持つことができる)。シグネチャは互換性があるので、関数ポインタ・ベースの構文を使用する場合、コンパイラは型の不一致を検出するのに役立ちます。文字列ベースのSIGNALとSLOT構文は、実行時に型の不一致を検出します。シグナルとスロットは疎結合である:シグナルを発信するクラスは、どのスロットがシグナルを受信するかは知りませんし、気にしません。Qtのシグナルとスロットのメカニズムは、シグナルをスロットに接続すると、そのスロットが適切なタイミングでシグナルのパラメータで呼び出されることを保証します。シグナルとスロットは、任意の型の引数をいくつでも取ることができます。これらは完全に型安全です。

QObject 、またはそのサブクラス(例えば、QWidget )を継承するすべてのクラスは、シグナルとスロットを含むことができます。シグナルは、オブジェクトが他のオブジェクトにとって興味深い方法で状態を変化させたときに発せられます。オブジェクトが行うコミュニケーションはこれだけだ。オブジェクトは、自分が発するシグナルを何かが受信しているかどうかは知らないし、気にもしない。これは真の情報のカプセル化であり、オブジェクトをソフトウェア・コンポーネントとして使用できることを保証する。

スロットはシグナルを受信するために使うこともできるが、通常のメンバー関数でもある。オブジェクトがシグナルを受信しているかどうかを知らないように、スロットもシグナルが接続されているかどうかを知りません。このため、Qtでは真に独立したコンポーネントを作成することができます。

1つのスロットにはいくつでもシグナルを接続することができ、1つのシグナルは必要なだけ多くのスロットに接続することができます。シグナルを別のシグナルに直接接続することも可能です。(この場合、最初のシグナルが発せられるとすぐに2番目のシグナルが発せられます)。

シグナルとスロットを組み合わせることで、強力なコンポーネント・プログラミングのメカニズムが構成されます。

シグナル

シグナルは、オブジェクトの内部状態が変化し、そのオブジェクトのクライアントやオーナーが興味を持ちそうな場合に、オブジェクトから発信されます。シグナルはパブリックアクセス関数であり、どこからでも発することができますが、シグナルを定義したクラスとそのサブクラスからのみ発することをお勧めします。

シグナルが発信されると、それに接続されたスロットは通常、通常の関数呼び出しのように即座に実行されます。この場合、シグナルとスロットのメカニズムはGUIのイベントループから完全に独立しています。すべてのスロットがリターンすると、emit ステートメントに続くコードが実行されます。queued connectionsこの場合、emit キーワードに続くコードは直ちに実行され、スロットは後で実行されます。

複数のスロットが1つのシグナルに接続されている場合、シグナルが発せられると、接続された順番にスロットが実行されます。

シグナルはmocによって自動的に生成されるので、.cpp

引数についての注意:これまでの経験から、シグナルやスロットは特殊な型を使わない方が再利用しやすいことがわかっています。QScrollBar::valueChanged() が仮にQScrollBar::Rangeのような特殊な型を使用する場合、QScrollBar のために特別に設計されたスロットにしか接続できません。異なる入力ウィジェットを接続することは不可能です。

スロット

スロットに接続されたシグナルが発信されると、スロットが呼び出されます。スロットは通常のC++関数であり、普通に呼び出すことができる。唯一の特徴は、スロットにシグナルを接続できることである。

スロットは通常のメンバ関数なので、直接呼び出す場合は通常のC++のルールに従います。しかし、スロットである以上、シグナルとスロットの接続を介して、アクセス・レベルに関係なく、どのコンポーネントからも呼び出すことができます。つまり、任意のクラスのインスタンスから発せられるシグナルによって、関係のないクラスのインスタンスでプライベート・スロットを呼び出すことができる。

スロットを仮想的に定義することもできます。

コールバックと比較すると、シグナルとスロットは柔軟性が増すため若干遅くなるが、実際のアプリケーションではその差は些細なものだ。一般的に、いくつかのスロットに接続されたシグナルを発すると、非仮想関数呼び出しで直接レシーバーを呼び出すよりも約10倍遅くなる。これは、接続オブジェクトを見つけ、すべての接続を安全に反復し(つまり、放出中に後続のレシーバーが破壊されていないことをチェックし)、汎用的な方法で任意のパラメータをマーシャルするために必要なオーバーヘッドです。10回の非仮想関数呼び出しは多いように聞こえるかもしれませんが、例えばnewdelete の操作に比べればはるかに少ないオーバーヘッドです。newdelete 、シグナルとスロットのオーバーヘッドは、関数呼び出しコストのごく一部に過ぎません。スロット内でシステムコールを行ったり、10個以上の関数を間接的に呼び出したりする場合も同様です。シグナルとスロットの仕組みのシンプルさと柔軟性は、オーバーヘッドに見合うだけの価値があります。

signalsslots と呼ばれる変数を定義している他のライブラリは、Qtベースのアプリケーションと一緒にコンパイルすると、コンパイラの警告やエラーを引き起こす可能性があることに注意してください。この問題を解決するには、#undef 、問題のあるプリプロセッサシンボルを指定してください。

小さな例

最小限のC++クラス宣言は次のようになります:

class Counter
{
public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }
    void setValue(int value);

private:
    int m_value;
};

小さなQObject ベースのクラスは次のようになります:

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

// Note. The Q_OBJECT macro starts a private section.
// To declare public members, use the 'public:' access modifier.
public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

QObject ベース・バージョンは同じ内部状態を持ち、その状態にアクセスするためのパブリック・メソッドを提供するが、それに加えてシグナルとスロットを使ったコンポーネント・プログラミングをサポートしている。このクラスは、valueChanged() というシグナルを発することで、自分の状態が変化したことを外部に伝えることができる。また、他のオブジェクトがシグナルを送ることができるスロットも持っている。

シグナルやスロットを含むクラスはすべて、宣言の先頭にQ_OBJECT と記述しなければならない。また、QObject から(直接的または間接的に)派生しなければなりません。

スロットはアプリケーション・プログラマーによって実装されます。以下は、Counter::setValue() スロットの実装例です:

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

emit 行は、新しい値を引数として、オブジェクトからシグナルvalueChanged() を発する。

次のコード・スニペットでは、2つのCounter オブジェクトを作成し、QObject::connect ()を使用して、最初のオブジェクトのvalueChanged() シグナルを2番目のオブジェクトのsetValue() スロットに接続します:

    Counter a, b;
    QObject::connect(&a, &Counter::valueChanged,
                     &b, &Counter::setValue);

    a.setValue(12);     // a.value() == 12, b.value() == 12
    b.setValue(48);     // a.value() == 12, b.value() == 48

a.setValue(12) を呼び出すと、avalueChanged(12) シグナルを発信し、bsetValue() スロットでそれを受信する、つまりb.setValue(12) が呼び出される。b は同じvalueChanged() シグナルを発するが、bvalueChanged() シグナルに接続されたスロットはないので、シグナルは無視される。

setValue() 関数は、value != m_value が呼び出された場合にのみ値を設定し、シグナルを発することに注意。これは、周期的な接続の場合(例えば、b.valueChanged()a.setValue() に接続されている場合)の無限ループを防ぐためである。

デフォルトでは、接続するたびにシグナルが発せられ、重複接続の場合は2つのシグナルが発せられます。これらの接続は、1回のdisconnect() 呼び出しですべて切断できる。Qt::UniqueConnection type を渡すと、重複接続でない場合のみ接続が行われる。すでに重複(同じオブジェクトの同じスロットにまったく同じシグナル)がある場合、接続は失敗し、connectはfalse を返します。

この例は、オブジェクトが互いの情報を知らなくても一緒に動作できることを示しています。これを可能にするには、オブジェクト同士を接続するだけでよく、簡単なQObject::connect ()関数呼び出しやuicの 自動接続機能で実現できる。

実際の例

以下は、メンバ関数のない単純なウィジェット・クラスのヘッダの例です。これは、シグナルとスロットをアプリケーションでどのように利用できるかを示すためのものです。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber QFrame と を経由して、シグナル・スロットの知識のほとんどを持つ を継承しています。 これは、組み込みの ウィジェットに多少似ています。QWidget QObject QLCDNumber

Q_OBJECT マクロはプリプロセッサによって展開され、moc によって実装されるいくつかのメンバ関数を宣言する。"undefined reference to vtable forLcdNumber" のようなコンパイラ・エラーが出る場合は、おそらくmoc を実行するのを忘れているか、リンク・コマンドに moc の出力を含めるのを忘れている。

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

クラス・コンストラクタとpublic メンバの後、signals クラスを宣言する。LcdNumber クラスは、不可能な値を表示するように要求されると、シグナルoverflow() を発する。

オーバーフローを気にしない場合、またはオーバーフローが発生しないとわかっている場合は、overflow() シグナルを無視することができます。

一方、数値がオーバーフローしたときに2つの異なるエラー関数を呼び出したい場合は、シグナルを2つの異なるスロットに接続するだけです。Qtは(接続した順番に)両方を呼び出します。

public slots:
    void display(int num);
    void display(double num);
    void display(const QString &str);
    void setHexMode();
    void setDecMode();
    void setOctMode();
    void setBinMode();
    void setSmallDecimalPoint(bool point);
};

#endif

スロットは、他のウィジェットの状態変化に関する情報を取得するために使用される受信関数です。LcdNumber 、上のコードが示すように、表示される数値を設定するために使用します。display() は、プログラムの他の部分とのクラスのインターフェースの一部なので、スロットはパブリックです。

サンプル・プログラムのいくつかは、QScrollBarvalueChanged() シグナルをdisplay() スロットに接続し、LCD の数値が継続的にスクロール・バーの値を示すようにしています。

display() シグナルをスロットに接続すると、Qtは適切なバージョンを選択します。コールバックを使用する場合、5つの異なる名前を見つけ、型を自分で管理する必要があります。

デフォルト引数のシグナルとスロット

シグナルとスロットのシグネチャには引数を含めることができ、引数にはデフォルト値を指定することができます。QObject::destroyed()を考えてみよう:

void destroyed(QObject* = nullptr);

QObject が削除されると、このQObject::destroyed() シグナルが発信される。削除されたQObject への参照がぶら下がっ ている可能性がある場合、このシグナルをキャッチし、それをクリーンアップできるようにしたい。適切なスロットのシグネチャは次のようになります:

void objectDestroyed(QObject* obj = nullptr);

シグナルをスロットに接続するには、QObject::connect()を使います。シグナルとスロットを接続する方法はいくつかあります。1つ目は、関数ポインタを使う方法です:

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

QObject::connect ()を関数ポインタと一緒に使うことにはいくつかの利点があります。まず、シグナルの引数がスロットの引数と互換性があるかどうかをコンパイラがチェックできます。また、必要に応じて、コンパイラが暗黙のうちに引数を変換することもできます。

ファンクタやC++11ラムダに接続することもできます:

connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });

どちらの場合も、connect() の呼び出しで、this をコンテキストとして提供します。コンテキスト・オブジェクトは、レシーバーがどのスレッドで実行されるべきかという情報を提供する。コンテキストを提供することで、レシーバーがコンテキスト・スレッドで実行されるようになるため、これは重要である。

送信者やコンテキストが破棄されると、ラムダは切断される。ファンクタの内部で使用されているオブジェクトが、シグナルが発せられたときにまだ生きているように注意する必要があります。

シグナルをスロットに接続するもう1つの方法は、QObject::connect ()とSIGNALSLOT マクロを使うことです。引数にデフォルト値がある場合、SIGNAL()SLOT() マクロに引数を含めるかどうかのルールは、SIGNAL() マクロに渡されるシグネチャは、SLOT() マクロに渡されるシグネチャより少ない引数を持ってはならないということです。

これらはすべて機能する:

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

しかし、これはうまくいかない:

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

...スロットはシグナルが送信しないQObject 。この接続はランタイムエラーを報告します。

このQObject::connect() オーバーロードを使用する場合、コンパイラによってシグナルとスロットの引数がチェックされないことに注意してください。

高度なシグナルとスロットの使用法

シグナルの送信者に関する情報が必要な場合、Qt はQObject::sender() 関数を提供します。この関数は、シグナルを送信したオブジェクトへのポインタを返します。

ラムダ式は、カスタム引数をスロットに渡す便利な方法です:

connect(action, &QAction::triggered, engine,
        [=]() { engine->processAction(action->text()); });

Qtとサードパーティのシグナルとスロットを使う

Qtとサードパーティのシグナルやスロットを併用することは可能です。同じプロジェクトで両方のメカニズムを使うこともできます。そのためには、CMakeのプロジェクトファイルに次のように記述します:

target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)

qmakeプロジェクトファイル(.pro)に次のように記述します:

CONFIG += no_keywords

これはQtにmocキーワードsignalsslotsemit を定義しないように指示するもので、これらの名前はBoostなどのサードパーティライブラリで使用されるためです。Qt のシグナルとスロットをno_keywords フラグで使い続けるには、ソース中の Qt moc キーワードを、対応する Qt マクロQ_SIGNALS (またはQ_SIGNAL)、Q_SLOTS (またはQ_SLOT)、Q_EMIT に置き換えるだけです。

Qtベースのライブラリにおけるシグナルとスロット

Qt ベースのライブラリのパブリック API では、signalsslots の代わりに、Q_SIGNALSQ_SLOTS というキーワードを使うべきです。そうしないと、QT_NO_KEYWORDS を定義しているプロジェクトで、そのようなライブラリを使用することが難しくなります。

この制限を強制するために、ライブラリの作成者は、ライブラリをビルドするときにプリプロセッサ定義QT_NO_SIGNALS_SLOTS_KEYWORDS を設定することができます。

この定義により、シグナルとスロットは除外されますが、他のQt固有のキーワードがライブラリの実装で使用できるかどうかは変わりません。

QLCDNumber,QObject::connect(),Meta-Object System,Qt's Property Systemも参照してください

本書に含まれる文書の著作権は、それぞれの所有者に帰属します 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。