Qt/C++ による GUI ソフト 3:シグナルとスロット

このページでは,シグナルとスロットという仕組みを練習します.
シグナルとスロットが共に Qt のクラスのメンバである場合から始めて,スロットの自作,シグナルの自作,シグナルとスロット間での値の授受と続きます.

シグナルとスロットは,「関係のない」オブジェクト間を接続するのに利用される機能です.
シグナルは signals 指示子で宣言された関数宣言です.emit マクロにより,任意の場所で発行できます.
シグナルが発行されると,そのシグナルに接続されたスロットが呼び出されます.スロットは slots 指示子で宣言された関数です.
シグナルとスロットを接続するには,QObject::connect() という関数を使います.

Before : 2:メニューバー Next : 4:3D レンダリングウィジェットの作成

インフォメーション

ドキュメント

以下は,メニューを動作させるのに使われる関数やクラスへのリンクです.

インストールした Debian パッケージ

このページは Debian 13 trixie で作成しています.

パッケージ名(バージョン)
g++(14.2.0)GNU C++ コンパイラ
make(4.4.1)コンパイルを制御するユーティリティ
qt6-base-dev(6.8.2)Qt6 アプリケーションのビルドに使用されるヘッダ開発ファイル

目次(ページ内リンク)


シグナルとスロットとを接続

シグナルとスロットの仕組みを利用して [ファイル(F)]-[終了(Q)] が動作するようにします.
QMenu に追加された QAction([終了] 等のメニューから実行される項目)は,クリックされることにより活性化し,シグナルを発行します.
QAction からシグナルが発行されると,そのシグナルに接続されたスロットが呼び出されます.
シグナルとスロットを接続するには,QObject::connect() という関数を使います.

ソースコード

2:メニューバーで作成したコードを簡略化して利用します.

main_window.h

シグナルとスロットの仕組みを使うには,Q_OBJECT というマクロを追加する必要があります.これを忘れるとコンパイルエラーになります.
QObject Class には,

という意味のことが記されています.


#include <QMainWindow>
#include <QMenuBar>

class Main_Window : public QMainWindow {
  Q_OBJECT

 private:
  QMenuBar *menu_bar;
  QMenu *file_menu;
  QAction *action_quit;

 public:
  Main_Window(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
};

main_window.cc

connect() という関数を使い,アクション Main_Window::action_quit が発するシグナルと,スロット QWidget::close() とを接続します.
connect() については,このパラグラフの最後でまとめます.


#include "main_window.h"

Main_Window::Main_Window(QWidget *parent, Qt::WindowFlags f)
 : QMainWindow(parent, f),
   menu_bar(menuBar())
{
 resize(320, 240);
 setWindowTitle("Main_Window");

 file_menu = new QMenu("ファイル(&F)", this);
 menu_bar->addMenu(file_menu);

 action_quit = new QAction("終了(&Q)", this);
 file_menu->addAction(action_quit);

 //Main_Window::action_quit が発するシグナルと,QWidget::close() とを接続
 connect(action_quit, SIGNAL(triggered()), this, SLOT(close()));
}

main.cc


#include "main_window.h"
#include <QApplication>

int main(int argc, char *argv[]) {
 QApplication app(argc, argv);

 Main_Window window(nullptr, Qt::Window);
 window.show();

 return app.exec();
}

QAction Class には,以下のシグナルが記載されています.

tmp.pro


TEMPLATE = app
TARGET = my_window
INCLUDEPATH += .
QT += widgets
SOURCES += main.cc main_window.cc
HEADERS += main_window.h

ビルドと実行

終了メニューの実行

手順は 1:メインウィンドウから不変です.ソースファイルが tmp というディテクトリにあるとします.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ ./my_window &
実行してメニューバーで [ファイル(F)]-[終了(Q)] と操作するとウィンドウが閉じました.

QObject::connect()

QObject Class には,Static Public Members として以下の connect() 関数が挙げられています.
これら connect() 関数の戻り値は,接続へのハンドル QMetaObject::Connection です.ハンドルは,後で接続を切断するために使用できます.
1 個のシグナルは,複数のスロットやシグナルに接続することができます.
connect() を列挙すると,

  1. QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
  2. QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
  3. QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
  4. QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)
  5. QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)

これら connect() 関数の戻り値は,QMetaObject::Connection Calass です.リファレンスマニュアルの説明には,「接続へのハンドルを表す.このハンドルは,接続が有効かどうかを確認したり,QObject::disconnect() を使って接続を切断したりするのに使える」と書いてあります.

このページでは 1 のタイプを使っています.クラス宣言の引数とコード内で関数呼び出しに使っている引数を対応付けて記すと,

  1. const QObject *sender : action_quit
  2. const QMetaMethod &signal : SIGNAL(triggered())
  3. const QObject *receiver : this
  4. const QMetaMethod &method : SLOT(close())

SIGNAL と SLOT というマクロの実体は,C++ のプリプロセッサ命令とのことです.
QMetaMethod クラスは,関数に関する情報を格納しておくためのクラスです.
7:レンダリング その2 では 3 番めの書式(ファンクタではなくてラムダ式ですが)を使っています.


スロットを自作

メニューに [ファイル(F)]-[開く(O)] を追加して,動作するようにします.もっとも,通常のソフトウェアのようにファイルダイアログが開いて…というような動作ではなく,端末に文字列を出力するだけです.

ソースコード

通信の流れをまとめておきます.

  1. [ファイル(F)]-[開く(O)] の本体である action_open が活性化すると,シグナル triggered() が発行される
  2. その triggered() と予め connect されている自作スロット slot_open() が呼び出される

スロットは,slots 指示子に続いて宣言される関数です.シグナルと接続できる以外は,通常の C++ 関数です.

main_window.h


#include <QMainWindow>
#include <QMenuBar>

class Main_Window : public QMainWindow {
  Q_OBJECT

 private:
  QMenuBar *menu_bar;
  QMenu *file_menu;
  QAction *action_open;  //[ファイル(F)]-[開く(O)] の本体
  QAction *action_quit;

 private slots:
  void slot_open();  //[ファイル(F)]-[開く(O)] で呼び出される関数の宣言

 public:
  Main_Window(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
};

main_window.cc

ヘッダで宣言した自作スロット void slot_open() を実装します.端末に関数名を出力するだけですが.
connect() を使い,メニュー [開く(O)] の本体 *action_open と自作スロット slot_open() とを接続します


#include "main_window.h"
#include <iostream>

Main_Window::Main_Window(QWidget *parent, Qt::WindowFlags f)
 : QMainWindow(parent, f),
   menu_bar(menuBar())
{
 resize(320, 240);
 setWindowTitle("Main_Window");

 file_menu = new QMenu("ファイル(&F)", this);
 menu_bar->addMenu(file_menu);

 action_open = new QAction("開く(&O)", this);
 file_menu->addAction(action_open);

 action_quit = new QAction("終了(&Q)", this);
 file_menu->addAction(action_quit);

 connect(action_quit, SIGNAL(triggered()), this, SLOT(close()));

 //メニュー [開く(O)] の本体 *action_open と自作スロット slot_open() とを接続
 connect(action_open, SIGNAL(triggered()), this, SLOT(slot_open()));
}

//[ファイル(F)]-[開く(O)] で呼び出されるスロット関数の定義
void Main_Window::slot_open() {
 std::cout << __PRETTY_FUNCTION__ << std::endl;
}

main.cc および window.pro

変更がないので省略します.

ビルドと実行

開くメニューの実行

手順は上と同じです.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ ./my_window &
実行してメニューバーで [ファイル(F)]-[開く(O)] と操作すると,端末に

void Main_Window::slot_open()

と出力されました.


シグナルを自作

上のコードでは QAction のトリガーイベントで発行されるシグナルと自作の関数を接続しました.
シグナルを自作することも可能です.Signals & Slots にサンプルコード付きで解説されています.

サンプルコードを作成してみました.コードを簡単にする目的で,同じクラス内でシグナルとスロットを作成しています.
これだと関数呼び出しの方が楽ですが,シグナルさえ発行しておけば,後でスロットを追加できるので処理を増やしていくのが楽とか,別のクラスの private 関数を呼び出すのに使うとか,いろいろ応用できそうです.

ソースコード

通信の流れをまとめておきます.

  1. [ファイル(F)]-[開く(O)] の本体である action_open が活性化すると,シグナル triggered() が発行される
  2. その triggered() と予め connect されたスロット slot_open() が呼び出される
  3. slot_open() 内でシグナル signal_opened が発行される
  4. signal_opend に予め connect されているスロット slot_after_opend() が呼び出される

main_window.h


#include <QMainWindow>
#include <QMenuBar>

class Main_Window : public QMainWindow {
  Q_OBJECT

 private:
  QMenuBar *menu_bar;
  QMenu *file_menu;
  QAction *action_open;
  QAction *action_quit;

 //signals 指示子に続いて戻り値のない関数を宣言をすれば,シグナルが定義できる
 signals:
  void signal_opened();

 private slots:
  void slot_open();
  void slot_after_opened();  //新たに作成する自作スロットの宣言.自作シグナル signal_opened と接続予定

 public:
  Main_Window(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
};

main_window.cc


#include "main_window.h"
#include <iostream>

Main_Window::Main_Window(QWidget *parent, Qt::WindowFlags f)
 : QMainWindow(parent, f),
   menu_bar(menuBar())
{
 resize(320, 240);
 setWindowTitle("Main_Window");

 file_menu = new QMenu("ファイル(&F)", this);
 menu_bar->addMenu(file_menu);

 action_open = new QAction("開く(&O)", this);
 file_menu->addAction(action_open);

 action_quit = new QAction("終了(&Q)", this);
 file_menu->addAction(action_quit);

 connect(action_quit, SIGNAL(triggered()), this, SLOT(close()));
 connect(action_open, SIGNAL(triggered()), this, SLOT(slot_open()));

 //自作シグナルと自作スロットとを接続
 connect(this, SIGNAL(signal_opened()), this, SLOT(slot_after_opened()));
}

void Main_Window::slot_open() {
 std::cout << __PRETTY_FUNCTION__ << std::endl;

 //シグナルの発行.発行したい箇所で emit キーワードを使う
 emit signal_opened();
}

//signal_opened と接続されたスロットの定義
void Main_Window::slot_after_opened() {
 std::cout << __PRETTY_FUNCTION__ << std::endl;
}

main.cc および window.pro

変更がないので省略します.

ビルドと実行

手順は上と同じです.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ ./my_window &
実行してメニューバーで [ファイル(F)]-[開く(O)] と操作すると,端末に

void Main_Window::slot_open()
void Main_Window::slot_after_opened()

と出力されました.


自作シグナルと自作スロット間で値の授受

上のコードで,シグナルとスロットに引数を付けてみたら動作したので,記しておきます.

ソースコード

main_window.h


#include <QMainWindow>
#include <QMenuBar>

class Main_Window : public QMainWindow {
  Q_OBJECT

 private:
  QMenuBar *menu_bar;
  QMenu *file_menu;
  QAction *action_open;
  QAction *action_quit;

 signals:
  void signal_opened(int val);  //シグナルに引数を与えてみた

 private slots:
  void slot_open();
  void slot_after_opened(int val);  //スロットに引数を与えてみた

 public:
  Main_Window(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
};

main_window.cc


#include "main_window.h"
#include <iostream>

Main_Window::Main_Window(QWidget *parent, Qt::WindowFlags f)
 : QMainWindow(parent, f),
   menu_bar(menuBar())
{
 resize(320, 240);
 setWindowTitle("Main_Window");

 file_menu = new QMenu("ファイル(&F)", this);
 menu_bar->addMenu(file_menu);

 action_open = new QAction("開く(&O)", this);
 file_menu->addAction(action_open);

 action_quit = new QAction("終了(&Q)", this);
 file_menu->addAction(action_quit);

 connect(action_quit, SIGNAL(triggered()), this, SLOT(close()));
 connect(action_open, SIGNAL(triggered()), this, SLOT(slot_open()));

 //自作シグナルと自作スロットとを接続してみた.シグナルとスロットの関数の引数は型のみを記す
 connect(this, SIGNAL(signal_opened(int)), this, SLOT(slot_after_opened(int)));
}

void Main_Window::slot_open() {
 std::cout << __PRETTY_FUNCTION__ << std::endl;

 //引数付きシグナルを発行してみた
 int val = 123;
 emit signal_opened(val);
}

void Main_Window::slot_after_opened(int val) {
 std::cout << __PRETTY_FUNCTION__ << std::endl;
 std::cout << val << std::endl;
}

main.cc および window.pro

変更がないので省略します.

ビルドと実行

手順は上と同じです.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ ./my_window &
実行してメニューバーで [ファイル(F)]-[開く(O)] と操作すると,端末に

void Main_Window::slot_open()
void Main_Window::slot_after_opened(int)
123

と出力されました.