用事件队列解决QT GUI的操作顺序问题
Posted schips
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用事件队列解决QT GUI的操作顺序问题相关的知识,希望对你有一定的参考价值。
原文: https://www.cnblogs.com/Philip-Tell-Truth/p/6295186.html
GUI操作顺序问题引发异常
有时候我们使用写GUI程序的时候会遇到这样的问题:比如在程序中,建立了一个列表的GUI。这个列表是随着时间不断更新的,而且操作也会读取这个列表GUI的内容。
如果这个程序是多线程的程序,而且只是除了GUI的线程不操作,只是其他线程操作这个列表GUI,那么这个问题很简单,只用加互斥锁就可以了。但如果GUI线程自己本身也要操作这个列表,那么这个问题就很麻烦了。
我们可以很容易地想到一种场景,比如GUI线程读了列表的一些表项(比如选定),此时线程中的某个方法keep了这些表项的指针,然而此时很不幸别的线程有一个请求需要删除列表中的一些表项,并且这些表项有一些包含在了我们的选定内容里,我们知道几乎所有的语言操作GUI时都要进入GUI线程里面操作,那么我们刚才选定表项的那个方法会被打断,然后进入删除表项方法,在删除了表项以后再次回到选定表项方法时,我们的选定的表项有一些已经被删除了,此时我们再进行操作很有可能不符合我们的要求。
如果你是用一般是用C#,JAVA这种不用自己管理内存的语言,那还好,只是结果可能不对,但是如果是用C++这种需要我们自己管理内存的来写,很有可能我们会操作一个被释放了内存的对象,然后程序崩掉,这样的情况是我们不想看到的。
用事件队列来解决问题:
下面用一幅图来表示如何设计事件队列:
当然图中虽然是只有三种操作,如果你想,你可以设计出更多的操作,比如读操作,你可以细分为复制表项中的信息和给表项中对应的内容进行操作等。
这样设计以后,就一定程度上消除了GUI打断操作的问题(比如我们会再遇到我们上面的那种访问了一个被析构了的对象问题)。
在Qt中我们可以这样写:(ConnectionView这个对象就是我上面说的那种表项)
class ItemsOpsBase
{
public:
virtual void doOperation(ConnectionView *view) = 0;
virtual ~ItemsOpsBase() = default;
};
class DeleteItem : public ItemsOpsBase
{
public:
DeleteItem(qintptr target,qint32 connectionIndex)
:ItemsOpsBase(), _target(target),_connectionIndex(connectionIndex){ }
void doOperation(ConnectionView *view)override;
~DeleteItem() = default;
private:
qintptr _target;
qint32 _connectionIndex;
};
class UpdatePulse :public ItemsOpsBase
{
public:
UpdatePulse(qintptr descriptor,qint32 currentTime)
:ItemsOpsBase(), _descriptor(descriptor),_currentTime(currentTime){ }
void doOperation(ConnectionView *view)override;
~UpdatePulse() = default;
private:
qintptr _descriptor;
qint32 _currentTime;
};
class UpdateRemark : public ItemsOpsBase
{
public:
UpdateRemark(qintptr descriptor, const QString &remark)
: ItemsOpsBase(),_remark(remark),_descriptor(descriptor){ }
void doOperation(ConnectionView *view)override;
~UpdateRemark() = default;
private:
QString _remark;
qintptr _descriptor;
};
class TestConnection : public ItemsOpsBase
{
public:
void doOperation(ConnectionView *view)override;
};
class TestConnectionProducer : public QThread
{
public:
void run()override;
};
class CopySelectedItemInformProducer : public QThread
{
public:
void run()override;
};
class DisconnectTargetsProducer : public QThread
{
public:
void run()override;
};
class DeleteItemProducer :public QThread
{
public:
DeleteItemProducer(qintptr target, qint32 connectionIndex)
: QThread(),_target(target),_connectionIndex(connectionIndex) { }
void run()override;
private:
qintptr _target;
qint32 _connectionIndex;
};
class UpdatePulseProducer :public QThread
{
public:
UpdatePulseProducer(qintptr descriptor, qint32 currentTime)
:QThread(),_descriptor(descriptor),_currentTime(currentTime){ }
protected:
void run()override;
private:
qintptr _descriptor;
qint32 _currentTime;
};
class UpdateRemarkProducer : public QThread
{
public:
UpdateRemarkProducer(qintptr descriptor, const QString &remark)
:QThread(),_remark(remark),_descriptor(descriptor){ }
protected:
void run()override;
private:
QString _remark;
qintptr _descriptor;
};
class ConsumerHelper :public QThread
{
public:
ConsumerHelper(ConnectionView *view)
:QThread(),_view(view){ }
~ConsumerHelper();
protected:
void run() override;
private:
ConnectionView *_view;
ConsumerHelper(const ConsumerHelper &other) = delete;
ConsumerHelper(const ConsumerHelper &&other) = delete;
ConsumerHelper &operator=(const ConsumerHelper &other) = delete;
};
互斥锁以及队列的代码:
static QQueue<QSharedPointer<ItemsOpsBase>> &opQueue()
{
static QQueue<QSharedPointer<ItemsOpsBase>> queue;
return queue;
}
static QSharedPointer<ItemsOpsBase> endOperation;
static QMutex &opQueueLock()
{
static QMutex mutex;
return mutex;
}
static QWaitCondition &opQueueIsAvailable()
{
static QWaitCondition flag;
return flag;
}
ConsumerHelper是一个消费者线程,一直监视着队列的动向,当需要一个某个操作的时候,我们就可以引发一个对象操作的线程,把对应操作加入队列中(为什么需要开一个线程,是为了方便互斥),比如下面我需要一个删除操作:
删除操作的代码:
void DeleteItem::doOperation(ConnectionView *view)
{
qRegisterMetaType<qintptr>("qintptr");
qRegisterMetaType<TcpConnectionHandler *>("TcpConnectionHandler *");
QMetaObject::invokeMethod(view, "deleteConnection",Qt::QueuedConnection, Q_ARG(qintptr, _target), Q_ARG(qint32, _connectionIndex));
}
void DeleteItemProducer::run()
{
QSharedPointer<ItemsOpsBase> op = QSharedPointer<ItemsOpsBase>(new DeleteItem(_target,_connectionIndex));
QMutexLocker locker(&opQueueLock());
opQueue().enqueue(op);
opQueueIsAvailable().wakeOne();
}
消费者线程的代码:
void ConsumerHelper::run()
{
forever
{
QSharedPointer<ItemsOpsBase> opPointer;
{
QMutexLocker locker(&opQueueLock());
if (opQueue().isEmpty())
opQueueIsAvailable().wait(&opQueueLock());
opPointer = opQueue().dequeue();
if (opPointer == endOperation)
break;
}
{
if(!opPointer.isNull())
opPointer->doOperation(_view);
}
}
}
ConsumerHelper::~ConsumerHelper()
{
{
QMutexLocker locker(&opQueueLock());
while(!opQueue().isEmpty())
opQueue().dequeue();
opQueue().enqueue(endOperation);
opQueueIsAvailable().wakeOne();
}
wait();//注意这里是wait在次线程上的
}
这个时候我只需要在需要用到删除操作的地方用:
DeleteItemProducer *deleteItemProducer = new DeleteItemProducer(target,index);
connect(deleteItemProducer, &QThread::finished, deleteItemProducer, &QThread::deleteLater);
deleteItemProducer->start();
以上是关于用事件队列解决QT GUI的操作顺序问题的主要内容,如果未能解决你的问题,请参考以下文章
GUI 编程 —— QT 的 QSlider 鼠标点击定位问题