程序示例精选
Qt+C++串口调试接收发送数据曲线图
如需安装运行环境或远程调试,见文章底部个人 QQ名片,由专业技术人员远程协助!
前言
这篇博客针对<<Qt+C++串口调试接收发送数据曲线图>>编写代码,代码整洁,规则,易读。 学习与应用推荐首选。
文章目录
一、所需工具软件
二、使用步骤
1. 引入库
2. 代码实现
3. 运行结果
三、在线协助
一、所需工具软件
1. VS, Qt
2. C++
二、使用步骤
1.引入库
#include <QAction>
#include <QCheckBox>
#include <QDragEnterEvent>
#include <QDebug>
#include <QLineEdit>
#include <QMenu>
#include <QMenuBar>
#include <QtSerialPort/QSerialPort>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QGroupBox>
#include <QTextBrowser>
#include <QtWidgets/QFileDialog>
#include <QTimer>
#include <QtCore/QSettings>
#include <QtCore/QProcess>
#include <QStatusBar>
#include <QSplitter>
#include <data/SerialReadWriter.h>
#include <data/TcpServerReadWriter.h>
#include <data/TcpClientReadWriter.h>
#include <QRadioButton>
#include <QButtonGroup>
#include <data/BridgeReadWriter.h>
#include <QMimeData>
#include <QtSerialPort/QSerialPortInfo>
#include <data/SerialBridgeReadWriter.h>
#include <utils/FileUtil.h>
#include <QTextCodec>
2. 代码实现
代码如下:
void MainWindow::openReadWriter() {
if (_readWriter != nullptr) {
_readWriter->close();
delete _readWriter;
_readWriter = nullptr;
emit serialStateChanged(false);
}
bool result;
if (readWriterButtonGroup->checkedButton() == serialRadioButton) {
_serialType = SerialType::Normal;
auto settings = new SerialSettings();
settings->name = serialPortNameComboBox->currentText();
settings->baudRate = serialPortBaudRateComboBox->currentText().toInt();
settings->dataBits = (QSerialPort::DataBits) serialPortDataBitsComboBox->currentText().toInt();
settings->stopBits = (QSerialPort::StopBits) serialPortStopBitsComboBox->currentData().toInt();
settings->parity = (QSerialPort::Parity) serialPortParityComboBox->currentData().toInt();
auto readWriter = new SerialReadWriter(this);
readWriter->setSerialSettings(*settings);
qDebug() << settings->name << settings->baudRate << settings->dataBits << settings->stopBits
<< settings->parity;
result = readWriter->open();
if (!result) {
showWarning(tr("消息"), tr("串口被占用或者不存在"));
return;
}
_readWriter = readWriter;
_serialType = SerialType::Normal;
} else if (readWriterButtonGroup->checkedButton() == tcpServerRadioButton) {
_serialType = SerialType::TcpServer;
auto address = tcpAddressLineEdit->text();
bool ok;
auto port = tcpPortLineEdit->text().toInt(&ok);
if (!ok) {
showMessage("", tr("端口格式不正确"));
return;
}
auto readWriter = new TcpServerReadWriter(this);
readWriter->
setAddress(address);
readWriter->
setPort(port);
qDebug() << address << port;
result = readWriter->open();
if (!result) {
showWarning("", tr("建立服务器失败"));
return;
}
connect(readWriter, &TcpServerReadWriter::currentSocketChanged, this, &MainWindow::updateTcpClient);
connect(readWriter, &TcpServerReadWriter::connectionClosed, this, &MainWindow::clearTcpClient);
_readWriter = readWriter;
} else if (readWriterButtonGroup->checkedButton() == tcpClientRadioButton) {
_serialType = SerialType::TcpClient;
auto address = tcpAddressLineEdit->text();
bool ok;
auto port = tcpPortLineEdit->text().toInt(&ok);
if (!ok) {
showMessage("", tr("端口格式不正确"));
return;
}
auto readWriter = new TcpClientReadWriter(this);
readWriter->setAddress(address);
readWriter->setPort(port);
qDebug() << address << port;
result = readWriter->open();
if (!result) {
showError("", tr("连接服务器失败"));
return;
}
_readWriter = readWriter;
} else if (readWriterButtonGroup->checkedButton() == serialBridgeRadioButton) {
_serialType = SerialType::SerialBridge;
auto settings1 = new SerialSettings();
settings1->name = serialPortNameComboBox->currentText();
settings1->baudRate = serialPortBaudRateComboBox->currentText().toInt();
settings1->dataBits = (QSerialPort::DataBits) serialPortDataBitsComboBox->currentText().toInt();
settings1->stopBits = (QSerialPort::StopBits) serialPortStopBitsComboBox->currentData().toInt();
settings1->parity = (QSerialPort::Parity) serialPortParityComboBox->currentData().toInt();
auto settings2 = new SerialSettings();
settings2->name = secondSerialPortNameComboBox->currentText();
settings2->baudRate = secondSerialPortBaudRateComboBox->currentText().toInt();
settings2->dataBits = (QSerialPort::DataBits) secondSerialPortDataBitsComboBox->currentText().toInt();
settings2->stopBits = (QSerialPort::StopBits) secondSerialPortStopBitsComboBox->currentText().toInt();
settings2->parity = (QSerialPort::Parity) secondSerialPortParityComboBox->currentText().toInt();
auto readWriter = new SerialBridgeReadWriter(this);
readWriter->setSettings(*settings1, *settings2);
result = readWriter->open();
if (!result) {
showWarning(tr("消息"), QString(tr("串口被占用或者不存在,%1")).arg(readWriter->settingsText()));
return;
}
connect(readWriter, &SerialBridgeReadWriter::serial1DataRead, [this](const QByteArray &data) {
showSendData(data);
});
connect(readWriter, &SerialBridgeReadWriter::serial2DataRead, [this](const QByteArray &data) {
showReadData(data);
});
_readWriter = readWriter;
} else {
_serialType = SerialType::Bridge;
auto settings = new SerialSettings();
settings->name = serialPortNameComboBox->currentText();
settings->baudRate = serialPortBaudRateComboBox->currentText().toInt();
settings->dataBits = (QSerialPort::DataBits) serialPortDataBitsComboBox->currentText().toInt();
settings->stopBits = (QSerialPort::StopBits) serialPortStopBitsComboBox->currentData().toInt();
settings->parity = (QSerialPort::Parity) serialPortParityComboBox->currentData().toInt();
auto address = tcpAddressLineEdit->text();
bool ok;
auto port = tcpPortLineEdit->text().toInt(&ok);
if (!ok) {
showMessage("", tr("端口格式不正确"));
return;
}
auto readWriter = new BridgeReadWriter(this);
readWriter->setSettings(*settings, address, static_cast<qint16>(port));
result = readWriter->open();
if (!result) {
showWarning(tr("消息"), tr("串口被占用或者不存在"));
return;
}
connect(readWriter, &BridgeReadWriter::currentSocketChanged,
this, &MainWindow::updateTcpClient);
connect(readWriter, &BridgeReadWriter::connectionClosed,
this, &MainWindow::clearTcpClient);
connect(readWriter, &BridgeReadWriter::serialDataRead, [this](const QByteArray &data) {
showSendData(data);
});
connect(readWriter, &BridgeReadWriter::tcpDataRead, [this](const QByteArray &data) {
showReadData(data);
});
_readWriter = readWriter;
}
connect(_readWriter, &AbstractReadWriter::readyRead,
this, &MainWindow::readData);
emit serialStateChanged(result);
}
void MainWindow::closeReadWriter() {
stopAutoSend();
if (_readWriter != nullptr) {
_readWriter->close();
delete _readWriter;
_readWriter = nullptr;
}
emit serialStateChanged(false);
}
void MainWindow::createConnect() {
connect(readWriterButtonGroup, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled),
[=](QAbstractButton *button, bool checked) {
if (checked && isReadWriterOpen()) {
SerialType serialType;
if (button == tcpServerRadioButton) {
serialType = SerialType::TcpServer;
} else if (button == tcpClientRadioButton) {
serialType = SerialType::TcpClient;
} else if (button == bridgeRadioButton) {
serialType = SerialType::Bridge;
} else {
serialType = SerialType::Normal;
}
if (serialType != _serialType) {
if (showWarning("", tr("串口配置已经改变,是否重新打开串口?"))) {
openReadWriter();
}
}
}
});
connect(this, &MainWindow::serialStateChanged, [this](bool isOpen) {
setOpenButtonText(isOpen);
QString stateText;
if (isOpen) {
stateText = QString(tr("串口打开成功,%1")).arg(_readWriter->settingsText());
} else {
stateText = QString(tr("串口关闭"));
}
skipSendCount = 0;
updateStatusMessage(stateText);
});
connect(this, &MainWindow::readBytesChanged, this, &MainWindow::updateReadBytes);
connect(this, &MainWindow::writeBytesChanged, this, &MainWindow::updateWriteBytes);
connect(this, &MainWindow::currentWriteCountChanged, this, &MainWindow::updateCurrentWriteCount);
connect(openSerialButton, &QPushButton::clicked, [=](bool value) {
if (!isReadWriterOpen()) {
openReadWriter();
} else {
closeReadWriter();
}
});
connect(refreshSerialButton, &QPushButton::clicked, [=] {
_dirty = true;
updateSerialPortNames();
});
connect(saveReceiveDataButton, &QPushButton::clicked, this, &MainWindow::saveReceivedData);
connect(clearReceiveDataButton, &QPushButton::clicked, this, &MainWindow::clearReceivedData);
connect(saveSentDataButton, &QPushButton::clicked, this, &MainWindow::saveSentData);
connect(clearSentDataButton, &QPushButton::clicked, this, &MainWindow::clearSentData);
connect(autoSendCheckBox, &QCheckBox::clicked, [this] {
autoSendTimer->stop();
});
connect(loopSendCheckBox, &QCheckBox::stateChanged, [this] {
_loopSend = loopSendCheckBox->isChecked();
});
connect(resetLoopSendButton, &QPushButton::clicked, [this] {
skipSendCount = 0;
serialController->setCurrentCount(0);
emit currentWriteCountChanged(0);
});
connect(currentSendCountLineEdit, &QLineEdit::editingFinished, [this] {
bool ok;
auto newCount = currentSendCountLineEdit->text().toInt(&ok);
if (ok) {
serialController->setCurrentCount(newCount);
} else {
currentSendCountLineEdit->setText(QString::number(serialController->getCurrentCount()));
}
});
connect(sendLineButton, &QPushButton::clicked, [this] {
if (!isReadWriterConnected()) {
handlerSerialNotOpen();
return;
}
if (autoSendState == AutoSendState::Sending) {
stopAutoSend();
} else {
if (_dirty) {
_dirty = false;
_sendType = SendType::Line;
updateSendData(hexCheckBox->isChecked(), sendTextEdit->toPlainText());
updateSendType();
}
sendNextData();
startAutoSendTimerIfNeed();
}
if (autoSendState == AutoSendState::Sending) {
sendLineButton->setText(tr("停止"));
} else {
resetSendButtonText();
}
});
connect(processTextButton, &QPushButton::clicked, [this] {
openDataProcessDialog(sendTextEdit->toPlainText());
});
connect(clearTextButton, &QPushButton::clicked, [this]{
sendTextEdit->clear();
});
connect(lineReturnButtonGroup, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled),
[=](QAbstractButton *button, bool checked) {
if (checked) {
if (button == sendRReturnLineButton) {
lineReturn = QByteArray("\r");
} else if (button == sendNReturnLineButton) {
lineReturn = QByteArray("\n");
} else {
lineReturn = QByteArray("\r\n");
}
}
});
connect(autoSendTimer, &QTimer::timeout,
[this] {
sendNextData();
});
connect(hexCheckBox, &QCheckBox::stateChanged, [this] {
this->_dirty = true;
});
connect(sendTextEdit, &QTextEdit::textChanged, [this] {
this->_dirty = true;
});
}
void MainWindow::setOpenButtonText(bool isOpen) {
if (isOpen) {
openSerialButton->setText(tr("关闭"));
} else {
openSerialButton->setText("打开");
}
}
void MainWindow::createActions() {
openAct = new QAction(tr("&打开(&O)"), this);
openAct->setShortcut(QKeySequence::Open);
openAct->setStatusTip(tr("打开一个文件"));
connect(openAct, &QAction::triggered, this, &MainWindow::open);
saveAct = new QAction(tr("&保存(&S)"), this);
saveAct->setShortcut(QKeySequence::Save);
saveAct->setStatusTip(tr("保存一个文件"));
connect(saveAct, &QAction::triggered, this, &MainWindow::save);
validateDataAct = new QAction(tr("计算校验(&E)"), this);
validateDataAct->setShortcut(tr("Ctrl+E"));
validateDataAct->setStatusTip(tr("计算数据校验值"));
connect(validateDataAct, &QAction::triggered, this, &MainWindow::openDataValidator);
convertDataAct = new QAction(tr("数据转换(&T)"));
convertDataAct->setShortcut(tr("Ctrl+T"));
convertDataAct->setStatusTip(tr("数据转换"));
connect(convertDataAct, &QAction::triggered, this, &MainWindow::openConvertDataDialog);
dataProcessAct = new QAction(tr("数据处理(&P)"));
dataProcessAct->setShortcut(tr("Ctrl+P"));
dataProcessAct->setStatusTip(tr("数据处理"));
connect(dataProcessAct, &QAction::triggered, [this] {
openDataProcessDialog("");
});
}
void MainWindow::createMenu() {
fileMenu = menuBar()->addMenu(tr("文件(&F)"));
fileMenu->addAction(openAct);
fileMenu->addAction(saveAct);
toolMenu = menuBar()->addMenu(tr("工具(&T)"));
toolMenu->addAction(validateDataAct);
toolMenu->addAction(convertDataAct);
toolMenu->addAction(dataProcessAct);
}
void MainWindow::open() {
auto lastDir = runConfig->lastDir;
QString fileName = QFileDialog::getOpenFileName(this, tr("打开数据文件"), lastDir, "");
if (fileName.isEmpty()) {
return;
}
QFile file(fileName);
if (file.open(QIODevice::ReadOnly)) {
runConfig->lastDir = getFileDir(fileName);
auto data = file.readAll();
sendTextEdit->setText(QString::fromLocal8Bit(data));
}
}
void MainWindow::save() {
saveReceivedData();
}
void MainWindow::openDataValidator() {
CalculateCheckSumDialog dialog(this);
dialog.setModal(true);
dialog.exec();
}
void MainWindow::openConvertDataDialog() {
ConvertDataDialog dialog(this);
dialog.setModal(true);
dialog.exec();
}
void MainWindow::openDataProcessDialog(const QString &text) {
DataProcessDialog dialog(text, this);
dialog.setModal(true);
int result = dialog.exec();
if (result == QDialog::Accepted) {
sendTextEdit->setText(dialog.text());
}
}
void MainWindow::displayReceiveData(const QByteArray &data) {
if (pauseReceiveCheckBox->isChecked()) {
return;
}
static QString s;
s.clear();
if (addReceiveTimestampCheckBox->isChecked()) {
s.append("[").append(getTimestamp()).append("] ");
}
if (!s.isEmpty()) {
s.append(" ");
}
if (displayReceiveDataAsHexCheckBox->isChecked()) {
s.append(dataToHex(data));
} else {
s.append(QString::fromLocal8Bit(data));
}
if (addLineReturnCheckBox->isChecked() || addReceiveTimestampCheckBox->isChecked()) {
receiveDataBrowser->append(s);
} else {
auto text = receiveDataBrowser->toPlainText();
text.append(s);
receiveDataBrowser->setText(text);
receiveDataBrowser->moveCursor(QTextCursor::End);
}
}
void MainWindow::displaySentData(const QByteArray &data) {
if (displaySendDataAsHexCheckBox->isChecked()) {
sendDataBrowser->append(dataToHex(data));
} else {
sendDataBrowser->append(QString::fromLocal8Bit(data));
}
}
void MainWindow::sendNextData() {
if (isReadWriterConnected()) {
if (skipSendCount > 0) {
auto delay = skipSendCount * sendIntervalLineEdit->text().toInt();
updateStatusMessage(QString("%1毫秒后发送下一行").arg(delay));
skipSendCount--;
return;
}
qDebug() << "sendNextData readEnd:" << serialController->readEnd() << "current:"
<< serialController->getCurrentCount();
if (!_loopSend && autoSendCheckBox->isChecked() && serialController->readEnd()) {
serialController->setCurrentCount(0);
stopAutoSend();
return;
}
auto data = serialController->readNextFrame();
if (data.isEmpty()) {
updateStatusMessage(tr("空行,不发送"));
if (autoSendCheckBox->isChecked()) {
auto emptyDelay = emptyLineDelayLindEdit->text().toInt();
auto sendInterval = sendIntervalLineEdit->text().toInt();
if (emptyDelay > sendInterval) {
skipSendCount = emptyDelay / sendInterval;
if (emptyDelay % sendInterval != 0) {
skipSendCount += 1;
}
skipSendCount--;
updateStatusMessage(QString(tr("空行,%1毫秒后发送下一行")).arg(emptyDelay));
}
}
emit currentWriteCountChanged(serialController->getCurrentCount());
return;
}
writeData(data);
if (sendLineReturnCheckBox->isChecked()) {
writeData(lineReturn);
}
if (hexCheckBox->isChecked()) {
updateStatusMessage(QString(tr("发送 %1")).arg(QString(dataToHex(data))));
} else {
updateStatusMessage(QString(tr("发送 %1")).arg(QString(data)));
}
emit currentWriteCountChanged(serialController->getCurrentCount());
} else {
handlerSerialNotOpen();
}
}
void MainWindow::updateSendData(bool isHex, const QString &text) {
if (serialController != nullptr) {
QStringList lines = getLines(text);
QList<QByteArray> dataList;
if (isHex) {
for (auto &line :lines) {
dataList << dataFromHex(line);
}
} else {
for (auto &line:lines) {
dataList << line.toLocal8Bit();
}
}
serialController->setData(dataList);
totalSendCount = serialController->getTotalCount();
updateTotalSendCount(totalSendCount);
}
}
void MainWindow::readSettings() {
qDebug() << "readSettings";
updateSerialPortNames();
QSettings settings("Zhou Jinlong", "Serial Wizard");
settings.beginGroup("Basic");
auto serialType = SerialType(settings.value("serial_type", static_cast<int >(SerialType::Normal)).toInt());
if (serialType == SerialType::TcpServer) {
tcpServerRadioButton->setChecked(true);
} else if (serialType == SerialType::TcpClient) {
tcpClientRadioButton->setChecked(true);
} else if (serialType == SerialType::Bridge) {
bridgeRadioButton->setChecked(true);
} else if (serialType == SerialType::SerialBridge) {
serialBridgeRadioButton->setChecked(true);
} else {
serialRadioButton->setChecked(true);
}
_serialType = serialType;
settings.beginGroup("SerialSettings");
auto nameIndex = settings.value("name", 0).toInt();
auto baudRateIndex = settings.value("baud_rate", 5).toInt();
auto dataBitsIndex = (QSerialPort::DataBits) settings.value("data_bits", 3).toInt();
auto stopBitsIndex = (QSerialPort::StopBits) settings.value("stop_bits", 0).toInt();
auto parityIndex = (QSerialPort::Parity) settings.value("parity", 0).toInt();
auto sendText = settings.value("send_text", "").toString();
auto maxCount = serialPortNameComboBox->maxCount();
if (nameIndex > maxCount - 1) {
nameIndex = 0;
}
serialPortNameComboBox->setCurrentIndex(nameIndex);
serialPortBaudRateComboBox->setCurrentIndex(baudRateIndex);
serialPortDataBitsComboBox->setCurrentIndex(dataBitsIndex);
serialPortStopBitsComboBox->setCurrentIndex(stopBitsIndex);
serialPortParityComboBox->setCurrentIndex(parityIndex);
auto name2Index = settings.value("name2", 0).toInt();
auto baudRate2Index = settings.value("baud_rate2", 5).toInt();
auto dataBits2Index = (QSerialPort::DataBits) settings.value("data_bits2", 3).toInt();
auto stopBits2Index = (QSerialPort::StopBits) settings.value("stop_bits2", 0).toInt();
auto parity2Index = (QSerialPort::Parity) settings.value("parity2", 0).toInt();
auto maxCount2 = serialPortNameComboBox->maxCount();
if (name2Index > maxCount2 - 1) {
name2Index = 0;
}
secondSerialPortNameComboBox->setCurrentIndex(name2Index);
secondSerialPortBaudRateComboBox->setCurrentIndex(baudRate2Index);
secondSerialPortDataBitsComboBox->setCurrentIndex(dataBits2Index);
secondSerialPortStopBitsComboBox->setCurrentIndex(stopBits2Index);
secondSerialPortParityComboBox->setCurrentIndex(parity2Index);
settings.beginGroup("SerialReceiveSettings");
auto addLineReturn = settings.value("add_line_return", true).toBool();
auto displayReceiveDataAsHex = settings.value("display_receive_data_as_hex", false).toBool();
auto addTimestamp = settings.value("add_timestamp", false).toBool();
addLineReturnCheckBox->setChecked(addLineReturn);
displayReceiveDataAsHexCheckBox->setChecked(displayReceiveDataAsHex);
addReceiveTimestampCheckBox->setChecked(addTimestamp);
settings.beginGroup("SerialSendSettings");
auto sendAsHex = settings.value("send_as_hex", false).toBool();
auto displaySendData = settings.value("display_send_data", false).toBool();
auto displaySendDataAsHex = settings.value("display_send_data_as_hex", false).toBool();
auto autoSend = settings.value("auto_send", false).toBool();
auto autoSendInterval = settings.value("auto_send_interval", 100).toInt();
auto emptyLineDelay = settings.value("empty_line_delay", 0).toInt();
auto loopSend = settings.value("loop_send", false).toBool();
hexCheckBox->setChecked(sendAsHex);
displaySendDataCheckBox->setChecked(displaySendData);
displaySendDataAsHexCheckBox->setChecked(displaySendDataAsHex);
autoSendCheckBox->setChecked(autoSend);
loopSendCheckBox->setChecked(loopSend);
sendIntervalLineEdit->setText(QString::number(autoSendInterval));
emptyLineDelayLindEdit->setText(QString::number(emptyLineDelay));
auto sendLineReturn = settings.value("send_line_return", false).toBool();
sendLineReturnCheckBox->setChecked(sendLineReturn);
auto sendLineReturnType = LineReturn(
settings.value("send_line_return_type", static_cast<int >(LineReturn::RN)).toInt());
if (sendLineReturnType == LineReturn::R) {
sendRReturnLineButton->setChecked(true);
} else if (sendLineReturnType == LineReturn::N) {
sendNReturnLineButton->setChecked(true);
} else {
sendRNLineReturnButton->setChecked(true);
}
settings.beginGroup("TcpSettings");
auto ipList = getNetworkInterfaces();
auto ipAddress = settings.value("tcp_address", "").toString();
QString selectAddress = "";
if (!ipAddress.isEmpty() && !ipList.isEmpty()) {
auto found = false;
for (const auto &ip:ipList) {
if (getIpAddress(ip) == ipAddress) {
selectAddress = ipAddress;
found = true;
break;
}
}
if (!found) {
selectAddress = getIpAddress(ipList.first());
}
}
if (selectAddress.isEmpty()) {
if (!ipList.isEmpty()) {
do {
for (const auto &ip:ipList) {
if (ip.type() == QNetworkInterface::Wifi && !getIpAddress(ip).isEmpty()) {
selectAddress = getIpAddress(ip);
break;
}
}
if (!selectAddress.isEmpty()) {
break;
}
for (const auto &ip:ipList) {
if (ip.type() == QNetworkInterface::Ethernet && !getIpAddress(ip).isEmpty()) {
selectAddress = getIpAddress(ip);
}
}
if (!selectAddress.isEmpty()) {
break;
}
selectAddress = getIpAddress(ipList.first());
} while (false);
}
}
tcpAddressLineEdit->setText(selectAddress);
auto tcpPort = settings.value("tcp_port").toInt();
tcpPortLineEdit->setText(QString::number(tcpPort));
sendTextEdit->setText(sendText);
settings.beginGroup("RunConfig");
auto lastDir = settings.value("last_dir", "").toString();
auto lastFilePath = settings.value("last_file_path", "").toString();
runConfig = new RunConfig;
runConfig->lastDir = lastDir;
runConfig->lastFilePath = lastFilePath;
_loopSend = loopSend;
serialController = new LineSerialController();
updateSendType();
}
3. 运行结果
动画演示
三、在线协助:
如需安装运行环境或远程调试,见文章底部个人 QQ 名片,由专业技术人员远程协助!
1)远程安装运行环境,代码调试
2)Qt, C++, Python入门指导
3)界面美化
4)软件制作
当前文章连接: Python+Qt桌面端与网页端人工客服沟通工具_alicema1111的博客-CSDN博客
博主推荐文章: python人脸识别统计人数qt窗体-CSDN博客
博主推荐文章: Python Yolov5火焰烟雾识别源码分享-CSDN博客
Python OpenCV识别行人入口进出人数统计_python识别人数-CSDN博客
个人博客主页: alicema1111的博客_CSDN博客-Python,C++,网页领域博主
博主所有文章点这里 : alicema1111的博客_CSDN博客-Python,C++,网页领域博主