feat: AiAnalysis - 16通道AI数据UDP实时采集与分析
功能: - UDP端口监听,实时接收数据 - 解析16通道逗号分隔的ADC数据 - 实时波形绘制(支持滚轮缩放、右键拖拽、双击恢复) - 数据自动保存到 data_端口号/ 目录 - 支持打开历史CSV文件回放查看 - 16通道独立颜色,可切换显示/隐藏 - 深色主题界面
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
build/
|
||||||
|
.vscode/
|
||||||
|
*.user
|
||||||
|
*.pro.user
|
||||||
|
.DS_Store
|
||||||
|
data_*/
|
||||||
22
CMakeLists.txt
Normal file
22
CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(AiAnalysis LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
|
||||||
|
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Network)
|
||||||
|
|
||||||
|
add_executable(AiAnalysis
|
||||||
|
main.cpp
|
||||||
|
mainwindow.cpp
|
||||||
|
mainwindow.h
|
||||||
|
udpreceiver.cpp
|
||||||
|
udpreceiver.h
|
||||||
|
plotwidget.cpp
|
||||||
|
plotwidget.h
|
||||||
|
datamanager.cpp
|
||||||
|
datamanager.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(AiAnalysis Qt5::Core Qt5::Widgets Qt5::Network)
|
||||||
127
datamanager.cpp
Normal file
127
datamanager.cpp
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#include "datamanager.h"
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
DataManager::DataManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_recording(false)
|
||||||
|
, m_headerWritten(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DataManager::~DataManager()
|
||||||
|
{
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DataManager::startRecording(const QString &dirPath)
|
||||||
|
{
|
||||||
|
stopRecording();
|
||||||
|
|
||||||
|
QDir dir(dirPath);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
if (!dir.mkpath(".")) {
|
||||||
|
qWarning() << "无法创建目录:" << dirPath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString fileName = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss") + ".csv";
|
||||||
|
m_filePath = dir.filePath(fileName);
|
||||||
|
|
||||||
|
m_file.setFileName(m_filePath);
|
||||||
|
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
qWarning() << "无法创建文件:" << m_filePath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stream.setDevice(&m_file);
|
||||||
|
m_headerWritten = false;
|
||||||
|
m_recording = true;
|
||||||
|
|
||||||
|
qDebug() << "开始记录数据到:" << m_filePath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataManager::stopRecording()
|
||||||
|
{
|
||||||
|
if (m_recording) {
|
||||||
|
m_stream.flush();
|
||||||
|
m_file.close();
|
||||||
|
m_recording = false;
|
||||||
|
qDebug() << "停止记录, 文件:" << m_filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DataManager::isRecording() const
|
||||||
|
{
|
||||||
|
return m_recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DataManager::currentFilePath() const
|
||||||
|
{
|
||||||
|
return m_filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataManager::appendDataPoint(const QVector<int> &values)
|
||||||
|
{
|
||||||
|
if (!m_recording) return;
|
||||||
|
|
||||||
|
if (!m_headerWritten) {
|
||||||
|
// 写入表头
|
||||||
|
QStringList headers;
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
headers << QString("ch%1").arg(i);
|
||||||
|
}
|
||||||
|
m_stream << headers.join(",") << "\n";
|
||||||
|
m_headerWritten = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts;
|
||||||
|
for (int v : values) {
|
||||||
|
parts << QString::number(v);
|
||||||
|
}
|
||||||
|
m_stream << parts.join(",") << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QVector<double>> DataManager::loadFile(const QString &filePath, QString &error)
|
||||||
|
{
|
||||||
|
QVector<QVector<double>> result(16);
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
error = QString("无法打开文件: %1").arg(filePath);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
bool firstLine = true;
|
||||||
|
|
||||||
|
while (!stream.atEnd()) {
|
||||||
|
QString line = stream.readLine().trimmed();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
|
||||||
|
// 跳过表头
|
||||||
|
if (firstLine) {
|
||||||
|
firstLine = false;
|
||||||
|
// 检查是否是表头(包含非数字字符如ch)
|
||||||
|
if (line.contains("ch", Qt::CaseInsensitive) && !line.at(0).isDigit()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = line.split(",");
|
||||||
|
if (parts.size() < 16) continue;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
bool ok;
|
||||||
|
double val = parts[i].toDouble(&ok);
|
||||||
|
if (ok) {
|
||||||
|
result[i].append(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
34
datamanager.h
Normal file
34
datamanager.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#ifndef DATAMANAGER_H
|
||||||
|
#define DATAMANAGER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
|
class DataManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DataManager(QObject *parent = nullptr);
|
||||||
|
~DataManager();
|
||||||
|
|
||||||
|
bool startRecording(const QString &dirPath);
|
||||||
|
void stopRecording();
|
||||||
|
bool isRecording() const;
|
||||||
|
void appendDataPoint(const QVector<int> &values);
|
||||||
|
QString currentFilePath() const;
|
||||||
|
|
||||||
|
static QVector<QVector<double>> loadFile(const QString &filePath, QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QFile m_file;
|
||||||
|
QTextStream m_stream;
|
||||||
|
bool m_recording;
|
||||||
|
QString m_filePath;
|
||||||
|
bool m_headerWritten;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DATAMANAGER_H
|
||||||
15
main.cpp
Normal file
15
main.cpp
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#include <QApplication>
|
||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
app.setApplicationName("AiAnalysis");
|
||||||
|
app.setApplicationVersion("1.0");
|
||||||
|
|
||||||
|
MainWindow w;
|
||||||
|
w.resize(1200, 800);
|
||||||
|
w.show();
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
332
mainwindow.cpp
Normal file
332
mainwindow.cpp
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
#include "mainwindow.h"
|
||||||
|
#include "plotwidget.h"
|
||||||
|
#include "udpreceiver.h"
|
||||||
|
#include "datamanager.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QStatusBar>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
|
: QMainWindow(parent)
|
||||||
|
, m_udpReceiver(new UdpReceiver(this))
|
||||||
|
, m_dataManager(new DataManager(this))
|
||||||
|
, m_isReceiving(false)
|
||||||
|
, m_packetCount(0)
|
||||||
|
{
|
||||||
|
setWindowTitle("AiAnalysis - AI数据实时分析");
|
||||||
|
setupUi();
|
||||||
|
|
||||||
|
// UDP数据接收
|
||||||
|
connect(m_udpReceiver, &UdpReceiver::dataReceived, this, &MainWindow::onDataReceived);
|
||||||
|
connect(m_udpReceiver, &UdpReceiver::errorOccurred, this, [this](const QString &err) {
|
||||||
|
QMessageBox::warning(this, "错误", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow::~MainWindow()
|
||||||
|
{
|
||||||
|
stopReceiving();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::setupUi()
|
||||||
|
{
|
||||||
|
QWidget *centralWidget = new QWidget(this);
|
||||||
|
setCentralWidget(centralWidget);
|
||||||
|
|
||||||
|
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
|
||||||
|
mainLayout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
mainLayout->setSpacing(6);
|
||||||
|
|
||||||
|
// ========== 顶部控制栏 ==========
|
||||||
|
QHBoxLayout *controlLayout = new QHBoxLayout();
|
||||||
|
|
||||||
|
QLabel *portLabel = new QLabel("UDP端口:");
|
||||||
|
portLabel->setStyleSheet("color: #ccc; font-size: 13px;");
|
||||||
|
|
||||||
|
m_portEdit = new QLineEdit();
|
||||||
|
m_portEdit->setPlaceholderText("输入端口号");
|
||||||
|
m_portEdit->setMaximumWidth(120);
|
||||||
|
m_portEdit->setStyleSheet(
|
||||||
|
"QLineEdit { background: #2a2a3e; color: #fff; border: 1px solid #555; "
|
||||||
|
"border-radius: 4px; padding: 4px 8px; font-size: 13px; }"
|
||||||
|
);
|
||||||
|
|
||||||
|
m_startStopBtn = new QPushButton("开始监听");
|
||||||
|
m_startStopBtn->setStyleSheet(
|
||||||
|
"QPushButton { background: #2ecc71; color: #fff; border: none; "
|
||||||
|
"border-radius: 4px; padding: 6px 20px; font-size: 13px; font-weight: bold; }"
|
||||||
|
"QPushButton:hover { background: #27ae60; }"
|
||||||
|
);
|
||||||
|
connect(m_startStopBtn, &QPushButton::clicked, this, &MainWindow::onStartStop);
|
||||||
|
|
||||||
|
m_openFileBtn = new QPushButton("打开文件");
|
||||||
|
m_openFileBtn->setStyleSheet(
|
||||||
|
"QPushButton { background: #3498db; color: #fff; border: none; "
|
||||||
|
"border-radius: 4px; padding: 6px 16px; font-size: 13px; }"
|
||||||
|
"QPushButton:hover { background: #2980b9; }"
|
||||||
|
);
|
||||||
|
connect(m_openFileBtn, &QPushButton::clicked, this, &MainWindow::onOpenFile);
|
||||||
|
|
||||||
|
m_clearBtn = new QPushButton("清除");
|
||||||
|
m_clearBtn->setStyleSheet(
|
||||||
|
"QPushButton { background: #e74c3c; color: #fff; border: none; "
|
||||||
|
"border-radius: 4px; padding: 6px 16px; font-size: 13px; }"
|
||||||
|
"QPushButton:hover { background: #c0392b; }"
|
||||||
|
);
|
||||||
|
connect(m_clearBtn, &QPushButton::clicked, this, &MainWindow::onClear);
|
||||||
|
|
||||||
|
QLabel *displayLabel = new QLabel("显示点数:");
|
||||||
|
displayLabel->setStyleSheet("color: #ccc; font-size: 13px;");
|
||||||
|
|
||||||
|
m_displaySpinBox = new QSpinBox();
|
||||||
|
m_displaySpinBox->setRange(50, 10000);
|
||||||
|
m_displaySpinBox->setValue(500);
|
||||||
|
m_displaySpinBox->setSingleStep(100);
|
||||||
|
m_displaySpinBox->setStyleSheet(
|
||||||
|
"QSpinBox { background: #2a2a3e; color: #fff; border: 1px solid #555; "
|
||||||
|
"border-radius: 4px; padding: 4px; font-size: 13px; }"
|
||||||
|
);
|
||||||
|
connect(m_displaySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
|
||||||
|
this, &MainWindow::onDisplayPointsChanged);
|
||||||
|
|
||||||
|
controlLayout->addWidget(portLabel);
|
||||||
|
controlLayout->addWidget(m_portEdit);
|
||||||
|
controlLayout->addWidget(m_startStopBtn);
|
||||||
|
controlLayout->addSpacing(20);
|
||||||
|
controlLayout->addWidget(m_openFileBtn);
|
||||||
|
controlLayout->addWidget(m_clearBtn);
|
||||||
|
controlLayout->addSpacing(20);
|
||||||
|
controlLayout->addWidget(displayLabel);
|
||||||
|
controlLayout->addWidget(m_displaySpinBox);
|
||||||
|
controlLayout->addStretch();
|
||||||
|
|
||||||
|
mainLayout->addLayout(controlLayout);
|
||||||
|
|
||||||
|
// ========== 中间区域: 绘图 + 通道选择 ==========
|
||||||
|
QHBoxLayout *centerLayout = new QHBoxLayout();
|
||||||
|
|
||||||
|
// 绘图区域
|
||||||
|
m_plotWidget = new PlotWidget(this);
|
||||||
|
m_plotWidget->setTitle("AI 数据实时波形");
|
||||||
|
centerLayout->addWidget(m_plotWidget, 1);
|
||||||
|
|
||||||
|
// 右侧通道选择面板
|
||||||
|
QGroupBox *channelGroup = new QGroupBox("通道选择");
|
||||||
|
channelGroup->setStyleSheet(
|
||||||
|
"QGroupBox { color: #ccc; border: 1px solid #444; border-radius: 4px; "
|
||||||
|
"margin-top: 12px; padding-top: 16px; font-size: 12px; }"
|
||||||
|
"QGroupBox::title { subcontrol-origin: margin; left: 10px; }"
|
||||||
|
);
|
||||||
|
|
||||||
|
QVBoxLayout *channelLayout = new QVBoxLayout(channelGroup);
|
||||||
|
channelLayout->setSpacing(2);
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
QCheckBox *check = new QCheckBox(QString("ch%1").arg(i));
|
||||||
|
check->setChecked(true);
|
||||||
|
check->setStyleSheet(
|
||||||
|
"QCheckBox { color: #aaa; font-size: 11px; spacing: 4px; }"
|
||||||
|
"QCheckBox::indicator { width: 12px; height: 12px; }"
|
||||||
|
"QCheckBox::indicator:checked { background: #3498db; }"
|
||||||
|
);
|
||||||
|
connect(check, &QCheckBox::toggled, this, [this, i](bool checked) {
|
||||||
|
onToggleChannel(i, checked);
|
||||||
|
});
|
||||||
|
channelLayout->addWidget(check);
|
||||||
|
m_channelChecks.append(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
channelLayout->addStretch();
|
||||||
|
|
||||||
|
QScrollArea *scrollArea = new QScrollArea();
|
||||||
|
scrollArea->setWidget(channelGroup);
|
||||||
|
scrollArea->setWidgetResizable(true);
|
||||||
|
scrollArea->setMaximumWidth(120);
|
||||||
|
scrollArea->setStyleSheet("QScrollArea { border: none; background: transparent; }");
|
||||||
|
|
||||||
|
centerLayout->addWidget(scrollArea);
|
||||||
|
mainLayout->addLayout(centerLayout, 1);
|
||||||
|
|
||||||
|
// ========== 底部状态栏 ==========
|
||||||
|
QHBoxLayout *statusLayout = new QHBoxLayout();
|
||||||
|
|
||||||
|
m_statusLabel = new QLabel("就绪 - 等待开始监听");
|
||||||
|
m_statusLabel->setStyleSheet("color: #888; font-size: 12px;");
|
||||||
|
|
||||||
|
m_packetLabel = new QLabel("收到: 0 包");
|
||||||
|
m_packetLabel->setStyleSheet("color: #888; font-size: 12px;");
|
||||||
|
|
||||||
|
QLabel *hintLabel = new QLabel("提示: 滚轮缩放 | 右键拖拽平移 | 双击恢复自动范围");
|
||||||
|
hintLabel->setStyleSheet("color: #555; font-size: 11px;");
|
||||||
|
|
||||||
|
statusLayout->addWidget(m_statusLabel);
|
||||||
|
statusLayout->addStretch();
|
||||||
|
statusLayout->addWidget(m_packetLabel);
|
||||||
|
statusLayout->addSpacing(20);
|
||||||
|
statusLayout->addWidget(hintLabel);
|
||||||
|
|
||||||
|
mainLayout->addLayout(statusLayout);
|
||||||
|
|
||||||
|
// 全局样式
|
||||||
|
setStyleSheet("QMainWindow { background-color: #1a1a2e; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onStartStop()
|
||||||
|
{
|
||||||
|
if (m_isReceiving) {
|
||||||
|
stopReceiving();
|
||||||
|
} else {
|
||||||
|
startReceiving();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::startReceiving()
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
quint16 port = m_portEdit->text().toUShort(&ok);
|
||||||
|
if (!ok || port == 0) {
|
||||||
|
QMessageBox::warning(this, "错误", "请输入有效的端口号 (1-65535)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_udpReceiver->start(port)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除旧数据
|
||||||
|
m_plotWidget->clear();
|
||||||
|
m_packetCount = 0;
|
||||||
|
m_packetLabel->setText("收到: 0 包");
|
||||||
|
|
||||||
|
// 创建数据保存目录
|
||||||
|
m_dataDir = QApplication::applicationDirPath() + "/data_" + QString::number(port);
|
||||||
|
m_dataManager->startRecording(m_dataDir);
|
||||||
|
|
||||||
|
m_isReceiving = true;
|
||||||
|
m_portEdit->setEnabled(false);
|
||||||
|
m_startStopBtn->setText("停止监听");
|
||||||
|
m_startStopBtn->setStyleSheet(
|
||||||
|
"QPushButton { background: #e74c3c; color: #fff; border: none; "
|
||||||
|
"border-radius: 4px; padding: 6px 20px; font-size: 13px; font-weight: bold; }"
|
||||||
|
"QPushButton:hover { background: #c0392b; }"
|
||||||
|
);
|
||||||
|
|
||||||
|
m_statusLabel->setText(QString("监听中 - 端口 %1 | 保存至 %2").arg(port).arg(m_dataDir));
|
||||||
|
m_statusLabel->setStyleSheet("color: #2ecc71; font-size: 12px;");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::stopReceiving()
|
||||||
|
{
|
||||||
|
m_udpReceiver->stop();
|
||||||
|
m_dataManager->stopRecording();
|
||||||
|
|
||||||
|
m_isReceiving = false;
|
||||||
|
m_portEdit->setEnabled(true);
|
||||||
|
m_startStopBtn->setText("开始监听");
|
||||||
|
m_startStopBtn->setStyleSheet(
|
||||||
|
"QPushButton { background: #2ecc71; color: #fff; border: none; "
|
||||||
|
"border-radius: 4px; padding: 6px 20px; font-size: 13px; font-weight: bold; }"
|
||||||
|
"QPushButton:hover { background: #27ae60; }"
|
||||||
|
);
|
||||||
|
|
||||||
|
m_statusLabel->setText(QString("已停止 - 共收到 %1 包").arg(m_packetCount));
|
||||||
|
m_statusLabel->setStyleSheet("color: #e74c3c; font-size: 12px;");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onOpenFile()
|
||||||
|
{
|
||||||
|
QString filePath = QFileDialog::getOpenFileName(
|
||||||
|
this, "打开数据文件", m_dataDir, "CSV文件 (*.csv);;所有文件 (*)");
|
||||||
|
|
||||||
|
if (filePath.isEmpty()) return;
|
||||||
|
|
||||||
|
stopReceiving();
|
||||||
|
loadAndDisplayFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadAndDisplayFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
auto channels = DataManager::loadFile(filePath, error);
|
||||||
|
|
||||||
|
if (!error.isEmpty() && channels[0].isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "错误", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_plotWidget->setAllData(channels);
|
||||||
|
m_plotWidget->setTitle(QString("回放: %1").arg(QFileInfo(filePath).fileName()));
|
||||||
|
|
||||||
|
m_statusLabel->setText(QString("回放模式 - %1 | %2 个数据点")
|
||||||
|
.arg(QFileInfo(filePath).fileName())
|
||||||
|
.arg(m_plotWidget->totalPoints()));
|
||||||
|
m_statusLabel->setStyleSheet("color: #3498db; font-size: 12px;");
|
||||||
|
|
||||||
|
m_packetLabel->setText(QString("已加载: %1 点").arg(m_plotWidget->totalPoints()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onClear()
|
||||||
|
{
|
||||||
|
m_plotWidget->clear();
|
||||||
|
m_packetCount = 0;
|
||||||
|
m_packetLabel->setText("收到: 0 包");
|
||||||
|
m_statusLabel->setText("已清除");
|
||||||
|
m_statusLabel->setStyleSheet("color: #888; font-size: 12px;");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onDataReceived(const QByteArray &data)
|
||||||
|
{
|
||||||
|
QString raw = QString::fromUtf8(data).trimmed();
|
||||||
|
if (raw.isEmpty()) return;
|
||||||
|
|
||||||
|
// 可能一个UDP包包含多行
|
||||||
|
QStringList lines = raw.split('\n', Qt::SkipEmptyParts);
|
||||||
|
for (const QString &line : lines) {
|
||||||
|
processDataLine(line.trimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::processDataLine(const QString &line)
|
||||||
|
{
|
||||||
|
QStringList parts = line.split(',');
|
||||||
|
if (parts.size() < 16) return;
|
||||||
|
|
||||||
|
QVector<int> values(16);
|
||||||
|
bool allOk = true;
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
bool ok;
|
||||||
|
values[i] = parts[i].toInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
allOk = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allOk) return;
|
||||||
|
|
||||||
|
// 转换为double给绘图
|
||||||
|
QVector<double> dvalues(16);
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
dvalues[i] = static_cast<double>(values[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_plotWidget->addDataPoint(dvalues);
|
||||||
|
m_dataManager->appendDataPoint(values);
|
||||||
|
|
||||||
|
m_packetCount++;
|
||||||
|
m_packetLabel->setText(QString("收到: %1 包").arg(m_packetCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onDisplayPointsChanged(int count)
|
||||||
|
{
|
||||||
|
m_plotWidget->setDisplayPointCount(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onToggleChannel(int channel, bool visible)
|
||||||
|
{
|
||||||
|
m_plotWidget->setChannelVisible(channel, visible);
|
||||||
|
}
|
||||||
67
mainwindow.h
Normal file
67
mainwindow.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#ifndef MAINWINDOW_H
|
||||||
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
class PlotWidget;
|
||||||
|
class UdpReceiver;
|
||||||
|
class DataManager;
|
||||||
|
|
||||||
|
class MainWindow : public QMainWindow
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MainWindow(QWidget *parent = nullptr);
|
||||||
|
~MainWindow();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onStartStop();
|
||||||
|
void onOpenFile();
|
||||||
|
void onClear();
|
||||||
|
void onDataReceived(const QByteArray &data);
|
||||||
|
void onDisplayPointsChanged(int count);
|
||||||
|
void onToggleChannel(int channel, bool visible);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupUi();
|
||||||
|
void startReceiving();
|
||||||
|
void stopReceiving();
|
||||||
|
void processDataLine(const QString &line);
|
||||||
|
void loadAndDisplayFile(const QString &filePath);
|
||||||
|
|
||||||
|
// 控件
|
||||||
|
QLineEdit *m_portEdit;
|
||||||
|
QPushButton *m_startStopBtn;
|
||||||
|
QPushButton *m_openFileBtn;
|
||||||
|
QPushButton *m_clearBtn;
|
||||||
|
QLabel *m_statusLabel;
|
||||||
|
QLabel *m_packetLabel;
|
||||||
|
QSpinBox *m_displaySpinBox;
|
||||||
|
PlotWidget *m_plotWidget;
|
||||||
|
|
||||||
|
// 功能组件
|
||||||
|
UdpReceiver *m_udpReceiver;
|
||||||
|
DataManager *m_dataManager;
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
bool m_isReceiving;
|
||||||
|
int m_packetCount;
|
||||||
|
QString m_dataDir;
|
||||||
|
|
||||||
|
// 通道选择复选框
|
||||||
|
QList<QCheckBox*> m_channelChecks;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // MAINWINDOW_H
|
||||||
456
plotwidget.cpp
Normal file
456
plotwidget.cpp
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
#include "plotwidget.h"
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QtMath>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
PlotWidget::PlotWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_totalPoints(0)
|
||||||
|
, m_displayPoints(500)
|
||||||
|
, m_yMin(0)
|
||||||
|
, m_yMax(65535)
|
||||||
|
, m_autoYRange(true)
|
||||||
|
, m_autoScroll(true)
|
||||||
|
, m_dragging(false)
|
||||||
|
, m_marginLeft(70)
|
||||||
|
, m_marginRight(160)
|
||||||
|
, m_marginTop(30)
|
||||||
|
, m_marginBottom(50)
|
||||||
|
, m_bgColor("#1a1a2e")
|
||||||
|
, m_gridColor("#333355")
|
||||||
|
, m_gridColorMinor("#252545")
|
||||||
|
, m_textColor("#cccccc")
|
||||||
|
, m_axisColor("#888888")
|
||||||
|
, m_dirty(true)
|
||||||
|
{
|
||||||
|
m_data.resize(16);
|
||||||
|
m_channelVisible.fill(true, 16);
|
||||||
|
m_colors = defaultColors();
|
||||||
|
|
||||||
|
setMouseTracking(true);
|
||||||
|
setMinimumSize(400, 300);
|
||||||
|
|
||||||
|
// 定时刷新(约30fps)
|
||||||
|
m_refreshTimer = new QTimer(this);
|
||||||
|
connect(m_refreshTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
if (m_dirty) {
|
||||||
|
m_dirty = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
m_refreshTimer->start(33);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QColor> PlotWidget::defaultColors()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
QColor("#FF4444"), QColor("#44FF44"), QColor("#4488FF"), QColor("#FFAA00"),
|
||||||
|
QColor("#FF44FF"), QColor("#44FFFF"), QColor("#FFFF44"), QColor("#FF8888"),
|
||||||
|
QColor("#88FF88"), QColor("#8888FF"), QColor("#CC6600"), QColor("#FF66CC"),
|
||||||
|
QColor("#66FFCC"), QColor("#CCFF66"), QColor("#AA66FF"), QColor("#FF6666"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::addDataPoint(const QVector<double> &values)
|
||||||
|
{
|
||||||
|
if (values.size() < 16) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
m_data[i].append(values[i]);
|
||||||
|
}
|
||||||
|
m_totalPoints++;
|
||||||
|
m_dirty = true;
|
||||||
|
|
||||||
|
if (m_autoYRange && m_totalPoints % 50 == 0) {
|
||||||
|
autoRangeY();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::setAllData(const QVector<QVector<double>> &channels)
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
if (channels.size() >= 16) {
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
m_data[i] = channels[i];
|
||||||
|
}
|
||||||
|
m_totalPoints = channels.isEmpty() ? 0 : channels[0].size();
|
||||||
|
}
|
||||||
|
m_autoScroll = true;
|
||||||
|
m_autoYRange = true;
|
||||||
|
autoRangeY();
|
||||||
|
m_dirty = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::clear()
|
||||||
|
{
|
||||||
|
for (auto &ch : m_data) {
|
||||||
|
ch.clear();
|
||||||
|
}
|
||||||
|
m_totalPoints = 0;
|
||||||
|
m_autoScroll = true;
|
||||||
|
m_autoYRange = true;
|
||||||
|
m_dirty = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::setChannelVisible(int channel, bool visible)
|
||||||
|
{
|
||||||
|
if (channel >= 0 && channel < 16) {
|
||||||
|
m_channelVisible[channel] = visible;
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotWidget::isChannelVisible(int channel) const
|
||||||
|
{
|
||||||
|
return channel >= 0 && channel < 16 ? m_channelVisible[channel] : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::setDisplayPointCount(int count)
|
||||||
|
{
|
||||||
|
m_displayPoints = qMax(10, count);
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PlotWidget::displayPointCount() const
|
||||||
|
{
|
||||||
|
return m_displayPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::setYRange(double min, double max)
|
||||||
|
{
|
||||||
|
m_yMin = min;
|
||||||
|
m_yMax = max;
|
||||||
|
m_autoYRange = false;
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::autoRangeY()
|
||||||
|
{
|
||||||
|
if (m_totalPoints == 0) {
|
||||||
|
m_yMin = 0;
|
||||||
|
m_yMax = 65535;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double globalMin = 1e18, globalMax = -1e18;
|
||||||
|
int startIdx = qMax(0, m_totalPoints - m_displayPoints);
|
||||||
|
|
||||||
|
for (int ch = 0; ch < 16; ++ch) {
|
||||||
|
if (!m_channelVisible[ch]) continue;
|
||||||
|
const auto &channel = m_data[ch];
|
||||||
|
for (int i = startIdx; i < channel.size(); ++i) {
|
||||||
|
double v = channel[i];
|
||||||
|
if (v < globalMin) globalMin = v;
|
||||||
|
if (v > globalMax) globalMax = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalMax - globalMin < 1.0) {
|
||||||
|
globalMin -= 50;
|
||||||
|
globalMax += 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
double margin = (globalMax - globalMin) * 0.1;
|
||||||
|
m_yMin = globalMin - margin;
|
||||||
|
m_yMax = globalMax + margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::setTitle(const QString &title)
|
||||||
|
{
|
||||||
|
m_title = title;
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PlotWidget::totalPoints() const
|
||||||
|
{
|
||||||
|
return m_totalPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF PlotWidget::plotArea() const
|
||||||
|
{
|
||||||
|
return QRectF(m_marginLeft, m_marginTop,
|
||||||
|
width() - m_marginLeft - m_marginRight,
|
||||||
|
height() - m_marginTop - m_marginBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF PlotWidget::dataToWidget(double x, double y) const
|
||||||
|
{
|
||||||
|
QRectF area = plotArea();
|
||||||
|
double px = area.left() + x * area.width();
|
||||||
|
double py = area.bottom() - ((y - m_yMin) / (m_yMax - m_yMin)) * area.height();
|
||||||
|
return QPointF(px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ Paint ================
|
||||||
|
|
||||||
|
void PlotWidget::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|
||||||
|
drawBackground(painter);
|
||||||
|
drawGrid(painter);
|
||||||
|
drawCurves(painter);
|
||||||
|
drawAxes(painter);
|
||||||
|
drawLegend(painter);
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
if (!m_title.isEmpty()) {
|
||||||
|
painter.setPen(m_textColor);
|
||||||
|
QFont titleFont = font();
|
||||||
|
titleFont.setPointSize(12);
|
||||||
|
titleFont.setBold(true);
|
||||||
|
painter.setFont(titleFont);
|
||||||
|
painter.drawText(QRect(0, 2, width(), m_marginTop - 4), Qt::AlignCenter, m_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据点计数
|
||||||
|
painter.setPen(QColor("#666666"));
|
||||||
|
QFont smallFont = font();
|
||||||
|
smallFont.setPointSize(8);
|
||||||
|
painter.setFont(smallFont);
|
||||||
|
QString info = QString("总点数: %1 显示: %2 范围: [%3, %4]")
|
||||||
|
.arg(m_totalPoints)
|
||||||
|
.arg(m_displayPoints)
|
||||||
|
.arg(m_yMin, 0, 'f', 0)
|
||||||
|
.arg(m_yMax, 0, 'f', 0);
|
||||||
|
painter.drawText(QRect(m_marginLeft, height() - m_marginBottom + 30,
|
||||||
|
width() - m_marginLeft - m_marginRight, 20),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::drawBackground(QPainter &painter)
|
||||||
|
{
|
||||||
|
painter.fillRect(rect(), m_bgColor);
|
||||||
|
QRectF area = plotArea();
|
||||||
|
painter.fillRect(area, QColor("#16213e"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::drawGrid(QPainter &painter)
|
||||||
|
{
|
||||||
|
QRectF area = plotArea();
|
||||||
|
if (area.width() <= 0 || area.height() <= 0) return;
|
||||||
|
|
||||||
|
// 计算合适的刻度
|
||||||
|
double range = m_yMax - m_yMin;
|
||||||
|
if (range <= 0) return;
|
||||||
|
|
||||||
|
double roughStep = range / 8.0;
|
||||||
|
double exponent = qFloor(log10(roughStep));
|
||||||
|
double mantissa = roughStep / qPow(10, exponent);
|
||||||
|
|
||||||
|
double gridStep;
|
||||||
|
if (mantissa < 1.5) gridStep = qPow(10, exponent);
|
||||||
|
else if (mantissa < 3.5) gridStep = 2 * qPow(10, exponent);
|
||||||
|
else if (mantissa < 7.5) gridStep = 5 * qPow(10, exponent);
|
||||||
|
else gridStep = 10 * qPow(10, exponent);
|
||||||
|
|
||||||
|
if (gridStep <= 0) gridStep = 1;
|
||||||
|
|
||||||
|
double firstGrid = qCeil(m_yMin / gridStep) * gridStep;
|
||||||
|
|
||||||
|
QPen majorPen(m_gridColor, 1, Qt::SolidLine);
|
||||||
|
QPen minorPen(m_gridColorMinor, 1, Qt::DotLine);
|
||||||
|
|
||||||
|
// Y轴网格
|
||||||
|
for (double y = firstGrid; y <= m_yMax; y += gridStep) {
|
||||||
|
painter.setPen(majorPen);
|
||||||
|
QPointF p = dataToWidget(0, y);
|
||||||
|
painter.drawLine(QPointF(area.left(), p.y()), QPointF(area.right(), p.y()));
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
painter.setPen(m_textColor);
|
||||||
|
QFont gridFont = font();
|
||||||
|
gridFont.setPointSize(8);
|
||||||
|
painter.setFont(gridFont);
|
||||||
|
painter.drawText(QRectF(0, p.y() - 10, m_marginLeft - 5, 20),
|
||||||
|
Qt::AlignRight | Qt::AlignVCenter,
|
||||||
|
QString::number(y, 'f', 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// X轴网格(时间/采样点)
|
||||||
|
int startIdx = qMax(0, m_totalPoints - m_displayPoints);
|
||||||
|
if (m_autoScroll) {
|
||||||
|
startIdx = qMax(0, m_totalPoints - m_displayPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画几条竖线
|
||||||
|
int xStep = m_displayPoints / 5;
|
||||||
|
if (xStep < 1) xStep = 1;
|
||||||
|
|
||||||
|
// 竖网格线(基于显示的采样点)
|
||||||
|
for (int i = 0; i <= m_displayPoints; i += xStep) {
|
||||||
|
double t = (double)i / m_displayPoints;
|
||||||
|
QPointF p = dataToWidget(t, 0);
|
||||||
|
painter.setPen(minorPen);
|
||||||
|
painter.drawLine(QPointF(p.x(), area.top()), QPointF(p.x(), area.bottom()));
|
||||||
|
|
||||||
|
painter.setPen(m_textColor);
|
||||||
|
QFont gridFont = font();
|
||||||
|
gridFont.setPointSize(8);
|
||||||
|
painter.setFont(gridFont);
|
||||||
|
int sampleIdx = startIdx + i;
|
||||||
|
painter.drawText(QRectF(p.x() - 30, area.bottom() + 3, 60, 15),
|
||||||
|
Qt::AlignCenter, QString::number(sampleIdx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::drawCurves(QPainter &painter)
|
||||||
|
{
|
||||||
|
QRectF area = plotArea();
|
||||||
|
if (area.width() <= 0 || area.height() <= 0) return;
|
||||||
|
if (m_totalPoints == 0) return;
|
||||||
|
|
||||||
|
int startIdx = qMax(0, m_totalPoints - m_displayPoints);
|
||||||
|
int visibleCount = m_totalPoints - startIdx;
|
||||||
|
if (visibleCount < 2) return;
|
||||||
|
|
||||||
|
for (int ch = 0; ch < 16; ++ch) {
|
||||||
|
if (!m_channelVisible[ch]) continue;
|
||||||
|
const auto &channel = m_data[ch];
|
||||||
|
if (channel.size() <= startIdx) continue;
|
||||||
|
|
||||||
|
QPen pen(m_colors[ch], 1.2);
|
||||||
|
painter.setPen(pen);
|
||||||
|
|
||||||
|
QPainterPath path;
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
int endIdx = channel.size();
|
||||||
|
for (int i = startIdx; i < endIdx; ++i) {
|
||||||
|
double y = channel[i];
|
||||||
|
double t = (double)(i - startIdx) / (visibleCount - 1);
|
||||||
|
QPointF pt = dataToWidget(t, y);
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
path.moveTo(pt);
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
path.lineTo(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.drawPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::drawAxes(QPainter &painter)
|
||||||
|
{
|
||||||
|
QRectF area = plotArea();
|
||||||
|
|
||||||
|
// Y轴
|
||||||
|
painter.setPen(QPen(m_axisColor, 1.5));
|
||||||
|
painter.drawLine(area.topLeft(), area.bottomLeft());
|
||||||
|
|
||||||
|
// X轴
|
||||||
|
painter.drawLine(area.bottomLeft(), area.bottomRight());
|
||||||
|
|
||||||
|
// Y轴标签
|
||||||
|
painter.setPen(m_textColor);
|
||||||
|
QFont labelFont = font();
|
||||||
|
labelFont.setPointSize(9);
|
||||||
|
painter.setFont(labelFont);
|
||||||
|
|
||||||
|
painter.save();
|
||||||
|
painter.translate(12, area.center().y());
|
||||||
|
painter.rotate(-90);
|
||||||
|
painter.drawText(QRectF(-50, -10, 100, 20), Qt::AlignCenter, "ADC Value");
|
||||||
|
painter.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::drawLegend(QPainter &painter)
|
||||||
|
{
|
||||||
|
QRectF area = plotArea();
|
||||||
|
int legendX = area.right() + 10;
|
||||||
|
int legendY = area.top() + 5;
|
||||||
|
int itemHeight = 18;
|
||||||
|
|
||||||
|
painter.setPen(m_textColor);
|
||||||
|
QFont legendFont = font();
|
||||||
|
legendFont.setPointSize(8);
|
||||||
|
painter.setFont(legendFont);
|
||||||
|
|
||||||
|
for (int ch = 0; ch < 16; ++ch) {
|
||||||
|
int y = legendY + ch * itemHeight;
|
||||||
|
|
||||||
|
// 颜色方块
|
||||||
|
QColor color = m_channelVisible[ch] ? m_colors[ch] : QColor("#444444");
|
||||||
|
painter.fillRect(QRect(legendX, y + 2, 12, 12), color);
|
||||||
|
|
||||||
|
// 通道名
|
||||||
|
QString label = QString("ch%1").arg(ch);
|
||||||
|
painter.setPen(m_channelVisible[ch] ? m_textColor : QColor("#666666"));
|
||||||
|
painter.drawText(QRect(legendX + 16, y, 50, itemHeight),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ Mouse Events ================
|
||||||
|
|
||||||
|
void PlotWidget::wheelEvent(QWheelEvent *event)
|
||||||
|
{
|
||||||
|
QRectF area = plotArea();
|
||||||
|
if (!area.contains(event->position())) return;
|
||||||
|
|
||||||
|
double zoomFactor = 1.15;
|
||||||
|
if (event->angleDelta().y() < 0) {
|
||||||
|
zoomFactor = 1.0 / zoomFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
double centerY = m_yMin + (m_yMax - m_yMin) / 2.0;
|
||||||
|
double halfRange = (m_yMax - m_yMin) / 2.0 * zoomFactor;
|
||||||
|
|
||||||
|
m_yMin = centerY - halfRange;
|
||||||
|
m_yMax = centerY + halfRange;
|
||||||
|
m_autoYRange = false;
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->button() == Qt::RightButton) {
|
||||||
|
m_dragging = true;
|
||||||
|
m_dragStart = event->pos(); // mousePressEvent still uses pos()
|
||||||
|
m_dragYMinStart = m_yMin;
|
||||||
|
m_dragYMaxStart = m_yMax;
|
||||||
|
setCursor(Qt::ClosedHandCursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::mouseMoveEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (m_dragging) {
|
||||||
|
QRectF area = plotArea();
|
||||||
|
double dy = (event->pos().y() - m_dragStart.y()) / area.height() * (m_yMax - m_yMin);
|
||||||
|
m_yMin = m_dragYMinStart - dy;
|
||||||
|
m_yMax = m_dragYMaxStart - dy;
|
||||||
|
m_autoYRange = false;
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->button() == Qt::RightButton) {
|
||||||
|
m_dragging = false;
|
||||||
|
setCursor(Qt::ArrowCursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::mouseDoubleClickEvent(QMouseEvent *)
|
||||||
|
{
|
||||||
|
// 双击恢复自动范围
|
||||||
|
m_autoYRange = true;
|
||||||
|
m_autoScroll = true;
|
||||||
|
autoRangeY();
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::resizeEvent(QResizeEvent *)
|
||||||
|
{
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
89
plotwidget.h
Normal file
89
plotwidget.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#ifndef PLOTWIDGET_H
|
||||||
|
#define PLOTWIDGET_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
|
||||||
|
class PlotWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PlotWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void addDataPoint(const QVector<double> &values);
|
||||||
|
void setAllData(const QVector<QVector<double>> &channels);
|
||||||
|
void clear();
|
||||||
|
void setChannelVisible(int channel, bool visible);
|
||||||
|
bool isChannelVisible(int channel) const;
|
||||||
|
void setDisplayPointCount(int count);
|
||||||
|
int displayPointCount() const;
|
||||||
|
void setYRange(double min, double max);
|
||||||
|
void autoRangeY();
|
||||||
|
void setTitle(const QString &title);
|
||||||
|
int totalPoints() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
void wheelEvent(QWheelEvent *event) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||||
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawBackground(QPainter &painter);
|
||||||
|
void drawGrid(QPainter &painter);
|
||||||
|
void drawCurves(QPainter &painter);
|
||||||
|
void drawAxes(QPainter &painter);
|
||||||
|
void drawLegend(QPainter &painter);
|
||||||
|
|
||||||
|
QRectF plotArea() const;
|
||||||
|
QPointF dataToWidget(double x, double y) const;
|
||||||
|
|
||||||
|
static QList<QColor> defaultColors();
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
QVector<QVector<double>> m_data; // 16个通道
|
||||||
|
QVector<bool> m_channelVisible;
|
||||||
|
int m_totalPoints;
|
||||||
|
|
||||||
|
// 视图参数
|
||||||
|
int m_displayPoints;
|
||||||
|
double m_yMin, m_yMax;
|
||||||
|
bool m_autoYRange;
|
||||||
|
bool m_autoScroll;
|
||||||
|
|
||||||
|
// 鼠标交互
|
||||||
|
bool m_dragging;
|
||||||
|
QPoint m_dragStart;
|
||||||
|
double m_dragYMinStart, m_dragYMaxStart;
|
||||||
|
|
||||||
|
// 边距
|
||||||
|
int m_marginLeft;
|
||||||
|
int m_marginRight;
|
||||||
|
int m_marginTop;
|
||||||
|
int m_marginBottom;
|
||||||
|
|
||||||
|
// 颜色
|
||||||
|
QList<QColor> m_colors;
|
||||||
|
QColor m_bgColor;
|
||||||
|
QColor m_gridColor;
|
||||||
|
QColor m_gridColorMinor;
|
||||||
|
QColor m_textColor;
|
||||||
|
QColor m_axisColor;
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
QString m_title;
|
||||||
|
|
||||||
|
// 刷新定时器
|
||||||
|
QTimer *m_refreshTimer;
|
||||||
|
bool m_dirty;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PLOTWIDGET_H
|
||||||
52
udpreceiver.cpp
Normal file
52
udpreceiver.cpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include "udpreceiver.h"
|
||||||
|
#include <QNetworkInterface>
|
||||||
|
|
||||||
|
UdpReceiver::UdpReceiver(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_socket(new QUdpSocket(this))
|
||||||
|
, m_port(0)
|
||||||
|
{
|
||||||
|
connect(m_socket, &QUdpSocket::readyRead, this, &UdpReceiver::onReadyRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
UdpReceiver::~UdpReceiver()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UdpReceiver::start(quint16 port)
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
m_port = port;
|
||||||
|
|
||||||
|
if (!m_socket->bind(QHostAddress::AnyIPv4, port, QUdpSocket::ShareAddress)) {
|
||||||
|
emit errorOccurred(QString("无法绑定端口 %1: %2").arg(port).arg(m_socket->errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置足够大的缓冲区
|
||||||
|
m_socket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, 8 * 1024 * 1024);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UdpReceiver::stop()
|
||||||
|
{
|
||||||
|
m_socket->close();
|
||||||
|
m_port = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UdpReceiver::isRunning() const
|
||||||
|
{
|
||||||
|
return m_socket->state() == QAbstractSocket::BoundState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UdpReceiver::onReadyRead()
|
||||||
|
{
|
||||||
|
while (m_socket->hasPendingDatagrams()) {
|
||||||
|
QByteArray datagram;
|
||||||
|
datagram.resize(m_socket->pendingDatagramSize());
|
||||||
|
m_socket->readDatagram(datagram.data(), datagram.size());
|
||||||
|
emit dataReceived(datagram);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
udpreceiver.h
Normal file
32
udpreceiver.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef UDPRECEIVER_H
|
||||||
|
#define UDPRECEIVER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QUdpSocket>
|
||||||
|
#include <QHostAddress>
|
||||||
|
|
||||||
|
class UdpReceiver : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit UdpReceiver(QObject *parent = nullptr);
|
||||||
|
~UdpReceiver();
|
||||||
|
|
||||||
|
bool start(quint16 port);
|
||||||
|
void stop();
|
||||||
|
bool isRunning() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void dataReceived(const QByteArray &data);
|
||||||
|
void errorOccurred(const QString &error);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onReadyRead();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QUdpSocket *m_socket;
|
||||||
|
quint16 m_port;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // UDPRECEIVER_H
|
||||||
Reference in New Issue
Block a user