Qt GUIアプリでのマルチスレッド・非同期ソケット通信 [C++] [Qt5/Qt6]

[2022.11] Qt5/Qt6 両対応.

Qt Creator を Qt5/Qt6 両対応にする

[2022.11] Fedora 37

Qt の開発環境は Qt Creator か, GUI フォームだけ Qt Designer を使う。通常, 前者.

Fedora 37 の Qt Creator は, 初期設定では Qt5 のみ使うようになっている。Qt5/Qt6 両対応にする.

Fedora でパッケージされているのは Qt Creator v8.0.1. これは Qt5 でビルドされている。依存するのは qt5-qtbase-devel パッケージ。これと別に, qt6-qtbase-devel-6.3.1-4.fc37.x86_64 をインストールする。

Qt Creator の画面で, Edit > Preferences で設定画面を開く. 左側の Kits を選ぶと, Kits タブに Desktop (default) が表示され, Qt version: に利用可能な Qt が選択できる。ここで, 右の [Manage...] ボタンをクリック.

Qt Versions タブに移る. Qt 5.15 のみが表示されているはず。[Add...] ボタンで Qt6 を追加する。

これだけでいけるはず。

ほかには, https://www.qt.io/download-open-source から Qt Online Installer をダウンロードする方法もある。Qt5.15, Qt6.2, Qt6.3, Qt6.4, Qt Creator 8.0, Qt Design Studio v3.8 をインストールできる。Qt Design Studio は, Qt Designer とは別の, 2D/3D のUIの見た目を定義するソフトウェアのようだ。QML と C++ コードを生成。Fedora ではパッケージされていない。

試してみたが, 上手く Fedora ディストリビューションの環境に馴染ませることができなかった。Fedora パッケージを使おう。

何を作るか

前ページと同じものを Qt5/Qt6 で作る。

スレッドをつくる

Qt5, Qt6の互換性は高い。非互換な部分も, workaround で両対応にできる。以下のサンプルは、どちらでもビルドできる。

DNS lookup, read() がブロックするので、スレッドで並列化する。ソケットは blocking モードのままとする。非同期 I/O (asynchronous I/O) だからといって、non-blocking モードにしなければならないわけではない。

簡単に, QThread クラスから派生させる。スレッドを起動すると run() がコールバックされる.

connect_thread.h

C++
[RAW]
  1. // Qtにおけるマルチスレッドは、次の二つのやり方がある:
  2. // 1. QThread から派生させる.
  3. // run() メソッドが呼び出される.
  4. // 2. 標準の QThread と自前のワーカーオブジェクトを組み合わせる
  5. // myObject->moveToThread(targetThread)
  6. // これで myObject のスロットが <var>targetThread</var> スレッドで動く.
  7. class ConnectThread: public QThread
  8. {
  9. Q_OBJECT
  10. public:
  11. ConnectThread(QObject* parent = nullptr);
  12. ~ConnectThread();
  13. void requestNewFortune(const std::string& hostName, int port);
  14. protected:
  15. // start() を呼び出すと, 新しいスレッドで run() がコールバックされる.
  16. virtual void run() override;
  17. signals:
  18. void fortuneGot(const QString& fortune);
  19. void error(int socketError, const QString& message);
  20. private:
  21. std::string hostName;
  22. int port;
  23. };

起動する

メインスレッド側から, スレッドクラスのメソッドを呼び出す.

mainwindow.cpp

C++
[RAW]
  1. // Slot
  2. void MainWindow::on_pushButton_clicked()
  3. {
  4. if (m_thread)
  5. return;
  6. int port = ui->portLineEdit->text().toInt();
  7. if (port <= 0 || port > 65535) {
  8. QMessageBox msgBox(this);
  9. msgBox.setText("ポート番号は 1..65535");
  10. msgBox.setStandardButtons(QMessageBox::Ok);
  11. msgBox.setIcon(QMessageBox::Critical);
  12. msgBox.exec();
  13. return;
  14. }
  15. ui->pushButton->setEnabled(false);
  16. // 値がないことによるボタン無効と区別する
  17. QApplication::setOverrideCursor(Qt::WaitCursor);
  18. m_thread = new ConnectThread();
  19. connect(m_thread, &ConnectThread::fortuneGot, this, &MainWindow::showFortune);
  20. connect(m_thread, &ConnectThread::error, this, &MainWindow::displayError);
  21. m_thread->requestNewFortune( ui->hostLineEdit->text().toStdString(),
  22. port );
  23. }

スレッドクラスで start() を呼び出して、子スレッドを起動する。

Blocking モードのソケットで、順番に処理するだけ。

子スレッドからシグナルを emit すれば、あとは Qt がメインスレッドへの伝達、スレッド切り替えをよしなにしてくれる。すごい楽。

connect_thread.cpp

C++
[RAW]
  1. ConnectThread::ConnectThread(QObject* parent):
  2. QThread(parent)
  3. {
  4. }
  5. ConnectThread::~ConnectThread()
  6. {
  7. }
  8. void ConnectThread::requestNewFortune(const string& hostName, int port)
  9. {
  10. // パラメータはメンバ変数にて渡す
  11. this->hostName = hostName;
  12. this->port = port;
  13. // スレッドを起動.
  14. start();
  15. }
  16. // Starting point.
  17. void ConnectThread::run()
  18. {
  19. SOCKET sockfd = connect_to_server(hostName.c_str(), port);
  20. if ( sockfd == INVALID_SOCKET ) {
  21. emit error(errno, tr("client error") );
  22. return;
  23. }
  24. char fortune_buf[1000];
  25. char* p = fortune_buf;
  26. while (true) {
  27. int r = recv(sockfd, p,
  28. sizeof(fortune_buf) - (p - fortune_buf) - 1, 0);
  29. if (r < 0) {
  30. if (errno == EINTR)
  31. continue;
  32. emit error(errno, tr("read() error") );
  33. closesocket(sockfd);
  34. return;
  35. }
  36. p[r] = '\0';
  37. if ( !r )
  38. break;
  39. p += r;
  40. }
  41. closesocket(sockfd);
  42. // シグナルを発生
  43. emit fortuneGot(fortune_buf);
  44. }

グルッと回って、シグナルを受け取って、表示するだけ。

mainwindow.cpp

C++
[RAW]
  1. // Slot: 子スレッドからコールバック
  2. void MainWindow::showFortune(const QString& fortune)
  3. {
  4. ui->textBrowser->setText(fortune);
  5. delete_thread();
  6. QApplication::restoreOverrideCursor();
  7. enableGetFortuneButton();
  8. }
  9. // Slot
  10. void MainWindow::displayError(int socketError, const QString& message)
  11. {
  12. ui->textBrowser->setText(message);
  13. delete_thread();
  14. QApplication::restoreOverrideCursor();
  15. enableGetFortuneButton();
  16. }

これだけ! カンタンすぎる。