Qtの基礎 - タイマ

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

QTimerクラスは、時間ベースのイベントを処理するためのクラスである。
主に、一定の間隔で特定の処理を実行する場合に使用する。

QTimerクラスは、まず、QTimerオブジェクトを作成して、startメソッドを呼び出して起動する。
タイマ時間が過ぎると、timeoutシグナルが発行される。
このシグナルを任意のスロットに接続することにより、定期的に処理を実行することができる。

タイマの精度は、OSやハードウェアに依存する。
ミリ秒単位での制御が可能であるが、極端に短い間隔を設定する場合、システムの負荷が高くなる可能性があるため注意が必要である。

QTimerクラスには、単発のタイマと繰り返しのタイマが存在する。
単発のタイマはsingleShotメソッド (staticメソッド) を使用してに設定することができる。
一方、繰り返しのタイマは、QTimerオブジェクトを使用して実装する。

QTimerクラスのメリットとして、Qtのイベントループと統合されているため、他のQtのコンポーネントとシームレスに連携できることが挙げられる。
また、マルチスレッド環境でも安全に使用できるよう設計されている。

タイマの制御には、startメソッドの他にstopメソッドがあり、これを使用してタイマを一時停止することができる。
また、isActiveメソッドを使用して、タイマが現在アクティブかどうかを確認することもできる。

QTimerクラスは、UIの更新、ネットワーク操作のタイムアウト、アニメーションの制御等、様々な用途に活用できる便利なクラスである。
ただし、過度に多くのタイマを同時に使用する場合は、アプリケーションのパフォーマンスに影響を与える可能性があるため、適切な設計と使用が求められる。


単発のタイマ

コンソールアプリケーション

以下の例では、単発タイマを使用して、指定時間後に1度だけメッセージを表示している。

 // TimerExample.hファイル
 
 #include <QCoreApplication>
 #include <QTimer>
 #include <stdexcept>
 #include <QDebug>
 
 class TimerExample : public QObject
 {
    Q_OBJECT
 
 public:
    TimerExample(QObject *parent = nullptr) : QObject(parent) {}
 
    void startTimer(int milliseconds)
    {
       // startTimer(0)のように呼び出す場合は例外エラーとする
       try {
          if (milliseconds <= 0) {
             throw std::invalid_argument("Timer duration must be positive");
          }
 
          QTimer::singleShot(milliseconds, this, &TimerExample::onTimeout);
          qDebug() << "Timer started for" << milliseconds << "milliseconds";
       }
       catch (const std::exception& e) {
          qCritical() << "エラー: " << e.what();
       }
    }
 
 private slots:
    void onTimeout()
    {
        qDebug() << "Timer expired!";
        emit finished();
    }
 
 signals:
    void finished();
 };


 // main.cppファイル
 
 #include "TimerExample.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    TimerExample example;
    QObject::connect(&example, &TimerExample::finished, &a, &QCoreApplication::quit);
 
    example.startTimer(5000);  // 5秒後にタイマイベント開始
 
    return a.exec();
 }


QWidgetアプリケーション

以下の例では、QTimerクラスの単発タイマ機能を使用して、UIを定期的に更新している。

具体的なタイマの動作を以下に示す。

  • startCountdownスロットにて、10秒のカウントダウンを開始する。
  • updateTimerスロットにて、1秒ごとにカウントダウンを更新して、UIを更新する。
  • QTimer::singleShotメソッドを再帰的に使用して、1秒ごとの更新を実現する。


 // CountdownWindow.hファイル
 
 #include <QApplication>
 #include <QMainWindow>
 #include <QVBoxLayout>
 #include <QLabel>
 #include <QPushButton>
 #include <QTimer>
 
 class CountdownWindow : public QMainWindow
 {
    Q_OBJECT
 
 private:
    int m_secondsLeft;
    QLabel *m_timeLabel;
    QPushButton *m_startButton;
 
 public:
    CountdownWindow(QWidget *parent = nullptr) : QMainWindow(parent), m_secondsLeft(10)
    {
       setWindowTitle("Countdown Timer");
 
       QWidget *centralWidget = new QWidget(this);
       setCentralWidget(centralWidget);
 
       QVBoxLayout *layout = new QVBoxLayout(centralWidget);
 
       m_timeLabel = new QLabel("Time left: 10 seconds", this);
       layout->addWidget(m_timeLabel);
 
       m_startButton = new QPushButton("Start Countdown", this);
       layout->addWidget(m_startButton);
 
       connect(m_startButton, &QPushButton::clicked, this, &CountdownWindow::startCountdown);
    }
 
 private slots:
    void startCountdown()
    {
       m_secondsLeft = 10;
       updateDisplay();
       m_startButton->setEnabled(false);
 
       // 1秒ごとにupdateTimerを呼び出す
       QTimer::singleShot(1000, this, &CountdownWindow::updateTimer);
    }
 
    void updateTimer()
    {
       m_secondsLeft--;
       updateDisplay();
 
       if (m_secondsLeft > 0) {
          // カウントダウンが終わっていない場合、再度タイマをセット
          QTimer::singleShot(1000, this, &CountdownWindow::updateTimer);
       }
       else {
          m_startButton->setEnabled(true);
       }
    }
 
    void updateDisplay()
    {
       m_timeLabel->setText(QString("Time left: %1 seconds").arg(m_secondsLeft));
    }
 };


 // main.cppファイル
 
 #include "CountdownWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);
 
    CountdownWindow window;
    window.resize(300, 150);
    window.show();
 
    return app.exec();
 }



