Qtの信号とスロット

信号とスロットは、オブジェクト間の通信に使用されます。 シグナルとスロットのメカニズムはQtの主な機能であり、おそらく他のフレームワークが提供する機能とは異なる部分です。



はじめに



GUIプログラミングでは、1つのウィジェットを変更するときに、別のウィジェットに通知することがよくあります。 一般に、あらゆるタイプのオブジェクトが他のオブジェクトと通信できるようにします。 たとえば、ユーザーが[閉じる]ボタンをクリックした場合、おそらくウィンドウを閉じる()関数を呼び出す必要があります。

他のライブラリは、コールバックを使用してこの種の通信を実現します。 コールバックは関数へのポインターであるため、関数からイベントを通知する場合は、この関数の別の関数(コールバック)へのポインターを渡します。 関数は必要に応じてコールバックします。 コールバックには2つの主な欠点があります。 まず、タイプセーフではありません。 関数が正しい引数を使用してコールバックを行うことを確認することはできません。 第二に、コールバックはそれを呼び出す関数と密接に関連しています。なぜなら、この関数はどのコールバックを行うべきかを正確に知る必要があるからです。



信号とスロット



Qtは別の手法-シグナルとスロットを使用します。 特定のイベントが発生すると、信号が生成されます。 スロットは、特定の信号に応答して呼び出される関数です。 Qtウィジェットには多くの定義済みのシグナルとスロットがありますが、いつでも子クラスを作成してシグナルとスロットを追加できます。



信号とスロット



シグナルとスロットのメカニズムはタイプセーフです。 信号の署名は、受信スロットの署名と一致する必要があります。 (実際、スロットは、追加の引数を無視できるため、受信する信号よりも短いシグネチャを持つことができます)。 署名は同等であるため、コンパイラは型の不一致を検出するのに役立ちます。 信号とスロットは疎結合です。 信号を生成するクラスは、どのスロットがそれを受信するかを知らず、気にしません。 シグナルとQtスロットのメカニズムにより、シグナルをスロットに接続すると、適切なタイミングでシグナルパラメータでスロットが呼び出されます。 シグナルとスロットは、任意のタイプの引数をいくつでも受け入れることができます。 それらは完全にタイプセーフです。

QObjectまたはその子クラス(QWidgetなど)から継承されたすべてのクラスには、シグナルとスロットを含めることができます。 信号は、オブジェクトの状態が変化したときにオブジェクトによって生成されるため、他のオブジェクトが関心を持つ可能性があります。 ただし、彼は自分の信号に受信機がないことを知らず、気にしません。

スロットは信号の受信に使用できますが、通常のメンバー機能でもあります。 オブジェクトがシグナルの受信者について何も知らないのと同様に、スロットはそれに接続されているシグナルについて何も知りません。 これにより、Qtを使用して完全に独立したコンポーネントを作成できます。

必要な数の信号を1つのスロットに接続できます。また、1つの信号を必要な数のスロットに接続できます。 信号を別の信号に接続することもできます(これにより、最初の信号が表示された直後に2番目の信号が生成されます)。

信号とスロットは、強力なコンポーネント作成メカニズムを構成します。



小さな例



C ++のクラス記述は次のようになります。

class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  1. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  2. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  3. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  4. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  5. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  6. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  7. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  8. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



  9. class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .



class Counter { public : Counter() { m_value = 0; } int value () const { return m_value; } void setValue( int value ); private : int m_value; }; * This source code was highlighted with Source Code Highlighter .





QObjectから継承されたクラスは次のようになります。





  1. #include <QObject>
  2. クラス Counter: public QObject
  3. {
  4. Q_OBJECT
  5. 公開
  6. カウンター(){m_value = 0; }
  7. int value () const { return m_value; }
  8. 公開スロット:
  9. void setValue( int value );
  10. 信号:
  11. void valueChanged( int newValue);
  12. プライベート
  13. int m_value;
  14. };
*このソースコードは、 ソースコードハイライターで強調表示されました。


QObjectから継承されたクラスは同じ内部状態を持ち、この状態にアクセスするためのパブリックメソッドを提供しますが、さらにシグナルとスロットの使用をサポートしています。 このクラスは、valueChanged()シグナルを生成することにより、状態が変化したことを外部に伝えることができ、他のオブジェクトがシグナルを送信できるスロットを持っています。

シグナルとスロットを含むすべてのクラスは、説明の最初にQ_OBJECTマクロを指定する必要があります。 また、QObjectの子孫(直接または間接)でなければなりません。

スロットはプログラマーによって実装されます。 Counter :: setValue()スロットの可能な実装は次のとおりです。





  1. void Counter :: setValue( int value
  2. {
  3. ifvalue != m_value){
  4. m_value = value ;
  5. emit valueChanged( value );
  6. }
  7. }
*このソースコードは、 ソースコードハイライターで強調表示されました。


emitキーワードは、引数として新しい値を持つオブジェクトのvalueChanged()シグナルを生成します。

次の例では、Counter型の2つのオブジェクトを作成し、静的関数QObject :: connect()を使用して、最初の信号valueChanged()を2番目のsetValue()スロットに接続します。





  1. カウンターa、b;
  2. QObject :: connect(&a、SIGNAL(valueChanged( int ))、
  3. &b、SLOT(setValue( int )));
  4. a.setValue(12); // a.value()== 12、b.value()== 12
  5. b.setValue(48); // a.value()== 12、b.value()== 48
*このソースコードは、 ソースコードハイライターで強調表示されました。


a.setValue(12)を呼び出すと、シグナルvalueChanged(12)が生成され、setValue()スロットでオブジェクトbが受信されます。 関数b.setValue(12)が呼び出されます。 bは同じvalueChanged()シグナルを生成しますが、どのスロットにも接続されていないため、このシグナルは無視されます。

setValue()関数は新しい値を設定し、値がある場合にのみ信号を生成することに注意してください!= M_value。 これにより、循環結合の場合の無限ループが防止されます(たとえば、b.valueChanged()がa.setValue()に接続される場合)。

接続ごとに信号が生成されます。 接続が複製されると、2つの信号が生成されます。 QObject :: disconnect()関数を使用していつでも切断できます。

上記の例は、お互いについて何も知らなくてもオブジェクトがどのように連携できるかを示しています。 これを有効にするには、オブジェクトを相互に接続する必要があります。これは、QObject :: connect()関数を呼び出すか、uicプログラムの自動接続プロパティを使用することで実現できます。



コンパイル例



メタオブジェクトコンパイラ(moc)は、ソースファイル内のクラス記述を調べて、メタオブジェクトを初期化するC ++コードを生成します。 メタオブジェクトには、すべての信号とスロットの名前、およびこれらの関数へのポインターが含まれています。

mocプログラムを実行して、信号とスロットを含むクラスを記述することにより、コンパイルして他のアプリケーションオブジェクトファイルにリンクする必要があるソースファイルを取得します。 qmakeを使用すると、mocを自動的に呼び出すためのルールがプロジェクトのMakefileに追加されます。



信号



信号は、オブジェクトの状態が変化したときにオブジェクトによって生成されるため、他のオブジェクトが関心を持つ場合があります。 シグナルまたはその子孫を定義するクラスのみがシグナルを生成できます。

シグナルが生成されると、通常のプロシージャコールのように、シグナルが接続されているスロットは通常すぐに実行されます。 これが発生すると、信号と信号およびスロットのメカニズムは、グラフィカルインターフェイスのイベントループから完全に独立します。 シグナルのリリースに続くコードの実行は、すべてのスロットを終了した直後に発生します。 キューに入れられた接続が使用される場合、状況はわずかに異なります。 この場合、emitキーワードの後のコードはすぐに実行を継続し、スロットは後で実行されます。

複数のスロットが同じ信号に接続されている場合、信号が生成された後、スロットはランダムな順序で次々に実行されます。

シグナルはmocプログラムによって自動的に生成されるため、ソースコードに実装しないでください。 値を返さない場合があります(つまり、void型を使用します)。

引数に関する注意:経験から、特別な型を使用しない限り、プログラムを作成するときに信号とスロットを再利用するのが簡単であることが示されています。 たとえば、信号QScrollBar :: valueChanged()が仮想のQScrollBar :: Rangeのような特別なタイプを使用する場合、その専用に設計されたスロットにのみ接続できます。



スロット



スロットは、接続されている信号が生成されるときに呼び出されます。 スロットはC ++の通常の関数であり、通常の方法で呼び出すことができます。 唯一の機能は、信号を接続できることです。

スロットは通常のメンバー関数であるため、直接呼び出された場合、通常のC ++ルールに従います。 ただし、スロットと同様に、アクセスレベルに関係なく、信号からスロットへの接続を介して、任意のコンポーネントから呼び出すことができます。 これは、任意のクラスのオブジェクトによって生成された信号が、無関係なクラスのオブジェクトの保護された(プライベート)スロットを引き起こす可能性があることを意味します。

スロットは仮想と宣言することもできます。これは時には非常に便利です。

コールバックと比較すると、シグナルとスロットは柔軟性が向上しているため、若干遅くなりますが、実際のアプリケーションでは違いは顕著ではありません。 一般に、一部のスロットに接続される信号の生成は、非仮想関数を呼び出すときにレシーバーを直接呼び出すよりも平均で10倍遅くなります。 これらのオーバーヘッドは、オブジェクトの位置を特定し、すべての接続を安全にソートするために(つまり、後続のレシーバーが信号リリース中に破壊されなかったことを確認するため)、一般的なパラメーターを転送するために必要です。 10個の非仮想プロシージャの呼び出しは高価に思えるかもしれませんが、たとえば、オブジェクトを作成または削除する操作よりも安価です。 暗黙的にオブジェクトの作成を必要とする文字列、ベクトル、またはリストを作成していますが、シグナルとスロットのコストは、すべてのプロシージャコールの中でごくわずかなコストを占めています。

スロットに対してシステムコールを行う場合でも、10個を超える関数を間接的に呼び出す場合でも同じことが言えます。 i586-500では、1つのスロットに接続された1秒あたり約2,000,000の信号、または2つのスロットに接続された場合に1秒あたり1,200,000の信号を生成できます。 シグナルおよびスロットメカニズムのシンプルさと柔軟性により、プログラムのユーザーが気付かないような追加コストが発生します。

シグナルまたはスロットという名前の変数を定義するライブラリは、Qtで記述されたプログラムでコンパイルするときに警告またはコンパイラエラーを引き起こす可能性があることに注意してください。 この問題を解決するには、#undefディレクティブを使用して、干渉するプリプロセッサシンボルの定義を削除する必要があります。



メタオブジェクト情報



メタオブジェクトには、オブジェクトの名前などの追加情報が含まれています。 また、オブジェクトが特定のクラスを継承しているかどうかを確認することもできます。例:





  1. ifwidget- > inherits( "QAbstractButton" )){
  2. QAbstractButton * button = static_cast <QAbstractButton *>(ウィジェット);
  3. ボタン->トグル();
  4. }
*このソースコードは、 ソースコードハイライターで強調表示されました。


メタオブジェクト情報はqobject_cast <T>()でも使用されます。これはQObject :: inherits()に似ていますが、エラーが発生しにくいです:





  1. if (QAbstractButton * button = qobject_cast <QAbstractButton *>(ウィジェット))
  2. ボタン->トグル();
*このソースコードは、 ソースコードハイライターで強調表示されました。




実際の例



以下は、コメント付きの簡単なウィジェットの例です。





  1. #ifndef LCDNUMBER_H
  2. #define LCDNUMBER_H
  3. #include <QFrame>
  4. クラス LcdNumber: パブリック QFrame
  5. {
  6. Q_OBJECT
*このソースコードは、 ソースコードハイライターで強調表示されました。


LcdNumberクラスはQObjectを継承します。QObjectクラスには、QFrameクラスとQWidgetクラスを介したシグナルとスロットに関する最も多くの情報が含まれています。 組み込みのQLCDNumberウィジェットのように見えます。

Q_OBJECTマクロは、mocプログラムによって実装されるいくつかのメンバー関数を宣言するようにプリプロセッサに指示します。 コンパイル中に「LcdNumberのvtableへの未定義参照」というエントリが表示された場合、mocを実行するのを忘れたか、リンクコマンドにその作業の結果を追加した可能性があります。





  1. 公開
  2. LcdNumber(QWidget * parent = 0);
*このソースコードは、 ソースコードハイライターで強調表示されました。


これは明示的にmocに適用されるわけではありませんが、Qwidgetクラスを継承する場合、おそらくコンストラクターに親引数を持たせ、それを親クラスのコンストラクターに渡したいでしょう。

一部のデストラクタとメンバー関数はここでは省略されています。 mocはメンバー関数を無視します。





  1. 信号:
  2. ボイドオーバーフロー();
*このソースコードは、 ソースコードハイライターで強調表示されました。


LcdNumberは、不可能な値を表示するように求められたときに信号を生成します。

オーバーフローが気にならない場合、またはオーバーフローが発生しないことがわかっている場合は、この信号を無視できます。 どこにも接続しないでください。

一方、このエラーに応答するために2つの異なる関数を呼び出したい場合は、この関数を2つの異なるスロットに接続するだけです。 Qtは両方を(ランダムな順序で)呼び出します。





  1. 公開スロット:
  2. 無効表示( int num);
  3. voidディスプレイ( ダブル num);
  4. voidディスプレイ( const QString&str);
  5. void setHexMode();
  6. void setDecMode();
  7. void setOctMode();
  8. void setBinMode();
  9. void setSmallDecimalPoint( ブールポイント);
  10. };
  11. #endif
*このソースコードは、 ソースコードハイライターで強調表示されました。


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

display()関数がオーバーロードされていることに注意してください。 Qtは、信号とスロットを接続するときに適切なバージョンを選択します。 コールバックでは、5つの異なる名前を探して、自分で型を制御する必要があります。

この例では、いくつかのマイナーメンバー関数が省略されています。



信号とスロットの高度な使用



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

QSignalMapperクラスは、多くの信号が同じスロットに接続されている場合に必要であり、このスロットは各信号に対して異なる応答をする必要があります。

開くファイルを決定する3つのボタンがあると仮定します:Tax File、Accounts File、またはReport File。

目的のファイルを開くには、信号QPushButton :: clicked()をreadFile()スロットに接続します。 次に、QSignalMapperクラスの関数setMapping()を使用して、すべての信号をQSignalMapperオブジェクトに変換します。





  1. signalMapper = new QSignalMapper( this );
  2. signalMapper-> setMapping(taxFileButton、QString( "taxfile.txt" ));
  3. signalMapper-> setMapping(accountFileButton、QString( "accountsfile.txt" ));
  4. signalMapper-> setMapping(reportFileButton、QString( "reportfile.txt" ));
  5. 接続(taxFileButton、シグナル(クリック())、
  6. signalMapper、SLOT(map()));
  7. connect(accountFileButton、SIGNAL(クリック())、
  8. signalMapper、SLOT(map()));
  9. 接続(reportFileButton、SIGNAL(クリック()))、
  10. signalMapper、SLOT(map()));
*このソースコードは、 ソースコードハイライターで強調表示されました。


次に、マップされた()シグナルをreadFile()スロットに接続します。このスロットでは、押されたボタンに応じて異なるファイルが開かれます。





  1. connect(signalMapper、SIGNAL(mapped( const QString&))、
  2. this 、SLOT(readFile( const QString&)));
*このソースコードは、 ソースコードハイライターで強調表示されました。




サードパーティのシグナルとスロットでQtを使用する



サードパーティのシグナルおよびスロットメカニズムでQtを使用できます。 1つのプロジェクトで複数のメカニズムを使用できます。 これを行うには、プロジェクトファイル(.pro)に次の行を追加します。

CONFIG += no_keywords





このオプションは、mocのキーワード(信号、スロット、および放出)を定義しないようにQtに指示します。これらの名前は、Boostなどのサードパーティライブラリによって使用されるためです。 no_keywordsフラグが設定されたQtシグナルとスロットを使用するには、ソースファイル内のQt mocキーワードのすべての使用を対応するマクロ(Q_SIGNALS、Q_SLOTS、およびQ_EMIT)に置き換えるだけです。



All Articles