Qt如何使一个按钮响应时,插槽连接到它的click()信号正在运行

Qt how to make a button responsive when the slot connected to its clicked() signal is running?

本文关键字:click 信号 运行 连接 何使一 按钮 响应 插槽 Qt      更新时间:2023-10-16

我有一个连接到它的click()信号的插槽按钮。

槽是一个耗时的任务,需要花费很多秒。

当尝试在插槽结束之前重新点击按钮时,我不能。

如何使按钮响应时运行插槽连接到其click()信号?

在GUI中运行的任何东西都不能阻塞,也不能花费太多时间来完成。不要在GUI线程中执行长时间运行的操作。这将解决你的问题。

现在你可能会问:那么我怎么做长时间运行的事情呢?您可以在其他线程中执行它们,但这并不意味着必须看到QThread的实例。

你必须隔离数据和对数据的操作。假设我们有如下的类:

class MyData {
public:
    MyData() {}
    /// Loads and pre-processes the data
    bool load(const QString & file) { QThread::sleep(5); return true; }
    /// Transforms the data
    void transform() { QThread::sleep(2); }
};
using MyDataPtr = std::shared_ptr<MyData>;
Q_DECLARE_METATYPE(MyDataPtr)
int main(...) { qRegisterMetaType<MyDataPtr>(); ... }

为了使它更普遍,我们假设复制和移动MyData是昂贵的。

如果操作是"生成新数据"的类型,比如读取文件,处理数据和构建数据结构,您可以在工作线程中实例化数据并将其传递给GUI线程:

class GuiClass : public QWidget {
    Q_OBJECT
    MyDataPtr data;
    Q_SIGNAL void hasData(const MyDataPtr &);
public:
    GuiClass() {
        connect(this, &GuiClass::hasData,
                this, [this](const MyDataPtr & newData){ data = newData; });
    }
    Q_SLOT void makeData(const QString & file) {
        QtConcurrent::run([=]{
            auto data = std::make_shared<MyData>();
            if (data->load(file))
                emit hasData(data);
        });
    }

如果您需要转换已经存在的数据,一些常见的方法是:

  1. 复制数据,在工作线程中转换,然后在转换后交换。

        Q_SLOT void transformData1() {
            MyDataPtr copy = std::make_shared<MyData>(*data);
            QtConcurrent::run([=]{
                copy->transform();
                emit hasData(copy);
            });
        }
    
  2. 暂时删除数据,转换数据,然后再放回去。UI将不得不处理丢失的数据,例如,当data为null时显示"忙"指示灯。

       Q_SLOT void transformData2() {
            MyDataPtr data = this->data;
            this->data.reset();
            QtConcurrent::run([=]{
                data->transform();
                emit hasData(data);
            });
        }
    

如果MyData很容易移动,并且默认值具有"null"/"empty"的含义,则可以通过值来保留它:

class GuiClassVal : public QWidget {
    Q_OBJECT
    MyData data;
    struct DataEvent : public QEvent {
        static const QEvent::Type type;
        MyData data;
        DataEvent(const MyData & data) : QEvent{type}, data{data} {}
        DataEvent(MyData && data) : QEvent{type}, data{std::move(data)} {}
    };
    /// This method is thread-safe.
    void setData(MyData && data) {
        QCoreApplication::postEvent(this, new DataEvent{std::move(data)});
    }
    /// This method is thread-safe.
    void setData(const MyData & data) {
        QCoreApplication::postEvent(this, new DataEvent{data});
    }
    bool event(QEvent *event) override {
        if (event->type() == DataEvent::type) {
            data.~MyData();
            new (&data) MyData{std::move(static_cast<DataEvent*>(event)->data)};
            return true;
        }
        return QWidget::event(event);
    }
    void transformInEvent(DataEvent * ev){
        ev->data.transform();
        QCoreApplication::postEvent(this, ev);
    };
public:
    Q_SLOT void makeData(const QString & file) {
        QtConcurrent::run([=]{
            MyData data;
            if (data.load(file)) setData(std::move(data));
        });
    }
    Q_SLOT void transformData1() {
        QtConcurrent::run(this, &GuiClassVal::transformInEvent, new DataEvent{data});
    }
    Q_SLOT void transformData2() {
        QtConcurrent::run(this, &GuiClassVal::transformInEvent, new DataEvent{std::move(data)});
    }
};
const QEvent::Type GuiClassVal::DataEvent::type = (QEvent::Type)QEvent::registerEventType();

基于事件的魔力酱当然可以被分解。

您有两个高级选择:

  • 将耗时的选择生成到一个线程中:最简单的方法是使用QtConcurrent::run(),在GUI端,您需要担心在操作执行时适当地处理所有GUI事件。
  • 在你耗时的代码中,经常调用QApplication::processEvents(),使GUI保持响应(例如,在循环内部):如果耗时的操作是你不能跳出的第三方代码,这将不起作用,并且你仍然需要弄清楚如何处理GUI操作,而你的长时间运行的任务正在执行。

你会发现,到目前为止,第一个选择比第二个选择要好得多。