繰り返しタイマ

以下の例では、ダイアログを開いて画像を表示している。
1秒ごとに画像の大きさを変化させる。

ダイアログを閉じる時、タイマを解除する。

 // MainWindow.h
 
 private:
    int                     m_TimerID;
    int                     m_AdjustGraphicSize;
    std::unique_ptr<QLabel> m_pLabel;
 
 private:
    void GraphicTimer();
 
 protected:
    void timerEvent(QTimerEvent *pEvent);
 
 private slots:
    void CloseDialog();


 // MainWindow.cpp
 
 // モーダルダイアログに画像を貼り付けて、1秒のタイマを設定して表示する
 void MainWindow::GraphicTimer()
 {
    m_AdjustGraphicSize = 1;
 
    m_pLabel = std:make_unique<QLabel>;
    m_pLabel->setFixedSize(32, 32);
    m_pLabel->setScaledContents(true);
    QPixmap pixmap = QApplication::style()->standardPixmap(QStyle::SP_FileDialogContentsView);
    m_pLabel->setPixmap(pixmap);
 
    std::unique_ptr<QHBoxLayout> pHbox1 = std::make_unique<QHBoxLayout>;
    pHbox1->addWidget(m_pLabel);
    pHbox1->addStretch();
 
    std::unique_ptr<QPushButton> pBtn = std::make_unique<QPushButton>(tr("閉じる"));
    pBtn->setFixedSize(80, 28);
 
    // プッシュボタンのシグナルとCloseDialogスロットを接続する
    connect(pBtn, SIGNAL(clicked()), this, SLOT(CloseDialog()));
 
    std::unique_ptr<QHBoxLayout> pHbox2 = std::make_unique<QHBoxLayout>;
    pHbox2->addStretch();
    pHbox2->addWidget(pBtn);
    pHbox2->addStretch();
 
    std::unique_ptr<QVBoxLayout> pVbox = std::make_unique<QVBoxLayout>;
    pVbox->addLayout(pHbox1);
    pVbox->addStretch();
    pVbox->addLayout(pHbox2);
 
    std::unique_ptr<QDialog> Dlg = std::make_unique<QDialog>(this, 0);
    Dlg->setModal(true);
    Dlg->setSizeGripEnabled(false);
    Dlg->setWindowTitle(tr("タイマテスト"));
    Dlg->setMinimumSize(240, 280);
    Dlg->setMaximumSize(240, 280);
    Dlg->setLayout(pVbox);
 
    m_TimerID = startTimer(1000);
    Dlg->exec();
 
    killTimer(m_TimerID);
 }
 
 // イベント処理
 // ラベルサイズを32[px]〜200[px]の範囲で16[px]ずつ増減する
 // m_AdjustGraphicSizeの値により大小を決める
 // 全てのタイマイベントは当メソッドに来るため、タイマIDで処理を振り分ける
 // 変数m_AdjustGraphicSizeには、1または-1が代入される
 void MainWindow::timerEvent(QTimerEvent *pEvent)
 {
    if(pEvent->timerId() == m_TimerID)
    {
       int sz = m_pLabel->width() + m_AdjustGraphicSize * 16;
       m_pLabel->setFixedSize(sz, sz);
 
       if(sz > 200 || sz <= 32)
       {
          m_AdjustGraphicSize *= -1;
       }
    }
 }
 
 // ダイアログの終了処理
 // 送信元のウインドウがダイアログの場合、doneで終了する(doneの引数は、execの戻り値である)
 void MainWindow::CloseDialog()
 {
    QWidget *pWindow = static_cast<QWidget *>(sender())->window();
 
    if(pWindow->inherits("QDialog"))
    {
       QDialog *Dlg = static_cast<QDialog *>(pWindow);
       Dlg->done(0);
    }
 }



タイマの即時タイムアウト

QTimerクラスのtimeoutメソッドに、{}を渡す。

 timer->timeout({});


