項目簡介
本項目通過QT框架設計一款可以在Windows、Linux等平臺的跨平臺串口助手,串口功能能夠滿足基本的調試需求。
本項目采用的版本為:QT5.14 + visual studio 2022 進行開發。
項目源碼:https://github.com/say-Hai/MyCOMDemo
項目頁面:
一、創建開發環境
打開vs新建工程,選擇創建Qt Widgets Application
項目,選擇保存路徑后,配置QT的SerialPort模塊。
二、配置ui界面
打開工程的ui文件,設置本項目的ui頁面(可直接從本項目的ui文件中copy到自己的項目中;但是注意:需要暫時把
comboBoxNo_2
降級成普通QComboBox
)
三、編寫串口掃描代碼
通過
QSerialPortInfo::availablePorts
生成可用串口列表,(目前暫定在MyCOM.h的構造函數中編寫串口列表函數)
MyCOM::MyCOM(QWidget* parent): QMainWindow(parent)
{ui.setupUi(this);//創建串口列表QStringList comPort;foreach(const QSerialPortInfo & info, QSerialPortInfo::availablePorts()){comPort << info.portName();}ui.comboBoxNo_2->addItems(comPort);
}
四、“打開串口”按鈕設計
vs中無法使用Qt Creator的“轉到槽”功能,因此需要開發者自己綁定槽函數;具體操作步驟為:https://www.cnblogs.com/ybqjymy/p/17999513
注:解決vs + qt 導致的亂碼問題:出現中文的文件首行加上#pragma execution_character_set("utf-8")
當我們綁定好槽函數on_pushButtonOpen_clicked()
,接下來就是實現串口打開邏輯:以下為具體代碼
//Map定義代碼查看源文件
void MyCOM::on_pushButtonOpen_clicked()
{QSerialPort::BaudRate CombaudRate;QSerialPort::DataBits ComdataBits;QSerialPort::StopBits ComstopBits;QSerialPort::Parity ComParity;QString selectedBaudRate = ui.comboBoxComBaud_2->currentText();std::cout << selectedBaudRate.toStdString() << "\n";if (baudRateMap.contains(selectedBaudRate)) {CombaudRate = baudRateMap[selectedBaudRate];}else {// 如果用戶選擇了一個未知的波特率,可以設置默認值或提示錯誤CombaudRate = QSerialPort::Baud9600; // 默認值qWarning("Invalid baud rate selected. Defaulting to 9600.");}
//具體代碼查看源文件// 根據用戶選擇設置數據位// 根據用戶選擇設置停止位// 根據用戶選擇設置校驗方式//初始化串口MyCom.setBaudRate(CombaudRate);MyCom.setDataBits(ComdataBits);MyCom.setStopBits(ComstopBits);MyCom.setParity(ComParity);MyCom.setPortName(spTxt);//打開串口if (ui.pushButtonOpen_2->text() == "打開串口"){bool ComFlag;ComFlag = MyCom.open(QIODevice::ReadWrite);if (ComFlag == true)//串口打開成功{//串口下拉框設置為不可選ui.comboBoxCheck_2->setEnabled(false);//具體代碼查看源文件//使能相應按鈕等ui.pushButtonSend_2->setEnabled(true);//具體代碼查看源文件ui.pushButtonOpen_2->setText(" 關閉串口 ");}else{QMessageBox::critical(this, "錯誤提示", "串口打開失敗,該端口可能被占用或不存在!rnLinux系統可能為當前用戶無串口訪問權限!");}}else{MyCom.close();ui.pushButtonOpen_2->setText(" 打開串口 ");//具體代碼查看源文件//使相應的按鈕不可用ui.pushButtonSend_2->setEnabled(false);具體代碼查看源文件}
}
五、串口數據發送與接收
通過信號槽機制,在發送區發送數據,通過
&QIODevice::readyRead
信號來通知接收區函數&MyCOM::MyComRevSlot
打印串口發送的數據
代碼邏輯:
-
信號槽邏輯:當串口有數據可以讀取時,自動響應
MyComRevSlot
函數。connect(&MyCom, &QIODevice::readyRead, this, &MyCOM::MyComRevSlot);
-
發送區代碼邏輯:通過第四步中的“轉到槽”機制,在發送按鈕上綁定槽函數
on_pushButtonSend_clicked()
,再槽函數中接收發送區字符并通過MyCom.write(comSendData)
發送到串口。- 其中16進制發送需要將字符串格式化成16進制
QByteArray::fromHex(SendTemp.toUtf8()).data();
//精簡版,少了一些單選框的邏輯判斷 void MyCOM::on_pushButtonSend_clicked() {QByteArray comSendData;QString SendTemp;int temp;//讀取發送窗口數據SendTemp = ui.TextSend_2->toPlainText();//判斷發送格式,并格式化數據if (ui.checkBoxSendHex_2->checkState() != false)//16進制發送{comSendData = QByteArray::fromHex(SendTemp.toUtf8()).data();//獲取字符串}temp = MyCom.write(comSendData); }
- 其中16進制發送需要將字符串格式化成16進制
-
接收區代碼邏輯:通過信號槽機制來調用
MyComRevSlot
函數,利用MyCom.readAll()
讀取串口的數據,最后顯示到文本框內。//精簡版 void MyCOM::MyComRevSlot() {QByteArray MyComRevBUff;//接收數據緩存QString StrTemp, StrTimeDate, StrTemp1;//讀取串口接收到的數據,并格式化數據MyComRevBUff = MyCom.readAll();StrTemp = QString::fromLocal8Bit(MyComRevBUff);curDateTime = QDateTime::currentDateTime();StrTimeDate = curDateTime.toString("[yyyy-MM-dd hh:mm:ss.zzz]");StrTemp = MyComRevBUff.toHex().toUpper();//轉換為16進制數,并大寫for (int i = 0; i < StrTemp.length(); i += 2)//整理字符串,即添加空格{StrTemp1 += StrTemp.mid(i, 2);StrTemp1 += " ";}//添加時間頭StrTemp1.prepend(StrTimeDate);StrTemp1.append("\r\n");//后面添加換行ui.TextRev_2->insertPlainText(StrTemp1);//顯示數據ui.TextRev_2->moveCursor(QTextCursor::End);//光標移動到文本末尾 }
六、周期循環發送指令
通過定時器,實現周期性指令發送功能
-
創建定時器
QTimer* PriecSendTimer;
-
在構造函數中注冊定時器超時connect函數,調用
on_pushButtonSend_clicked()
connect(PriecSendTimer, &QTimer::timeout, this, [=]() {on_pushButtonSend_clicked(); });
-
通過信號槽機制,綁定選擇框狀態變化信號處理函數
-
編寫選擇框變化處理函數
void MyCOM::on_checkBoxPeriodicSend_stateChanged(int arg1) {if (arg1 == false){PriecSendTimer->stop();ui.lineEditTime->setEnabled(true);}else{PriecSendTimer->start(ui.lineEditTime->text().toInt());ui.lineEditTime->setEnabled(false);} }
七、接收流量統計及狀態欄設計
通過設計狀態欄來實時展示QLabel的相關數據
-
自定義變量
//添加自定義變量long ComSendSum, ComRevSum;//發送和接收流量統計變量QLabel* qlbSendSum, * qlbRevSum;//發送接收流量label對象QLabel* myLink, * MySource;
-
變量綁定狀態欄
//創建底部狀態欄及其相關部件 QStatusBar* STABar = statusBar();qlbSendSum = new QLabel(this); qlbRevSum = new QLabel(this); myLink = new QLabel(this); MySource = new QLabel(this); myLink->setMinimumSize(90, 20);// 設置標簽最小大小 MySource->setMinimumSize(90, 20); qlbSendSum->setMinimumSize(100, 20); qlbRevSum->setMinimumSize(100, 20); ComSendSum = 0; ComRevSum = 0;setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum); setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);STABar->addPermanentWidget(qlbSendSum);// 從右往左依次添加 STABar->addPermanentWidget(qlbRevSum); STABar->addWidget(myLink);// 從左往右依次添加 STABar->addWidget(MySource);myLink->setOpenExternalLinks(true);//狀態欄顯示官網、源碼鏈接 myLink->setText("<style> a {text-decoration: none} </style> <a href=\"http://8.134.156.7/\">--個人博客--"); MySource->setOpenExternalLinks(true); MySource->setText("<style> a {text-decoration: none} </style> <a href=\"https://github.com/say-Hai/MyCOMDemo\">--源代碼--");
-
自定義函數來更改自定義變量
void MyCOM::setNumOnLabel(QLabel* lbl, QString strS, long num) {QString strN = QString("%1").arg(num);QString str = strS + strN;lbl->setText(str); }
-
在發送/接收函數中調用自定義函數
//發送 temp = MyCom.write(comSendData); ComSendSum++; setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum);//接收 MyComRevBUff = MyCom.readAll(); StrTemp = QString::fromLocal8Bit(MyComRevBUff); ComRevSum++; setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);
八、數據區清空功能
void MyCOM::on_pushButtonClearRev_clicked()
{ui.TextRev_2->clear();ComSendSum = 0;ComRevSum = 0;setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum);setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);
}void MyCOM::on_pushButtonClearSend_clicked()
{ui.TextSend_2->clear();ComSendSum = 0;ComRevSum = 0;setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum);setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);
}
九、文件保存與讀取功能
通過文件的讀取快速實現對串口發送數據,通過寫入文件的方式保存串口的輸出。
-
讀取文件:通過
QFile aFile(aFileName);QByteArray text = aFile.readAll();
來獲取文本數據,并寫入到文本框中。//首先創建on_pushButtonRdFile_clicked信號槽機制打開文件夾選擇文件路徑 void MyCOM::on_pushButtonRdFile_clicked() {QString curPath = QDir::currentPath();QString dlgTitle = "打開一個文件"; //對話框標題QString filter = "文本文件(*.txt);;所有文件(*.*)"; //文件過濾器QString aFileName = QFileDialog::getOpenFileName(this, dlgTitle, curPath, filter);if (aFileName.isEmpty())return;openTextByIODevice(aFileName); } //通過openTextByIODevice來讀取文件 bool MyCOM::openTextByIODevice(const QString& aFileName) {QFile aFile(aFileName);if (!aFile.exists()) //文件不存在return false;if (!aFile.open(QIODevice::ReadOnly | QIODevice::Text))return false;QByteArray text = aFile.readAll();QString strText = byteArrayToUnicode(text);//編碼格式轉換,防止GBK中文亂碼ui.TextSend_2->setPlainText(strText);aFile.close();return true; } //其中防止編碼格式問題,通過byteArrayToUnicode進行編碼格式轉換 QString MyCOM::byteArrayToUnicode(const QByteArray& array) {QTextCodec::ConverterState state;// 先嘗試使用utf-8的方式把QByteArray轉換成QStringQString text = QTextCodec::codecForName("UTF-8")->toUnicode(array.constData(), array.size(), &state);// 如果轉換時無效字符數量大于0,說明編碼格式不對if (state.invalidChars > 0){// 再嘗試使用GBK的方式進行轉換,一般就能轉換正確(當然也可能是其它格式,但比較少見了)text = QTextCodec::codecForName("GBK")->toUnicode(array);}return text; }
-
寫入文件:選擇文件路徑->調用
aFile.write(strBytes, strBytes.length());
寫入文件void MyCOM::on_pushButtonSaveRev_clicked() {QString curFile = QDir::currentPath();QString dlgTitle = " 另存為一個文件 "; //對話框標題QString filter = " 文本文件(*.txt);;所有文件(*.*);;h文件(*.h);;c++文件(*.cpp) "; //文件過濾器QString aFileName = QFileDialog::getSaveFileName(this, dlgTitle, curFile, filter);if (aFileName.isEmpty())return;saveTextByIODevice(aFileName); } bool MyCOM::saveTextByIODevice(const QString& aFileName) {QFile aFile(aFileName);if (!aFile.open(QIODevice::WriteOnly | QIODevice::Text))return false;QString str = ui.TextRev_2->toPlainText();//整個內容作為字符串QByteArray strBytes = str.toUtf8();//轉換為字節數組aFile.write(strBytes, strBytes.length()); //寫入文件aFile.close();return true; }
十、多行發送功能
通過信號槽機制和定時器功能,實現對多行數據選擇的循環發送
具體邏輯:根據選擇框的狀態確定定時器狀態->通過定時器超時函數喚醒發送事件->在發送事件中確定此次需要發送的行數據->調用對應發送按鈕函數
-
通過選擇框的狀態變化來打開/關閉定時器發送
void MyCOM::on_checkBoxMuti_stateChanged(int arg) {if (!arg){PriecSendTimer->stop();//關閉定時器ui.lineEditTime->setEnabled(true);//使能對話框編輯}else{LastSend = 0;//從第一行開始發送ui.checkBoxPeriodicSend->setChecked(false);PriecSendTimer->start(ui.lineEditTime->text().toInt());ui.lineEditTime->setEnabled(false);//關閉對話框編輯} }
-
重構定時器超時響應函數,適配多行重復發送功能
connect(PriecSendTimer, &QTimer::timeout, this, [=]() {Pre_on_pushButtonSend_clicked(); });void MyCOM::Pre_on_pushButtonSend_clicked() {if (ui.checkBoxPeriodicMutiSend_2->isChecked() == true){while (LastSend < 10){if (checkBoxes[LastSend]->isChecked()){//發送對應行的數據on_pushButtonMuti_clicked(++LastSend);break;}LastSend++;}if (LastSend == 10){LastSend = 0;}}else{//普通發送on_pushButtonSend_clicked();} }
-
通過行索引觸發對應的點擊事件
void MyCOM::on_pushButtonMuti_clicked(int lineEditIndex) {QString Strtemp;switch (lineEditIndex) {case 1:Strtemp = ui.lineEditMuti1_2->text();break;case 2:Strtemp = ui.lineEditMuti2_2->text();break;//...后面對應的操作default:return; // 默認情況下不做任何操作}ui.TextSend_2->clear();ui.TextSend_2->insertPlainText(Strtemp);ui.TextSend_2->moveCursor(QTextCursor::End);MyCOM::on_pushButtonSend_clicked(); }
十一:自動刷新串口下拉框
實現方法:新建一個類繼承
QComboBox
類,重寫鼠標點擊事件使其調用掃描端口函數 -
新建
mycombobox
類,繼承QComBox#include <QComboBox> #include <QMouseEvent> #include <QSerialPort> #include <QSerialPortInfo>class mycombobox : public QComboBox {Q_OBJECT public:explicit mycombobox(QWidget* parent = nullptr);void mousePressEvent(QMouseEvent* event) override; signals: private:void scanActivatePort(); };
-
重寫掃描函數和鼠標點擊函數
mycombobox::mycombobox(QWidget* parent) : QComboBox(parent) {scanActivatePort(); }void mycombobox::mousePressEvent(QMouseEvent* event) {if (event->button() == Qt::LeftButton){scanActivatePort();showPopup();} }void mycombobox::scanActivatePort() {clear();//創建串口列表QStringList comPort;foreach(const QSerialPortInfo & info, QSerialPortInfo::availablePorts()){QString serialPortInfo = info.portName() + ": " + info.description();// 串口設備信息,芯片/驅動名稱comPort << serialPortInfo;}this->addItems(comPort); }
-
最后將
comboBoxNo_2
組件提升為mycombobox
類
到此整個軟件設計完畢
END:信號槽綁定圖
參考文獻:
[1] https://rymcu.com/portfolio/40