#include "EnergyCountPeakFitView.h" #include "CustomQwtPlot.h" #include "PlotRectItem.h" #include "csv.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "PlotRectItem.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MeasureAnalysisProjectModel.h" #include "BusyIndicator.h" QJsonObject PeakFitHistoryItem::toJson() const { QJsonObject obj; obj["timestamp"] = timestamp.toString(Qt::ISODate); QJsonArray curvesJson; for (const auto& curve : curveList) { QJsonObject curveObj; curveObj["amplitude"] = curve.amplitude; curveObj["sigma"] = curve.sigma; curveObj["sigmoidH"] = curve.sigmoidH; curveObj["sigmoidW"] = curve.sigmoidW; curveObj["baseline"] = curve.baseline; curveObj["center"] = curve.center; curveObj["xMin"] = curve.xMin; curveObj["xMax"] = curve.xMax; curveObj["fwhm"] = curve.fwhm; curveObj["area"] = curve.area; //保存框选区域 if (!curve.selectionRect.isNull()) { QJsonObject rectObj; rectObj["left"] = curve.selectionRect.left(); rectObj["top"] = curve.selectionRect.top(); rectObj["right"] = curve.selectionRect.right(); rectObj["bottom"] = curve.selectionRect.bottom(); curveObj["selectionRect"] = rectObj; } curvesJson.append(curveObj); } obj["curves"] = curvesJson; return obj; } PeakFitHistoryItem PeakFitHistoryItem::fromJson(const QJsonObject& obj) { PeakFitHistoryItem item; item.timestamp = QDateTime::fromString(obj["timestamp"].toString(), Qt::ISODate); QJsonArray curvesJson = obj["curves"].toArray(); for (const auto& value : curvesJson) { FitCurveData curve; QJsonObject curveObj = value.toObject(); curve.amplitude = curveObj["amplitude"].toDouble(); curve.sigma = curveObj["sigma"].toDouble(); curve.sigmoidH = curveObj["sigmoidH"].toDouble(); curve.sigmoidW = curveObj["sigmoidW"].toDouble(); curve.baseline = curveObj["baseline"].toDouble(); curve.center = curveObj["center"].toDouble(); curve.xMin = curveObj["xMin"].toDouble(); curve.xMax = curveObj["xMax"].toDouble(); curve.fwhm = curveObj["fwhm"].toDouble(0.0); curve.area = curveObj["area"].toDouble(0.0); //加载框选区域 if (curveObj.contains("selectionRect")) { QJsonObject rectObj = curveObj["selectionRect"].toObject(); curve.selectionRect = QRectF( rectObj["left"].toDouble(), rectObj["top"].toDouble(), rectObj["right"].toDouble() - rectObj["left"].toDouble(), rectObj["bottom"].toDouble() - rectObj["top"].toDouble() ); } item.curveList.append(curve); } return item; } EnergyCountPeakFitView::EnergyCountPeakFitView(QWidget* parent) : MeasureAnalysisView { parent } { this->setViewType(PlotFrame); QHBoxLayout* layout = new QHBoxLayout(this); this->_plot = new CustomQwtPlot(this); layout->addWidget(this->_plot); setupPlot(); this->_menu = new QMenu(this); setupMenu(); } EnergyCountPeakFitView::~EnergyCountPeakFitView() { LOG_DEBUG(QStringLiteral(u"%1析构.").arg(this->GetViewName())); delete _rubberBand; clearAllSelectionRects(); clearFitCurves(); } void EnergyCountPeakFitView::InitViewWorkspace(const QString& project_name) { Q_UNUSED(project_name); if (project_name.isEmpty()) return; auto project_model = ProjectList::Instance()->GetProjectModel(project_name); if (!project_model) return; QDir project_dir(project_model->GetProjectDir()); _workspace = project_dir.filePath(this->GetViewName()); if (!QDir(_workspace).exists()) project_dir.mkpath(_workspace); _historyFilePath = QDir(_workspace).filePath("peak_fit_history.json"); loadHistoryFromFile(); } void EnergyCountPeakFitView::SetAnalyzeDataFilename(const QMap& data_files_set) { if (!data_files_set.isEmpty()) { const QString& data_name = data_files_set.firstKey(); const QString& data_filename = data_files_set.first().toString(); if (QFileInfo(data_filename).exists()) { loadDataFromFile(data_name, data_filename); } } } void EnergyCountPeakFitView::setupPlot() { _plot->setCanvasBackground(Qt::white); QwtPlotCanvas* canvas = qobject_cast(_plot->canvas()); canvas->setFrameStyle(QFrame::NoFrame); QFont font = this->font(); font.setBold(false); QwtText energy_label = QStringLiteral(u"能量(KeV)"); energy_label.setFont(font); QwtText count_label = QStringLiteral(u"计数"); count_label.setFont(font); _plot->setAxisTitle(QwtPlot::xBottom, energy_label); _plot->setAxisTitle(QwtPlot::yLeft, count_label); // set axis auto scale _plot->setAxisAutoScale(QwtPlot::xBottom, true); _plot->setAxisAutoScale(QwtPlot::yLeft, true); _plot->enableAxis(QwtPlot::xBottom); _plot->enableAxis(QwtPlot::yLeft); // 设置QWT图例 QwtLegend* legend = new QwtLegend(); legend->setDefaultItemMode(QwtLegendData::ReadOnly); _plot->insertLegend(legend, QwtPlot::RightLegend); _plot->SetAxisDragScale(QwtPlot::xBottom, true); QwtPlotCurve *dummyFit = new QwtPlotCurve(QStringLiteral(u"拟合数据")); dummyFit->setPen(QPen(Qt::blue, 2)); dummyFit->setSamples(QVector(), QVector()); // 无数据 dummyFit->setVisible(false); // 不在画布上绘制 dummyFit->setItemAttribute(QwtPlotItem::Legend, true); // 保证图例显示(默认 true) dummyFit->attach(_plot); // 本底数据(虚拟,图例用) QwtPlotCurve *dummyBg = new QwtPlotCurve(QStringLiteral(u"本底数据")); dummyBg->setPen(QPen(Qt::yellow, 2)); dummyBg->setSamples(QVector(), QVector()); dummyBg->setVisible(false); dummyBg->attach(_plot); _data_selector = new CustomQwtPlotXaxisSelector(_plot->canvas()); _data_selector->setEnabled(false); // 启用鼠标追踪,并安装事件过滤器 _plot->canvas()->setMouseTracking(true); _plot->canvas()->installEventFilter(this); } void EnergyCountPeakFitView::setupMenu() { this->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &EnergyCountPeakFitView::customContextMenuRequested, [this](const QPoint& pos) { this->_menu->exec(this->mapToGlobal(pos)); }); QAction* action_plot_reset = this->_menu->addAction(QStringLiteral(u"还原")); action_plot_reset->setObjectName("plot_reset"); connect(action_plot_reset, &QAction::triggered, [this]() { this->_plot->ResetPlot(); }); this->_menu->addSeparator(); //QAction* action_set_curve_show = this->_menu->addAction(QStringLiteral(u"选择曲线")); //action_set_curve_show->setObjectName("curve_show_setting"); //connect(action_set_curve_show, &QAction::triggered, this, &EnergyCountPeakFitView::onActionCurveShowSetting); QAction* action_plot_config = this->_menu->addAction(QStringLiteral(u"图表配置")); action_plot_config->setObjectName("plot_config"); connect(action_plot_config, &QAction::triggered, this, &EnergyCountPeakFitView::onActionPlotConfigure); this->_menu->addSeparator(); QAction* action_refit_rect = this->_menu->addAction(QStringLiteral(u"重新拟合当前区域")); connect(action_refit_rect, &QAction::triggered, this, &EnergyCountPeakFitView::onActionRefitCurrentRect); //QAction* action_save_fit = this->_menu->addAction(QStringLiteral(u"保存当前拟合结果")); //connect(action_save_fit, &QAction::triggered, this, &EnergyCountPeakFitView::onActionSaveCurrentFit); QAction* action_show_history = this->_menu->addAction(QStringLiteral(u"峰拟合结果")); connect(action_show_history, &QAction::triggered, this, &EnergyCountPeakFitView::onActionShowFitHistory); //删除当前悬停的框选区域 QAction* action_delete_hovered_rect = this->_menu->addAction(QStringLiteral(u"删除当前框选区域")); connect(action_delete_hovered_rect, &QAction::triggered, this, &EnergyCountPeakFitView::onActionDeleteHoveredRect); //QAction* action_hint = this->_menu->addAction(QStringLiteral(u"框选提示:按住 Ctrl 键并拖动左键")); //action_hint->setEnabled(false); } void EnergyCountPeakFitView::loadDataFromFile(const QString& data_name, const QString& filename) { //加载新数据前清空旧数据 _currentFitRecords.clear(); clearFitCurves(); clearAllSelectionRects(); _plot->replot(); std::string address_str = QString(QStringLiteral(u"能量(KeV)")).toStdString(); std::string count_str = QString(QStringLiteral(u"计数")).toStdString(); io::CSVReader< 2, io::trim_chars<' ', '\t'>, io::double_quote_escape<',', '"'>, io::throw_on_overflow, io::empty_line_comment> reader(QStrToSysPath(filename)); reader.read_header(io::ignore_extra_column, address_str, count_str); double energy; unsigned long long energy_count; QVector x, y; while (reader.read_row(energy, energy_count)) { x.push_back(energy); y.push_back(energy_count); } // 绘制曲线 QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"原始数据")); curve->setPen(QPen(Qt::gray, 2)); // 原始数据统一灰色 curve->setSamples(x, y); _plot->AddCurve(curve,false); } void EnergyCountPeakFitView::onActionCurveShowSetting() { if (!_curve_show_setting_dlg) { _curve_show_setting_dlg = new QDialog(this, Qt::Dialog | Qt::WindowCloseButtonHint); _curve_show_setting_dlg->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); _curve_show_setting_dlg->setSizeGripEnabled(false); _curve_show_setting_dlg->setWindowTitle(QString(QStringLiteral(u"选择%1曲线显示").arg(this->_plot->title().text()))); _curve_show_setting_dlg->setWindowModality(Qt::WindowModal); _curve_show_setting_dlg->setModal(false); QVBoxLayout* layout = new QVBoxLayout(_curve_show_setting_dlg); QStringList curve_name_list; for (QwtPlotCurve* curve : this->_plot->GetCurveList()) { curve_name_list.append(curve->title().text()); } // 自动计算多列排布 int num_columns = std::sqrt(this->_plot->GetCurveList().size()); if (num_columns == 0) num_columns = 1; QVBoxLayout* checkbox_layout = new QVBoxLayout(); QHBoxLayout* checkbox_column_layout = new QHBoxLayout(); QMap curve_checkbox_map; auto addCurveToLayout = [&](const QString& curve_name) { QwtPlotCurve* curve = this->_plot->GetCurve(curve_name); QCheckBox* check_box = new QCheckBox(curve->title().text()); check_box->setChecked(curve->isVisible()); checkbox_column_layout->addWidget(check_box); connect(check_box, &QCheckBox::stateChanged, [curve](int state) { curve->setVisible(state == Qt::Checked); curve->plot()->replot(); }); curve_checkbox_map[curve] = check_box; if (checkbox_column_layout->count() >= num_columns) { checkbox_layout->addLayout(checkbox_column_layout); checkbox_column_layout = new QHBoxLayout(); } }; for (const QString& ch_name : curve_name_list) { addCurveToLayout(ch_name); } if (checkbox_column_layout->count() < num_columns) { checkbox_column_layout->addStretch(); } checkbox_layout->addLayout(checkbox_column_layout); // 全选和反选 auto curveCheckboxUpdate = [this, curve_checkbox_map]() { for (QwtPlotCurve* curve : this->_plot->GetCurveList()) { curve_checkbox_map[curve]->setChecked(curve->isVisible()); } }; QHBoxLayout* button_layout = new QHBoxLayout(); QPushButton* btn_all_select = new QPushButton(QString(QStringLiteral(u"全选"))); connect(btn_all_select, &QPushButton::clicked, [this, curveCheckboxUpdate]() { for (QwtPlotCurve* curve : this->_plot->GetCurveList()) { curve->setVisible(true); } curveCheckboxUpdate(); this->_plot->replot(); }); button_layout->addWidget(btn_all_select); QPushButton* btn_reserve_select = new QPushButton(QString(QStringLiteral(u"反选"))); connect(btn_reserve_select, &QPushButton::clicked, [this, curveCheckboxUpdate]() { for (QwtPlotCurve* curve : this->_plot->GetCurveList()) { curve->setVisible(!curve->isVisible()); } curveCheckboxUpdate(); this->_plot->replot(); }); button_layout->addWidget(btn_reserve_select); layout->addLayout(button_layout); layout->addLayout(checkbox_layout); } _curve_show_setting_dlg->show(); } void EnergyCountPeakFitView::onActionPlotConfigure() { } void EnergyCountPeakFitView::clearAllSelectionRects() { for (PlotRectItem* item : _selectionRectItems) { if (item) { item->detach(); delete item; } } _selectionRectItems.clear(); _plot->replot(); } bool EnergyCountPeakFitView::eventFilter(QObject* watched, QEvent* event) { if (watched == _plot->canvas()) { QMouseEvent* me = static_cast(event); // 按下 Ctrl+左键 开始框选 if (event->type() == QEvent::MouseButtonPress && me->button() == Qt::LeftButton /*&&*/ /*(me->modifiers() & Qt::ControlModifier)*/) { startSelection(me->pos()); return true; } // 移动时更新橡皮筋 else if (event->type() == QEvent::MouseMove && _isSelecting) { updateSelection(me->pos()); return true; } // 释放左键完成框选 else if (event->type() == QEvent::MouseButtonRelease && me->button() == Qt::LeftButton && _isSelecting) { finishSelection(); fadeSelectionRectBorders(); return true; } //鼠标移动时检测悬停 else if (event->type() == QEvent::MouseMove) { updateHoverState(me->pos()); return true; } } return MeasureAnalysisView::eventFilter(watched, event); } void EnergyCountPeakFitView::startSelection(const QPoint& pos) { _isSelecting = true; _selectionStart = pos; if (!_rubberBand) { _rubberBand = new QRubberBand(QRubberBand::Rectangle, _plot->canvas()); } int canvasHeight = _plot->canvas()->height(); // 初始矩形:宽度为0,高度为画布全高 _rubberBand->setGeometry(QRect(_selectionStart.x(), 0, 0, canvasHeight)); _rubberBand->show(); } void EnergyCountPeakFitView::updateSelection(const QPoint& pos) { if (!_rubberBand) return; int canvasHeight = _plot->canvas()->height(); int x1 = _selectionStart.x(); int x2 = pos.x(); int left = qMin(x1, x2); int width = qAbs(x1 - x2); _rubberBand->setGeometry(QRect(left, 0, width, canvasHeight)); } void EnergyCountPeakFitView::finishSelection() { if (_rubberBand) { QRect finalRect = _rubberBand->geometry(); _rubberBand->hide(); if (finalRect.width() > 2) { const QwtScaleMap xMap = _plot->canvasMap(QwtPlot::xBottom); double xMin = xMap.invTransform(finalRect.left()); double xMax = xMap.invTransform(finalRect.right()); if (xMin > xMax) std::swap(xMin, xMax); double yMin = _plot->axisScaleDiv(QwtPlot::yLeft).lowerBound(); double yMax = _plot->axisScaleDiv(QwtPlot::yLeft).upperBound(); QRectF plotRect(xMin, yMin, xMax - xMin, yMax - yMin); // addSelectionRect(plotRect); // 获取曲线数据 QList curves = _plot->GetCurveList(); if (!curves.isEmpty()) { QwtPlotCurve* originalCurve = curves.first(); QVector origX, origY; for (size_t i = 0; i < originalCurve->dataSize(); ++i) { origX.push_back(originalCurve->sample(i).x()); origY.push_back(originalCurve->sample(i).y()); } QVector roiX, roiY; for (int i = 0; i < origX.size(); ++i) { if (origX[i] >= xMin && origX[i] <= xMax) { roiX.push_back(origX[i]); roiY.push_back(origY[i]); } } if (roiX.size() >= 3) { QStringList algorithms; algorithms << QStringLiteral(u"y=A*exp(-pow(x-P,2)/(2*pow(delt,2))) + H/(1+exp((x-P)/W)) + C"); bool ok = false; QString selected = QInputDialog::getItem(this, QStringLiteral(u"选择拟合算法"), QStringLiteral(u"请选择用于当前框选区域的峰拟合算法:"), algorithms, 0, false, &ok); if (ok && selected == QStringLiteral(u"y=A*exp(-pow(x-P,2)/(2*pow(delt,2))) + H/(1+exp((x-P)/W)) + C")) { arma::vec armaRoiX(roiX.size()), armaRoiY(roiY.size()); for (int i = 0; i < roiX.size(); ++i) { armaRoiX(i) = roiX[i]; armaRoiY(i) = roiY[i]; } // 3. 执行拟合(曲线自动添加到主图) QList results = performPeakFitting(roiX, roiY/*, &userP0*/); if (results.isEmpty()) { qDebug()<< QStringLiteral(u"拟合结果:") << QStringLiteral(u"未检测到有效峰或拟合失败。"); } else { addSelectionRect(plotRect); fadeSelectionRectBorders(); // 框选边框变为虚线(与原逻辑一致) if (!_selectionRectItems.isEmpty() && !results.isEmpty()) { PlotRectItem* lastRect = _selectionRectItems.last(); const PeakFitResult& r = results.first(); // 传入:峰位、FWHM、面积、本底 lastRect->setPeakData(r.center, r.fwhm, r.area, r.baseline); _plot->replot(); // 刷新显示 } saveCurrentFitToHistory(); } } } else { qDebug() << QStringLiteral(u"框选区域内数据点不足3个,无法拟合。"); } } } } _isSelecting = false; } void EnergyCountPeakFitView::addSelectionRect(const QRectF& plotRect) { PlotRectItem* rectItem = new PlotRectItem("Selection"); rectItem->setAxes(QwtPlot::xBottom, QwtPlot::yLeft); rectItem->setRect(plotRect); // 设置填充为透明(无填充) rectItem->setBrush(QBrush(Qt::transparent)); // 或 Qt::NoBrush // 设置边框颜色为红色,线宽2 rectItem->setPen(QPen(Qt::red, 2)); rectItem->setSelectionType("current"); rectItem->setSelectionIndex(_selectionRectItems.size()); rectItem->attach(_plot); _selectionRectItems.append(rectItem); _plot->replot(); } void EnergyCountPeakFitView::fadeSelectionRectBorders() { for (PlotRectItem* item : _selectionRectItems) { if (item) { QPen fadedPen(Qt::red, 1, Qt::DashLine); // 可根据原样式调整 item->setPen(fadedPen); } } _plot->replot(); } QList EnergyCountPeakFitView::performPeakFitting(const QVector &x, const QVector &y) { QList results; if (x.size() != y.size() || x.size() < 3) { qDebug() << QStringLiteral(u"数据长度不足"); return results; } // 转换为 Armadillo 向量 arma::vec armaX(x.size()), armaY(y.size()); for (int i = 0; i < x.size(); ++i) { armaX(i) = x[i]; armaY(i) = y[i]; } arma::vec p0 = EstimatePhotonPeakModelInitialParams(armaX, armaY); arma::vec p_fit = NolinearLeastSquaresCurveFit::Lsqcurvefit(PhotonPeakModel, armaX, armaY, p0); double sigma = p_fit(1); double fwhm = sigma * 2.355; FitParams defaultParams; defaultParams.A = p_fit(0); defaultParams.delt = p_fit(1); defaultParams.H = p_fit(2); defaultParams.W = p_fit(3); defaultParams.C = p_fit(4); defaultParams.P = p_fit(5); PeakFitParamsDialog paramDlg(defaultParams, this); if (paramDlg.exec() != QDialog::Accepted) return results; // 用户取消 FitParams userParams = paramDlg.getParams(); AdaptiveSimpsonIntegrate::FitFunction fitFunc; fitFunc.A = userParams.A ;//p_fit(0); fitFunc.delt= userParams.delt;//p_fit(1); fitFunc.H = userParams.H ;//p_fit(2); fitFunc.W = userParams.W ;//p_fit(3); fitFunc.C = userParams.C ;//p_fit(4); fitFunc.P = userParams.P ;//p_fit(5); double lower = fitFunc.P - 3.0 * fitFunc.delt; double upper = fitFunc.P + 3.0 * fitFunc.delt; double area = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper); PeakFitResult result; result.center = p_fit(5); result.amplitude = p_fit(0); result.sigma = p_fit(1); result.fwhm = fwhm; result.area = area; result.baseline = p_fit(4); result.sigmoidH = p_fit(2); result.sigmoidW = p_fit(3); QList curves = createFitCurve(result, armaX); for (QwtPlotCurve* c : curves) { _fitCurves.append(c); } results.append(result); _plot->replot(); _lastFitResult = result; _lastFitParams = p_fit; _hasLastFit = true; CurrentFitRecord record; record.result = result; record.params = p_fit; record.xMin = armaX.min(); // 记录X范围 record.xMax = armaX.max(); _currentFitRecords.append(record); // 加入当前多曲线列表 return results; } QwtPlotCurve* EnergyCountPeakFitView::createFitCurve(const PeakFitResult& result, double xMin, double xMax, const QString& name) { const int numPoints = 200; QVector xs, ys,ysTw; double step = (xMax - xMin) / (numPoints - 1); arma::vec p(6); p(0) = result.amplitude; p(1) = result.sigma; p(2) = result.sigmoidH; p(3) = result.sigmoidW; p(4) = result.baseline; p(5) = result.center; for (int i = 0; i < numPoints; ++i) { double x = xMin + i * step; xs.append(x); double y = PhotonPeakModel(x, p); ys.append(y); } QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"拟合数据")); curve->setPen(QPen(Qt::blue, 2)); curve->setSamples(xs, ys); curve->attach(_plot); return curve; } QList EnergyCountPeakFitView::createFitCurve(const PeakFitResult &result, arma::vec xVec) { QList curveList; // 用于返回 QVector xs, ys ,ysTw; arma::vec p(6); p(0) = result.amplitude; p(1) = result.sigma; p(2) = result.sigmoidH; p(3) = result.sigmoidW; p(4) = result.baseline; p(5) = result.center; for (int i = 0; i < xVec.size(); ++i) { double x = xVec.at(i); xs.append(x); double y = PhotonPeakModel(x, p); ys.append(y); } arma::vec yTw = PhotonPeakModelTuowei(xVec,p); for (int i = 0;isetPen(QPen(Qt::blue, 2)); curve->setSamples(xs, ys); curve->setItemAttribute(QwtPlotItem::Legend, false); curve->attach(_plot); curveList.append(curve); // 加入列表 QwtPlotCurve* curveTw = new QwtPlotCurve(QStringLiteral(u"本底数据")); curveTw->setPen(QPen(Qt::yellow, 2)); curveTw->setSamples(xs, ysTw); curveTw->setItemAttribute(QwtPlotItem::Legend, false); curveTw->attach(_plot); curveList.append(curveTw); // 加入列表 return curveList; } void EnergyCountPeakFitView::clearFitCurves() { for (QwtPlotCurve* curve : _fitCurves) { curve->detach(); delete curve; } _fitCurves.clear(); _currentFitRecords.clear(); _plot->replot(); } void EnergyCountPeakFitView::loadHistoryFromFile() { if (!QFileInfo::exists(_historyFilePath)) return; QFile file(_historyFilePath); if (!file.open(QIODevice::ReadOnly)) return; QByteArray data = file.readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); if (!doc.isArray()) return; _fitHistoryList.clear(); QJsonArray arr = doc.array(); for (const auto& val : arr) { if (val.isObject()) _fitHistoryList.append(PeakFitHistoryItem::fromJson(val.toObject())); } } void EnergyCountPeakFitView::saveHistoryToFile() { QJsonArray arr; for (const auto& item : _fitHistoryList) { arr.append(item.toJson()); } QJsonDocument doc(arr); QFile file(_historyFilePath); if (file.open(QIODevice::WriteOnly)) { file.write(doc.toJson()); } } void EnergyCountPeakFitView::saveCurrentFitToHistory() { if (_currentFitRecords.isEmpty()) return; //只取最新的一次拟合结果(列表中的最后一条) const auto& latestRecord = _currentFitRecords.last(); int latestRectIndex = _currentFitRecords.size() - 1; // 对应的框选区域索引 PeakFitHistoryItem item; item.timestamp = QDateTime::currentDateTime(); // 仅构建当前这一次的曲线数据 FitCurveData curve; curve.amplitude = latestRecord.params(0); curve.sigma = latestRecord.params(1); curve.sigmoidH = latestRecord.params(2); curve.sigmoidW = latestRecord.params(3); curve.baseline = latestRecord.params(4); curve.center = latestRecord.params(5); curve.xMin = latestRecord.xMin; curve.xMax = latestRecord.xMax; curve.fwhm = latestRecord.result.fwhm; curve.area = latestRecord.result.area; // 关联对应的框选区域(最新的那个) if (latestRectIndex < _selectionRectItems.size()) { curve.selectionRect = _selectionRectItems[latestRectIndex]->rect(); } item.curveList.append(curve); // 仅添加这一条曲线 _fitHistoryList.append(item); saveHistoryToFile(); } void EnergyCountPeakFitView::displayFitFromHistory(const PeakFitHistoryItem& item) { // 遍历历史中的所有曲线,逐一重建 for (const auto& curve : item.curveList) { // 1. 生成X轴数据 const int numPoints = 200; QVector xs, ys, ysTw; double step = (curve.xMax - curve.xMin) / (numPoints - 1); // 2. 构建参数向量 arma::vec p(6); p(0) = curve.amplitude; p(1) = curve.sigma; p(2) = curve.sigmoidH; p(3) = curve.sigmoidW; p(4) = curve.baseline; p(5) = curve.center; // 3. 生成拟合曲线数据 for (int i = 0; i < numPoints; ++i) { double x = curve.xMin + i * step; xs.append(x); ys.append(PhotonPeakModel(x, p)); } // 4. 创建并添加拟合曲线 QwtPlotCurve* fitCurve = new QwtPlotCurve(QStringLiteral(u"历史拟合数据")); fitCurve->setPen(QPen(Qt::blue, 2)); fitCurve->setSamples(xs, ys); fitCurve->setItemAttribute(QwtPlotItem::Legend, false); fitCurve->attach(_plot); _fitCurves.append(fitCurve); // 5. 生成本底曲线数据 arma::vec xVec(numPoints); for (int i = 0; i < numPoints; ++i) xVec(i) = xs[i]; arma::vec yTw = PhotonPeakModelTuowei(xVec, p); for (int i = 0; i < yTw.size(); ++i) ysTw.append(yTw(i)); // 6. 创建并添加本底曲线 QwtPlotCurve* bgCurve = new QwtPlotCurve(QStringLiteral(u"历史本底数据")); bgCurve->setPen(QPen(Qt::yellow, 2, Qt::DashLine)); bgCurve->setSamples(xs, ysTw); bgCurve->setItemAttribute(QwtPlotItem::Legend, false); bgCurve->attach(_plot); _fitCurves.append(bgCurve); // 7. 恢复并绘制框选区域 if (!curve.selectionRect.isNull()) { PlotRectItem* histRect = new PlotRectItem("HistorySelection"); //显式绑定坐标轴,确保坐标映射正确 histRect->setAxes(QwtPlot::xBottom, QwtPlot::yLeft); histRect->setRect(curve.selectionRect); histRect->setBrush(QBrush(Qt::transparent)); // 历史框选区域用红色虚线区分 histRect->setPen(QPen(Qt::red, 2, Qt::DashLine)); histRect->setPeakData(curve.center, curve.fwhm, curve.area, curve.baseline); histRect->attach(_plot); _selectionRectItems.append(histRect); } } _plot->replot(); } void EnergyCountPeakFitView::onActionSaveCurrentFit() { saveCurrentFitToHistory(); } void EnergyCountPeakFitView::onActionShowFitHistory() { bool hasCurveData = false; for (const auto& item : _fitHistoryList) { if (!item.curveList.isEmpty()) { hasCurveData = true; break; } } if (!hasCurveData) { QMessageBox::information(this, QStringLiteral(u"峰拟合结果"), QStringLiteral(u"暂无历史拟合数据,请先进行拟合并保存。")); return; } QDialog dlg(this); dlg.setWindowTitle(QStringLiteral(u"峰拟合结果历史")); dlg.setMinimumSize(1000, 500); QVBoxLayout* layout = new QVBoxLayout(&dlg); QStandardItemModel* model = new QStandardItemModel(&dlg); QStringList headers; headers << "" << "振幅(A)" << "高斯宽度(delt)" << "Sigmoid幅度(H)" << "Sigmoid宽度(W)" << "基线(C)" << "峰中心(P)"; model->setHorizontalHeaderLabels(headers); QVector> rowToDataIndices; model->blockSignals(true); for (int historyIdx = 0; historyIdx < _fitHistoryList.size(); ++historyIdx) { const auto& historyItem = _fitHistoryList[historyIdx]; for (int curveIdx = 0; curveIdx < historyItem.curveList.size(); ++curveIdx) { const auto& curve = historyItem.curveList[curveIdx]; QList rowItems; QStandardItem* checkItem = new QStandardItem(); checkItem->setCheckable(true); checkItem->setEditable(false); bool isCurrentlyDisplayed = false; for (const auto& ref : _displayedHistoryCurves) { if (ref.historyIndex == historyIdx && ref.curveIndex == curveIdx) { isCurrentlyDisplayed = true; break; } } checkItem->setCheckState(isCurrentlyDisplayed ? Qt::Checked : Qt::Unchecked); rowItems.append(checkItem); // 2. 振幅 (A) QStandardItem* itemA = new QStandardItem(QString::number(curve.amplitude, 'f', 4)); itemA->setEditable(false); rowItems.append(itemA); // 3. Sigma (delt) QStandardItem* itemDelt = new QStandardItem(QString::number(curve.sigma, 'f', 4)); itemDelt->setEditable(false); rowItems.append(itemDelt); // 4. SigmoidH (H) QStandardItem* itemH = new QStandardItem(QString::number(curve.sigmoidH, 'f', 4)); itemH->setEditable(false); rowItems.append(itemH); // 5. SigmoidW (W) QStandardItem* itemW = new QStandardItem(QString::number(curve.sigmoidW, 'f', 4)); itemW->setEditable(false); rowItems.append(itemW); // 6. 本底 (C) QStandardItem* itemC = new QStandardItem(QString::number(curve.baseline, 'f', 4)); itemC->setEditable(false); rowItems.append(itemC); // 7. 峰位 (P) QStandardItem* itemP = new QStandardItem(QString::number(curve.center, 'f', 4)); itemP->setEditable(false); rowItems.append(itemP); model->appendRow(rowItems); rowToDataIndices.append(qMakePair(historyIdx, curveIdx)); } } model->blockSignals(false); QHBoxLayout* topBtnLayout = new QHBoxLayout(); QPushButton* btnSelectAll = new QPushButton(QStringLiteral(u"全选"), &dlg); QPushButton* btnInvertSelection = new QPushButton(QStringLiteral(u"反选"), &dlg); topBtnLayout->addWidget(btnSelectAll); topBtnLayout->addWidget(btnInvertSelection); topBtnLayout->addStretch(); layout->addLayout(topBtnLayout); QTableView* tableView = new QTableView(&dlg); tableView->setModel(model); tableView->setSelectionBehavior(QAbstractItemView::SelectRows); tableView->setSelectionMode(QAbstractItemView::SingleSelection); tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); tableView->horizontalHeader()->setStretchLastSection(true); tableView->verticalHeader()->setVisible(false); tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); layout->addWidget(tableView); QPushButton* btnDelete = new QPushButton(QStringLiteral(u"删除选中记录"), &dlg); QPushButton* btnClose = new QPushButton(QStringLiteral(u"关闭"), &dlg); QHBoxLayout* bottomBtnLayout = new QHBoxLayout(); bottomBtnLayout->addStretch(); bottomBtnLayout->addWidget(btnDelete); bottomBtnLayout->addWidget(btnClose); layout->addLayout(bottomBtnLayout); // --- 全选按钮 --- connect(btnSelectAll, &QPushButton::clicked, [&]() { model->blockSignals(true); for (int row = 0; row < model->rowCount(); ++row) { model->item(row, 0)->setCheckState(Qt::Checked); } model->blockSignals(false); tableView->viewport()->update(); QList selectedCurves; QList selectedRefs; for (int row = 0; row < model->rowCount(); ++row) { if (model->item(row, 0)->checkState() == Qt::Checked) { QPair indices = rowToDataIndices[row]; selectedCurves.append(_fitHistoryList[indices.first].curveList[indices.second]); DisplayedCurveRef ref; ref.historyIndex = indices.first; ref.curveIndex = indices.second; selectedRefs.append(ref); } } displaySelectedCurves(selectedCurves, selectedRefs); }); // --- 反选按钮 --- connect(btnInvertSelection, &QPushButton::clicked, [&]() { model->blockSignals(true); for (int row = 0; row < model->rowCount(); ++row) { QStandardItem* item = model->item(row, 0); item->setCheckState(item->checkState() == Qt::Checked ? Qt::Unchecked : Qt::Checked); } model->blockSignals(false); tableView->viewport()->update(); QList selectedCurves; QList selectedRefs; for (int row = 0; row < model->rowCount(); ++row) { if (model->item(row, 0)->checkState() == Qt::Checked) { QPair indices = rowToDataIndices[row]; selectedCurves.append(_fitHistoryList[indices.first].curveList[indices.second]); DisplayedCurveRef ref; ref.historyIndex = indices.first; ref.curveIndex = indices.second; selectedRefs.append(ref); } } displaySelectedCurves(selectedCurves, selectedRefs); }); // --- 复选框状态改变 --- connect(model, &QStandardItemModel::itemChanged, [&](QStandardItem* /*item*/) { QList selectedCurves; QList selectedRefs; for (int row = 0; row < model->rowCount(); ++row) { if (model->item(row, 0)->checkState() == Qt::Checked) { QPair indices = rowToDataIndices[row]; selectedCurves.append(_fitHistoryList[indices.first].curveList[indices.second]); DisplayedCurveRef ref; ref.historyIndex = indices.first; ref.curveIndex = indices.second; selectedRefs.append(ref); } } displaySelectedCurves(selectedCurves, selectedRefs); }); // --- 删除选中记录按钮(核心精准删除逻辑) --- connect(btnDelete, &QPushButton::clicked, [&]() { // 1. 收集要删除的历史索引 QSet historyIndicesToDelete; for (int row = 0; row < model->rowCount(); ++row) { if (model->item(row, 0)->checkState() == Qt::Checked) { QPair indices = rowToDataIndices[row]; historyIndicesToDelete.insert(indices.first); } } if (historyIndicesToDelete.isEmpty()) { QMessageBox::warning(&dlg, QStringLiteral(u"提示"), QStringLiteral(u"请先勾选要删除的记录。")); return; } // 2. 确认删除 QString msg = QStringLiteral(u"确定要删除选中的 %1 条历史记录吗?").arg(historyIndicesToDelete.size()); QMessageBox::StandardButton reply = QMessageBox::question(&dlg, QStringLiteral(u"确认删除"), msg, QMessageBox::Yes | QMessageBox::No); if (reply != QMessageBox::Yes) return; // 3. 先从界面上精准删除显示(从后往前删) QList sortedDisplayIndices; for (int i = 0; i < _displayedHistoryCurves.size(); ++i) { if (historyIndicesToDelete.contains(_displayedHistoryCurves[i].historyIndex)) { sortedDisplayIndices.append(i); } } std::sort(sortedDisplayIndices.begin(), sortedDisplayIndices.end(), std::greater()); for (int dispIdx : sortedDisplayIndices) { const auto& ref = _displayedHistoryCurves[dispIdx]; // 删除曲线(先删本底,再删拟合) if (ref.curveStartIndex + 1 < _fitCurves.size()) { _fitCurves[ref.curveStartIndex + 1]->detach(); delete _fitCurves[ref.curveStartIndex + 1]; _fitCurves.removeAt(ref.curveStartIndex + 1); _fitCurves[ref.curveStartIndex]->detach(); delete _fitCurves[ref.curveStartIndex]; _fitCurves.removeAt(ref.curveStartIndex); } // 删除框选 if (ref.rectIndex < _selectionRectItems.size()) { _selectionRectItems[ref.rectIndex]->detach(); delete _selectionRectItems[ref.rectIndex]; _selectionRectItems.removeAt(ref.rectIndex); } // 移除引用 _displayedHistoryCurves.removeAt(dispIdx); } // 4. 收集剩余显示的数据(用于重建) QList remainingCurves; QList remainingRefs; for (const auto& ref : _displayedHistoryCurves) { remainingCurves.append(_fitHistoryList[ref.historyIndex].curveList[ref.curveIndex]); remainingRefs.append(ref); } // 5. 更新剩余引用的 historyIndex(处理索引偏移) QList sortedHistoryIndices = historyIndicesToDelete.values(); std::sort(sortedHistoryIndices.begin(), sortedHistoryIndices.end(), std::greater()); for (auto& ref : remainingRefs) { int offset = 0; for (int delIdx : sortedHistoryIndices) { if (delIdx < ref.historyIndex) offset++; } ref.historyIndex -= offset; } // 6. 删除历史数据 for (int historyIdx : sortedHistoryIndices) { _fitHistoryList.removeAt(historyIdx); } // 7. 重建界面显示(确保索引正确) clearFitCurves(); clearAllSelectionRects(); _displayedHistoryCurves.clear(); if (!remainingCurves.isEmpty()) { displaySelectedCurves(remainingCurves, remainingRefs); } else { _plot->replot(); } // 8. 保存并刷新 saveHistoryToFile(); dlg.accept(); onActionShowFitHistory(); }); connect(btnClose, &QPushButton::clicked, &dlg, &QDialog::accept); dlg.exec(); } void EnergyCountPeakFitView::onActionDeleteHoveredRect() { if (!_hoveredRectItem) { QMessageBox::information(this, QStringLiteral(u"提示"), QStringLiteral(u"请先将鼠标移动到要删除的框选区域上(区域边框会变为实线),然后再点击此菜单。")); return; } //使用新接口获取类型和索引 QString rectType = _hoveredRectItem->selectionType(); int rectIndex = _hoveredRectItem->selectionIndex(); if (rectType == "current") { if (rectIndex < 0 || rectIndex >= _currentFitRecords.size()) { QMessageBox::warning(this, QStringLiteral(u"错误"), QStringLiteral(u"框选区域索引无效。")); return; } int curveStartIdx = rectIndex * 2; int historyIndex = _currentFitRecords[rectIndex].historyIndex; // 1. 删除拟合曲线和本底曲线 if (curveStartIdx + 1 < _fitCurves.size()) { _fitCurves[curveStartIdx + 1]->detach(); delete _fitCurves[curveStartIdx + 1]; _fitCurves.removeAt(curveStartIdx + 1); _fitCurves[curveStartIdx]->detach(); delete _fitCurves[curveStartIdx]; _fitCurves.removeAt(curveStartIdx); } // 2. 删除当前拟合记录 _currentFitRecords.removeAt(rectIndex); // 3. 删除框选区域 _hoveredRectItem->detach(); delete _hoveredRectItem; _selectionRectItems.removeAt(rectIndex); _hoveredRectItem = nullptr; // 4. 删除对应的历史记录 if (historyIndex >= 0 && historyIndex < _fitHistoryList.size()) { _fitHistoryList.removeAt(historyIndex); saveHistoryToFile(); // 5. 更新剩余当前记录的历史索引 for (auto& record : _currentFitRecords) { if (record.historyIndex > historyIndex) { record.historyIndex--; } } // 6. 更新历史显示记录的索引 for (auto& ref : _displayedHistoryCurves) { if (ref.historyIndex > historyIndex) { ref.historyIndex--; } if (ref.curveStartIndex > curveStartIdx) { ref.curveStartIndex -= 2; } if (ref.rectIndex > rectIndex) { ref.rectIndex--; } } } // 7. 更新剩余当前框选的索引 for (int i = rectIndex; i < _selectionRectItems.size(); ++i) { PlotRectItem* item = _selectionRectItems[i]; if (item->selectionType() == "current") { item->setSelectionIndex(i); } else if (item->selectionType() == "history") { // 更新历史框选的索引 for (int j = 0; j < _displayedHistoryCurves.size(); ++j) { if (_displayedHistoryCurves[j].rectIndex == i) { item->setSelectionIndex(j); break; } } } } } else if (rectType == "history") { if (rectIndex < 0 || rectIndex >= _displayedHistoryCurves.size()) { QMessageBox::warning(this, QStringLiteral(u"错误"), QStringLiteral(u"历史框选区域索引无效。")); return; } const DisplayedCurveRef& ref = _displayedHistoryCurves[rectIndex]; int historyIndex = ref.historyIndex; int curveStartIdx = ref.curveStartIndex; int rectIdx = ref.rectIndex; // 1. 删除拟合曲线和本底曲线 if (curveStartIdx + 1 < _fitCurves.size()) { _fitCurves[curveStartIdx + 1]->detach(); delete _fitCurves[curveStartIdx + 1]; _fitCurves.removeAt(curveStartIdx + 1); _fitCurves[curveStartIdx]->detach(); delete _fitCurves[curveStartIdx]; _fitCurves.removeAt(curveStartIdx); } // 2. 删除框选区域 _hoveredRectItem->detach(); delete _hoveredRectItem; _selectionRectItems.removeAt(rectIdx); _hoveredRectItem = nullptr; // 3. 从显示引用列表中移除 _displayedHistoryCurves.removeAt(rectIndex); // 4. 删除对应的历史记录 if (historyIndex >= 0 && historyIndex < _fitHistoryList.size()) { _fitHistoryList.removeAt(historyIndex); saveHistoryToFile(); // 5. 更新剩余历史显示记录的索引 for (auto& r : _displayedHistoryCurves) { if (r.historyIndex > historyIndex) { r.historyIndex--; } if (r.curveStartIndex > curveStartIdx) { r.curveStartIndex -= 2; } if (r.rectIndex > rectIdx) { r.rectIndex--; } } // 6. 更新当前记录的历史索引 for (auto& record : _currentFitRecords) { if (record.historyIndex > historyIndex) { record.historyIndex--; } } } // 7. 更新剩余历史框选的索引 for (int i = rectIndex; i < _displayedHistoryCurves.size(); ++i) { const DisplayedCurveRef& r = _displayedHistoryCurves[i]; PlotRectItem* item = _selectionRectItems[r.rectIndex]; item->setSelectionIndex(i); } // 8. 更新当前框选的索引 for (int i = 0; i < _currentFitRecords.size(); ++i) { PlotRectItem* item = _selectionRectItems[i]; if (item->selectionType() == "current" && item->selectionIndex() > rectIdx) { item->setSelectionIndex(item->selectionIndex() - 1); } } } _plot->replot(); } void EnergyCountPeakFitView::onActionRefitCurrentRect() { // 检查是否有悬停的框选区域 if (!_hoveredRectItem) { QMessageBox::information(this, QStringLiteral(u"提示"), QStringLiteral(u"请先将鼠标移动到要重新拟合的框选区域上(区域边框会变为实线),然后再点击此菜单。")); return; } // 获取框选类型 QString rectType = _hoveredRectItem->selectionType(); int rectIndex = _hoveredRectItem->selectionIndex(); if (rectType == "current") { int rectIdxInList = _selectionRectItems.indexOf(_hoveredRectItem); if (rectIdxInList == -1 || rectIdxInList >= _currentFitRecords.size()) { QMessageBox::warning(this, QStringLiteral(u"错误"), QStringLiteral(u"未找到对应区域的拟合记录,无法重新拟合。")); return; } const CurrentFitRecord& originalRecord = _currentFitRecords[rectIdxInList]; arma::vec originalParams = originalRecord.params; FitParams defaultParams; defaultParams.A = originalParams(0); defaultParams.delt = originalParams(1); defaultParams.H = originalParams(2); defaultParams.W = originalParams(3); defaultParams.C = originalParams(4); defaultParams.P = originalParams(5); PeakFitParamsDialog paramDlg(defaultParams, this); if (paramDlg.exec() != QDialog::Accepted) { return; } FitParams userParams = paramDlg.getParams(); arma::vec newParams(6); newParams(0) = userParams.A; newParams(1) = userParams.delt; newParams(2) = userParams.H; newParams(3) = userParams.W; newParams(4) = userParams.C; newParams(5) = userParams.P; double newFwhm = newParams(1) * 2.355; AdaptiveSimpsonIntegrate::FitFunction fitFunc; fitFunc.A = newParams(0); fitFunc.delt = newParams(1); fitFunc.H = newParams(2); fitFunc.W = newParams(3); fitFunc.C = newParams(4); fitFunc.P = newParams(5); double lower = fitFunc.P - 3.0 * fitFunc.delt; double upper = fitFunc.P + 3.0 * fitFunc.delt; double newArea = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper); PeakFitResult newResult; newResult.center = newParams(5); newResult.amplitude = newParams(0); newResult.sigma = newParams(1); newResult.fwhm = newFwhm; newResult.area = newArea; newResult.baseline = newParams(4); newResult.sigmoidH = newParams(2); newResult.sigmoidW = newParams(3); int curveStartIdx = rectIdxInList * 2; if (curveStartIdx + 1 < _fitCurves.size()) { _fitCurves[curveStartIdx]->detach(); _fitCurves[curveStartIdx + 1]->detach(); delete _fitCurves[curveStartIdx]; delete _fitCurves[curveStartIdx + 1]; _fitCurves.removeAt(curveStartIdx + 1); _fitCurves.removeAt(curveStartIdx); } arma::vec xVec; QList curves = _plot->GetCurveList(); if (!curves.isEmpty()) { QwtPlotCurve* originalCurve = curves.first(); QVector origX; for (size_t i = 0; i < originalCurve->dataSize(); ++i) { origX.push_back(originalCurve->sample(i).x()); } QVector roiX; for (int i = 0; i < origX.size(); ++i) { if (origX[i] >= originalRecord.xMin && origX[i] <= originalRecord.xMax) { roiX.push_back(origX[i]); } } xVec.set_size(roiX.size()); for (int i = 0; i < roiX.size(); ++i) { xVec(i) = roiX[i]; } } QList newCurves = createFitCurve(newResult, xVec); for (int i = 0; i < newCurves.size(); ++i) { _fitCurves.insert(curveStartIdx + i, newCurves[i]); } _hoveredRectItem->setPeakData(newResult.center, newResult.fwhm, newResult.area, newResult.baseline); CurrentFitRecord newRecord; newRecord.result = newResult; newRecord.params = newParams; newRecord.xMin = originalRecord.xMin; newRecord.xMax = originalRecord.xMax; newRecord.historyIndex = originalRecord.historyIndex; // 保留历史索引 _currentFitRecords.replace(rectIdxInList, newRecord); _plot->replot(); } else if (rectType == "history") { if (rectIndex < 0 || rectIndex >= _displayedHistoryCurves.size()) { QMessageBox::warning(this, QStringLiteral(u"错误"), QStringLiteral(u"未找到对应历史区域的拟合记录,无法重新拟合。")); return; } // 1. 获取历史数据引用 const DisplayedCurveRef& ref = _displayedHistoryCurves[rectIndex]; int historyIdx = ref.historyIndex; int curveIdxInHistory = ref.curveIndex; int curveStartIdx = ref.curveStartIndex; int rectIdxInList = ref.rectIndex; if (historyIdx < 0 || historyIdx >= _fitHistoryList.size()) return; if (curveIdxInHistory < 0 || curveIdxInHistory >= _fitHistoryList[historyIdx].curveList.size()) return; // 2. 从历史记录中提取原始参数 const FitCurveData& originalCurveData = _fitHistoryList[historyIdx].curveList[curveIdxInHistory]; FitParams defaultParams; defaultParams.A = originalCurveData.amplitude; defaultParams.delt = originalCurveData.sigma; defaultParams.H = originalCurveData.sigmoidH; defaultParams.W = originalCurveData.sigmoidW; defaultParams.C = originalCurveData.baseline; defaultParams.P = originalCurveData.center; // 3. 弹出参数编辑对话框 PeakFitParamsDialog paramDlg(defaultParams, this); if (paramDlg.exec() != QDialog::Accepted) { return; } // 4. 获取用户修改后的参数 FitParams userParams = paramDlg.getParams(); arma::vec newParams(6); newParams(0) = userParams.A; newParams(1) = userParams.delt; newParams(2) = userParams.H; newParams(3) = userParams.W; newParams(4) = userParams.C; newParams(5) = userParams.P; // 5. 计算新的 FWHM 和峰面积 double newFwhm = newParams(1) * 2.355; AdaptiveSimpsonIntegrate::FitFunction fitFunc; fitFunc.A = newParams(0); fitFunc.delt = newParams(1); fitFunc.H = newParams(2); fitFunc.W = newParams(3); fitFunc.C = newParams(4); fitFunc.P = newParams(5); double lower = fitFunc.P - 3.0 * fitFunc.delt; double upper = fitFunc.P + 3.0 * fitFunc.delt; double newArea = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper); // 6. 构建新的结果结构体 PeakFitResult newResult; newResult.center = newParams(5); newResult.amplitude = newParams(0); newResult.sigma = newParams(1); newResult.fwhm = newFwhm; newResult.area = newArea; newResult.baseline = newParams(4); newResult.sigmoidH = newParams(2); newResult.sigmoidW = newParams(3); // 7. 删除旧的拟合曲线和本底曲线 if (curveStartIdx + 1 < _fitCurves.size()) { _fitCurves[curveStartIdx]->detach(); _fitCurves[curveStartIdx + 1]->detach(); delete _fitCurves[curveStartIdx]; delete _fitCurves[curveStartIdx + 1]; _fitCurves.removeAt(curveStartIdx + 1); _fitCurves.removeAt(curveStartIdx); } // 8. 从原始数据中提取该区域的 X 值(用于重新生成曲线) arma::vec xVec; QList curves = _plot->GetCurveList(); if (!curves.isEmpty()) { QwtPlotCurve* originalCurve = curves.first(); QVector origX; for (size_t i = 0; i < originalCurve->dataSize(); ++i) { origX.push_back(originalCurve->sample(i).x()); } QVector roiX; for (int i = 0; i < origX.size(); ++i) { if (origX[i] >= originalCurveData.xMin && origX[i] <= originalCurveData.xMax) { roiX.push_back(origX[i]); } } xVec.set_size(roiX.size()); for (int i = 0; i < roiX.size(); ++i) { xVec(i) = roiX[i]; } } // 9. 生成新的拟合曲线并插入到原位置 QList newCurves = createFitCurve(newResult, xVec); for (int i = 0; i < newCurves.size(); ++i) { _fitCurves.insert(curveStartIdx + i, newCurves[i]); } // 10. 更新框选区域的峰数据标签 _hoveredRectItem->setPeakData(newResult.center, newResult.fwhm, newResult.area, newResult.baseline); // 11. 更新内存中的历史数据 FitCurveData newCurveData = originalCurveData; // 复制旧数据 newCurveData.amplitude = newParams(0); newCurveData.sigma = newParams(1); newCurveData.sigmoidH = newParams(2); newCurveData.sigmoidW = newParams(3); newCurveData.baseline = newParams(4); newCurveData.center = newParams(5); newCurveData.fwhm = newFwhm; newCurveData.area = newArea; // 替换历史列表中的数据 _fitHistoryList[historyIdx].curveList.replace(curveIdxInHistory, newCurveData); // 12. 自动保存到文件 saveHistoryToFile(); // 13. 重绘图表 _plot->replot(); QMessageBox::information(this, QStringLiteral(u"提示"), QStringLiteral(u"重新拟合成功,历史数据已更新。")); } } //检测鼠标是否悬停在框选区域上 void EnergyCountPeakFitView::updateHoverState(const QPoint& mousePos) { // 将鼠标像素坐标转换为 plot 坐标 const QwtScaleMap xMap = _plot->canvasMap(QwtPlot::xBottom); const QwtScaleMap yMap = _plot->canvasMap(QwtPlot::yLeft); const double x = xMap.invTransform(mousePos.x()); const double y = yMap.invTransform(mousePos.y()); const QPointF plotPos(x, y); PlotRectItem* newHoveredItem = nullptr; // 倒序遍历(优先检测最上层的区域) for (auto it = _selectionRectItems.rbegin(); it != _selectionRectItems.rend(); ++it) { PlotRectItem* item = *it; if (item->rect().contains(plotPos)) { newHoveredItem = item; break; } } // 更新悬停状态 if (newHoveredItem != _hoveredRectItem) { // 取消之前的悬停 if (_hoveredRectItem) { _hoveredRectItem->setHovered(false); } // 设置新的悬停 _hoveredRectItem = newHoveredItem; if (_hoveredRectItem) { _hoveredRectItem->setHovered(true); } // 重绘 _plot->replot(); } } void EnergyCountPeakFitView::displaySelectedCurves(const QList &curves, const QList& refs) { // 先清除之前的拟合曲线和框选区域 clearFitCurves(); clearAllSelectionRects(); //重建显示引用列表,记录索引 _displayedHistoryCurves.clear(); // 遍历显示曲线 for (int i = 0; i < curves.size(); ++i) { const auto& curve = curves[i]; const auto& ref = refs[i]; // 1. 生成X轴数据 const int numPoints = 200; QVector xs, ys, ysTw; double step = (curve.xMax - curve.xMin) / (numPoints - 1); // 2. 构建参数向量 arma::vec p(6); p(0) = curve.amplitude; p(1) = curve.sigma; p(2) = curve.sigmoidH; p(3) = curve.sigmoidW; p(4) = curve.baseline; p(5) = curve.center; // 3. 生成拟合曲线数据 for (int j = 0; j < numPoints; ++j) { double x = curve.xMin + j * step; xs.append(x); ys.append(PhotonPeakModel(x, p)); } //记录当前曲线和框选的起始索引 DisplayedCurveRef newRef; newRef.historyIndex = ref.historyIndex; newRef.curveIndex = ref.curveIndex; newRef.curveStartIndex = _fitCurves.size(); newRef.rectIndex = _selectionRectItems.size(); // 4. 创建并添加拟合曲线 QwtPlotCurve* fitCurve = new QwtPlotCurve(QStringLiteral(u"历史拟合数据")); fitCurve->setPen(QPen(Qt::blue, 2)); fitCurve->setSamples(xs, ys); fitCurve->setItemAttribute(QwtPlotItem::Legend, false); fitCurve->attach(_plot); _fitCurves.append(fitCurve); // 5. 生成本底曲线数据 arma::vec xVec(numPoints); for (int j = 0; j < numPoints; ++j) xVec(j) = xs[j]; arma::vec yTw = PhotonPeakModelTuowei(xVec, p); for (int j = 0; j < yTw.size(); ++j) ysTw.append(yTw(j)); // 6. 创建并添加本底曲线 QwtPlotCurve* bgCurve = new QwtPlotCurve(QStringLiteral(u"历史本底数据")); bgCurve->setPen(QPen(Qt::yellow, 2, Qt::DashLine)); bgCurve->setSamples(xs, ysTw); bgCurve->setItemAttribute(QwtPlotItem::Legend, false); bgCurve->attach(_plot); _fitCurves.append(bgCurve); // 7. 恢复并绘制框选区域 if (!curve.selectionRect.isNull()) { PlotRectItem* histRect = new PlotRectItem("HistorySelection"); histRect->setAxes(QwtPlot::xBottom, QwtPlot::yLeft); histRect->setRect(curve.selectionRect); histRect->setBrush(QBrush(Qt::transparent)); histRect->setPen(QPen(Qt::red, 2, Qt::DashLine)); histRect->setPeakData(curve.center, curve.fwhm, curve.area, curve.baseline); histRect->setSelectionType("history"); histRect->setSelectionIndex(_displayedHistoryCurves.size()); histRect->attach(_plot); _selectionRectItems.append(histRect); } // 8. 将记录了索引的 newRef 加入列表 _displayedHistoryCurves.append(newRef); } _plot->replot(); }