一般的に、timeoutシグナルを接続したスロット関数では、以降変更しない場合、
QTimerクラスのstartメソッドを実行する前に、直接スロット関数を1度呼ぶ。
しかし、接続するスロット関数を動的に変更する場合、timeoutメソッドを呼ぶだけの方が便利である。

以下の例では、プッシュボタンとラベルを配置して、プッシュボタンを押下した直後にタイマを開始している。
そして、プッシュボタンを押下し続けている間、1秒毎に1増加している。

 // MainWindow.hファイル
 
 #include <QMainWindow>
 #include <memory>
 
 class MainWindow : public QMainWindow
 {
    Q_OBJECT
 
 private:
    Ui::MainWindow          *ui;
    std::unique_ptr<QTimer> m_Timer;
    int                     m_Val;
 
 private:
    void timerFunc()
    {
       m_Val++;
       ui->label->setText(QString::number(m_Val));
    }
 
 public:
    explicit MainWindow(QWidget *parent = nullptr) : QMainWindow(parent), ui(new Ui::MainWindow), m_Timer(nullptr), m_Val(0)
    {
       ui->setupUi(this);
       ui->label->setText(QString::number(val));
 
       m_Timer = std::make_unique<QTimer>(this);
 
       connect(m_Timer, &QTimer::timeout, this, &MainWindow::timerFunc);
    }
 
    ~MainWindow()
    {
       delete ui;
    }
 
 private slots:
    void on_pushButton_pressed()
    {
       m_Timer->timeout({});
       m_Timer->start(1000);
    }
 
    void on_pushButton_released()
    {
       m_Timer->stop();
    }
 };


 // main.cppファイル
 
 #include "MainWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);
 
    MainWindow window;
    window.show();
 
    return app.exec();
 }



タイマを使用したスリープ

以下の例では、QEventLoopクラスを使用したスリープ処理である。
これにより、CPUに負荷を掛けずにイベントシステムを使用してタイマを終了することができる。

ボタン押下時にDelay関数を実行して、3秒間の待機を実演している。
待機中もUIは応答可能であり、他のイベントを処理することができる。

具体的な動作を以下に示す。

  • EventLoopクラスの使用
    QEventLoopクラスは、イベントの処理を一時的に独立したループで行うことを可能にする。
    これにより、アプリケーションのメインイベントループをブロックせずに待機することができる。
  • QTimerクラスとの組み合わせ
    QTimerクラスは、指定された時間後にtimeoutシグナルを発行する。
    このシグナルをQEventLoop::quitスロットに接続することにより、タイマが満了した時にループを終了する。
  • 効率的な待機
    この方法では、ビジーウェイトを使用せずに待機するため、CPUリソースを節約できる。
    アプリケーションは他のイベントに応答可能な状態を維持する。
  • 非同期処理
    onDelayButtonClickedスロットでは、QTimer::singleShotメソッドを使用して、performDelayを非同期で呼び出している。
    これにより、UIのフリーズを防いで、ユーザエクスペリエンスを向上させている。
  • 状態表示
    QLabelを使用して、遅延の開始と終了を表示している。
    これにより、ユーザに処理の進行状況を視覚的に伝えることができる。


 // MainWindow.hファイル
 
 #include <QMainWindow>
 #include <QEventLoop>
 #include <QPushButton>
 #include <QVBoxLayout>
 #include <QLabel>
 #include <QTimer>
 
 class MainWindow : public QMainWindow
 {
    Q_OBJECT
 
 private:
    QLabel *m_statusLabel;
 
 public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
       QWidget *centralWidget = new QWidget(this);
       setCentralWidget(centralWidget);
 
       QVBoxLayout *layout = new QVBoxLayout(centralWidget);
 
       QPushButton *delayButton = new QPushButton("Start 3 Second Delay", this);
       layout->addWidget(delayButton);
 
       m_statusLabel = new QLabel("Ready", this);
       layout->addWidget(m_statusLabel);
 
       connect(delayButton, &QPushButton::clicked, this, &MainWindow::onDelayButtonClicked);
    }
 
    void Delay(int ms)
    {
       QEventLoop loop;
       QTimer     timer;
 
       timer.setSingleShot(true);
       connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
       timer.start(ms);
 
       loop.exec();
    }
 
 private slots:
    void onDelayButtonClicked()
    {
       m_statusLabel->setText("Starting delay...");
       QTimer::singleShot(0, this, &MainWindow::performDelay);
    }
 
    void performDelay()
    {
       Delay(3000);  // 3秒の遅延
       m_statusLabel->setText("Delay finished!");
    }
 };


 // main.cppファイル
 
 #include "MainWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);
 
    MainWindow window;
    window.show();
 
    return app.exec();
 }