Stroustrup 的《使用 C++ 编程原理与实践》第三版中的 14.6 节(简单动画)使用了他的 PPP 图形库,而该库又使用了 QT。我正在尝试了解其中涉及的内容。 以下程序按预期工作:最初它显示两个黑色圆圈。 2 秒后,第一个圆圈的颜色变为红色。再过四秒后,第二个圆圈的颜色变为蓝色。 Stroustrup 写道:“您还可以添加一个操作(“回调”)作为参数。该操作在延迟后被调用”。因此,我尝试用执行此操作的代码替换指示的代码行。我期望输出完全相同,但事实并非如此。两秒时没有任何反应,但四秒时两个圆圈同时改变颜色。
#include "PPP/Simple_window.h"
#include "PPP/Graph.h"
int main(int /*argc*/, char * /*argv*/[])
{
using namespace Graph_lib;
Application app;
// Window initially displays 2 black circles
Simple_window w {Point{0,0}, 600, 400, "Callback problem"};
Circle c1{{175, 200}, 100};
Circle c2{{425, 200}, 100};
c1.set_fill_color(Color::black);
c2.set_fill_color(Color::black);
w.attach(c1);
w.attach(c2);
// Code to be replaced:
w.timer_wait(2000); //2000 milliseconds
c1.set_fill_color(Color::red);
w.timer_wait(4000);
c2.set_fill_color(Color::blue);
//////////////////////////////
// // I expected this code to have the same result:
// w.timer_wait(2000, [&]{c1.set_fill_color(Color::red);});
// w.timer_wait(4000, [&]{c2.set_fill_color(Color::blue);});
// //////////////////////////////////////////////
w.wait_for_button();
}
为什么输出不一样?有什么办法可以成功使用回调方法吗?
这是timer_wait的代码:
void WindowPrivate::timer_wait(int milliseconds, std::function<void()> cb)
{
if (!accept_waits)
return;
auto conn = std::make_shared<QMetaObject::Connection>();
*conn = QObject::connect(&user_timer, &QTimer::timeout,
[conn, func = std::move(cb)] {
QObject::disconnect(*conn);
func();
});
user_timer.start(milliseconds);
}
该问题是由于
WindowPrivate
始终使用相同的 QTimer 对象引起的;参见GUI_Private.h
:
QTimer user_timer{&nested_loop};
当顺序调用多个
timer_wait(timeout, callback)
函数时,它们实际上调用了计时器的 start(msec)
:
以 msec 毫秒的超时间隔启动或重新启动计时器。 如果计时器已经在运行,它将被停止并重新启动。
结果是第二次调用
timer_wait(timeout, callback)
时,计时器会立即 重置 并以新的超时时间 重新启动 。
这对于一般用途来说显然不能令人满意,但是由于 PPP 库的简单性质和目的,这也是可以理解的:它是一个用于教育目的的基础库,因此它的功能在某种程度上是有限的。我不知道以前版本的库(在切换到 Qt 之前)中的实现是否有不同的结果,但也许应该写信给 Stroustrups 并让他知道这个问题,至少应该记录下来,如果没有以某种方式修复。
在真实的程序中,您可能会使用两个不同的计时器,每个回调一个。
我认为尝试修复 PPP 实现没有多大意义,但您可以在这种情况下使用现有的 Qt 功能。
具体来说,您可以使用
QTimer::singleShot()
静态函数,它也可以与 lambda 一起使用:
#include <QTimer>
...
QTimer::singleShot(2000, [&]{c1.set_fill_color(Color::red);});
QTimer::singleShot(4000, [&]{c2.set_fill_color(Color::blue);});
请注意一个重要事实:QTimer 的上述用法期望函子中访问的任何内容在最终调用时仍然存在。
singleShot
静态函数的更合适用法是使用目标(QObject)的context,并在函数被销毁时自动断开连接。
或者,可以使用父对象(可以是“发送者”或“接收者”,具体取决于情况)构造实际的 QTimer 对象,然后将其
timeout
与相关函数连接。