经营淘宝店 cuteqt.taobao.com 一年有余,看过很多同学的 Qt 代码,有一些想法,想记录一下。
第一篇想讲一下 Qt 程序的架构
一般的,在数据处理流程中,通常会经历以下几个步骤:
数据协议解析(如以太网、串口、Wi-Fi、蓝牙、USB、CAN等)
后处理(如数据清洗、过滤和转换、数据聚合、数据压缩、加密和解密、特征提取、信号处理、图像处理、视频处理、统计分析、模式识别、自然语言处理和时间序列分析等)
界面渲染。
界面渲染通常在主线程上进行,而数据解析和后处理则在各自独立的线程中运行。
接下来,讲一下 Qt 如何创建多线程
让我们回顾下 Qt 文档中的示例代码
第一种是继承 QThread
class WorkerThread : public QThread{ Q_OBJECT void run() override { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); }signals: void resultReady(const QString &s);};void MyObject::startWorkInAThread(){ WorkerThread *workerThread = new WorkerThread(this); connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults); connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater); workerThread->start();}第二种是 moveToThread
xxxxxxxxxxclass Worker : public QObject{ Q_OBJECTpublic slots: void doWork(const QString ¶meter) { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); }signals: void resultReady(const QString &result);};class Controller : public QObject{ Q_OBJECT QThread workerThread;public: Controller() { Worker *worker = new Worker; worker->moveToThread(&workerThread); connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); connect(this, &Controller::operate, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &Controller::handleResults); workerThread.start(); } ~Controller() { workerThread.quit(); workerThread.wait(); }public slots: void handleResults(const QString &);signals: void operate(const QString &);};两种写法都有各自应用的场景
第一种写法的应用场景:你只想在子线程里做一件简单的事时。比如不停地接收串口的数据或者创建一个二进制文件。
第二种写法的应用场景:你想异步操作一个硬件,不阻塞 UI。比如调用摄像头 SDK 的各种 API。
一般来说,第二种场景更常见一些,而且第一种场景也可以用第二种写法改写。
这里为了演示,我选了一个我熟悉的场景,相机的使用。
相机作为 Server 接收 PC 上 Client 的 TCP 消息,可以被修改相机参数,也可以主动发送图像数据
为了简化,我们直接用 QTcpServer 写个软件,模拟相机硬件,然后再用 QTcpSocket 写个 PC 客户端软件,控制相机。
然后我们基于 Tcp 设计一个简单的自定义通信协议
这是消息头的定义:
第 1 字节为固定值 0x27
第 2 字节为消息类型,0x01 表示 设置参数 0x02 代表 获取参数 0x03 代表 获取图像
第 3 4 两字节为消息 ID,从 0 开始单调递增
第 5 6 7 8 四字节为后面要接收的消息体(也称为负载,即 Payload)的长度
下面的代码已经上传到 github https://github.com/abc881858/Camera
Server 端代码如下
【camerasever.pro】
xxxxxxxxxxQT += core gui networkCONFIG += c++17 cmdlineSOURCES += cameraserver.cpp main.cppHEADERS += cameraserver.h packet.h【packet.h】
x
enum MessageType { SET_PARAM = 0x01, GET_PARAM = 0x02, GET_IMAGE = 0x03};
struct PacketHeader { quint8 fixedByte{0}; quint8 messageType{0}; quint16 messageId{0}; quint32 payloadLength{0};};【main.cpp】
xxxxxxxxxx
int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); CameraServer server; server.listen(QHostAddress::AnyIPv4, 27000); return a.exec();}【cameraserver.h】
xxxxxxxxxx
class CameraServer : public QTcpServer { Q_OBJECTpublic: CameraServer(QObject *parent = nullptr); void sendParam(const QString ¶mName); void sendImage();protected: void incomingConnection(qintptr socketDescriptor) override;private: QTcpSocket *m_socket; QByteArray m_buffer; quint16 m_message_id{0}; PacketHeader m_header; bool m_reading_header{true}; QMap<QString, QString> parameters;private slots: void readClient(); void discardClient(); void setParam(const QString ¶mName, const QString ¶mValue);};【cameraserver.cpp】
xxxxxxxxxx
CameraServer::CameraServer(QObject *parent) : QTcpServer(parent), m_socket(nullptr) { parameters["brightness"] = "50"; parameters["contrast"] = "50";}
void CameraServer::incomingConnection(qintptr socketDescriptor) { if (m_socket) { m_socket->disconnectFromHost(); } m_socket = new QTcpSocket(this); m_socket->setSocketDescriptor(socketDescriptor); connect(m_socket, &QTcpSocket::readyRead, this, &CameraServer::readClient); connect(m_socket, &QTcpSocket::disconnected, this, &CameraServer::discardClient);}
void CameraServer::readClient() { m_buffer.append(m_socket->readAll()); if (m_reading_header && m_buffer.size() >= 8) { QDataStream headerStream(m_buffer); headerStream.setByteOrder(QDataStream::BigEndian); headerStream >> m_header.fixedByte; if (m_header.fixedByte != 0x27) { qDebug() << "Invalid fixed byte:" << m_header.fixedByte; return; } headerStream >> m_header.messageType; headerStream >> m_header.messageId; headerStream >> m_header.payloadLength; m_reading_header = false; m_buffer.remove(0, 8); }
if (!m_reading_header && m_buffer.size() >= int(m_header.payloadLength)) { QByteArray payload = m_buffer.left(m_header.payloadLength); m_buffer.remove(0, m_header.payloadLength); QDataStream payloadStream(payload); payloadStream.setByteOrder(QDataStream::BigEndian); switch (m_header.messageType) { case SET_PARAM: { QString paramName; QString paramValue; payloadStream >> paramName >> paramValue; setParam(paramName, paramValue); break; } case GET_PARAM: { QString paramName; payloadStream >> paramName; sendParam(paramName); break; } case GET_IMAGE: { sendImage(); break; } default: qDebug() << "Unknown message type:" << m_header.messageType; } m_header.fixedByte = 0; m_header.messageType = 0; m_header.messageId = 0; m_header.payloadLength = 0; m_reading_header = true; }}
void CameraServer::discardClient() { m_socket->deleteLater(); m_socket = nullptr;}
void CameraServer::setParam(const QString ¶mName, const QString ¶mValue) { parameters[paramName] = paramValue;}
void CameraServer::sendParam(const QString ¶mName) { if (!m_socket) return; QString paramValue = parameters.value(paramName, "unknown"); QByteArray payload; QDataStream tmp(&payload, QIODevice::WriteOnly); tmp.setByteOrder(QDataStream::BigEndian); tmp << paramName << paramValue; QByteArray response; QDataStream stream(&response, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::BigEndian); stream << quint8(0x27) << quint8(0x02) << quint16(++m_message_id) << quint32(payload.size()); m_socket->write(response); m_socket->write(payload);}
void CameraServer::sendImage() { if (!m_socket) return; QImage image(640, 480, QImage::Format_RGB32); image.fill(Qt::blue); QByteArray payload; QBuffer buffer(&payload); image.save(&buffer, "PNG"); QByteArray response; QDataStream stream(&response, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::BigEndian); stream << quint8(0x27) << quint8(0x03) << quint16(++m_message_id) << quint32(payload.size()); m_socket->write(response); m_socket->write(payload);}Client 端代码如下
【cameraclient.pro】
xxxxxxxxxxQT += core gui widgets network
CONFIG += c++17
SOURCES += \ cameraclient.cpp \ main.cpp \ mainwindow.cpp
HEADERS += \ cameraclient.h \ mainwindow.h \ packet.h
FORMS += \ mainwindow.ui【packet.h】
xxxxxxxxxx
enum MessageType { SET_PARAM = 0x01, GET_PARAM = 0x02, GET_IMAGE = 0x03};
struct PacketHeader { quint8 fixedByte{0}; quint8 messageType{0}; quint16 messageId{0}; quint32 payloadLength{0};};【main.cpp】
xxxxxxxxxx
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec();}【mainwindow.h】
xxxxxxxxxx
QT_BEGIN_NAMESPACEnamespace Ui { class MainWindow; }QT_END_NAMESPACE
class MainWindow : public QMainWindow{ Q_OBJECTpublic: MainWindow(QWidget *parent = nullptr); ~MainWindow();private: Ui::MainWindow *ui; CameraClient *m_camera_client; QThread m_camera_thread;public slots: void send_image(QImage image); void brightness_slot(QString str);private slots: void on_pushButton_clicked(); void on_pushButton_2_clicked(); void on_pushButton_3_clicked(); void on_pushButton_4_clicked();signals: void connectToServer(QString, quint16); void requestImage(); void setParam(QString, QString); void getParam(QString);};
【mainwindow.cpp】
xxxxxxxxxx
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow){ ui->setupUi(this); m_camera_client = new CameraClient; m_camera_client->moveToThread(&m_camera_thread); connect(&m_camera_thread, &QThread::finished, m_camera_client, &CameraClient::deleteLater); connect(this, &MainWindow::connectToServer, m_camera_client, &CameraClient::connectToServer); connect(this, &MainWindow::requestImage, m_camera_client, &CameraClient::requestImage); connect(this, &MainWindow::setParam, m_camera_client, &CameraClient::setParam); connect(this, &MainWindow::getParam, m_camera_client, &CameraClient::getParam); connect(m_camera_client, &CameraClient::send_image, this, &MainWindow::send_image); connect(m_camera_client, &CameraClient::brightness_signal, this, &MainWindow::brightness_slot); m_camera_thread.start();}
MainWindow::~MainWindow(){ m_camera_thread.quit(); m_camera_thread.wait(); delete ui;}
void MainWindow::on_pushButton_clicked(){ emit connectToServer("127.0.0.1", 27000);}
void MainWindow::on_pushButton_2_clicked(){ emit requestImage();}
void MainWindow::on_pushButton_3_clicked(){ emit setParam("brightness", QString::number(ui->spinBox->value()));}
void MainWindow::on_pushButton_4_clicked(){ emit getParam("brightness");}
void MainWindow::send_image(QImage image){ ui->label->setPixmap(QPixmap::fromImage(image));}
void MainWindow::brightness_slot(QString str){ ui->spinBox->setValue(str.toInt());}【mainwindow.ui】
xxxxxxxxxx <ui version="4.0"><class>MainWindow</class><widget class="QMainWindow" name="MainWindow"><property name="geometry"><rect><x>0</x><y>0</y><width>800</width><height>600</height></rect></property><property name="windowTitle"><string>MainWindow</string></property><widget class="QWidget" name="centralwidget"><layout class="QVBoxLayout" name="verticalLayout"><item><widget class="QWidget" name="widget" native="true"><property name="sizePolicy"><sizepolicy hsizetype="Preferred" vsizetype="Fixed"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><layout class="QHBoxLayout" name="horizontalLayout"><item><widget class="QPushButton" name="pushButton"><property name="text"><string>connect</string></property></widget></item><item><widget class="QPushButton" name="pushButton_2"><property name="text"><string>request image</string></property></widget></item><item><widget class="QPushButton" name="pushButton_3"><property name="text"><string>setParam</string></property></widget></item><item><widget class="QPushButton" name="pushButton_4"><property name="text"><string>getParam</string></property></widget></item><item><widget class="QSpinBox" name="spinBox"><property name="suffix"><string/></property><property name="prefix"><string>brightness: </string></property></widget></item><item><spacer name="horizontalSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></widget></item><item><widget class="QLabel" name="label"><property name="text"><string/></property></widget></item></layout></widget></widget><resources/><connections/></ui>【cameraclient.h】
xxxxxxxxxx
class CameraClient : public QObject{ Q_OBJECTpublic: CameraClient(QObject *parent = nullptr);private: QTcpSocket *m_socket; QByteArray m_buffer; quint16 m_message_id{0}; PacketHeader m_header; bool m_reading_header{true};public slots: void connectToServer(const QString &host, quint16 port); void requestImage(); void setParam(const QString ¶mName, const QString ¶mValue); void getParam(const QString ¶mName);private slots: void connected(); void readServer();signals: void image_signal(QImage); void brightness_signal(QString);};【cameraclient.cpp】
xxxxxxxxxx
CameraClient::CameraClient(QObject *parent) : QObject(parent){ m_socket = new QTcpSocket(this); connect(m_socket, &QTcpSocket::connected, this, &CameraClient::connected); connect(m_socket, &QTcpSocket::readyRead, this, &CameraClient::readServer);}
void CameraClient::connectToServer(const QString &host, quint16 port){ m_socket->connectToHost(QHostAddress(host), port);}
void CameraClient::requestImage(){ if (m_socket->state() == QAbstractSocket::ConnectedState) { QByteArray request; QDataStream stream(&request, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::BigEndian); stream << quint8(0x27) << quint8(0x03) << quint16(++m_message_id) << quint32(0);
qDebug() << "request" << request.toHex(' '); qDebug() << "request size" << request.size();
m_socket->write(request); }}
void CameraClient::setParam(const QString ¶mName, const QString ¶mValue){ if (m_socket->state() == QAbstractSocket::ConnectedState) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::BigEndian); stream << paramName << paramValue;
QByteArray request; QDataStream requestStream(&request, QIODevice::WriteOnly); requestStream.setByteOrder(QDataStream::BigEndian); requestStream << quint8(0x27) << quint8(0x01) << quint16(++m_message_id) << payload.size();
qDebug() << "payload" << payload.toHex(' '); qDebug() << "payload size" << payload.size();
m_socket->write(request); m_socket->write(payload); }}
void CameraClient::getParam(const QString ¶mName){ if (m_socket->state() == QAbstractSocket::ConnectedState) { QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::BigEndian); stream << paramName;
QByteArray request; QDataStream requestStream(&request, QIODevice::WriteOnly); requestStream.setByteOrder(QDataStream::BigEndian); requestStream << quint8(0x27) << quint8(0x02) << quint16(++m_message_id) << payload.size();
qDebug() << "payload" << payload.toHex(' '); qDebug() << "payload size" << payload.size();
m_socket->write(request); m_socket->write(payload); }}
void CameraClient::connected(){ qDebug() << __func__;}
void CameraClient::readServer(){ m_buffer.append(m_socket->readAll());
if (m_reading_header && m_buffer.size() >= 8) { QDataStream headerStream(m_buffer); headerStream.setByteOrder(QDataStream::BigEndian); headerStream >> m_header.fixedByte; if (m_header.fixedByte != 0x27) { qDebug() << "Invalid fixed byte:" << m_header.fixedByte; return; } headerStream >> m_header.messageType; headerStream >> m_header.messageId; headerStream >> m_header.payloadLength;
m_reading_header = false;
m_buffer.remove(0, 8); }
if (!m_reading_header && m_buffer.size() >= int(m_header.payloadLength)) { QByteArray payload = m_buffer.left(m_header.payloadLength); m_buffer.remove(0, m_header.payloadLength);
switch (m_header.messageType) { case SET_PARAM: { qDebug() << "Set parameter response received, messageId:" << m_header.messageId; break; } case GET_PARAM: { QDataStream payloadStream(payload); payloadStream.setByteOrder(QDataStream::BigEndian); QString paramName; QString paramValue; payloadStream >> paramName >> paramValue; qDebug() << "Get parameter response received, messageId:" << m_header.messageId << "paramName:" << paramName << "paramValue:" << paramValue; if(paramName == "brightness") { emit brightness_signal(paramValue); } break; } case GET_IMAGE: { QImage image(640, 480, QImage::Format_RGB32); image.loadFromData(payload, "PNG"); qDebug() << "Image received, messageId:" << m_header.messageId << "Image size:" << image.size(); emit image_signal(image); break; } default: qDebug() << "Unknown message type:" << m_header.messageType; }
m_header.fixedByte = 0; m_header.messageType = 0; m_header.messageId = 0; m_header.payloadLength = 0;
m_reading_header = true; }}
先运行 Server 再运行 Client,截图如下

Server 和 Client 处理数据的逻辑是很相似的。
CameraServer::readClient 和 CameraClient::readServer 即服务端和客户端最核心的逻辑。
代码非常浅显,这里就不多赘述。
我们主要来看下客户端的多线程部分。
这里的 MainWindow 就相当于 Controller,CameraClient 就相当于 Worker
注意,CameraClient 在 new 的时候,不要这样写:m_camera_client = new CameraClient(this);
应该这样,构造函数参数为空:m_camera_client = new CameraClient;
然后不要忘记在 MainWindow 的析构里添加 quit 和 wait
注意,先 quit 再 wait,这样才是优雅地退出线程的写法
最后,也是最重要的,关于函数调用的方式
比如我在主线程(即 MainWindow 所在线程),想通过子线程发送命令给服务端
我们不能通过指针 m_camera_client 直接调用 requestImage 函数
而应该先在 new 完 m_camera_client 的下一行(即 MainWindow 的构造函数),
写 connect(this, &MainWindow::requestImage, m_camera_client, &CameraClient::requestImage); 类似的代码
注意这个 this,一般非多线程的情况,我们很少让 this 发信号是吧 (#^.^#)
然后在 MainWindow 想调用 requestImage 函数的地方(我这里是 pushButton_2 按钮点击时),emit 一个信号,
当然,这个信号肯定要在 MainWindow 的头文件里用 signals 声明一下
这样对应的槽函数就会在子线程的队列中依次执行,实现多线程之间异步的通信。
需要强调的是,虽然 m_camera_client 已经 moveToThread 了,
但如果你直接通过指针访问 m_camera_client 的函数,它是在主线程运行的,
只有通过信号槽的方式,函数才会在子线程运行。
当然,原先 public 的函数,要改成 public slots 的槽函数
Qt 的多线程写起来很简单,啰嗦了这么多,主要是因为屡次看到别人写的稀奇古怪的 Qt 代码
比如在 run 里 new 一个 QTcpSocket,再加一个 exec 阻塞。
比如使用 event2/event.h 去写 callback 或者 用 std::thread + std::function 去写回调函数。
比如 this 发信号后再用 this 接收信号。
本质上都是对 Qt 信号槽以及 QThread 不够了解。