Qt多进程(二)QProcess+stdio

前言

本节将学习第一种最简单、也十分常用的进程间通信方法,即主进程使用QProcess类调用子进程,并通过它们之间的重定向系统管道,实现基于stdio标准输入输出的通信。

一、QProcess

QProcess 是 Qt 提供的一个类,用于启动外部程序(可执行文件),并与其进行通信和控制。它封装了底层操作系统(如 Windows 的 CreateProcess)的进程创建机制,并提供了跨平台的接口。

使用时较为简单,如果你只是希望启动一个子进程的话,可以直接这样:

dart 复制代码
#define IPC_APP "./IPCTest.exe"

m_process = new QProcess(this);
m_process->start(IPC_APP, {"stdio"});

IPC_APP是需要启动子进程的执行程序路径,这里直接调用同一个exe即可。

这里有两个需要注意的点:

1.{"stdio"}是传入的参数,本质上是一个QStringList,可以按照需要进行传入;

2.start这个方法是非分离式的进程调用,即子进程虽然是单独的进程,但它依附于父进程的实现,一旦父进程关闭了,子进程也会跟着关闭。
如果你希望子进程单独管理自己的生命周期,可以选择分离式的启动:

cpp 复制代码
m_process->startDetached(IPC_APP, {"stdio"});

QProcess还有一些可以利用的槽函数,比如:

1.QProcess::readyReadStandardOutput,代表有可以读取的标准输出数据;

2.QProcess::readyReadStandardError,代表出现了错误;

3.QProcess::finished,代表该进程已经完成结束。

利用这些信号槽,我们可以实现异步的进程调用,也就是非阻塞式的调用打开。

如果希望阻塞当前界面,等待子进程打开,甚至立即得到它的回复,可以利用wait相关的接口,阻塞等待:

1.process.waitForStarted(),等待启动是否成功;

2.process.waitForFinished(3000),等待是否子进程是否完成;

异步版和同步版,都会在之后的代码示例中展示!

二、main函数的参数列表

说到这里,有必要说一下main函数总的参数列表传递。我们知道main函数是C/C++中的程序入口,也是一个进程启动时最开始的地方。它有一个参数列表,代表外部调用时传入的参数。如果你在cmd中启动过程序(如ffmpeg),应该会对此有了解。

main函数的参数数量不固定,没记错的话第一位是启动路径,后续是真正传入的参数。

因为QProcess+stdio的子进程是一次性被调用的,然后即刻返回信息后就退出了,所以我们可以这样写:

cpp 复制代码
#include <QApplication>
#include <QTextStream>
#include <QThread>
#include <QDebug>
#include "mainwindow.h"

void runStdioMode()
{
    QTextStream in(stdin);
    QString input = in.readLine();
    QThread::msleep(500);
    QString output = "[Return-Stdio] Echo: " + input;
    QTextStream out(stdout);
    out << output << Qt::endl;
    out.flush();
}

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QStringList args = QApplication::arguments();

    qDebug()<<"main args"<<args;

    // 如果打开参数中包含stdio,代表当前进程由主进程调用,此时无界面直接返回
    if (args.contains("stdio")) {
        runStdioMode();
        return 0;
    }

    // 其他模式进入UI界面选择
    MainWindow mainWindow;
    mainWindow.show();

    return app.exec();
}

如果参数中包含"stdio",则进入该模式的处理,不进入后续的界面打开。

runStdioMode函数中,本质上就是对stdio的读取和写入。当然,其中我还加入了QThread::msleep(500)来模拟半秒钟的延时,用于观察父进程的阻塞情况。

这里多说一句,这种参数传递的方式在进程间调用打开时还蛮常用的,实际工作中有接触过需要调用另一个程序,并给它传递一系列字符串的需求。当时考虑如果需要进行一些通信方式的具体实现,感觉太麻烦了,最后想到了还有参数传递这一办法,实现起来也是相当轻松和简单。

所以,我认为main函数参数传递,也应该称得上是进程间通信,并且是最简单和最实用的一种方式。

三、stdio 通信机制

每个进程在启动时默认拥有三个标准流:

stdin(标准输入):用于接收外部输入(写入端) stdout(标准输出):用于输出正常数据(读取端)

