Return to Linux Life Edit

  ホーム · 全てのクラス · メインのクラス · 注釈付き · グループ別 · 関数一覧

シグナルとスロット

シグナルとスロットはオブジェクト同士のやりとりに使われます。シグナル・スロット機構は Qt の中心的な機能であり、おそらく他のフレームワークによって提供される機能とは大きく異なっている部分です。

はじめに

GUI プログラミングでは、あるウィジェットの変更を他のウィジェットにも通知したい場面があります。より一般的にいうと、あらゆるオブジェクト間でやりとりをしたいのです。例えば、ユーザーが Close ボタンをクリックしたら、ウインドウの close() 関数を呼び出したい、というようにです。

以前のツールキットではこれらのやりとりをコールバック関数を用いて実現していました。コールバックは関数のポインタであり、なんらかのイベントを通知してもらいたい場合はその処理をする関数にコールバック関数のポインタを渡します。処理関数は適切な時にその関数を呼び出すのです。しかし、コールバック関数には基本的な欠陥が2つあります:ひとつは、タイプ・セーフでは無いことです。コールバック関数が正しい引数で呼び出されるかどうか確かめる術はありません。もうひとつは、処理を行っている関数はコールバック関数について知らなければならず、コールバック関数と処理を行う関数とに強固な結合関係が発生してしまう、ということです。

シグナルとスロット

Qt では、コールバック関数の代わりとなるテクニックを用意しました:それがシグナルとスロットです。シグナルは特定のイベントが発生すると発行されます。Qt のウィジェットは既に多くの定義済みシグナルを持っていますが、サブクラスを作成することで自由に追加することも可能です。スロットはシグナルに対する反応として呼び出される関数です。Qt のウィジェットは既に多くのスロットについても定義済みですが、興味のあるシグナルを処理するためにサブクラスを作成して独自のスロットを追加することは、ごく当り前に行われます。

シグナル・スロット機構はタイプ・セーフです。シグナルの引数の型は受信側のスロットの引数の型と一致しなければなりません。(実際には余分な引数は無視されるため、スロットの引数の数はシグナルの引数の数よりも少なくても良くなっています。) 引数の型の互換性をとるため、コンパイラは型の不整合を検出することができるのです。また、シグナルとスロットは緩く結合しています。シグナルを発行するクラスは、シグナルを受信するスロットについて知っている必要も、注意を払う必要もありません。Qt のシグナル・スロット機構は、シグナルをスロットに接続すれば、スロットはシグナルの引数で適切に呼ばれる、ということを保証します。シグナルとスロットはどんな型の引数を何個でもとることができ、それらは完全にタイプ・セーフです。

全てのクラスの基底である QObject や、そのサブクラス(例えば QWidget)はシグナルとスロットを含むことができます。シグナルはオブジェクトの状態が変更されたときにオブジェクトによって発行します。これがオブジェクト同士のやりとりの全てです。発行されたシグナルをなにが受信するかについては、知る必要も注意を払う必要もありません。これが真の情報カプセル化であり、オブジェクトがソフトウェアコンポーネントとして使えるという証にもなります。

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

ひとつのスロットに複数のシグナルを欲しい分だけ接続することも、逆にひとつのシグナルを必要な分だけのスロットに接続することもできます。シグナルを直接他のシグナルに接続することさえ可能です。(この場合、最初のシグナルが発行されるとすぐに次のシグナルを発行する、という動作になります。)

シグナルとスロットを組み合わせることで、強力なコンポーネントプログラミング機構を生成することができるのです。

簡単な例

まず、以下のような 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
    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を基底としたバージョンは、前記したC++クラスと同じ内部状態を持ち、その状態にアクセスする公開メソッドを提供したうえで、シグナルとスロットを使ったコンポーネントプログラミングをサポートしています。オブジェクトの内部状態が変わったことを外部に通知するために、シグナル 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 オブジェクトを生成し、最初のオブジェクトのシグナル valueChanged() を2つ目のオブジェクトのスロット setValue() QObject::connect() を使って接続しています。

        Counter a, b;
        QObject::connect(&a, SIGNAL(valueChanged(int)),
                         &b, SLOT(setValue(int)));
        a.setValue(12);     // a.value() == 12, b.value() == 12
        b.setValue(48);     // a.value() == 12, b.value() == 48

関数として a.setValue(12) を呼び出すと、 a はシグナル valueChanged(12) を呼び出し、それを b はスロット setValue() で受信します。そして、 b.setValue(12) が呼び出されます。同様にして b もシグナル valueChanged() を通知しますが、 bのシグナル valueChanged() にはスロットが接続されていないため、無視されます。

この例で、 setValue() 関数は値を設定した後 value != m_valueの場合のみシグナルを通知していることに注意してください。これは、循環接続されているとき(例えば、 b.valueChanged() a.setValue()に接続されているとき)に無限ループに陥らないようにするためです。

シグナルはすべての接続先に通知されます。接続を複製した場合は、2つのシグナルが通知されるでしょう。接続はいつでも QObject::disconnect() を使って切断することができます。

この例ではオブジェクト同士が互いに中身を知り合う必要なく連携して動作できることを説明しました。これを実現するには、ただ互いを QObject::connect() 関数の呼び出し、または uic 自動接続 機能を使って接続すれば良いだけです。

例のコードのビルド

C++プリプロセッサは signals slots emit キーワードを置換もしくは削除して、Standard C++に適合させます。

まず moc をシグナルやスロットを含んだクラス定義にかけることで、コンパイル可能でアプリケーションのほかのオブジェクトとリンクできるC++ソースコードを生成します。ここで qmakeを使っていれば、プロジェクトの makefile に、自動的に moc を起動するルールを追加してくれます。

シグナル

シグナルは、オブジェクトの内部状態が変更されたときに、それを知りたがっているクライアントまたは所有者に対して通知されます。そのシグナルを定義したクラス、およびそのサブクラスのみがシグナルを通知可能です。

シグナルが通知されると、接続されたスロットは通常の関数呼び出しのように直ちに実行されます。つまり、シグナル・スロット機構はGUIのイベントループとは独立している、ということです。 emit 文によってすべてのスロットで一回だけコードが実行されます。これは キューをはさんで接続された場合では少し事情が異なります。この場合は emit キーワードの後ろの処理が即実行され、スロットの実行は後回しになります。

複数のスロットがひとつのシグナルに接続されている場合、シグナルの通知があるとスロットは任意の順番で実行されます。

シグナルは自動的に moc によって生成されるので、 .cpp ファイルの中で実装してはいけません。返り値を持たない(型が void)ものとします。

引数について:経験上、シグナルとスロットは特殊な型を使わないようにすればより再利用がしやすくなります。 QScrollBar::valueChangedが QScrollBar::Range のような仮想の型を使っている場合、 QScrollBarに接続できるのはそれ専用に作成されたスロットだけになります。そうなったら Tutorial 5 にあるような単純なコードを書くのは不可能です。

スロット

スロットは接続しているシグナルが通知されたときに呼び出されます。スロットはC++の関数でもあるので、普通に呼び出すことも可能です。シグナルに接続できる、という点だけが特殊といえます。スロットの引数はデフォルト値を持つことができず、シグナルと同様に特殊な型を引数とすることはほとんどありません。

スロットはちょっと違った、普通のメンバー関数なので、他のメンバー関数と同様アクセス制限を課すことができます。スロットのアクセス制限は誰が接続できるのか、ということを以下のように決定します。

スロットは仮想関数としても定義できます。これはいくつかの場面でとても有用です。

コールバックに比べるとシグナルとスロットはやや遅くなります.それは高い柔軟性の代償ですが,実際のアプリケーションではあまり違いがないでしょう.一般に,何らかのスロットに接続されたシグナルを通知するのは,レシーバを(仮想関数でなく)直に呼ぶのに比べ10倍ほど時間を食います.このオーヴァヘッドは接続オブジェクトを探し当て,全ての接続に渡って安全に繰り返し(すなわち以降のレシーバが通知の間に破棄されないようにする),ジェネリックな方法で全ての引数をマーシャライズするためのものです.非仮想関数の呼び出し10回分と言うと大げさですが, new delete といった処理に比べたら小さなものです。文字列や配列、リストといった裏で new deleteが必要な型の場合、シグナルとスロットのオーバーヘッドは全体の関数呼び出しのコストのほんの一部しか占めないでしょう。

