# 通过Qt优雅地调用命令行计算程序并抓取输出显示在窗口上
编写大型工程计算软件时,计算核心与图形操作界面混在一起不是一个好的实践。一则不容易实现跨平台,二则难以在非图形环境下部署和使用,三则这样写容易让二者的代码混在一起提高维护难度,四则图形库往往引入不必要的性能开销和难以预知的输入。所以,分别单独实现计算核心和图形操作界面,然后通过命令行调用、消息输出是一个很合适的方案。
这些天基于`Qt`搞了一个这样一套方案,同时以此为契机搞清楚了`Qt`的`信号(signal)/槽(slot)`机制,这里做一个总结。
## 功能点
+ 使用特定的命令行参数启动计算核心
+ 实时抓取计算核心的输出,显示在图形界面上
+ 计算过程中要保持GUI的正常响应
## 实现
### 调用计算核心
比较容易,`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`表达式本质上跟全局函数差不多,它被调用是不分时候的,如果捕获了一个局部变量,而该变量在执行时候已经失效了,真是坑死人不偿命:与“[[:Coding:Cpp:Qt_webengine_runJavaScript_cannot_assign_to_local_variable|Qt Webengine回调函数内赋值引发Segment Fault的灵异事件]]”的原理十分相似;
## GUI的更新
刚才已经封装出来了两个信号:`newMessageIncoming(QString newMessage, bool isError)`以及`computingFinished(bool isExitedNormally)`,在窗体这头`connect`一下,把数据和状态反映在GUI中,就OK了。
## 核心代码
#ifndef COMPUTEPROCESSWRAPPER_H
#define COMPUTEPROCESSWRAPPER_H
#include "LaunchOption.h"
#include
#include
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
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);
}