在日常的开发中,偶尔会执行一些可能很费时的代码,比如进行大规模的数据运算,生成或者拷贝文件,网络请求等,这些操作如果放在UI线程去做,一些操作,比如多点几次鼠标,或者是切换到其他程序再切换回来, 都很容易就会导致程序未响应,这是由于ui线程正在执行代码或者被阻塞住了,导致没法处理事件循环,系统认为你这个程序可能挂掉了,就会出现那个未响应提示,然后弹窗问用户,要不要强制干掉这个程序.
优秀的软件这方面都会处理得比较好,会给出一些进度条之类的,而在这方面我最赞赏的就是Android,它会禁止你在ui线程里面做费时操作,比如网络请求,一在ui线程请求网络,立马挂掉,逼着你去实现异步的网络请求,从而保证app的流畅度,早期的Android不会这样的,估计是因为卡顿被骂多了,而开发者又不自觉,不得已才做出这样的限制的吧.Windows没有做出这样的限制,但是也要自觉,避免未响应情况的发生,那么,如何避免呢?
未响应是由于事件没有及时处理导致的,所以是跟消息循环有关的,那么为了避免未响应,则有两个思路:
1.程序执行中间,驱动一下事件循环;
2.费时操作放到子线程,主线程空跑着,消息循环自然没有被阻塞.
举例说明,比如我们在界面上放了一个按钮,然后按钮的点击事件执行循环运算
int sum = 0;
for (int i = 0;i <100; i++)
{
ui.label->setText(QString::number(sum));
for (int j = 0; j <1000000000; j++)
{
sum+= (i*j) & 3;
}
sum += i & 3;
}
这个循环会一直占用着cpu,虽然第一层循环会设置label的值,但是在两层循环结束签并不能看到中间结果的显示,而是等循环完了才显示最后的结果(假设放一个进度条,同理,并不能看到进度的变化,而是一直是0%,最后一下子到100%).
按照第一种思路,我们可以使用QApplication的processEvents方法来进行中间的消息循环驱动,改动代码如下:
int sum = 0;
for (int i = 0; i <100; i++)
{
ui.label->setText(QString::number(sum));
for (int j = 0; j <1000000000; j++)
{
sum += (i*j) & 3;
}
sum += i & 3;
qApp->processEvents();
}
唯一的区别在于第一层的后面加个qApp->processEvents();这个函数就是qt的强制事件驱动,其实它是有参数,接个枚举值,
enum ProcessEventsFlag {
AllEvents = 0x00,
ExcludeUserInputEvents = 0x01,
ExcludeSocketNotifiers = 0x02,
WaitForMoreEvents = 0x04,
X11ExcludeTimers = 0x08,
EventLoopExec = 0x20,
DialogExec = 0x40
};
默认是AllEvents,也就是驱动所有的事件,这个枚举值在很多的教程里面会推荐使用ExcludeUserInputEvents,对此的解释是说忽略用户的输入,从而避免按钮被点击两次,代码重复执行的类似情况,但是其实使用ExcludeUserInputEvents来达到忽略用户输入的目的并不是一个很好的办法,因为它只是此次事件驱动忽略掉而已,一旦你费时的代码执行完了,它会一次将多次的点击事件传给你,到时候可能就会导致大麻烦,故不推荐使用.正确的做法应该是在按钮点击之后,将其置为不可点击,也就是ui.pushButton->setEnabled(false);而processEvents采用默认的AllEvents的方式,这样pushButton本身就不接受这个点击事件了,而这个事件也被处理了,后期不会造成问题.但是倘若你使用ExcludeUserInputEvents,而循环前将setEnabled(false),循环后设置setEnabled(true);,则会循环后多次触发点击事件,连setEnabled都不起作用了 ,意外不意外,惊喜不惊喜?
第二种思路,采用子线程,写异步代码会是一个问题,因为按钮按下去以后,等执行完是应该要有个提示,如果分开写,不在一个函数里面会使程序的复杂度变得很高,而且子线程的开辟,传参过去也会是个问题,好在c++11之后,我们有了std:: thread和lambda表达式及std:: condition_variable,这三个的组合,会让我们代码写起来很容易,代码如下:
int sum = 0;
ui.pushButton->setEnabled(false);
std::condition_variable kl_cv;
std::mutex kl_mtx;
std::unique_lock kl_lck(kl_mtx);
std::thread kl_thread([&]()
{
for (int i = 0; i <100; i++)
{
ui.label->setText(QString::number(sum));
for (int j = 0; j <10000000; j++)
{
sum += (i*j) & 3;
}
sum += i & 3;
}
kl_cv.notify_all();
});
kl_thread.detach();
while (kl_cv.wait_for(kl_lck, std::chrono::milliseconds(100)) == std::cv_status::timeout)
{
qApp->processEvents();
}
ui.pushButton->setEnabled(true);
这里面使用了std:: thread来开启了一个新变量,然后使用lambda捕获全部引用,避免了我们一个个手动传参的麻烦事,最后使用std::condition_variable条件变量的wait_for来进行线程控制,每过100毫秒就进行一次事件驱动,跑起来后,很顺滑,完美!
但是,这么多代码,难得每次都要重新写一次么?这样写起来也比较麻烦,那就把它写成两个宏吧,写完后代码简洁不少:
#define KeepLiveBegin {std::condition_variable kl_cv;std::mutex kl_mtx;std::unique_lock kl_lck(kl_mtx);\
std::thread kl_thread([&](){kl_mtx.lock(); kl_mtx.unlock();
#define KeepLiveEnd kl_cv.notify_all();});kl_thread.detach(); \
while (kl_cv.wait_for(kl_lck, std::chrono::milliseconds(100)) == std::cv_status::timeout){qApp->processEvents();}}
int sum = 0;
ui.pushButton->setEnabled(false);
KeepLiveBegin
for (int i = 0; i <100; i++)
{
ui.label->setText(QString::number(sum));
for (int j = 0; j <10000000; j++)
{
sum += (i*j) & 3;
}
sum += i & 3;
}
KeepLiveEnd
ui.pushButton->setEnabled(true);
样比之前,仅仅多了两行宏定义,使用起来也方便不少.
不过, setEnabled仍然看起来不舒服,如果界面上有很多的按钮,都要来一遍也不好维护,有个好办法可以很优雅地进行拦截,那就是使用事件过滤器,尤其是对QAppliction使用事件过滤器,能起到全局过滤的效果,我实现的代码如下:
class IgnoreEvent :public QObject
{
public:
IgnoreEvent(QObject* obj=qApp)
{
m_obj = obj;
m_obj->installEventFilter(this);
}
~IgnoreEvent()
{
m_obj->removeEventFilter(this);
}
bool eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::MouseButtonPress)
{
event->ignore();
return true;
}
return QObject::eventFilter(obj, event);
}
private:
QObject* m_obj;
};
然后点击事件的代码为
int sum = 0;
IgnoreEvent ignore;
KeepLiveBegin
for (int i = 0; i <100; i++)
{
ui.label->setText(QString::number(sum));
for (int j = 0; j <10000000; j++)
{
sum += (i*j) & 3;
}
sum += i & 3;
}
KeepLiveEnd
相比于之前,去掉了setEnable 然后声明了IgnoreEventignore; IgnoreEvent类最主要的是installEventFilter和removeEventFilter,这里全局过滤掉了键盘的按键操作和鼠标的点击操作.也可以过滤特定界面的事件,当然也可以把这个写进宏定义里面,代码就会更简洁了.
最后,完整代码如下:
#include
#include
#include
#define KeepLiveBegin(ignoreevent) {IgnoreEvent* kl_ie =NULL; if(ignoreevent)kl_ie = new IgnoreEvent(); std::condition_variable kl_cv;std::mutex kl_mtx;std::unique_lock kl_lck(kl_mtx);\
std::thread kl_thread([&](){kl_mtx.lock(); kl_mtx.unlock();
#define KeepLiveEnd kl_cv.notify_all();});kl_thread.detach(); \
while (kl_cv.wait_for(kl_lck, std::chrono::milliseconds(100)) == std::cv_status::timeout){qApp->processEvents();}if(kl_ie!=NULL) delete kl_ie;}
class IgnoreEvent :public QObject
{
public:
IgnoreEvent(QObject* obj=qApp)
{
m_obj = obj;
m_obj->installEventFilter(this);
}
~IgnoreEvent()
{
m_obj->removeEventFilter(this);
}
bool eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::MouseButtonPress)
{
event->ignore();
return true;
}
return QObject::eventFilter(obj, event);
}
private:
QObject* m_obj;
};
KeepLiveTest::KeepLiveTest(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
connect(ui.pushButton, &QPushButton::clicked, [this](){
int sum = 0;
KeepLiveBegin(true)
for (int i = 0; i <100; i++)
{
ui.label->setText(QString::number(sum));
for (int j = 0; j <10000000; j++)
{
sum += (i*j) & 3;
}
sum += i & 3;
}
KeepLiveEnd
});
}