stderr(标准错误):用于输出错误信息(读取端) 当主进程通过 QProcess 启动子进程时,Qt

可以将这些流重定向为管道(pipe),从而实现:

主进程 → 子进程:通过 QProcess::write() 写入子进程的 stdin 子进程 → 主进程:通过连接

readyReadStandardOutput() / readyReadStandardError() 信号,读取子进程的 stdout

/ stderr

以上是ai给予的回答,我的理解是父子进程会通过QProcess这个类在对方的标准输入输出中进行写入和读取。

四、代码演示

上一节已经说了,每一种IPC通信方式我都会实现它对应的界面类,这里我新增了StdioWindow界面:

cpp 复制代码
#ifndef STDIOWINDOW_H
#define STDIOWINDOW_H

#include <QWidget>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QProcess>
#include <QDateTime>

class StdioWindow : public QWidget
{
    Q_OBJECT

public:
    explicit StdioWindow(const QString &role, QWidget *parent = nullptr);

private slots:
    void onSendMessage();
    void onProcessReadyRead();
    void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
    void onProcessErrorOccurred(QProcess::ProcessError error);

private:
    void appendLog(const QString &msg);

    QString m_role;
    QTextEdit *m_logView;
    QLineEdit *m_inputEdit;
    QPushButton *m_sendButton;
    QProcess *m_process;
};

#endif // STDIOWINDOW_H
cpp 复制代码
#include "stdiowindow.h"
#include <QLabel>

#define IPC_APP "./IPCTest.exe"

StdioWindow::StdioWindow(const QString &role, QWidget *parent)
    : QWidget(parent)
    , m_role(role)
    , m_process(nullptr)
{
    setWindowTitle("Stdio Communication - " + role);
    resize(600, 500);

    m_logView = new QTextEdit();
    m_logView->setReadOnly(true);
    m_inputEdit = new QLineEdit();
    m_inputEdit->setPlaceholderText("Enter message to send to process...");
    m_sendButton = new QPushButton("Send Message");

    QVBoxLayout *mainLayout = new QVBoxLayout();
    mainLayout->addWidget(m_logView);
    mainLayout->addWidget(m_inputEdit);
    mainLayout->addWidget(m_sendButton);

    setLayout(mainLayout);

    connect(m_sendButton, &QPushButton::clicked, this, &StdioWindow::onSendMessage);

    appendLog("Stdio " + m_role + " initialized");
}

void StdioWindow::onSendMessage()
{
    QString msg = m_inputEdit->text();
    if (msg.isEmpty()) return;

#if 1
    // 异步版
    if (m_process && m_process->state() == QProcess::Running) {
        appendLog("⚠️ Previous process still running!");
        return;
    }

    appendLog("【Sending】Starting Worker (async)...");

    // 使用堆对象,确保生命周期足够长
    delete m_process;
    m_process = new QProcess(this);

    // 👇 关键:连接信号槽
    connect(m_process, &QProcess::readyReadStandardOutput, this, &StdioWindow::onProcessReadyRead);
    connect(m_process, &QProcess::readyReadStandardError, this, &StdioWindow::onProcessReadyRead); // 复用同函数
    connect(m_process, &QProcess::finished, this, &StdioWindow::onProcessFinished);

    m_process->start(IPC_APP, {"stdio"});
    // m_process->startDetached(IPC_APP, {"stdio"});
    if (!m_process->waitForStarted()) {
        appendLog("❌ Failed to start Worker!");
        delete m_process;
        m_process = nullptr;
        return;
    }

    // 发送消息
    QString message = "Async test: " + QDateTime::currentDateTime().toString();
    m_process->write(message.toUtf8() + "\n");
    m_process->closeWriteChannel();

    return;

#else
    // 同步版,局部变量
    appendLog("【Sending】Starting Worker...");

    QProcess process;
    // 启动 Worker 进程(注意:路径是相对的,需确保 WorkerApp 在同一目录)
    process.start(IPC_APP, {"stdio"});

    if (!process.waitForStarted()) {
        appendLog("❌ Failed to start Worker!");
        return;
    }


    // 发送消息到 Worker 的 stdin
    // QString message = "Hello from Master at " + QDateTime::currentDateTime().toString();

    // QString message = u8"你好!这是中文测试 🌟\n"
    //                   "This is a large text...\n"
    //                   + QString(1000, 'A') + "\n"  // 1000 个 A
    //                   + "Timestamp: " + QDateTime::currentDateTime().toString();

    process.write(msg.toUtf8() + "\n");
    process.closeWriteChannel(); // 告诉 Worker 输入结束

    // 等待 Worker 处理完成
    if (!process.waitForFinished(3000)) {
        appendLog("⚠️ Worker timeout!");
        return;
    }

    // 读取 Worker 的 stdout
    QByteArray output = process.readAllStandardOutput();
    QString result = QString::fromUtf8(output).trimmed();

    appendLog("【Received】" + result);

    // 检查是否有错误输出
    QByteArray error = process.readAllStandardError();
    if (!error.isEmpty()) {
        appendLog("【Error】" + QString::fromUtf8(error));
    }
#endif
}

