编写大型工程计算软件时,计算核心与图形操作界面混在一起不是一个好的实践。一则不容易实现跨平台,二则难以在非图形环境下部署和使用,三则这样写容易让二者的代码混在一起提高维护难度,四则图形库往往引入不必要的性能开销和难以预知的输入。所以,分别单独实现计算核心和图形操作界面,然后通过命令行调用、消息输出是一个很合适的方案。
这些天基于Qt
搞了一个这样一套方案,同时以此为契机搞清楚了Qt
的信号(signal)/槽(slot)
机制,这里做一个总结。
比较容易,Qt
已经提供了很好的封装:
computeProcess.setProgram(launchOption.executable); computeProcess.setArguments(launchOption.arguments); computeProcess.setWorkingDirectory(launchOption.runDirectory); computeProcess.start(); while (!computeProcess.waitForFinished()) { QThread::sleep(1); } emit computingFinished(computeProcess.exitStatus() == QProcess::NormalExit);
launchOption
就是个结构体,存放了一些字符串。start
是实际启动外部程序。computeProcess.waitForFinished()
看名字是等待到程序结束,但实际上默认最长只等三十秒,然后就返回一个false
回来。理论上可以传一个参数让他永久等待,不过想想还是用一个循环多次调用它比较好,需要的话,循环里面可以做一些别的事情,比如计数什么的。
最后一句emit ...
是发送一个信号,信号中包含了一个数据:程序是否正常退出。
可以对Process
对象执行相关的抓取函数来获得。但是单开一个线程反复尝试抓取(我在Java
里面就是这样干的!)相当的不优雅。利用Qt
的消息机制,可以将他们封装为信号:
// 在一个继承了QObject的类中 // 已经声明了: // public slots: // void doCompute(); // 以及 // signals: // void newMessageIncoming(QString newMessage, bool isError); // void computingFinished(bool isExitedNormally); connect(&computeProcess, &QProcess::readyReadStandardOutput, [this]() { emit newMessageIncoming(computeProcess.readAllStandardOutput(), false); }); connect(&computeProcess, &QProcess::readyReadStandardError, [this]() { emit newMessageIncoming(computeProcess.readAllStandardError(), true); });
等于是,把Process
中Qt原生提供的的“有标准输出/错误输出”信号,连接到了两个lambda
表达式史上,这两个表达式所执行的内容则是读出所有的新来的信息,并且将他们封装到一个自定义的信号newMessageIncoming
中,发射出去。
对于信号的槽的几点认识:
消息/signal
是不需要实现的,它只是一种约定,规定消息的“名字”以及格式(其中包含哪些参数以及参数类型);很好奇强行实现一个会怎么样;emit
出去,与之相connect
的那些slot
被调用,传递进来的参数的值就是emit
时写进去的那几个;slot
被调用时应该是保证了线程安全,因为我尝试过手动开一个线程来执行Process
,然后从这个线程里面去直接向窗口中的文本框增加数据,结果运行时会报警告说“从另外一个线程操作GUI是不安全的”;slot
被调用时应该默认是新开了一个线程的,因为上面操作、启动Process
的那些代码是放在一个叫doCompute()
的槽里面的,显然执行它需要很长时间,但主动的这个线程并没有被阻塞;所以目前还不很清楚为啥通过slot
去修改GUI就没有警告了。slot
有是否public
之分,显然有些slot
传递进来的数据是有要求的,不能让外人乱搞;signal
是没有public
与否的概念的,因为传出去之后大家都看到了;connect
的时候,操作对象不是某个类
,而是某个类的某个实例
,所以需要两个指针;Qt 5
之后,slot
也可以是一个lambda
表达式,这时就不用(也不能)指定接受对象了;但是这种lambda
表达式写起来要特别小心捕获范围,因为lambda
表达式本质上跟全局函数差不多,它被调用是不分时候的,如果捕获了一个局部变量,而该变量在执行时候已经失效了,真是坑死人不偿命:与“Qt Webengine回调函数内赋值引发Segment Fault的灵异事件”的原理十分相似;
刚才已经封装出来了两个信号:newMessageIncoming(QString newMessage, bool isError)
以及computingFinished(bool isExitedNormally)
,在窗体这头connect
一下,把数据和状态反映在GUI中,就OK了。
#ifndef COMPUTEPROCESSWRAPPER_H #define COMPUTEPROCESSWRAPPER_H #include "LaunchOption.h" #include <QObject> #include <QProcess> class ComputeProcessWrapper : public QObject { Q_OBJECT public: ComputeProcessWrapper(); LaunchOption launchOption; QProcess computeProcess; public slots: void doCompute(); signals: void newMessageIncoming(QString newMessage, bool isError); void computingFinished(bool isExitedNormally); private: void setProcess(); }; #endif // COMPUTEPROCESSWRAPPER_H
#include "ComputeProcessWrapper.h" #include <QThread> ComputeProcessWrapper::ComputeProcessWrapper() { connect(&computeProcess, &QProcess::readyReadStandardOutput, [this]() { emit newMessageIncoming(computeProcess.readAllStandardOutput(), false); }); connect(&computeProcess, &QProcess::readyReadStandardError, [this]() { emit newMessageIncoming(computeProcess.readAllStandardError(), true); }); } void ComputeProcessWrapper::setProcess() { computeProcess.setProgram(launchOption.executable); computeProcess.setArguments(launchOption.arguments); computeProcess.setWorkingDirectory(launchOption.runDirectory); } void ComputeProcessWrapper::doCompute() { setProcess(); computeProcess.start(); while (!computeProcess.waitForFinished()) { QThread::sleep(1); } emit computingFinished(computeProcess.exitStatus() == QProcess::NormalExit); }