通过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表达式本质上跟全局函数差不多,它被调用是不分时候的,如果捕获了一个局部变量,而该变量在执行时候已经失效了,真是坑死人不偿命:与“Qt Webengine回调函数内赋值引发Segment Fault的灵异事件”的原理十分相似;

刚才已经封装出来了两个信号:newMessageIncoming(QString newMessage, bool isError)以及computingFinished(bool isExitedNormally),在窗体这头connect一下,把数据和状态反映在GUI中,就OK了。

ComputeProcessWrapper.h
#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
ComputeProcessWrapper.cpp
#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);
}
  • 最后更改: 2019/07/20 16:15