如果你停止一个QTimer,它会发出超时()信号吗?

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

本文关键字:超时 信号 QTimer 如果 一个      更新时间:2023-10-16

我试图理解QTimer的操作。 我触发了超时((信号,但是如果我提前停止计时器,我无法在文档中找到是否发出超时((信号。

基本上,如何在计时器完成计数之前强制超时((? 只是通过以最小毫秒增量重新启动计时器来破解?

myTimer->start(1);

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

在Qt

4和Qt 5中,你都不能直接从类外发出QTimer::timeout。这是一个私有信号:在Qt 4中,它被声明为private,在Qt 5中,它被声明为私有类型的参数QObjectPrivate

不过,您可以调用它:

// 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");

在Qt 5中,moc生成的QTimer::qt_static_metacall为我们构建了私有参数:

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

您还可以通过向计时器发送计时器事件来使计时器像超时一样运行:

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

这两种方法都适用于Qt 4和Qt 5。

由于您希望在停止活动计时器时发出超时,因此解决方案分别是:

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();
}

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();
}

信号将立即发出,而不是通过事件循环中的Qt代码发出。这应该不是问题,因为emitTimeoutAndStop将使用堆栈上的事件循环进行调用。我们断言这一事实。如果您希望支持从绑定到同一计时器timeout信号的代码调用emitTimeoutAndStop而无需重新输入所述代码,那么您必须使用下面的ChattyTimer或来自其他答案的解决方案。

如果您所需要的不是计时器,而只是一个即时的单发射信号,那么QObject::destroyed为此目的很有用。它将在块的末尾发出,source超出范围并被破坏。

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

或者,您可以有一个小型帮助程序类:

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

由于您可以将多个信号连接到单个接收器,因此将定时器和此类信号源连接到接收器可能会更清晰,而不是试图绕过计时器的行为。

另一方面,如果这种"停止时发出信号"的计时器在几个地方都很有用,那么最好将其实际实现为专用类 - 这并不难。重用QTimer类是不可能的,因为stop()槽不是虚拟的,因此ChattyTimer不能用Liskov替换QTimer。这将是一个等待发生的错误 - 在难以找到的错误类别中。

有几个行为细节需要注意。这可能表明更改像计时器这样基本的东西的行为是棘手的 - 你永远不知道什么代码可能会做出在QTimer中明显正确的假设,但当stop()可能会发出超时时就不是这样了。将所有这些放在一个不是QTimer的类中是个好主意 - 它真的不是!

  1. QTimer一样,超时事件总是从事件循环中发出。要让它立即从stop()发出,请设置immediateStopTimeout

  2. 停止时isActive行为有两种可能的概括(与QTimer相比(:

    • stop返回后立即变为 false,即使稍后会发出最终timeout,或者
    • 指示事件循环是否可以发出超时事件,如果最终timeout信号延迟,则在stop()后将保持true

    我选择第一个行为作为默认行为。设置 activeUntilLastTimeout 以选择第二个行为。

  3. 对于
  4. 停止时timerIdremainingTime的行为(相对于QTimer的行为(,有三种可能的概括:

    • isActive()为 false 时返回 -1,否则返回有效的标识符/时间(即遵循选定的isActive()行为(,
    • stop返回后立即变得-1,即使最终的timeout稍后会发出,
    • 每当事件循环仍可能发出超时事件时,返回有效的 ID/时间。

    我为timerIdremainingTime选择了第一个行为,它在其他方面是不可配置的。

// 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; }
};

如果你停止一个QTimer,它会发出超时((信号吗?

不。

基本上,如何在计时器完成之前强制超时(( 计数?只需以最小的毫秒重新启动计时器即可破解 增加?

在计时器上调用 stop((,然后自己发出信号。 您可以通过子类化 QTimer 并在 QTimer 子类中调用发出信号的方法来实现这一点:

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

。但是,如果您不想费心制作子类,更简单的方法是将信号添加到您自己的类中,并将该信号连接到 QTimer 对象的 timeout(( 信号(当然只这样做一次(:

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

。然后你的停止和射击方法可以像这样完成:

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

至少在 4.8 及更高版本中(不确定早期版本(实际上很容易做到这一点:只需setInterval(0)(就像您在问题中建议的那样,尽管无需停止计时器并重新启动它(。

此应用程序将立即打印"计时器已过期"并退出:

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();
}