前言
本节将学习第一种最简单、也十分常用的进程间通信方法,即主进程使用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 脚本)、一次性任务(导出文件、计算校验和)、简单插件架构(主程序 ↔ 脚本)。
不管怎样,继续加油吧!