简体   繁体   中英

If you stop a QTimer, will it emit a timeout() signal?

I'm trying to understand the operation of a QTimer. I have things triggered off timeout() signals, but I am unable to find in the documentation if a timeout() signal is emitted if I stop the timer early.

Basically, how can I force a timeout() before the timer finishes counting? Just hack by restarting the timer with the minimum ms increment?

myTimer->start(1);

http://qt-project.org/doc/qt-5/qtimer.html#timeout

In neither Qt 4 nor Qt 5 can you directly emit QTimer::timeout from outside of the class. It's a private signal: in Qt 4, it's declared as private , in Qt 5, it's declared with an argument of a private type QObjectPrivate .

You can invoke it, though:

// fast, guaranteed to work in Qt 4 and 5
myTimer->QTimer::qt_metacall(QMetaObject::InvokeMetaMethod, 5, {});

// slower, has to look up the method by name
QMetaObject::invokeMethod(myTimer, "timeout");

In Qt 5, the moc-generated QTimer::qt_static_metacall constructs the private argument for us:

//...
case 0: _t->timeout(QPrivateSignal()); break;

You can also make the timer act as if it had timed out by sending it a timer event:

void emitTimeout(QTimer * timer) {
  Q_ASSERT(timer);
  QTimerEvent event{timer->timerId()};
  QCoreApplication::sendEvent(timer, &event);
}

Both methods work on both Qt 4 and Qt 5.

Since you're looking to emit a timeout on stopping an active timer, the solutions would be, respectively:

void emitTimeoutAndStop(QTimer * timer) {
  Q_ASSERT(timer);
  Q_ASSERT(QAbstractEventDispatcher::instance()); // event loop must be on the stack
  if (!timer->isActive()) return;
  timer->QTimer::qt_metacall(QMetaObject::InvokeMetaMethod, 5, {});
  timer->stop();
}

or

void emitTimeoutAndStop(QTimer * timer) {
  Q_ASSERT(timer);
  Q_ASSERT(QAbstractEventDispatcher::instance()); // event loop must be on the stack
  if (!timer->isActive()) return;
  QTimerEvent event{timer->timerId()};
  QCoreApplication::sendEvent(timer, &event);
  timer->stop();
}

The signals will be emitted immediately, and not by Qt code from the event loop. This shouldn't be a problem, since emitTimeoutAndStop will be invoked with an event loop on the stack. We assert that fact. If you wish to support invoking emitTimeoutAndStop from code tied to the same timer's timeout signal without reentering said code, then you have to use the ChattyTimer below, or the solution from another answer .

If all you need isn't quite a timer, but just an immediate, single-emission signal, QObject::destroyed is useful for that purpose. It will be emitted at the end of the block, where source goes out of scope and is destructed.

{ QObject source;
  connect(&source, &QObject::destroyed, ...); }

Alternatively, you can have a small helper class:

// signalsource.h
#pragma once
#include <QObject>
class SignalSource : public QObject {
  Q_OBJECT
public:
  Q_SIGNAL void signal();
  SignalSource(QObject * parent = {}) : QObject(parent) {}
};

Since you can connect multiple signals to a single receiver, perhaps it would make things clearer to connect both a timer and such a signal source to the receiver, instead of trying to hack around timer's behavior.

On the other hand, if such "signaling upon stopping" timer is something you find useful in several places, it'd be better to actually implement it as a dedicated class - it's not all that hard. Re-using the QTimer class is not possible, since the stop() slot is not virtual, and thus ChattyTimer is not Liskov-substitutable for a QTimer . This would be a bug waiting to happen - in the class of bugs that are hard to find.

There are several behavioral details that demand attention. This perhaps indicates that changing the behavior of something as fundamental as a timer is tricky - you never know what code might make assumptions that are obviously correct in QTimer , but not so when stop() might emit a timeout. It is a good idea to have all this in a class that is not-a QTimer - it really is not!

  1. As in QTimer , the timeout event is always emitted from the event loop. To have it emitted immediately from stop() , set immediateStopTimeout .

  2. There are two generalizations possible of isActive behavior upon stop (vs. that of QTimer ):

    • becomes false immediately after stop returns, even if the final timeout will be emitted later, or
    • indicates whether timeout events may be emitted by the event loop, and will remain true after a stop() if the final timeout signal is deferred.

    I chose the first behavior to be the default. Set activeUntilLastTimeout to select the second behavior.

  3. There are three generalizations possible of the behavior of each of timerId and remainingTime upon stop (vs. that of QTimer ):

    • returns -1 when isActive() is false, or a valid identifier/time otherwise (ie follows chosen isActive() behavior),
    • becomes -1 immediately after stop returns, even if the final timeout will be emitted later,
    • returns a valid id/time whenever timeout events may still be emitted by the event loop.

    I chose the first behavior for both timerId and remainingTime , and it is not otherwise configurable.

// https://github.com/KubaO/stackoverflown/tree/master/questions/chattytimer-25695203
// chattytimer.h
#pragma once
#include <QAbstractEventDispatcher>
#include <QBasicTimer>
#include <QTimerEvent>
class ChattyTimer : public QObject {
   Q_OBJECT
   Q_PROPERTY(bool active READ isActive)
   Q_PROPERTY(int remainingTime READ remainingTime)
   Q_PROPERTY(int interval READ interval WRITE setInterval)
   Q_PROPERTY(bool singleShot READ singleShot WRITE setSingleShot)
   Q_PROPERTY(Qt::TimerType timerType READ timerType WRITE setTimerType)
   Q_PROPERTY(bool immediateStopTimeout READ immediateStopTimeout WRITE setImmediateStopTimeout)
   Q_PROPERTY(bool activeUntilLastTimeout READ activeUntilLastTimeout WRITE setActiveUntilLastTimeout)
   Qt::TimerType m_type = Qt::CoarseTimer;
   bool m_singleShot = false;
   bool m_stopTimeout = false;
   bool m_immediateStopTimeout = false;
   bool m_activeUntilLastTimeout = false;
   QBasicTimer m_timer;
   int m_interval = 0;
   void timerEvent(QTimerEvent * ev) override {
      if (ev->timerId() != m_timer.timerId()) return;
      if (m_singleShot || m_stopTimeout) m_timer.stop();
      m_stopTimeout = false;
      emit timeout({});
   }
public:
   ChattyTimer(QObject * parent = {}) : QObject(parent) {}
   Q_SLOT void start(int msec) {
      m_interval = msec;
      start();
   }
   Q_SLOT void start() {
      m_stopTimeout = false;
      m_timer.stop(); // don't emit the signal here
      m_timer.start(m_interval, m_type, this);
   }
   Q_SLOT void stop() {
      if (!isActive()) return;
      m_timer.stop();
      m_stopTimeout = !m_immediateStopTimeout;
      if (m_immediateStopTimeout)
         emit timeout({});
      else // defer to the event loop
         m_timer.start(0, this);
   }
   Q_SIGNAL void timeout(QPrivateSignal);
   int timerId() const {
      return isActive() ? m_timer.timerId() : -1;
   }
   bool isActive() const {
      return m_timer.isActive() && (m_activeUntilLastTimeout || !m_stopTimeout);
   }
   int remainingTime() const {
      return
            isActive()
            ? QAbstractEventDispatcher::instance()->remainingTime(m_timer.timerId())
            : -1;
   }
   int interval() const { return m_interval; }
   void setInterval(int msec) {
      m_interval = msec;
      if (!isActive()) return;
      m_timer.stop(); // don't emit the signal here
      start();
   }
   bool singleShot() const { return m_singleShot; }
   void setSingleShot(bool s) { m_singleShot = s; }
   Qt::TimerType timerType() const { return m_type; }
   void setTimerType(Qt::TimerType t) { m_type = t; }
   bool immediateStopTimeout() const { return m_immediateStopTimeout; }
   void setImmediateStopTimeout(bool s) { m_immediateStopTimeout = s; }
   bool activeUntilLastTimeout() const { return m_activeUntilLastTimeout; }
   void setActiveUntilLastTimeout(bool s) { m_activeUntilLastTimeout = s; }
};

If you stop a QTimer, will it emit a timeout() signal?

No.

Basically, how can I force a timeout() before the timer finishes counting? Just hack by restarting the timer with the minimum ms increment?

Call stop() on the timer, and then cause the signal to be emitted yourself. You could do this by subclassing QTimer and calling a method in your QTimer subclass that emits the signal:

void MyQTimer :: EmitTimeoutSignal() {emit timeout();}

… but if you don't want to go to the bother of making a subclass, an easier approach would be to add a signal to your own class and connect that signal to the QTimer object's timeout() signal (do this only once of course):

connect(this, SIGNAL(MyTimeoutSignal()), myTimer, SIGNAL(timeout()));

… and then your stop-and-fire method would could be done like this:

myTimer->stop();
emit MyTimeoutSignal();

It's actually quite easy to do this, at least in 4.8 and later (not sure about earlier versions): simply setInterval(0) (much like you suggest in your question, although there's no need to stop the timer and restart it).

This app will immediately print "Timer expired" and exit:

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

  QTimer timer;
  timer.setSingleShot(true);
  timer.setInterval(1000 * 60 * 60); // one hour

  QObject::connect(
      &timer,  &QTimer::timeout,
      [&]()
      {
        std::cout << "Timer expired" << std::endl;
        app.exit();
      });

  QTimer::singleShot(
      0, //trigger immediately once QtEventLoop is running
      [&]()
      {
        timer.start();
        timer.setInterval(0);  // Comment this out to run for an hour.
      });

  app.exec();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM