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 脚本)、一次性任务(导出文件、计算校验和)、简单插件架构(主程序 ↔ 脚本)。

不管怎样,继续加油吧!

相关推荐
码农葫芦侠2 小时前
Qt 跨线程内存管理陷阱:QSharedPointer、deleteLater() 与 QPointer 的致命组合
开发语言·数据库·qt
d111111111d2 小时前
C语言中,malloc和free是什么,在STM32中使用限制是什么,该如何使用?
c语言·开发语言·笔记·stm32·单片机·嵌入式硬件·学习
网安_秋刀鱼2 小时前
【java安全】shiro鉴权绕过
java·开发语言·安全
白昼流星!2 小时前
C++内存四区与new操作符详解
开发语言·c++
tyatyatya2 小时前
MATLAB三维绘图教程:plot3/mesh/surf/contour函数详解与实例
开发语言·matlab
十五年专注C++开发2 小时前
标准C++操作文件方法总结
开发语言·c++·文件操作·ifstream
浔川python社2 小时前
《C++ 小程序编写系列》(第四部):实战:简易图书管理系统(类与对象篇)
java·开发语言·apache
浔川python社2 小时前
《C++ 小程序编写系列》(第五部):实战:多角色图书管理系统(继承与多态篇)
开发语言·c++
CC.GG2 小时前
【Qt】信号和槽
开发语言·数据库·qt