如何防止QT程序未响应

       在日常的开发中,偶尔会执行一些可能很费时的代码,比如进行大规模的数据运算,生成或者拷贝文件,网络请求等,这些操作如果放在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 <std::mutex> 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 <std::mutex> 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 <thread>
#include <mutex>
#include <condition_variable>

#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 <std::mutex> 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
	});
}

 


展开阅读全文

没有更多推荐了,返回首页