Qt中保持GUI响应

Keeping the GUI Responsive

原文作者: Witold Wysota

译者: Jason Lee @ http://blog.csdn.net/jasonblog

 

在 QtCentre 里的人们经常提到一个反复出现的问题:长操作期间 GUI 界面无响应。这个问题不难解决,并且有多种应对方案,因此我在这里列出一些针对不同情况的可能的解决方案。

 

长操作

第一件事是列出这个问题域并且列出可以采取的解决方案。前面涉及的这个问题可能以两种形式出现。第一种情况是程序在执行计算密集型任务,也就是需要经过一系列操作才能得到最后的结果。比如快速傅里叶变换。

另一种情况是程序必须在已经触发的活动(比如从网络上下载东西)结束后才能继续算法的下一步。这种情况,就本身而言,通过使用 Qt 是比较容易避免的,因为大多数的异步操作在 Qt 中可以采用信号和槽的机制来实现,所以你可以将之连接到一个槽中来继续算法的下一步。

在计算过程中(无论以何种方式使用信号和槽)所有的事件处理都会被暂停。因此, GUI 不会被刷新,用户输入不会被处理,网络活动停止以及定时器不运作——程序看起来被冻结了并且,事实上,程序中不耗时的那部分也被冻结了。“长操作”是多长呢?任何使得程序无法即时响应用户的事情都算长。一秒钟是长,任何超过两秒钟的操作肯定太长了。

本文旨在保持程序功能,防止终端用户被一个不响应的 GUI (以及网络和定时器)所惹恼。为了做到这个目标首先需要看看可能的解决方案和问题的主因。

我们可以通过两种方式来获取计算型任务的结果——通过在主线程(单一线程方案)或者在单独的线程(多线程解决方案)进行计算。后者较多地为人所知并且在 JAVA 中得到应用,但是有时候在使用单一线程会更好的情况下它也被滥用了。与主流观点相反,线程通常使你的程序变慢而非变快,因此除非你确定你的程序可以通过使用多线程变得更优良,否则不要因为你会就随处创建新线程。

该问题域可以看做是两种情形组成的。我们不一定能够将问题细化成更小的步骤、循环或者子问题(通常它不应该是一个整体)。如果问题可以细化,它们也不一定互相依赖。如果它们之间是独立的,我们可以任意处理它们。否则,我们必须同步我们的任务。最坏的情况下,我们只能一次处理一些并且只有上一步结束了才能开始下一步。充分考虑这些因素,我们可以选择不同的解决方案。

 

人工的事件处理

最基本的解决方案就是显示地要求 Qt 在计算过程的某个点处理悬挂事件。为此,需要周期性地调用QCoreApplication::processEvents() 。以下是示例:


这种方法有明显的缺点。比如,假设你想并发地执行两个循环——调用其中一个会暂定另一个,直到该调用结束(所以不能在不同任务之间不能分配计算量)。它也造成了程序对事件处理的延迟。更重要的是代码难以阅读和分析,因此这种方案只适合短而简单的、在单一线程中处理的问题,比如闪屏或短期操作监控。

 

    for (int i = 3; i <= sqrt(x) && isPrime; i += 2) {
        label->setText(tr("Checking %1...").arg(i));
        if (x % i == 0)
            isPrime = false;
        QCoreApplication::processEvents();
        if (!pushButton->isChecked()) {
            label->setText(tr("Aborted"));
            return;
        }
    }

使用任务线程

另一种不同的解决方案就是在单独的线程中执行长操作从而避免阻塞主事件循环。假如任务是由第三方库通过阻塞方式执行,这是特别有效的。在这种情况下,它是不可能去打扰 GUI 处理悬挂事件的。

一种可以完全控制独立线程的方式是使用 QThread 。可以继承它并且重写它的 run() 函数,或者调用QThread:exec() 来启动线程的事件循环,或者两者共用:继承然后必要的时候在 run() 函数里调用 exec() 。可以通过信号和槽来连接主线程——只要记住 QueuedConnection 会被使用到或者其它线程可能失去稳定性然后导致你的程序崩溃。

由于有很多多线程相关材料,所以这里不列举代码示例了,而把注意力集中在其它地方。

 

在本地事件循环中等待

要介绍的下一种处理等待异步任务结束的方案已经完结。这里,将要讨论如何在一个网络操作结束前阻塞程序流,并不阻塞事件处理。从本质上说,我们可以做的就是:

    task.start();
    while (!task.isFinished())
        QCoreApplication::processEvents();

这叫做忙等待——频繁判断一个条件是否满足。大多数情况下这是一个坏主意,它占用了全部 CPU 资源并且有着人工事件处理的所有缺点。

幸运的是, Qt 有一个类来帮助我们处理这项任务: QEventLoop 是程序和模态对话框在它们 exec() 调用里使用的相同的类。每个该类的实例都连接到主事件分发机制,并且一旦它的 exec() 函数激活,它就开始处理事件直到使用 quit() 停止。

我们利用这种机制结合信号和槽使得异步操作转化为同步操作——我们可以开启一个本地事件循环然后通过特定对象的特定信号告知它结束退出:

    QNetworkAccessManager manager;
    QEventLoop q;
    QTimer tT;
   
    tT.setSingleShot(true);
    connect(&tT, SIGNAL(timeout()), &q, SLOT(quit()));
    connect(&manager, SIGNAL(finished(QNetworkReply*)),
            &q, SLOT(quit()));
    QNetworkReply *reply = manager.get(QNetworkRequest(
                   QUrl("http://www.qtcentre.org")));
   
    tT.start(5000); // 5s timeout
    q.exec();
   
    if(tT.isActive()){
        // download complete
        tT.stop();
    } else {
        // timeout
    }


我们使用一个网络访问管理类获取远程 URL 。由于它工作在异步方式,我们创建了一个本地事件循环来等待下载者的结束信号。此外,我们实例化一个定时器在五秒后来结束事件循环以免发生错误。通过连接合适的信号,提交请求和开始定时器,我们进入了新建的事件循环。对 exec() 的调用会在下载结束或者五秒结束后返回。我们通过判断定时器是否还在运作来判断是哪个原因引发结束。然后我们处理结果或者告知用户下载失败。

在这里需要注意另外两件事。一个通过 QxtSignalWaiter 类实现类似的解决方案是 libqxt 项目的一部分。另外一件事是,对于一些操作而言, Qt 提供了一系列的“等待”方法(比如 QIODevice::waitForBytesWritten() )或多或少地做些和上面代码片段一样的事,但并没有运行一个事件循环。然而,“等待”方案会冻结 GUI 因为它们没有运行它们自己的时间循环。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值