QT - 除非关闭,否则主窗口不会更新

QT - Mainwindow doesn't update unless it's closed

本文关键字:窗口 更新 QT      更新时间:2023-10-16

我正在尝试通过每 500 毫秒在线程中调用updateGUI函数来更新主窗口。除非我关闭窗口,否则将显示窗口,但不会使用新值更新窗口。当我这样做时,将打开一个新窗口,其中包含新值。我找到了这个问题,但它没有回答我的问题。我知道(如qt文档中所述)

QApplication::exec进入主事件循环并等待exit()被称为。

我尝试使用processEvents()但主窗口反复打开和关闭,速度非常快,我什至看不到它。这是我的代码:

float distanceToObject;
bool objectDetected;
Modes currentMode;
void timerStart(std::function<void(void)> func, unsigned int interval)
{
std::thread([func, interval]()
{
while (true)
{
auto x = std::chrono::steady_clock::now() + std::chrono::milliseconds(interval);
func();
std::this_thread::sleep_until(x);
}
}).detach();
}
int updateGUI(void)
{
int argc = 0;
char **argv = NULL;
QApplication a(argc, argv);
MainWindow w;
// Set text of a label
w.setDistance(QString::number(distanceToObject));
// Also update objectDetected and currentMode values
w.show();
//a.processEvents();
return a.exec();
}
void sendMsg(void)
{
// Send heartbeat signal to another device
}
void receiveMsg(void)
{
// Read messages from the other device and update the variables
// These two values change continuously
objectDetected = true;
distanceToObject = 5.4;
}
void decide(void)   
{
// The core function of the program. Takes relatively long time
// Run a decision-making algorithm which makes decisions based on the values received from the other device. 
// Update some variables according to the made decisions
currentMode = Auto;
// Execute functions according to the made decisions. 
setMode(currentMode);
}
int main(void)
{
timerStart(updateGUI, 500);
timerStart(sendMsg, 1000);
timerStart(receiveMsg, 10); 
timerStart(decide, 500);
}

如何使用变量的值正确更新主窗口?

您的线程不会更新MainWindow,但它会在每次迭代时创建一个全新的QApplicationMainWindow。您的线程应该卡在QApplication::exec内,直到您退出应用程序(例如,通过关闭窗口)。只有这样,线程的循环才能取得进一步的进展。

通常,从主线程外部执行更新时必须非常小心,因为通常 GUI 操作必须在主线程内部执行。

考虑使用QThread

,它已经带有自己的事件循环,您可以使用它使用相应的插槽通知/更新窗口。

如果没有关于你实际想要实现的目标的进一步细节,就不可能给你进一步的方向。我至少建议您在主线程中创建QApplicationMainWindow(例如main)。然后这取决于您要"更新"的内容。如果您需要处理一些数据,则可以在第二个线程中执行此操作,并使用信号槽将结果发送到MainWindow实例。如果您需要绘制到窗口上,那么这要么必须直接在主线程中完成,要么您可能会找到一种方法来渲染到单独的缓冲区中(即QImage),然后将此缓冲区发送到主线程以将其绘制到窗口中。


我试着勾勒出这样的事情是如何做到的。但是,请注意,这既不完整,也不可编译,而只是一个大纲。

首先,你有你的MainWindow并添加一个signal,它通知所有观察者开始做他们的工作(稍后会变得清晰)。此外,您添加slots,只要您的某个值发生更改,就会调用这些值。这些slots在主线程中运行(并且是MainWindow的成员),因此可以根据需要更新窗口:

class MainWindow : public QMainWindow
{
Q_OBJECT
public:
// constructors and stuff
void startWorking()
{
emit startWorkers();   
}
public slots:
void onModeChanged(Modes m)
{
// update your window with new mode
}
void onDistanceChanged(float distance)
{
// update your window with new distance
}
signals:
void startWorkers();
};

接下来,你构建一个Worker类,它封装了你喜欢做的所有"后台工作"(基本上是你的线程在原始代码中所做的):

class Worker : public QObject
{
Q_OBJECT
public:
// constructors and stuff
public slots:
void doWork()
{
while(!done)
{
// do stuff ...
Modes m = // change mode
emit modeModified(m);
// do stuff ...
float distance = // compute distance
emit distanceModified(distance);
// do stuff ...
}
}
signals:
void modeModified(Modes m);
void distanceModified(float distance);
};

请注意,Worker必须继承QObject,并且您的doWork方法必须是public slot。此外,您还可以为希望MainWindow了解的每个值添加一个signal。不需要实现它们,因为它是由Qt MOC(元对象编译器)生成的。每当其中一个相应的值发生更改时,只需emit相应的signal并传递新值即可。

最后,您将所有内容放在一起:

int main(int argc, char* argv[])
{
QApplication app(argc, argv);
MainWindow window;
// create a worker object
Worker* worker = new Worker;
// connect signals and slots between worker and main window
QObject::connect(worker, &Worker::modeModified, 
&window, &MainWindow::onModeChanged);
QObject::connect(worker, &Worker::distanceModified, 
&window, &MainWindow::onDistanceChanged);
QObject::connect(&window, &MainWindow::startWorkers, 
worker, &Worker::doWork);
// create a new thread
QThread* thread = new QThread;
// send worker to work inside this new thread
worker->moveToThread(thread);
thread->start();
// show window and start doing work    
window.show();
window.startWorking();
// start main loop
int result = app.exec();    
// join worker thread and perform cleanup
return result;
}

好吧,让我们来看看吧。首先,在主线程中创建QApplicationMainWindow。接下来,创建Worker对象的实例(可以在此处创建多个)。然后,您将worker的信号connectwindow的插槽,反之亦然。建立这些连接后,每当您emit信号时,Qt都会调用连接的插槽(并传输传递的值)。请注意,此连接跨线程边界工作。每当从与接收对象的线程不同的线程发出信号时,Qt将发送一条消息,该消息在接收对象的线程中处理。

然后你告诉Qt,你希望你的worker使用QObject::moveToThread存在于另一个线程中。有关如何正确使用其中QThread和对象的非常详细的说明,请参阅此处。

剩下的就简单了。showwindow并开始处理。在这里,不同的方式是可能的。我在这里只调用startWorking方法,然后emitstartWorkers信号,连接到workerdoWork方法,这样doWork将在另一个线程收到该信号后开始执行。

然后调用运行主线程事件循环的QApplication::exec,其中所有这些信号都由Qt处理。一旦您的应用程序关闭(例如,通过调用quit或关闭主窗口),exec方法就会返回,您就会回到main。请注意,您需要正确关闭线程(例如,通过发送停止while循环的加法信号)并加入它。您还应该删除所有分配的对象(workerthread)。为了代码示例的简单性,我在这里省略了这一点。


回答您的问题

我有很多函数,例如 updateClips 和 mavReceive,应该定期调用并彼此独立运行。我应该为每个函数创建一个不同的 Worker 类,因为每个函数都有不同的信号,并为每个函数创建一个 QThread 对象,对吗?我不再需要启动计时器()了?如果是,我如何控制每个函数的调用间隔(用于在 startTimer() 中完成)

从评论:

答案在很大程度上取决于您所说的"应该定期调用"的确切含义。谁应该打电话给他们?用户?还是应该定期执行?

因此,原则上,您可以在一个线程中拥有多个工作线程。但是,如果它们应该一直工作(旋转一段时间循环),那就没有意义了,因为一个正在运行,而所有其他的都被阻止了。在这种情况下,每个工作线程都有一个线程。

如果我理解正确,您有兴趣定期更新某些内容(例如每 500 毫秒)。在这种情况下,我强烈建议使用QTimer.您可以设置一个间隔,然后启动它。然后,计时器将定期emittimeout信号,您可以将其连接到您想要执行的任何功能(更准确地说是slot)。

Worker的更新版本可能如下所示:

class Worker : public QObject
{
Q_OBJECT
public:
Worker()
{
QObject::connect(&modeTimer_, &QTimer::timeout,
this, &Worker::onModeTimerTimeout);
QObject::connect(&distanceTimer_, &QTimer::timeout,
this, &Worker::onDistanceTimerTimeout);
modeTimer_.start(500); // emit timeout() every 500ms
distanceTimer_.start(100); // emit timeout() every 100ms
}
public slots:    
void onModeTimerTimeout()
{
// recompute mode
Modes m = // ...
emit modeModified(m);
}
void onDistanceTimerTimeout()
{
// recompute distance
float distance = // ...
emit distanceModified(distance);
}
signals:
void modeModified(Modes m);
void distanceModified(float distance);
private:
QTimer modeTimer_;
QTimer distanceTimer_;
};

注意,在构造函数中建立的连接。每当其中一个计时器超时时,就会调用连接的slot。然后,这个插槽可以计算它需要的任何内容,然后使用与以前相同的signal将结果发送回主线程中的MainWindow

因此,如您所见,您可以在一个Worker(因此,一个线程)内有多个计时器/重新计算/更新信号。但是,实现的关键点是计算需要多长时间。如果它们需要很长时间(例如几乎与间隔一样长),那么您应该考虑使用多个线程来加速计算(意思是:在每个线程中执行一次计算)。当我慢慢地似乎更清楚地了解您想要实现的目标时,我想知道您是否只是关于您在问题中"滥用"线程的这些定期更新。如果确实如此,那么您根本不需要该线程和Worker。然后只需将定时器添加到您的MainWindow,并将其timeout信号直接连接到MainWindow的相应slot