Qt Quick 中的阻塞操作
Blocking operation in Qt Quick
我想在单击按钮时执行一项较长的任务。我希望此任务阻止 UI,因为在任务完成之前应用程序无法运行。但是,我想向用户表明正在发生某些事情,因此我有一个 BusyIndicator
(在渲染线程上运行)并设置为在操作开始之前显示。但是,它永远不会呈现。为什么?
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QDateTime>
#include <QDebug>
class Task : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
Task() : mRunning(false) {}
Q_INVOKABLE void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
// Try to ensure that the scene graph has time to begin the busy indicator
// animation on the render thread.
Q_ASSERT(QMetaObject::invokeMethod(this, "doRun", Qt::QueuedConnection));
}
bool running() const {
return mRunning;
}
signals:
void runningChanged();
private:
Q_INVOKABLE void doRun() {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
bool mRunning;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Task task;
engine.rootContext()->setContextProperty("task", &task);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.6
import QtQuick.Window 2.2
import Qt.labs.controls 1.0
Window {
width: 600
height: 400
visible: true
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: task.running ? "Running task" : "Run task"
onClicked: task.run()
}
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: task.running
onRunningChanged: print("BusyIndicator running =", running)
}
}
}
调试输出在事件顺序方面看起来是正确的:
setting running property to true
qml: BusyIndicator running = true
beginning long, blocking operation
finished long, blocking operation
setting running property to false
qml: BusyIndicator running = false
使用 Qt::QueuedConnection
调用函数并不能保证 BusyIndicator
有机会开始动画。它只是 guarantees that:
The slot is invoked when control returns to the event loop of the receiver's thread.
Another solution 听起来很有希望的是 QTimer::singleShot()
:
QTimer::singleShot(0, this, SLOT(doRun()));
As a special case, a QTimer with a timeout of 0 will time out as soon as all the events in the window system's event queue have been processed. This can be used to do heavy work while providing a snappy user interface [...]
但是,这也行不通。我不确定为什么。可能是在内部,rendering/animation 不是通过排队调用完成的,因此超时发生得太早了。
您可以指定任意的等待时间:
QTimer::singleShot(10, this, SLOT(doRun()));
这会起作用,但不是很好;这只是猜测。
您需要的是 reliable way 知道场景图何时开始动画。
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QDateTime>
#include <QDebug>
#include <QTimer>
#include <QQuickWindow>
class Task : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
Task(QObject *parent = 0) :
QObject(parent),
mRunning(false) {
}
signals:
void runningChanged();
public slots:
void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine*>(parent());
QQuickWindow *window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
connect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
}
bool running() const {
return mRunning;
}
private slots:
void doRun() {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine*>(parent());
QQuickWindow *window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
disconnect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
private:
bool mRunning;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Task task(&engine);
engine.rootContext()->setContextProperty("task", &task);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.6
import QtQuick.Window 2.2
import Qt.labs.controls 1.0
Window {
width: 600
height: 400
visible: true
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: task.running ? "Running task" : "Run task"
onClicked: task.run()
}
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: task.running
onRunningChanged: print("BusyIndicator running =", running)
}
}
}
此解决方案依赖于对应用程序 window 的访问权限,这 并不好,但它消除了任何猜测。请注意,如果我们之后不断开信号,则每次场景图完成同步时都会继续调用它,因此执行此操作很重要。
如果您有多个操作需要此类解决方案,请考虑创建一个可重复使用的 class:
class BlockingTask : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
BlockingTask(QQmlApplicationEngine *engine) :
mEngine(engine),
mRunning(false) {
}
bool running() const {
return mRunning;
}
signals:
void runningChanged();
public slots:
void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
QQuickWindow *window = qobject_cast<QQuickWindow*>(mEngine->rootObjects().first());
connect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
}
protected:
virtual void execute() = 0;
private slots:
void doRun() {
QQuickWindow *window = qobject_cast<QQuickWindow*>(mEngine->rootObjects().first());
disconnect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
execute();
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
private:
QQmlApplicationEngine *mEngine;
bool mRunning;
};
那么子class们只需要担心他们的逻辑:
class Task : public BlockingTask
{
Q_OBJECT
public:
Task(QQmlApplicationEngine *engine) :
BlockingTask(engine) {
}
protected:
void execute() Q_DECL_OVERRIDE {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
}
};
QML 中的大多数动画都依赖于主线程中管理的属性,因此当主 UI 线程被阻塞时也会被阻塞。查看 http://doc.qt.io/qt-5/qml-qtquick-animator.html 以了解在主线程被阻塞时可以 运行 的动画。如果可能的话,我会将操作移到另一个线程中,这样会简单得多,并且还允许例如从 UI.
取消操作
我想在单击按钮时执行一项较长的任务。我希望此任务阻止 UI,因为在任务完成之前应用程序无法运行。但是,我想向用户表明正在发生某些事情,因此我有一个 BusyIndicator
(在渲染线程上运行)并设置为在操作开始之前显示。但是,它永远不会呈现。为什么?
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QDateTime>
#include <QDebug>
class Task : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
Task() : mRunning(false) {}
Q_INVOKABLE void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
// Try to ensure that the scene graph has time to begin the busy indicator
// animation on the render thread.
Q_ASSERT(QMetaObject::invokeMethod(this, "doRun", Qt::QueuedConnection));
}
bool running() const {
return mRunning;
}
signals:
void runningChanged();
private:
Q_INVOKABLE void doRun() {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
bool mRunning;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Task task;
engine.rootContext()->setContextProperty("task", &task);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.6
import QtQuick.Window 2.2
import Qt.labs.controls 1.0
Window {
width: 600
height: 400
visible: true
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: task.running ? "Running task" : "Run task"
onClicked: task.run()
}
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: task.running
onRunningChanged: print("BusyIndicator running =", running)
}
}
}
调试输出在事件顺序方面看起来是正确的:
setting running property to true
qml: BusyIndicator running = true
beginning long, blocking operation
finished long, blocking operation
setting running property to false
qml: BusyIndicator running = false
使用 Qt::QueuedConnection
调用函数并不能保证 BusyIndicator
有机会开始动画。它只是 guarantees that:
The slot is invoked when control returns to the event loop of the receiver's thread.
Another solution 听起来很有希望的是 QTimer::singleShot()
:
QTimer::singleShot(0, this, SLOT(doRun()));
As a special case, a QTimer with a timeout of 0 will time out as soon as all the events in the window system's event queue have been processed. This can be used to do heavy work while providing a snappy user interface [...]
但是,这也行不通。我不确定为什么。可能是在内部,rendering/animation 不是通过排队调用完成的,因此超时发生得太早了。
您可以指定任意的等待时间:
QTimer::singleShot(10, this, SLOT(doRun()));
这会起作用,但不是很好;这只是猜测。
您需要的是 reliable way 知道场景图何时开始动画。
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QDateTime>
#include <QDebug>
#include <QTimer>
#include <QQuickWindow>
class Task : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
Task(QObject *parent = 0) :
QObject(parent),
mRunning(false) {
}
signals:
void runningChanged();
public slots:
void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine*>(parent());
QQuickWindow *window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
connect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
}
bool running() const {
return mRunning;
}
private slots:
void doRun() {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine*>(parent());
QQuickWindow *window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
disconnect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
private:
bool mRunning;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Task task(&engine);
engine.rootContext()->setContextProperty("task", &task);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.6
import QtQuick.Window 2.2
import Qt.labs.controls 1.0
Window {
width: 600
height: 400
visible: true
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: task.running ? "Running task" : "Run task"
onClicked: task.run()
}
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: task.running
onRunningChanged: print("BusyIndicator running =", running)
}
}
}
此解决方案依赖于对应用程序 window 的访问权限,这 并不好,但它消除了任何猜测。请注意,如果我们之后不断开信号,则每次场景图完成同步时都会继续调用它,因此执行此操作很重要。
如果您有多个操作需要此类解决方案,请考虑创建一个可重复使用的 class:
class BlockingTask : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
BlockingTask(QQmlApplicationEngine *engine) :
mEngine(engine),
mRunning(false) {
}
bool running() const {
return mRunning;
}
signals:
void runningChanged();
public slots:
void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
QQuickWindow *window = qobject_cast<QQuickWindow*>(mEngine->rootObjects().first());
connect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
}
protected:
virtual void execute() = 0;
private slots:
void doRun() {
QQuickWindow *window = qobject_cast<QQuickWindow*>(mEngine->rootObjects().first());
disconnect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
execute();
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
private:
QQmlApplicationEngine *mEngine;
bool mRunning;
};
那么子class们只需要担心他们的逻辑:
class Task : public BlockingTask
{
Q_OBJECT
public:
Task(QQmlApplicationEngine *engine) :
BlockingTask(engine) {
}
protected:
void execute() Q_DECL_OVERRIDE {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
}
};
QML 中的大多数动画都依赖于主线程中管理的属性,因此当主 UI 线程被阻塞时也会被阻塞。查看 http://doc.qt.io/qt-5/qml-qtquick-animator.html 以了解在主线程被阻塞时可以 运行 的动画。如果可能的话,我会将操作移到另一个线程中,这样会简单得多,并且还允许例如从 UI.
取消操作