スロット内でシステムコールを呼び出したり、10個以上の関数を経由して呼び出しているとしても同様のことが言えます。i586-500MHzのシステム上では、ひとつのスロットが接続されたシグナルで毎秒2,000,000回の通知が可能です。2つのスロットが接続されている場合は毎秒1,200,000回です。シグナル・スロット機構が持つ単純さと柔軟さは、ユーザーが気づかないくらいですが、オーバーヘッドに払う価値の分はあるでしょう。

他のライブラリで変数に signals slots と言った変数を宣言している場合、コンパイラはQtベースのアプリケーションと一緒にコンパイルすると警告やエラーを出力します。この問題を解決するには、 #undef でプリプロセッサの利用するシンボルを未定義にするなどの方法があります。

メタオブジェクトの情報

メタオブジェクトコンパイラ(moc)はC++ファイルにあるクラス定義を解析し、メタオブジェクトを初期化するC++コードを生成します。メタオブジェクトはメンバーのシグナルとスロット全部の名前と、その関数へのポインタを保持しています。

メタオブジェクトはまた、 class nameといったオブジェクトについての追加情報も保持しています。オブジェクトが特定のクラスを継承( inherits )している場合、例えば、以下のようなコードが記述できます。

        if (widget->inherits("QAbstractButton")) {
            QAbstractButton *button = static_cast<QAbstractButton *>(widget);
            button->toggle();
        }

メタオブジェクトが持つ情報は qobject_cast<T>() という QObject::inherits() に似た、よりエラー耐性のある機能にも利用されています。

        if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
            button->toggle();

詳細については Meta-Object System を参照してください。

実践的な例

ここで、コメントを付記したウィジェットのサンプルをあげます。

    #ifndef LCDNUMBER_H
    #define LCDNUMBER_H
    #include <QFrame>
    class LcdNumber : public QFrame
    {
        Q_OBJECT

LcdNumber はシグナルとスレッドについてのほとんどの処理を定義している QObject QFrame QWidgetを経由して継承しています。これは組み込みの QLCDNumber ウィジェットに似たクラスです。

まず最初の Q_OBJECT マクロはプリプロセッサによっていくつかのメンバー関数の宣言へと展開されます。これらの関数は mocで実装されます。この行で"undefined reference to vtable for LcdNumber" というコンパイルエラーが出た場合、おそらく moc を実行すること、 または moc の出力をリンクのコマンドに含めていないと思われます。

    public:
        LcdNumber(QWidget *parent = 0);

これは moc とは直接関係ありませんが、 QWidget を継承したクラスを定義する場合、 parent 引数として親ウィジェットをコンストラクタに渡し、基底クラスのコンストラクタに渡す必要があります。

いくつかのデストラクタとメンバー関数はここでは省略します。 moc はメンバー関数は無視します。

    signals:
        void overflow();

LcdNumber は表示不可能な値を表示するよう要求されるとシグナルを通知します。

オーバーフローを気にしない場合、またはオーバーフローは発生しないと明らかな場合は、このシグナル overflow() はスロットを接続しないことで無視できます。

反対に数値がオーバーフローしたときに二つの異なったエラー関数を呼び出したいときは、単にシグナルを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() は残りのプログラムにとってクラスインターフェースでもあるので、スロットは公開されています。

いくつかの例題プログラムではシグナル valueChanged() QScrollBar で定義され、スロット display() に接続されています。そうして、LCD number は連続してスクロールバーの値を表示しています。

ここで、 display() がオーバーロードされていることに注目してください。Qt ではシグナルとスロットを接続するときに適切なバーションを選択してくれます。コールバック関数を使用した場合だったら型にあった名前の関数を5つの中から選び出していなければいけないところです。

いくつかの無関係なメンバー関数についてはこの例では省略します。

詳細は Meta-Object System Qt's Property Systemを参照してください。


Copyright © 2005 Trolltech Trademarks
Qt 4.0.0