void StdioWindow::onProcessReadyRead()
{
    if (m_process) {
        QByteArray output = m_process->readAllStandardOutput();
        if (!output.isEmpty()) {
            appendLog("【Received】" + QString::fromUtf8(output).trimmed());
        }
        QByteArray error = m_process->readAllStandardError();
        if (!error.isEmpty()) {
            appendLog("【Error】" + QString::fromUtf8(error));
        }
    }
}

void StdioWindow::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    if (exitStatus == QProcess::NormalExit && exitCode == 0) {
        QByteArray output = m_process->readAllStandardOutput();
        appendLog("Process completed successfully: " + QString::fromUtf8(output).trimmed());
    } else {
        QByteArray error = m_process->readAllStandardError();
        appendLog("Process failed with exit code " + QString::number(exitCode) + ": " + QString::fromUtf8(error));
    }
}

void StdioWindow::onProcessErrorOccurred(QProcess::ProcessError error)
{
    appendLog("Process error occurred: " + m_process->errorString());
}

void StdioWindow::appendLog(const QString &msg)
{
    m_logView->append(QDateTime::currentDateTime().toString("hh:mm:ss") + " | " + msg);
}

这里除了必要的一些UI代码之外,我们主要观察onSendMessage函数和其他QProcess相关的槽函数。

在onSendMessage中,也就是需要发送消息的时候,我们动态创建了QProcess对象,并实现了异步和同步的不同方式的调用。由于比较简单,就不啰嗦解释了。

成功调用子进程后,我们立即往里面写入了字符串:

cpp 复制代码
	// 发送消息
    QString message = "Async test: " + QDateTime::currentDateTime().toString();
    m_process->write(message.toUtf8() + "\n");
    m_process->closeWriteChannel();

write方法顾名思义,最后还调用了一次closeWriteChannel,代表关闭了写入的通道,以此通知子进程已经输入完毕。

在异步设计中,onProcessFinished里面调用了m_process->readAllStandardOutput()接口来获取输出的消息,以此实现获取子进程发送给父进程的消息,也就实现了进程间的通信。

如果你问我异步和同步选哪种,我会偏向于异步。其利用了Qt中的信号槽机制,符合Qt程序的设计美学,不阻塞界面的使用体验也应该是我们所追求的。

另外,如果想要传递中文字符串,使用QString的toUtf8()即可。当然,在不同系统环境下,可能需要进行调试。

最后,贴上运行效果图:

五、总结

QProcess+stdio是一种相当简单和使用的进程间通信方式。

它的优点有很多,首先它使用了Qt进程类,天生兼容跨平台机制,使用时无需理会内部的具体实现,也有相关信号槽进行调用实现。其次它足够简单直接,无网络端口和额外内存的花销。

它的缺点也十分明显,那就是它依赖QProcess和stdio,适合进程调用时的一次性通信,难以实现长时间的双向持续对话。如果不用信号槽的话,可能有阻塞UI的问题。

但不管怎么说,QProcess不仅使用,也应该是Qt开发者必须掌握的一个核心类。

QProcess适用的场景也有很多:比如,调用外部工具(如 FFmpeg、Python 脚本)、一次性任务(导出文件、计算校验和)、简单插件架构(主程序 ↔ 脚本)。

不管怎样,继续加油吧!

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa10 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio