#include "DataCollectionWidget.h" #include #include #include #include #include #include #include #include #include #include #include DataCollectionWidget::DataCollectionWidget(QWidget *parent) : QWidget(parent) { setupTabDataCollection(); } DataCollectionWidget::~DataCollectionWidget() { } void DataCollectionWidget::onSetCurrentTabIndex(int index) { this->currentTabIndex = index; if (currentTabIndex == 3) { this->show(); } else { this->hide(); } } // 设置数据采集标签页 void DataCollectionWidget::setupTabDataCollection() { // 修改这里:从 dataMonitoringContent 改为 dataCollectionContent QHBoxLayout *mainLayout = new QHBoxLayout(this); // 创建水平布局 // 使用QSplitter进行左右栏的动态分割 QSplitter *horizontalSplitter = new QSplitter(Qt::Horizontal); // 创建一个水平方向的QSplitter // 左栏布局 setupTabDataCollectionLeftPanel(horizontalSplitter); // 右栏布局 setupTabDataCollectionRightPanel(horizontalSplitter); // 设置QSplitter的初始分割比例为1:4,即左栏占五分之一,右栏占五分之四 horizontalSplitter->setStretchFactor(0, 1); // 左侧面板的拉伸因子为1 horizontalSplitter->setStretchFactor(1, 4); // 右侧面板的拉伸因子为4 // 将QSplitter添加到主布局中 mainLayout->addWidget(horizontalSplitter); } void DataCollectionWidget::setupTabDataCollectionLeftPanel(QSplitter *horizontalSplitter) { // 创建垂直布局 QVBoxLayout *leftLayout = new QVBoxLayout(); // 创建树形控件 QTreeWidget *treeWidget = new QTreeWidget(); treeWidget->setHeaderLabels({"数据采集项", "操作"}); treeWidget->setColumnCount(2); // 设置列宽模式和固定宽度 treeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); // 第一列自动伸展 treeWidget->header()->setSectionResizeMode(1, QHeaderView::Fixed); // 第二列固定宽度 treeWidget->setColumnWidth(1, 100); // 设置第二列宽度为100像素 treeWidget->header()->setStretchLastSection(false); // 禁止最后一列自动伸展 // 添加树形控件到布局 leftLayout->addWidget(treeWidget); // 创建水平布局来放置按钮 QHBoxLayout *buttonLayout1 = new QHBoxLayout(); // 创建添加数据和清空数据按钮 QPushButton *addDataButton = new QPushButton("添加数据"); QPushButton *addDataProfileButton = new QPushButton("添加采集模板"); QPushButton *clearDataButton = new QPushButton("清空数据"); // 连接添加数据按钮的点击信号 connect(addDataButton, &QPushButton::clicked, this, [=]() { // 创建输入对话框 bool ok; QString dataName = QInputDialog::getText(this, "添加数据接口", "请输入数据接口名称:", QLineEdit::Normal, "", &ok); // 如果用户点击确定且输入不为空 if (ok && !dataName.isEmpty()) { // 创建新的树节点 QTreeWidgetItem *item = new QTreeWidgetItem(treeWidget); item->setText(0, dataName); // 设置第一列为数据名称 // 创建删除按钮 QPushButton *deleteButton = new QPushButton("删除"); treeWidget->setItemWidget(item, 1, deleteButton); // 设置第二列为删除按钮 // 连接删除按钮的点击信号 connect(deleteButton, &QPushButton::clicked, this, [=]() { // 获取按钮所在的item的索引 int index = treeWidget->indexOfTopLevelItem(item); if (index != -1) { // 删除该项 delete treeWidget->takeTopLevelItem(index); } }); } }); // 连接清空数据按钮的点击信号 connect(clearDataButton, &QPushButton::clicked, this, [=]() { // 弹出确认对话框 QMessageBox::StandardButton reply; reply = QMessageBox::question(this, "确认清空", "确定要清空所有数据吗?", QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { treeWidget->clear(); // 清空树形控件 } }); // 将按钮添加到水平布局中 buttonLayout1->addWidget(addDataButton); buttonLayout1->addWidget(addDataProfileButton); buttonLayout1->addWidget(clearDataButton); // 将水平按钮布局添加到左侧主布局中 leftLayout->addLayout(buttonLayout1); // 创建按钮布局 QVBoxLayout *buttonLayout = new QVBoxLayout(); // 创建上部分网格布局 QGridLayout *gridLayout = new QGridLayout(); // 第一列 - Labels QLabel *freqLabel = new QLabel("采集频率"); QLabel *durationLabel = new QLabel("采集时长"); QLabel *pathLabel = new QLabel("保存路径"); gridLayout->addWidget(freqLabel, 0, 0); gridLayout->addWidget(durationLabel, 1, 0); gridLayout->addWidget(pathLabel, 2, 0); // 第二列 - LineEdits QLineEdit *freqEdit = new QLineEdit(); freqEdit->setPlaceholderText("请输入采集频率"); QLineEdit *durationEdit = new QLineEdit(); durationEdit->setPlaceholderText("请输入采集时长"); QLineEdit *pathEdit = new QLineEdit(); pathEdit->setPlaceholderText("请输入保存路径"); gridLayout->addWidget(freqEdit, 0, 1); gridLayout->addWidget(durationEdit, 1, 1); gridLayout->addWidget(pathEdit, 2, 1); // 设置输入验证器,只允许输入数字和小数点 QDoubleValidator *doubleValidator = new QDoubleValidator(0, 999999, 6, this); doubleValidator->setNotation(QDoubleValidator::StandardNotation); freqEdit->setValidator(doubleValidator); durationEdit->setValidator(doubleValidator); // 第三列 - 单位标签和选择按钮 QLabel *hzLabel = new QLabel("Hz"); QLabel *sLabel = new QLabel("s"); QPushButton *selectPathButton = new QPushButton("..."); gridLayout->addWidget(hzLabel, 0, 2); gridLayout->addWidget(sLabel, 1, 2); gridLayout->addWidget(selectPathButton, 2, 2); // 连接选择路径按钮的点击信号 connect(selectPathButton, &QPushButton::clicked, this, [=]() { QString dir = QFileDialog::getExistingDirectory(this, "选择保存路径", QDir::currentPath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!dir.isEmpty()) { pathEdit->setText(dir); } }); connect(addDataProfileButton, &QPushButton::clicked, this, [=]() { // 弹出文件选择对话框 QString fileName = QFileDialog::getOpenFileName(this, "选择采集模板", QDir::currentPath(), "采集模板文件 (*.xml)"); if (!fileName.isEmpty()) { QFile file(fileName); file.open(QIODevice::ReadOnly | QIODevice::Text); QDomDocument doc; if (!doc.setContent(&file)) { QMessageBox::warning(this, "错误", "无法解析XML文件"); return; } QDomElement root = doc.documentElement(); if (root.tagName() != "DataCollection") { QMessageBox::warning(this, "错误", "无效的XML格式"); return; } // 读取采集数据名称 QDomElement collectionData = root.firstChildElement("CollectionData"); if (!collectionData.isNull()) { QDomElement dataName = collectionData.firstChildElement("DataName"); while (!dataName.isNull()) { QString name = dataName.text(); // 创建新的树节点 QTreeWidgetItem *item = new QTreeWidgetItem(treeWidget); item->setText(0, name); // 创建删除按钮 QPushButton *deleteButton = new QPushButton("删除"); treeWidget->setItemWidget(item, 1, deleteButton); // 连接删除按钮的点击信号 connect(deleteButton, &QPushButton::clicked, this, [=]() { int index = treeWidget->indexOfTopLevelItem(item); if (index != -1) { delete treeWidget->takeTopLevelItem(index); } }); dataName = dataName.nextSiblingElement("DataName"); } } // 读取并设置采集频率 QDomElement freqElement = root.firstChildElement("DataFrequency"); if (!freqElement.isNull()) { freqEdit->setText(freqElement.text()); } // 读取并设置采集时长 QDomElement timeElement = root.firstChildElement("DataTime"); if (!timeElement.isNull()) { durationEdit->setText(timeElement.text()); } // 读取并设置保存路径 QDomElement pathElement = root.firstChildElement("DataPath"); if (!pathElement.isNull()) { pathEdit->setText(pathElement.text()); } file.close(); } }); // 添加网格布局到按钮布局 buttonLayout->addLayout(gridLayout); // 创建水平布局用于底部按钮 QHBoxLayout *bottomButtonLayout = new QHBoxLayout(); // 创建开始和终止按钮 QPushButton *startButton = new QPushButton("开始采集"); startButton->setObjectName("startDataCollectionButton"); QPushButton *stopButton = new QPushButton("终止采集"); bottomButtonLayout->addWidget(startButton); bottomButtonLayout->addWidget(stopButton); connect(startButton, &QPushButton::clicked, this, [=]() { if (treeWidget->topLevelItemCount() == 0) { QMessageBox::warning(this, "提示", "请先添加数据接口"); return; } QStringList collectionDataNames; for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) { collectionDataNames << treeWidget->topLevelItem(i)->text(0); } if (freqEdit->text().isEmpty() || durationEdit->text().isEmpty() || pathEdit->text().isEmpty()) { QMessageBox::warning(this, "提示", "请填写采集频率、采集时长和保存路径"); return; } double collectionFrequency = freqEdit->text().toDouble(); double collectionTime = durationEdit->text().toDouble(); QString collectionPath = pathEdit->text(); DataCollectionThread *dataCollectionThread = new DataCollectionThread( collectionDataNames, collectionFrequency, collectionTime, collectionPath); connect(dataCollectionThread, &DataCollectionThread::DataCollectionStatus, this, &DataCollectionWidget::onDataCollectionStatus); connect(dataCollectionThread, &DataCollectionThread::UpdateDataCollectionTime, this, &DataCollectionWidget::onUpdateDataCollectionTime); connect(this, &DataCollectionWidget::stopDataCollection, dataCollectionThread, &DataCollectionThread::onStopDataCollection); dataCollectionThread->start(); startButton->setEnabled(false); }); connect(stopButton, &QPushButton::clicked, this, [=]() { emit stopDataCollection(); }); // 添加底部按钮布局到主按钮布局 buttonLayout->addLayout(bottomButtonLayout); // 将按钮布局添加到主布局 leftLayout->addLayout(buttonLayout); // 创建一个QWidget作为左栏的容器,并设置其布局为leftLayout QWidget *leftPanel = new QWidget(); leftPanel->setLayout(leftLayout); horizontalSplitter->addWidget(leftPanel); } void DataCollectionWidget::onUpdateDataCollectionTime(const unsigned int newTime) { QPushButton *startButton = findChild("startDataCollectionButton"); if (startButton) { startButton->setText(QString::number(newTime)); } } void DataCollectionWidget::onDataCollectionStatus(const bool &status, const QString &filePath) { QPushButton *startButton = findChild("startDataCollectionButton"); if (startButton) { if (status) { QMessageBox::StandardButton reply = QMessageBox::question( this, "提示", QString("数据采集完成,文件已保存至:%1\n是否立即打开文件?").arg(filePath), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { openDataCollectionFile(filePath); } } else { QMessageBox::information(this, "提示", "数据采集异常终止"); } startButton->setText("开始采集"); startButton->setEnabled(true); } } void DataCollectionWidget::setupTabDataCollectionRightPanel(QSplitter *horizontalSplitter) { QWidget *rightPanel = new QWidget(); horizontalSplitter->addWidget(rightPanel); // 创建右侧面板的主布局 QHBoxLayout *rightLayout = new QHBoxLayout(rightPanel); // 创建左侧的TabWidget QTabWidget *leftTabWidget = new QTabWidget(); leftTabWidget->setTabPosition(QTabWidget::North); leftTabWidget->setMovable(true); leftTabWidget->setTabsClosable(true); // 设置所有标签页可关闭 dataPlotIndex = 0; // 创建默认的第一个标签页 QWidget *firstTab = new QWidget(); QVBoxLayout *firstTabLayout = new QVBoxLayout(firstTab); XNCustomPlot *plot = new XNCustomPlot(); plot->setObjectName(QString("dataPlot%1").arg(dataPlotIndex)); setupPlotWidget(plot); firstTabLayout->addWidget(plot); plot->addGraph(); plot->graph(0)->setPen(QPen(Qt::green, 2)); plot->addGraph(); plot->graph(1)->setPen(QPen(Qt::red, 2)); firstTabLayout->addWidget(plot); int firstTabIndex = leftTabWidget->addTab(firstTab, QString("绘图%1").arg(dataPlotIndex)); dataPlotIndex++; // 添加"+"标签页 QWidget *addTab = new QWidget(); int addTabIndex = leftTabWidget->addTab(addTab, "+"); // 移除"+"标签页的关闭按钮 leftTabWidget->tabBar()->setTabButton(addTabIndex, QTabBar::RightSide, nullptr); // 连接标签切换信号 connect(leftTabWidget, &QTabWidget::currentChanged, [=](int index) { if (index == leftTabWidget->count() - 1) { if (leftTabWidget->property("ignoreChange").toBool()) { leftTabWidget->setCurrentIndex(0); return; } // 如果点击了"+"标签(且不是因为删除标签页导致的切换) int newIndex = leftTabWidget->count() - 1; // 创建新的标签页 QWidget *newTab = new QWidget(); QVBoxLayout *newTabLayout = new QVBoxLayout(newTab); XNCustomPlot *newPlot = new XNCustomPlot(); newPlot->setObjectName(QString("dataPlot%1").arg(dataPlotIndex)); setupPlotWidget(newPlot); newPlot->addGraph(); newPlot->graph(0)->setPen(QPen(Qt::green, 2)); newPlot->addGraph(); newPlot->graph(1)->setPen(QPen(Qt::red, 2)); newTabLayout->addWidget(newPlot); // 在"+"标签之前插入新标签 leftTabWidget->insertTab(newIndex, newTab, QString("绘图%1").arg(dataPlotIndex)); dataPlotIndex++; // 选中新创建的标签 leftTabWidget->setCurrentIndex(newIndex); } XNCustomPlot *currentPlot = findChild(QString("dataPlot%1").arg(index)); if (currentPlot) { currentPlot->setOpenGl(false); currentPlot->replot(); currentPlot->setOpenGl(true); currentPlot->replot(); } }); // 连接标签关闭信号 connect(leftTabWidget, &QTabWidget::tabCloseRequested, [=](int index) { // 设置标记,表示即将因为删除标签页而改变当前标签 leftTabWidget->setProperty("ignoreChange", true); // 删除指定的标签页 leftTabWidget->removeTab(index); // 如果删除后只剩下"+"标签页,则创建一个新的标签页 if (leftTabWidget->count() == 1) { QWidget *newTab = new QWidget(); QVBoxLayout *newTabLayout = new QVBoxLayout(newTab); XNCustomPlot *newPlot = new XNCustomPlot(); newPlot->setObjectName("dataPlot1"); setupPlotWidget(newPlot); newPlot->addGraph(); newPlot->graph(0)->setPen(QPen(Qt::green, 2)); newPlot->addGraph(); newPlot->graph(1)->setPen(QPen(Qt::red, 2)); newTabLayout->addWidget(newPlot); // 在"+"标签之前插入新标签 leftTabWidget->insertTab(0, newTab, QString("绘图%1").arg(dataPlotIndex)); leftTabWidget->setCurrentIndex(0); dataPlotIndex++; } // 清除标记 leftTabWidget->setProperty("ignoreChange", false); }); // 创建右侧的垂直布局,用于放置两个TreeWidget QVBoxLayout *rightTreeLayout = new QVBoxLayout(); // 创建上部TreeWidget QTreeWidget *collectionDataTreeWidget = new QTreeWidget(); collectionDataTreeWidget->setHeaderLabel("采集数据"); collectionDataTreeWidget->setObjectName("collectionDataTreeWidget"); connect(collectionDataTreeWidget, &QTreeWidget::itemClicked, this, [=](QTreeWidgetItem *item, int column) { QString varName = item->text(0); if (collectionData.contains(varName)) { int currentIndex = leftTabWidget->currentIndex(); QCustomPlot *currentPlot = findChild(QString("dataPlot%1").arg(currentIndex)); if (currentPlot) { currentPlot->graph(0)->setData(collectionData["Time"], collectionData[varName]); currentPlot->graph(0)->setPen(QPen(Qt::green, 2)); currentPlot->graph(0)->setName(varName); // 自动调整坐标轴范围以适应数据 currentPlot->xAxis->rescale(); currentPlot->yAxis->rescale(); // 设置图例 currentPlot->legend->setVisible(true); currentPlot->legend->setFont(QFont("Helvetica", 10)); currentPlot->replot(); } } }); // 创建水平布局用于放置按钮 QHBoxLayout *collectionDataButtonLayout = new QHBoxLayout(); // 创建打开和关闭按钮 QPushButton *openCollectionDataButton = new QPushButton("打开"); QPushButton *closeCollectionDataButton = new QPushButton("关闭"); connect(openCollectionDataButton, &QPushButton::clicked, this, [=]() { QString fileName = QFileDialog::getOpenFileName( this, "选择采集数据文件", QDir::currentPath(), "采集数据文件 (*.csv)"); openDataCollectionFile(fileName); }); connect(closeCollectionDataButton, &QPushButton::clicked, this, [=]() { collectionData.clear(); collectionDataTreeWidget->clear(); }); // 将按钮添加到水平布局 collectionDataButtonLayout->addWidget(openCollectionDataButton); collectionDataButtonLayout->addWidget(closeCollectionDataButton); // 创建一个容器widget来容纳按钮布局 QWidget *collectionDataButtonContainer = new QWidget(); collectionDataButtonContainer->setLayout(collectionDataButtonLayout); // 创建下部TreeWidget QTreeWidget *referenceDataTreeWidget = new QTreeWidget(); referenceDataTreeWidget->setHeaderLabel("参考数据"); connect(referenceDataTreeWidget, &QTreeWidget::itemClicked, this, [=](QTreeWidgetItem *item, int column) { QString varName = item->text(0); if (referenceData.contains(varName)) { int currentIndex = leftTabWidget->currentIndex(); QCustomPlot *currentPlot = findChild(QString("dataPlot%1").arg(currentIndex)); if (currentPlot) { currentPlot->graph(1)->setData(referenceData["Time"], referenceData[varName]); currentPlot->graph(1)->setPen(QPen(Qt::red, 2)); currentPlot->graph(1)->setName(varName); // 自动调整坐标轴范围以适应数据 currentPlot->xAxis->rescale(); currentPlot->yAxis->rescale(); // 设置图例 currentPlot->legend->setVisible(true); currentPlot->legend->setFont(QFont("Helvetica", 10)); currentPlot->replot(); } } }); QHBoxLayout *referenceDataButtonLayout = new QHBoxLayout(); QPushButton *openReferenceDataButton = new QPushButton("打开"); QPushButton *closeReferenceDataButton = new QPushButton("关闭"); connect(openReferenceDataButton, &QPushButton::clicked, this, [=]() { QString fileName = QFileDialog::getOpenFileName( this, "选择参考数据文件", QDir::currentPath(), "参考数据文件 (*.csv)"); if (!fileName.isEmpty()) { QFile file(fileName); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in(&file); // 读取第一行作为标题 QString headerLine = in.readLine(); QStringList headers = headerLine.split(","); referenceData["Time"] = QVector(); // 第一次读取时,从headers中获取变量名并初始化vectors if (referenceData.isEmpty()) { for (int i = 1; i < headers.size(); i++) { referenceData[headers[i]] = QVector(); } } // 清空现有数据 referenceData.clear(); while (!in.atEnd()) { QString line = in.readLine(); QStringList fields = line.split(","); referenceData["Time"].append(fields[0].toDouble()); for (int i = 1; i < fields.size() && i < headers.size(); i++) { bool ok; double value = fields[i].toDouble(&ok); if (ok) { referenceData[headers[i]].append(value); } } } file.close(); // 清空现有树形结构 referenceDataTreeWidget->clear(); // 将第一列数据名称添加到树形结构中 for (auto it = referenceData.begin(); it != referenceData.end(); ++it) { if (it.key() != "" && it.key() != "Time") { QTreeWidgetItem *item = new QTreeWidgetItem(referenceDataTreeWidget); item->setText(0, it.key()); } } } else { QMessageBox::warning(this, "错误", "无法打开文件!"); } } }); connect(closeReferenceDataButton, &QPushButton::clicked, this, [=]() { referenceData.clear(); referenceDataTreeWidget->clear(); }); referenceDataButtonLayout->addWidget(openReferenceDataButton); referenceDataButtonLayout->addWidget(closeReferenceDataButton); QWidget *referenceDataButtonContainer = new QWidget(); referenceDataButtonContainer->setLayout(referenceDataButtonLayout); // 将两个TreeWidget添加到右侧垂直布局 rightTreeLayout->addWidget(collectionDataTreeWidget); // 将按钮容器添加到右侧垂直布局 rightTreeLayout->addWidget(collectionDataButtonContainer); // 将参考数据TreeWidget添加到右侧垂直布局 rightTreeLayout->addWidget(referenceDataTreeWidget); // 将按钮容器添加到右侧垂直布局 rightTreeLayout->addWidget(referenceDataButtonContainer); // 创建一个容器widget来容纳右侧的树形视图 QWidget *rightTreeContainer = new QWidget(); rightTreeContainer->setLayout(rightTreeLayout); // 创建一个水平分割器来分隔TabWidget和右侧树形视图 QSplitter *rightSplitter = new QSplitter(Qt::Horizontal); rightSplitter->addWidget(leftTabWidget); rightSplitter->addWidget(rightTreeContainer); rightSplitter->setStretchFactor(0, 4); // TabWidget占据更多空间 rightSplitter->setStretchFactor(1, 1); // 将分割器添加到右侧面板的主布局 rightLayout->addWidget(rightSplitter); rightLayout->setContentsMargins(0, 0, 0, 0); } void DataCollectionWidget::openDataCollectionFile(const QString &filePath) { if (!filePath.isEmpty()) { QFile file(filePath); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in(&file); // 读取第一行作为标题 QString headerLine = in.readLine(); QStringList headers = headerLine.split(","); collectionData["Time"] = QVector(); // 第一次读取时,从headers中获取变量名并初始化vectors if (collectionData.isEmpty()) { for (int i = 1; i < headers.size(); i++) { collectionData[headers[i]] = QVector(); } } // 清空现有数据 collectionData.clear(); while (!in.atEnd()) { QString line = in.readLine(); QStringList fields = line.split(","); collectionData["Time"].append(fields[0].toDouble()); // 将每一列的数据添加到对应变量名的vector中 for (int i = 1; i < fields.size() && i < headers.size(); i++) { bool ok; double value = fields[i].toDouble(&ok); if (ok) { collectionData[headers[i]].append(value); } } } file.close(); QTreeWidget *collectionDataTreeWidget = findChild("collectionDataTreeWidget"); if (collectionDataTreeWidget) { // 清空现有树形结构 collectionDataTreeWidget->clear(); // 将第一列数据名称添加到树形结构中 for (auto it = collectionData.begin(); it != collectionData.end(); ++it) { if (it.key() != "" && it.key() != "Time") { QTreeWidgetItem *item = new QTreeWidgetItem(collectionDataTreeWidget); item->setText(0, it.key()); } } } } else { QMessageBox::warning(this, "错误", "无法打开文件!"); } } } void DataCollectionWidget::setupPlotWidget(XNCustomPlot *customPlot) { customPlot->setBackground(QBrush(QColor("#19232D"))); customPlot->xAxis->setLabelColor(Qt::white); customPlot->yAxis->setLabelColor(Qt::white); customPlot->xAxis->setLabel("时间(s)"); customPlot->yAxis->setLabel("值"); customPlot->xAxis->setTickLabelRotation(60); customPlot->xAxis->setTickLabelColor(Qt::white); customPlot->yAxis->setTickLabelColor(Qt::white); customPlot->xAxis->grid()->setVisible(true); customPlot->yAxis->grid()->setVisible(true); customPlot->setNotAntialiasedElements(QCP::aeAll); // 关闭抗锯齿 customPlot->setPlottingHints(QCP::phFastPolylines); // 使用快速绘制 customPlot->setNoAntialiasingOnDrag(true); // 拖动时关闭抗锯齿 // 优化图表性能 customPlot->setBufferDevicePixelRatio(1.0); // 设置缓冲区像素比 customPlot->setOpenGl(true); // 启用OpenGL加速(如果支持) // 确保图表可以接收鼠标事件 customPlot->setMouseTracking(true); customPlot->setAttribute(Qt::WA_Hover); customPlot->setFocusPolicy(Qt::StrongFocus); // 启用所有交互选项 customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectItems | QCP::iSelectAxes | QCP::iSelectLegend); // 创建新的顶层层级 customPlot->addLayer("topmost", customPlot->layer("main"), QCustomPlot::limAbove); QCPLayer *topmostLayer = customPlot->layer("topmost"); topmostLayer->setMode(QCPLayer::lmBuffered); // 将所有现有图层移到新层级下方 for (int i = 0; i < customPlot->layerCount(); ++i) { QCPLayer *layer = customPlot->layer(i); if (layer != topmostLayer) { layer->setMode(QCPLayer::lmBuffered); customPlot->moveLayer(layer, topmostLayer, QCustomPlot::limBelow); } } // 创建坐标标签 m_coordLabel = new QCPItemText(customPlot); m_coordLabel->setPositionAlignment(Qt::AlignTop | Qt::AlignLeft); m_coordLabel->setTextAlignment(Qt::AlignLeft); m_coordLabel->setVisible(false); m_coordLabel->setPadding(QMargins(8, 8, 8, 8)); // 增加内边距 m_coordLabel->setBrush(QBrush(QColor(40, 40, 40, 255))); m_coordLabel->setPen(QPen(QColor(80, 80, 80, 255))); m_coordLabel->setColor(Qt::white); QFont labelFont = QFont("Arial", 16); labelFont.setBold(true); m_coordLabel->setFont(labelFont); m_coordLabel->setLayer("topmost"); // 使用最顶层 // 创建点追踪器 m_tracer = new QCPItemTracer(customPlot); m_tracer->setStyle(QCPItemTracer::tsCircle); m_tracer->setPen(QPen(Qt::red)); m_tracer->setBrush(Qt::red); m_tracer->setSize(8); m_tracer->setVisible(false); m_tracer->setLayer("topmost"); // 使用最顶层 // 连接鼠标事件 connect(customPlot, &XNCustomPlot::mousePressed, this, [=](QMouseEvent *event) { if (event->button() == Qt::LeftButton && customPlot->graph(0)) { double x = customPlot->xAxis->pixelToCoord(event->pos().x()); double y = customPlot->yAxis->pixelToCoord(event->pos().y()); if (!customPlot->graph(0)->data()->isEmpty()) { double minDistance = std::numeric_limits::max(); QCPGraphDataContainer::const_iterator closestPoint; bool foundPoint = false; for (auto it = customPlot->graph(0)->data()->begin(); it != customPlot->graph(0)->data()->end(); ++it) { double distance = qSqrt(qPow(it->key - x, 2) + qPow(it->value - y, 2)); if (distance < minDistance) { minDistance = distance; closestPoint = it; foundPoint = true; } } if (foundPoint) { m_tracer->setGraph(customPlot->graph(0)); m_tracer->setGraphKey(closestPoint->key); m_tracer->setVisible(true); m_coordLabel->position->setCoords(closestPoint->key, closestPoint->value); m_coordLabel->setText(QString("时间: %1\n数值: %2") .arg(closestPoint->key) .arg(closestPoint->value)); m_coordLabel->setVisible(true); customPlot->replot(); } } } }); // 确保在初始化时重新绘制一次 customPlot->replot(); }