diff --git a/bin/nuclideLib.db b/bin/nuclideLib.db index 759e8f5..da005d1 100644 Binary files a/bin/nuclideLib.db and b/bin/nuclideLib.db differ diff --git a/src/2DSpectralCompliance/TwoDSpectralCompliance.cpp b/src/2DSpectralCompliance/TwoDSpectralCompliance.cpp index 56b379f..abfa457 100644 --- a/src/2DSpectralCompliance/TwoDSpectralCompliance.cpp +++ b/src/2DSpectralCompliance/TwoDSpectralCompliance.cpp @@ -22,6 +22,9 @@ #include #include #include +#include "BusyIndicator.h" +#include + class HeatMapColorMap : public QwtLinearColorMap { public: @@ -45,6 +48,8 @@ TwoDSpectralCompliance::TwoDSpectralCompliance(QWidget *parent) : layout->addWidget(this->_plot); setupPlot(); createFloatingInfoWidget(); + _busy_indicator = new BusyIndicator(this); + } TwoDSpectralCompliance::~TwoDSpectralCompliance() @@ -64,10 +69,15 @@ void TwoDSpectralCompliance::SetAnalyzeDataFilename(const QMapStart(); readCsv(csvFile); generateSurfaceData(); updateSpectrogram(); + _busy_indicator->Stop(); + }; + QThread* load_data_thread = QThread::create(functionToRun); + load_data_thread->start(); } @@ -432,3 +442,12 @@ bool TwoDSpectralCompliance::eventFilter(QObject *obj, QEvent *event) } return QWidget::eventFilter(obj, event); } + +void TwoDSpectralCompliance::showEvent(QShowEvent *e) +{ + Q_UNUSED(e); + if (_busy_indicator) { + _busy_indicator->setGeometry(this->rect()); + this->update(); + } +} diff --git a/src/2DSpectralCompliance/TwoDSpectralCompliance.h b/src/2DSpectralCompliance/TwoDSpectralCompliance.h index 5daaff1..45af5d7 100644 --- a/src/2DSpectralCompliance/TwoDSpectralCompliance.h +++ b/src/2DSpectralCompliance/TwoDSpectralCompliance.h @@ -11,6 +11,7 @@ class QwtPlotSpectrogram; class QPushButton; class QLineEdit; class ScatterPlotItem; +class BusyIndicator; namespace Ui { class TwoDSpectralCompliance; } @@ -34,6 +35,7 @@ public: protected: void resizeEvent(QResizeEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override; + virtual void showEvent(QShowEvent* e) override final; private: void setupPlot(); @@ -80,6 +82,8 @@ private: QPoint m_dragPosition; bool m_dragging = false; + BusyIndicator* _busy_indicator = nullptr; + }; #endif // TWODSPECTRALCOMPLIANCE_H diff --git a/src/CountRateAnalysisView/CountRateAnalysisView.cpp b/src/CountRateAnalysisView/CountRateAnalysisView.cpp index c71b10b..674dd32 100644 --- a/src/CountRateAnalysisView/CountRateAnalysisView.cpp +++ b/src/CountRateAnalysisView/CountRateAnalysisView.cpp @@ -15,6 +15,8 @@ #include #include #include "csv.h" +#include +#include "BusyIndicator.h" CountRateAnalysisView::CountRateAnalysisView(QWidget *parent) : MeasureAnalysisView(parent), @@ -24,6 +26,7 @@ CountRateAnalysisView::CountRateAnalysisView(QWidget *parent) : this->setViewType(PlotFrame); InitUi(); + _busy_indicator = new BusyIndicator(this); } @@ -36,13 +39,28 @@ void CountRateAnalysisView::InitViewWorkspace(const QString &project_name) { } - void CountRateAnalysisView::SetAnalyzeDataFilename(const QMap &data_files_set) { if(!data_files_set.isEmpty()) { - m_AllData = getParticleInjectTimeData(data_files_set.first().toString()); - setData(m_AllData); + auto functionToRun = [this,data_files_set] + { + _busy_indicator->Start(); + m_AllData = getParticleInjectTimeData(data_files_set.first().toString()); + setData(m_AllData); + _busy_indicator->Stop(); + }; + QThread* load_data_thread = QThread::create(functionToRun); + load_data_thread->start(); + } +} + +void CountRateAnalysisView::showEvent(QShowEvent *e) +{ + Q_UNUSED(e); + if (_busy_indicator) { + _busy_indicator->setGeometry(this->rect()); + this->update(); } } @@ -119,7 +137,6 @@ QVector CountRateAnalysisView::getParticleInjectTimeData(QSt rec.dTime = time_count; records.append(rec); } - return records; return records; } diff --git a/src/CountRateAnalysisView/CountRateAnalysisView.h b/src/CountRateAnalysisView/CountRateAnalysisView.h index ce76c94..20e8d77 100644 --- a/src/CountRateAnalysisView/CountRateAnalysisView.h +++ b/src/CountRateAnalysisView/CountRateAnalysisView.h @@ -7,6 +7,8 @@ #include "MeasureAnalysisView.h" class CustomQwtPlot; +class BusyIndicator; + namespace Ui { class CountRateAnalysisView; @@ -23,6 +25,8 @@ public: virtual void InitViewWorkspace(const QString& project_name) override final; virtual void SetAnalyzeDataFilename(const QMap& data_files_set); +protected: + virtual void showEvent(QShowEvent* e) override final; void setData(QVector data); //获取数据 @@ -33,8 +37,10 @@ private: private: Ui::CountRateAnalysisView *ui; + BusyIndicator* _busy_indicator = nullptr; CustomQwtPlot *plot; QVector m_AllData;//存储的所有的粒子入射时间数据 }; #endif //COUNTRATEANALYSIS_H + diff --git a/src/EnergyCountPeakFitView/EnergyCountPeakFitView.cpp b/src/EnergyCountPeakFitView/EnergyCountPeakFitView.cpp index 3cc12a6..954afde 100644 --- a/src/EnergyCountPeakFitView/EnergyCountPeakFitView.cpp +++ b/src/EnergyCountPeakFitView/EnergyCountPeakFitView.cpp @@ -23,7 +23,7 @@ #include "PlotRectItem.h" #include #include - +#include #include #include #include @@ -31,22 +31,43 @@ #include #include #include +#include +#include +#include #include "MeasureAnalysisProjectModel.h" +#include "BusyIndicator.h" QJsonObject PeakFitHistoryItem::toJson() const { QJsonObject obj; obj["timestamp"] = timestamp.toString(Qt::ISODate); - obj["center"] = center; - obj["fwhm"] = fwhm; - obj["area"] = area; - obj["amplitude"] = amplitude; - obj["sigma"] = sigma; - obj["sigmoidH"] = sigmoidH; - obj["sigmoidW"] = sigmoidW; - obj["baseline"] = baseline; - obj["xMin"] = xMin; - obj["xMax"] = xMax; + 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; } @@ -54,16 +75,34 @@ PeakFitHistoryItem PeakFitHistoryItem::fromJson(const QJsonObject& obj) { PeakFitHistoryItem item; item.timestamp = QDateTime::fromString(obj["timestamp"].toString(), Qt::ISODate); - item.center = obj["center"].toDouble(); - item.fwhm = obj["fwhm"].toDouble(); - item.area = obj["area"].toDouble(); - item.amplitude = obj["amplitude"].toDouble(); - item.sigma = obj["sigma"].toDouble(); - item.sigmoidH = obj["sigmoidH"].toDouble(); - item.sigmoidW = obj["sigmoidW"].toDouble(); - item.baseline = obj["baseline"].toDouble(); - item.xMin = obj["xMin"].toDouble(); - item.xMax = obj["xMax"].toDouble(); + 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; } @@ -127,6 +166,7 @@ void EnergyCountPeakFitView::setupPlot() 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); @@ -141,8 +181,21 @@ void EnergyCountPeakFitView::setupPlot() QwtLegend* legend = new QwtLegend(); legend->setDefaultItemMode(QwtLegendData::ReadOnly); _plot->insertLegend(legend, QwtPlot::RightLegend); + _plot->SetAxisDragScale(QwtPlot::xBottom, true); - _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); @@ -175,29 +228,27 @@ void EnergyCountPeakFitView::setupMenu() connect(action_plot_config, &QAction::triggered, this, &EnergyCountPeakFitView::onActionPlotConfigure); this->_menu->addSeparator(); - QAction* action_clear_rect = this->_menu->addAction(QStringLiteral(u"清除框选标记")); - connect(action_clear_rect, &QAction::triggered, this, &EnergyCountPeakFitView::clearAllSelectionRects); - -// this->_menu->addSeparator(); -// QAction* action_select_algorithm = this->_menu->addAction(QStringLiteral(u"选择拟合算法")); - - QAction* action_clear_fit = this->_menu->addAction(QStringLiteral(u"清除拟合曲线")); - connect(action_clear_fit, &QAction::triggered, this, &EnergyCountPeakFitView::clearFitCurves); - - // [NEW] 保存当前拟合结果 - QAction* action_save_fit = this->_menu->addAction(QStringLiteral(u"保存当前拟合结果")); - connect(action_save_fit, &QAction::triggered, this, &EnergyCountPeakFitView::onActionSaveCurrentFit); - - // [NEW] 查看历史拟合结果 - QAction* action_show_history = this->_menu->addAction(QStringLiteral(u"峰拟合结果")); - connect(action_show_history, &QAction::triggered, this, &EnergyCountPeakFitView::onActionShowFitHistory); + 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(); @@ -219,9 +270,10 @@ void EnergyCountPeakFitView::loadDataFromFile(const QString &data_name, const QS } // 绘制曲线 QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"原始数据")); + curve->setPen(QPen(Qt::gray, 2)); // 原始数据统一灰色 curve->setSamples(x, y); - curve->setPen(QPen(Qt::red, 2)); // 原始数据统一红色 - _plot->AddCurve(curve); + _plot->AddCurve(curve,false); + } void EnergyCountPeakFitView::onActionCurveShowSetting() @@ -340,10 +392,15 @@ bool EnergyCountPeakFitView::eventFilter(QObject* watched, QEvent* event) // 释放左键完成框选 else if (event->type() == QEvent::MouseButtonRelease && me->button() == Qt::LeftButton && _isSelecting) { - finishSelection(); + finishSelection(); fadeSelectionRectBorders(); return true; } + //鼠标移动时检测悬停 + else if (event->type() == QEvent::MouseMove) { + updateHoverState(me->pos()); + return true; + } } return MeasureAnalysisView::eventFilter(watched, event); } @@ -376,104 +433,81 @@ void EnergyCountPeakFitView::finishSelection() { if (_rubberBand) { - QRect finalRect = _rubberBand->geometry(); - _rubberBand->hide(); + 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); + 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); + 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()); + // 获取曲线数据 + 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]); } + } - 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"光子峰拟合算法"); + bool ok = false; + QString selected = QInputDialog::getItem(this, + QStringLiteral(u"选择拟合算法"), + QStringLiteral(u"请选择用于当前框选区域的峰拟合算法:"), + algorithms, + 0, + false, + &ok); + if (ok && selected == QStringLiteral(u"光子峰拟合算法")) + { + 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(); } } - - if (roiX.size() >= 3) { - QStringList algorithms; - algorithms << QStringLiteral(u"光子峰拟合算法"); - bool ok = false; - QString selected = QInputDialog::getItem(this, - QStringLiteral(u"选择拟合算法"), - QStringLiteral(u"请选择用于当前框选区域的峰拟合算法:"), - algorithms, - 0, - false, - &ok); - if (ok && selected == QStringLiteral(u"光子峰拟合算法")) { - arma::vec armaRoiX(roiX.size()), armaRoiY(roiY.size()); - for (int i = 0; i < roiX.size(); ++i) { - armaRoiX(i) = roiX[i]; - armaRoiY(i) = roiY[i]; - } - // 1. 自动估算默认初始参数(基于整个 ROI,或取第一个峰) - arma::vec defaultP0; - bool defaultValid = false; - try { - defaultP0 = EstimatePhotonPeakModelInitialParams(armaRoiX, armaRoiY); - defaultValid = true; - } catch (...) { - // 估算失败,使用硬编码默认值 - } - if (!defaultValid) { - double midEnergy = (roiX.first() + roiX.last()) / 2.0; - defaultP0 = {100.0, 2.0, 50.0, 2.0, 10.0, midEnergy}; - } - - // 2. 弹出参数设置对话框 - FitParams defaultParams; - defaultParams.A = defaultP0(0); - defaultParams.delt = defaultP0(1); - defaultParams.H = defaultP0(2); - defaultParams.W = defaultP0(3); - defaultParams.C = defaultP0(4); - defaultParams.P = defaultP0(5); - PeakFitParamsDialog paramDlg(defaultParams, this); - if (paramDlg.exec() != QDialog::Accepted) - return; // 用户取消 - - FitParams userParams = paramDlg.getParams(); - arma::vec userP0 = {userParams.A, userParams.delt, userParams.H, - userParams.W, userParams.C, userParams.P}; - - // 3. 执行拟合(曲线自动添加到主图) - QList results = performPeakFitting(roiX, roiY, &userP0); - if (results.isEmpty()) { - qDebug()<< QStringLiteral(u"拟合结果:") << QStringLiteral(u"未检测到有效峰或拟合失败。"); - } else { -// QMessageBox::information(this, "拟合结果", QString("成功拟合 %1 个峰,曲线已显示在图区。").arg(results.size())); - for (const auto& r : results) { - qDebug() << QStringLiteral(u"峰中心= %1 keV, FWHM= %2, 面积= %3") - .arg(r.center).arg(r.fwhm).arg(r.area); - } - } - } - } else { - qDebug() << QStringLiteral(u"框选区域内数据点不足3个,无法拟合。"); - } + } else { + qDebug() << QStringLiteral(u"框选区域内数据点不足3个,无法拟合。"); } } } - _isSelecting = false; + } + _isSelecting = false; } @@ -487,6 +521,8 @@ void EnergyCountPeakFitView::addSelectionRect(const QRectF &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(); @@ -503,12 +539,10 @@ void EnergyCountPeakFitView::fadeSelectionRectBorders() _plot->replot(); } -QList EnergyCountPeakFitView::performPeakFitting(const QVector &x, const QVector &y, const arma::vec* userP0) + + +QList EnergyCountPeakFitView::performPeakFitting(const QVector &x, const QVector &y) { - // 清除之前的拟合曲线(每次拟合全新绘制) -// clearFitCurves(); - - QList results; if (x.size() != y.size() || x.size() < 3) { @@ -522,102 +556,31 @@ QList EnergyCountPeakFitView::performPeakFitting(const QVector peaks; - try { - peaks = peakFinder.FindPeaks(spec_data, step_w); - } catch (const std::string& s) { - qDebug() << QStringLiteral(u"FindPeaks 异常:") << s.c_str(); - return results; - } catch (const std::exception& e) { - qDebug() << QStringLiteral(u"FindPeaks 异常:") << e.what(); - return results; - } catch (...) { - qDebug() << QStringLiteral(u"FindPeaks 未知异常"); - return results; - } - - if (peaks.empty()) { - qDebug() << QStringLiteral(u"未检测到峰。"); - return results; - } - - // 选择最重要的峰(例如幅值最大的峰) - auto bestPeak = *std::max_element(peaks.begin(), peaks.end(), - [](const FindPeaksBySvd::PeakInfo& a, const FindPeaksBySvd::PeakInfo& b) { - return a.height < b.height; - }); - int idxCenter = bestPeak.pos; - if (idxCenter < 0 || idxCenter >= x.size()) - return results; - double centerEnergy = armaX(idxCenter); - - const double fitRangeEnergy = 15.0; - // 提取局部数据 - QVector localX, localY; - for (int i = 0; i < x.size(); ++i) { - if (x[i] >= centerEnergy - fitRangeEnergy && x[i] <= centerEnergy + fitRangeEnergy) { - localX.push_back(x[i]); - localY.push_back(y[i]); - } - } - if (localX.size() < 5) { - qDebug() << QStringLiteral(u"局部数据点不足5,跳过拟合"); - return results; - } - - arma::vec xLocal(localX.size()), yLocal(localY.size()); - for (int i = 0; i < localX.size(); ++i) { - xLocal(i) = localX[i]; - yLocal(i) = localY[i]; - } - - arma::vec p0; - if (userP0 && userP0->n_elem == 6) { - p0 = *userP0; - } else { - try { - p0 = EstimatePhotonPeakModelInitialParams(xLocal, yLocal); - } catch (...) { - qDebug() << QStringLiteral(u"初始参数估算失败,跳过拟合"); - return results; - } - } - - arma::vec p_fit; - try { - p_fit = NolinearLeastSquaresCurveFit::Lsqcurvefit(PhotonPeakModel, xLocal, yLocal, p0); - } catch (const std::exception& e) { - qDebug() << QStringLiteral(u"拟合失败:") << e.what(); - return results; - } - + 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 = p_fit(0); - fitFunc.delt= p_fit(1); - fitFunc.H = p_fit(2); - fitFunc.W = p_fit(3); - fitFunc.C = p_fit(4); - fitFunc.P = p_fit(5); + 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); @@ -632,29 +595,33 @@ QList EnergyCountPeakFitView::performPeakFitting(const QVectorsetPen(QPen(Qt::blue, 2)); - _fitCurves.append(fitCurve); + QList curves = createFitCurve(result, armaX); + for (QwtPlotCurve* c : curves) { + _fitCurves.append(c); + } + results.append(result); _plot->replot(); _lastFitResult = result; _lastFitParams = p_fit; - _lastXMin = xMinPlot; - _lastXMax = xMaxPlot; + _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; + QVector xs, ys,ysTw; double step = (xMax - xMin) / (numPoints - 1); arma::vec p(6); @@ -669,8 +636,10 @@ QwtPlotCurve *EnergyCountPeakFitView::createFitCurve(const PeakFitResult &result double x = xMin + i * step; xs.append(x); double y = PhotonPeakModel(x, p); - ys.append(y); + ys.append(y); } + + QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"拟合数据")); curve->setPen(QPen(Qt::blue, 2)); curve->setSamples(xs, ys); @@ -678,15 +647,60 @@ QwtPlotCurve *EnergyCountPeakFitView::createFitCurve(const PeakFitResult &result 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(); - _plot->replot(); + curve->detach(); + delete curve; + } + _fitCurves.clear(); + + _currentFitRecords.clear(); + + _plot->replot(); } void EnergyCountPeakFitView::loadHistoryFromFile() @@ -726,69 +740,103 @@ void EnergyCountPeakFitView::saveHistoryToFile() void EnergyCountPeakFitView::saveCurrentFitToHistory() { - if (!_hasLastFit) { - QMessageBox::information(this, QStringLiteral(u"提示"), - QStringLiteral(u"没有可保存的拟合结果,请先框选区域进行拟合。")); - return; - } + if (_currentFitRecords.isEmpty()) return; + + //只取最新的一次拟合结果(列表中的最后一条) + const auto& latestRecord = _currentFitRecords.last(); + int latestRectIndex = _currentFitRecords.size() - 1; // 对应的框选区域索引 PeakFitHistoryItem item; item.timestamp = QDateTime::currentDateTime(); - item.center = _lastFitResult.center; - item.fwhm = _lastFitResult.fwhm; - item.area = _lastFitResult.area; - item.amplitude = _lastFitParams(0); - item.sigma = _lastFitParams(1); - item.sigmoidH = _lastFitParams(2); - item.sigmoidW = _lastFitParams(3); - item.baseline = _lastFitParams(4); - item.xMin = _lastXMin; - item.xMax = _lastXMax; + + // 仅构建当前这一次的曲线数据 + 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(); - - QMessageBox::information(this, QStringLiteral(u"保存成功"), - QStringLiteral(u"已保存拟合结果(峰中心 = %1 keV)").arg(item.center)); } void EnergyCountPeakFitView::displayFitFromHistory(const PeakFitHistoryItem& item) { -// clearFitCurves(); + // 遍历历史中的所有曲线,逐一重建 + for (const auto& curve : item.curveList) { + // 1. 生成X轴数据 + const int numPoints = 200; + QVector xs, ys, ysTw; + double step = (curve.xMax - curve.xMin) / (numPoints - 1); - arma::vec p(6); - p(0) = item.amplitude; - p(1) = item.sigma; - p(2) = item.sigmoidH; - p(3) = item.sigmoidW; - p(4) = item.baseline; - p(5) = item.center; + // 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; - const int numPoints = 200; - QVector xs, ys; - double step = (item.xMax - item.xMin) / (numPoints - 1); - for (int i = 0; i < numPoints; ++i) { - double x = item.xMin + i * step; - xs.append(x); - ys.append(PhotonPeakModel(x, p)); - } + // 3. 生成拟合曲线数据 + for (int i = 0; i < numPoints; ++i) { + double x = curve.xMin + i * step; + xs.append(x); + ys.append(PhotonPeakModel(x, p)); + } - QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"历史拟合曲线")); - curve->setPen(QPen(Qt::blue, 2, Qt::DashLine)); - curve->setSamples(xs, ys); - curve->attach(_plot); - _fitCurves.append(curve); + // 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); - QwtPlotMarker* infoMarker = new QwtPlotMarker(); - infoMarker->setValue(item.center, 0); - infoMarker->setLabel(QStringLiteral(u"历史峰: %1 keV\nFWHM: %2") - .arg(item.center, 0, 'f', 2) - .arg(item.fwhm, 0, 'f', 2)); - infoMarker->setLabelAlignment(Qt::AlignTop | Qt::AlignRight); - infoMarker->attach(_plot); + // 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"); - _plot->replot(); + //显式绑定坐标轴,确保坐标映射正确 + 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() @@ -798,59 +846,804 @@ void EnergyCountPeakFitView::onActionSaveCurrentFit() void EnergyCountPeakFitView::onActionShowFitHistory() { - if (_fitHistoryList.isEmpty()) { - QMessageBox::information(this, QStringLiteral(u"峰拟合结果"), + 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.setMinimumWidth(450); + dlg.setMinimumSize(1000, 500); QVBoxLayout* layout = new QVBoxLayout(&dlg); - QListWidget* listWidget = new QListWidget(&dlg); - for (const auto& item : _fitHistoryList) { - QString text = QStringLiteral(u"%1 峰中心: %2 keV FWHM: %3 面积: %4") - .arg(item.timestamp.toString("yyyy-MM-dd hh:mm:ss")) - .arg(item.center, 0, 'f', 2) - .arg(item.fwhm, 0, 'f', 2) - .arg(item.area, 0, 'f', 0); - listWidget->addItem(text); + 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)); + } } - layout->addWidget(listWidget); + model->blockSignals(false); - QPushButton* btnShow = new QPushButton(QStringLiteral(u"显示选中曲线"), &dlg); - QPushButton* btnDelete = new QPushButton(QStringLiteral(u"删除选中"), &dlg); - QHBoxLayout* btnLayout = new QHBoxLayout(); - btnLayout->addStretch(); - btnLayout->addWidget(btnShow); - btnLayout->addWidget(btnDelete); - layout->addLayout(btnLayout); + 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); - connect(btnShow, &QPushButton::clicked, [&]() { - int row = listWidget->currentRow(); - if (row >= 0 && row < _fitHistoryList.size()) { - displayFitFromHistory(_fitHistoryList[row]); - dlg.accept(); - } else { - QMessageBox::warning(&dlg, QStringLiteral(u"提示"), QStringLiteral(u"请先选择一条记录。")); + 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, [&]() { - int row = listWidget->currentRow(); - if (row >= 0) { - _fitHistoryList.removeAt(row); - saveHistoryToFile(); - delete listWidget->takeItem(row); - QMessageBox::information(&dlg, QStringLiteral(u"删除"), QStringLiteral(u"已删除该记录。")); - if (_fitHistoryList.isEmpty()) - dlg.close(); - } else { - QMessageBox::warning(&dlg, QStringLiteral(u"提示"), QStringLiteral(u"请先选择要删除的记录。")); + // 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(); +} diff --git a/src/EnergyCountPeakFitView/EnergyCountPeakFitView.h b/src/EnergyCountPeakFitView/EnergyCountPeakFitView.h index 7f65802..0bf2867 100644 --- a/src/EnergyCountPeakFitView/EnergyCountPeakFitView.h +++ b/src/EnergyCountPeakFitView/EnergyCountPeakFitView.h @@ -27,22 +27,45 @@ struct PeakFitResult double baseline; // 常数项 C double sigmoidH; // Sigmoid 项高度 H double sigmoidW; // Sigmoid 项宽度 W - }; -struct PeakFitHistoryItem { - QDateTime timestamp; // 时间戳 - double center; // 峰中心能量 (keV) - double fwhm; // 半高宽 - double area; // 峰面积 - double amplitude; // 幅度 A - double sigma; // 标准差 delt - double sigmoidH; // H - double sigmoidW; // W - double baseline; // C - double xMin; // 拟合曲线显示范围左边界 - double xMax; // 拟合曲线显示范围右边界 +}; - QJsonObject toJson() const; - static PeakFitHistoryItem fromJson(const QJsonObject& obj); +struct FitCurveData { + double amplitude; // 峰幅度 + double sigma; // 高斯sigma + double sigmoidH; // Sigmoid高度 + double sigmoidW; // Sigmoid宽度 + double baseline; // 本底 + double center; // 峰中心 + double xMin; // 曲线X范围最小值 + double xMax; // 曲线X范围最大值 + QRectF selectionRect; //关联的框选区域 + + double fwhm; // 半高宽 + double area; // 峰面积 +}; + +struct PeakFitHistoryItem { + QDateTime timestamp; + QList curveList; //存储多条曲线 + + QJsonObject toJson() const; + static PeakFitHistoryItem fromJson(const QJsonObject& obj); +}; + +struct CurrentFitRecord { + PeakFitResult result; // 拟合结果 + arma::vec params; // 完整拟合参数 + double xMin; // X范围最小值 + double xMax; // X范围最大值 + int historyIndex; // 对应_fitHistoryList中的索引,-1表示未保存 + CurrentFitRecord() : historyIndex(-1) {} // 构造函数初始化 +}; + +struct DisplayedCurveRef { + int historyIndex; // 对应 _fitHistoryList 的索引 + int curveIndex; // 对应 curveList 的索引 + int curveStartIndex; //对应 _fitCurves 中的起始索引(一条历史曲线对应2条Qwt曲线:拟合+本底) + int rectIndex; //对应 _selectionRectItems 中的索引 }; class PlotRectItem; // 前向声明 @@ -51,6 +74,7 @@ class CustomQwtPlot; class CustomQwtPlotXaxisSelector; class QwtPlotPicker; class QwtPlotCurve; +class BusyIndicator; class EnergyCountPeakFitView : public MeasureAnalysisView { @@ -72,16 +96,21 @@ private: void saveHistoryToFile(); void saveCurrentFitToHistory(); // 保存当前拟合结果 void displayFitFromHistory(const PeakFitHistoryItem& item); // 显示历史拟合曲线 + + //处理鼠标悬停检测 + void updateHoverState(const QPoint& mousePos); + //显示选中的曲线 + void displaySelectedCurves(const QList& curves, const QList& refs); private slots: void onActionCurveShowSetting(); void onActionPlotConfigure(); void clearAllSelectionRects(); void clearFitCurves(); // 清除所有拟合曲线 - - - // [NEW] 新增菜单槽函数 - void onActionSaveCurrentFit(); - void onActionShowFitHistory(); + void onActionSaveCurrentFit(); + void onActionShowFitHistory(); + void onActionDeleteHoveredRect(); + //重新拟合当前悬停的框选区域 + void onActionRefitCurrentRect(); protected: bool eventFilter(QObject* watched, QEvent* event) override; // 事件过滤器 @@ -92,9 +121,12 @@ private: void addSelectionRect(const QRectF& plotRect); void fadeSelectionRectBorders(); -QList performPeakFitting(const QVector& x, const QVector& y, const arma::vec* userP0 = nullptr); +QList performPeakFitting(const QVector& x, const QVector& y/*, const arma::vec* userP0 = nullptr*/); // 根据拟合参数生成曲线 QwtPlotCurve* createFitCurve(const PeakFitResult& result, double xMin, double xMax, const QString& name); + + QList createFitCurve(const PeakFitResult& result, arma::vec xVec); + private: CustomQwtPlot* _plot = nullptr; QMenu* _menu = nullptr; @@ -112,8 +144,7 @@ private: QList _fitCurves; - // [NEW] 历史记录相关成员 - QList _fitHistoryList; + QString _historyFilePath; // [NEW] 最近一次拟合结果(用于手动保存) @@ -123,6 +154,14 @@ private: bool _hasLastFit = false; QString _workspace; + //存储当前所有拟合结果(用于多曲线管理) + QList _currentFitRecords; + //历史记录列表 + QList _fitHistoryList; + //当前悬停的框选区域 + PlotRectItem* _hoveredRectItem = nullptr; + QList _displayedHistoryCurves; + }; #endif // ENERGYCOUNTPEAKFITVIEW_H diff --git a/src/EnergyCountPeakFitView/PlotRectItem.cpp b/src/EnergyCountPeakFitView/PlotRectItem.cpp index 3c686c5..ac32b93 100644 --- a/src/EnergyCountPeakFitView/PlotRectItem.cpp +++ b/src/EnergyCountPeakFitView/PlotRectItem.cpp @@ -2,23 +2,63 @@ #include #include PlotRectItem::PlotRectItem(const QString &title) - : QwtPlotItem() { + : QwtPlotItem(), m_isHovered(false), m_hasPeakData(false) + ,m_selectionType(""), m_selectionIndex(-1) +{ setTitle(title); setZ(1000); - m_pen = QPen(Qt::red, 2, Qt::SolidLine); + // 初始化普通状态画笔:红色虚线,线宽2 + m_normalPen = QPen(Qt::red, 2, Qt::DashLine); + // 初始化悬停状态画笔:红色实线,线宽2 + m_hoverPen = QPen(Qt::red, 2, Qt::SolidLine); + m_brush = QBrush(QColor(255, 0, 0, 30)); } void PlotRectItem::setRect(const QRectF &rect) { m_rect = rect; + itemChanged(); // 触发重绘 } void PlotRectItem::setPen(const QPen &pen) { - m_pen = pen; + m_normalPen = pen; + // 同步悬停画笔的颜色和宽度,仅改变线型 + m_hoverPen = pen; + m_hoverPen.setStyle(Qt::SolidLine); + itemChanged(); } void PlotRectItem::setBrush(const QBrush &brush) { m_brush = brush; + itemChanged(); +} + +//设置悬停状态 +void PlotRectItem::setHovered(bool hovered) { + if (m_isHovered != hovered) { + m_isHovered = hovered; + itemChanged(); // 触发重绘 + } +} + +//获取悬停状态 +bool PlotRectItem::isHovered() const { + return m_isHovered; +} + +//获取矩形区域 +QRectF PlotRectItem::rect() const { + return m_rect; +} + +void PlotRectItem::setPeakData(double center, double fwhm, double area, double baseline) +{ + m_hasPeakData = true; + m_peakCenter = center; + m_fwhm = fwhm; + m_peakArea = area; + m_baseline = baseline; + itemChanged(); // 触发重绘 } void PlotRectItem::draw(QPainter *painter, @@ -31,10 +71,45 @@ void PlotRectItem::draw(QPainter *painter, const QRect rect(QPoint(x1, y1), QPoint(x2, y2)); painter->save(); - painter->setPen(m_pen); + // 根据悬停状态选择画笔 + painter->setPen(m_isHovered ? m_hoverPen : m_normalPen); painter->setBrush(m_brush); painter->drawRect(rect); painter->restore(); + + if (m_hasPeakData) { + painter->save(); + painter->setRenderHint(QPainter::TextAntialiasing); + + // 构建文本内容 + QString text = QString("峰位: %1 keV\n" + "FWHM: %2\n" + "面积: %3\n" + "本底: %4") + .arg(m_peakCenter, 0, 'f', 2) + .arg(m_fwhm, 0, 'f', 2) + .arg(m_peakArea, 0, 'f', 2) + .arg(m_baseline, 0, 'f', 2); + + // 计算文本位置:矩形右上角 + 偏移量 + QPoint textPos(x2 + 10, y2); + + // 绘制半透明背景框(增强可读性) + QFont font = painter->font(); + font.setPointSize(9); // 稍微小一点 + painter->setFont(font); + + QFontMetrics fm(font); + QRect textRect = fm.boundingRect(QRect(0, 0, 1, 1), Qt::AlignLeft, text); + textRect.moveTopLeft(textPos); + textRect.adjust(-5, -3, 5, 3); // 增加边距 + + painter->fillRect(textRect, QColor(255, 255, 255, 220)); // 白底微透 + painter->setPen(Qt::black); + painter->drawText(textRect, Qt::AlignLeft, text); + + painter->restore(); + } } QRectF PlotRectItem::boundingRect() const { diff --git a/src/EnergyCountPeakFitView/PlotRectItem.h b/src/EnergyCountPeakFitView/PlotRectItem.h index f49c087..2fabf4d 100644 --- a/src/EnergyCountPeakFitView/PlotRectItem.h +++ b/src/EnergyCountPeakFitView/PlotRectItem.h @@ -13,16 +13,42 @@ public: void setPen(const QPen &pen); void setBrush(const QBrush &brush); + //设置/获取悬停状态 + void setHovered(bool hovered); + bool isHovered() const; + //获取当前矩形区域(用于碰撞检测) + QRectF rect() const; + //设置要显示的峰拟合数据 + void setPeakData(double center, double fwhm, double area, double baseline); + void setSelectionType(const QString& type) { m_selectionType = type; } + QString selectionType() const { return m_selectionType; } + + void setSelectionIndex(int index) { m_selectionIndex = index; } + int selectionIndex() const { return m_selectionIndex; } virtual void draw(QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap, const QRectF &canvasRect) const override; virtual QRectF boundingRect() const override; + + private: QRectF m_rect; - QPen m_pen; + QPen m_normalPen; //普通状态画笔(虚线) + QPen m_hoverPen; //悬停状态画笔(实线) QBrush m_brush; + bool m_isHovered; //悬停状态标志 + + + bool m_hasPeakData; + double m_peakCenter; + double m_fwhm; + double m_peakArea; + double m_baseline; + + QString m_selectionType; // "current" 或 "history" + int m_selectionIndex; // 对应的索引 }; #endif // PLOTRECTITEM_H diff --git a/src/NuclideLib/NuclideEditDialog.cpp b/src/NuclideLib/NuclideEditDialog.cpp new file mode 100644 index 0000000..d40996d --- /dev/null +++ b/src/NuclideLib/NuclideEditDialog.cpp @@ -0,0 +1,101 @@ +#include "NuclideEditDialog.h" + +NuclideEditDialog::NuclideEditDialog(bool isEdit, const QStringList& nuclideData, QWidget *parent) + : QDialog(parent), m_isEdit(isEdit) +{ + // 编辑模式下提取ID和已有数据 + if (isEdit && !nuclideData.isEmpty()) { + m_id = nuclideData[0]; // 第一个元素是ID + } + initUI(); + + // 编辑模式:填充已有数据(字段顺序不变,仅数据库字段名修改) + if (isEdit && nuclideData.size() >= 6) { + m_leName->setText(nuclideData[1]); + m_leHalfLife->setText(nuclideData[2]); + m_leHalfLifeUnc->setText(nuclideData[3]); + m_leParentNuclide->setText(nuclideData[4]); + m_leChildNuclide->setText(nuclideData[5]); + } +} + +void NuclideEditDialog::initUI() +{ + setWindowTitle(m_isEdit ? "编辑核素信息" : "添加核素信息"); + setFixedSize(400, 220); // 稍微增加高度适配新校验提示 + + // 表单布局(控件名称不变,仅数据库映射修改) + QFormLayout *formLayout = new QFormLayout(this); + m_leName = new QLineEdit(this); + m_leHalfLife = new QLineEdit(this); + m_leHalfLifeUnc = new QLineEdit(this); + m_leParentNuclide = new QLineEdit(this); + m_leChildNuclide = new QLineEdit(this); + + // 设置表单项(标签不变,用户界面无感知) + formLayout->addRow("核素名称", m_leName); + formLayout->addRow("半衰期", m_leHalfLife); + formLayout->addRow("半衰期不确定度", m_leHalfLifeUnc); + formLayout->addRow("母体核素名称", m_leParentNuclide); + formLayout->addRow("子体核素名称", m_leChildNuclide); + + // 按钮盒 + QDialogButtonBox *btnBox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, + Qt::Horizontal, this); + formLayout->addRow(btnBox); + + // 信号连接 + connect(btnBox, &QDialogButtonBox::accepted, this, [this]() { + if (validateInput()) { + accept(); + } + }); + connect(btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +bool NuclideEditDialog::validateInput() +{ + // 必填项校验 + if (m_leName->text().trimmed().isEmpty()) { + QMessageBox::warning(this, "输入错误", "核素名称不能为空!"); + return false; + } + if (m_leHalfLife->text().trimmed().isEmpty()) { + QMessageBox::warning(this, "输入错误", "半衰期不能为空!"); + return false; + } + if (m_leHalfLifeUnc->text().trimmed().isEmpty()) { + QMessageBox::warning(this, "输入错误", "半衰期不确定度不能为空!"); + return false; + } + + bool ok; + // 半衰期校验(保留原逻辑,TEXT类型可存储带单位的字符串) + m_leHalfLife->text().toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "输入错误", "半衰期必须为数字(可带单位,如12.5d)!"); + return false; + } + m_leHalfLifeUnc->text().toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "输入错误", "半衰期不确定度必须为有效数字!"); + return false; + } + + return true; +} + +QStringList NuclideEditDialog::getNuclideData() const +{ + QStringList data; + if (m_isEdit) { + data << m_id; // 编辑模式返回ID + } + data << m_leName->text().trimmed() + << m_leHalfLife->text().trimmed() + << m_leHalfLifeUnc->text().trimmed() + << m_leParentNuclide->text().trimmed() + << m_leChildNuclide->text().trimmed(); + return data; +} diff --git a/src/NuclideLib/NuclideEditDialog.h b/src/NuclideLib/NuclideEditDialog.h new file mode 100644 index 0000000..15176b2 --- /dev/null +++ b/src/NuclideLib/NuclideEditDialog.h @@ -0,0 +1,35 @@ +#ifndef NUCLIDEEDITDIALOG_H +#define NUCLIDEEDITDIALOG_H + +#include +#include +#include +#include +#include + +class NuclideEditDialog : public QDialog +{ + Q_OBJECT +public: + // 构造函数:isEdit=true为编辑模式,传入已有数据;false为添加模式 + explicit NuclideEditDialog(bool isEdit = false, const QStringList& nuclideData = {}, QWidget *parent = nullptr); + ~NuclideEditDialog() = default; + + // 获取用户输入的核素数据(编辑模式:ID+5个字段;添加模式:5个字段) + QStringList getNuclideData() const; + +private: + void initUI(); // 初始化界面 + bool validateInput(); // 输入校验(新增半衰期不确定度的数字校验) + + QLineEdit *m_leName; // 核素名称(对应NUCLIDE_NAME) + QLineEdit *m_leHalfLife; // 半衰期(对应HALF_LIFE) + QLineEdit *m_leHalfLifeUnc; // 半衰期不确定度(对应HALF_LIFE_UNCERTAINTY) + QLineEdit *m_leParentNuclide; // 母体核素名称(对应PARENT_NUCLIDE_NAME) + QLineEdit *m_leChildNuclide; // 子体核素名称(对应CHILD_NUCLIDE_NAME) + + bool m_isEdit; // 是否为编辑模式 + QString m_id; // 编辑模式下的核素ID +}; + +#endif // NUCLIDEEDITDIALOG_H diff --git a/src/NuclideLib/NuclideLib.cpp b/src/NuclideLib/NuclideLib.cpp index 8318a2d..6573f04 100644 --- a/src/NuclideLib/NuclideLib.cpp +++ b/src/NuclideLib/NuclideLib.cpp @@ -7,7 +7,9 @@ #include #include #include - +#include +#include +#include "NuclideRayListDialog.h" ButtonDelegate::ButtonDelegate(QObject *parent) : QStyledItemDelegate(parent) {} void ButtonDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const @@ -62,10 +64,20 @@ NuclideLibManage::NuclideLibManage(QWidget *parent) : ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); ui->tableView->setFocusPolicy(Qt::NoFocus); + QString dbPath = QCoreApplication::applicationDirPath() + "/nuclideLib.db"; // 绝对路径(推荐) + qDebug()<tableView->setItemDelegateForColumn(7, m_delegate); connect(m_delegate, &ButtonDelegate::buttonClicked, this, &NuclideLibManage::onButtonClicked); + loadNuclideData(); + } NuclideLibManage::~NuclideLibManage() @@ -75,45 +87,42 @@ NuclideLibManage::~NuclideLibManage() void NuclideLibManage::loadNuclideData() { - QString sql = "SELECT ID, NAME, HALF_LIFE, HALF_LIFE_UNC, PARENT_NUCLIDE, CHILD_NUCLIDE FROM tbl_nuclide_library;"; + QString sql = "SELECT ID, NUCLIDE_NAME, HALF_LIFE, HALF_LIFE_UNCERTAINTY, PARENT_NUCLIDE_NAME, CHILD_NUCLIDE_NAME FROM nuclideLib;"; auto rows = SqliteManager::instance().selectRows(sql); + m_listNuclide.clear(); m_model->setRowCount(0); - int row = 0; for (const auto& rec : rows) { QStringList item; item << rec["ID"].toString() - << rec["NAME"].toString() + << rec["NUCLIDE_NAME"].toString() << rec["HALF_LIFE"].toString() - << rec["HALF_LIFE_UNC"].toString() - << rec["PARENT_NUCLIDE"].toString() - << rec["CHILD_NUCLIDE"].toString(); + << rec["HALF_LIFE_UNCERTAINTY"].toString() // 对应新字段名 + << rec["PARENT_NUCLIDE_NAME"].toString() // 对应新字段名 + << rec["CHILD_NUCLIDE_NAME"].toString(); // 对应新字段名 m_listNuclide.append(item); - // 插入模型行 + // 插入模型行(界面显示逻辑不变) QList rowItems; // 第0列:ID(隐藏) QStandardItem *idItem = new QStandardItem(item[0]); idItem->setTextAlignment(Qt::AlignCenter); idItem->setFlags(idItem->flags() & ~Qt::ItemIsEditable); rowItems << idItem; - // 第1列:序号(行号+1) QStandardItem *seqItem = new QStandardItem(QString::number(row + 1)); seqItem->setTextAlignment(Qt::AlignCenter); seqItem->setFlags(seqItem->flags() & ~Qt::ItemIsEditable); rowItems << seqItem; - // 第2~6列:核素名称、半衰期等 - for (int col = 1; col <= 5; ++col) { // item的下标1~5对应NAME到CHILD_NUCLIDE + for (int col = 1; col <= 5; ++col) { QStandardItem *dataItem = new QStandardItem(item[col]); dataItem->setTextAlignment(Qt::AlignCenter); dataItem->setFlags(dataItem->flags() & ~Qt::ItemIsEditable); rowItems << dataItem; } - - // 第7列:操作列(存储空字符串,委托负责绘制按钮) + // 第7列:操作列 QStandardItem *opItem = new QStandardItem(""); opItem->setFlags(opItem->flags() & ~Qt::ItemIsEditable); rowItems << opItem; @@ -134,7 +143,27 @@ QStringList NuclideLibManage::getNuclideData(const QString &id) void NuclideLibManage::on_pushButton_add_clicked() { - loadNuclideData(); // 刷新数据 + NuclideEditDialog dlg(false, {}, this); + if (dlg.exec() == QDialog::Accepted) { + QStringList data = dlg.getNuclideData(); + if (data.size() < 5) return; + + QMap fieldValues; + fieldValues["NUCLIDE_NAME"] = data[0]; + fieldValues["HALF_LIFE"] = data[1]; + fieldValues["HALF_LIFE_UNCERTAINTY"] = data[2].toDouble(); // 显式转换为REAL类型 + fieldValues["PARENT_NUCLIDE_NAME"] = data[3]; + fieldValues["CHILD_NUCLIDE_NAME"] = data[4]; + fieldValues["CREATE_TIME"] = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + + qint64 newId = SqliteManager::instance().insertRow("nuclideLib", fieldValues); + if (newId > 0) { + QMessageBox::information(this, "成功", "核素添加成功!"); + loadNuclideData(); + } else { + QMessageBox::warning(this, "失败", "添加失败:" + SqliteManager::instance().lastError()); + } + } } void NuclideLibManage::on_pushButton_edit_clicked() @@ -144,10 +173,40 @@ void NuclideLibManage::on_pushButton_edit_clicked() QMessageBox::information(this, "提示", "请先选择要修改的核素行!"); return; } + int row = selected.first().row(); - QString id = m_model->index(row, 0).data().toString(); // 隐藏的ID列 + QString id = m_model->index(row, 0).data().toString(); QStringList data = getNuclideData(id); if (data.isEmpty()) return; + + NuclideEditDialog dlg(true, data, this); + if (dlg.exec() == QDialog::Accepted) { + QStringList newData = dlg.getNuclideData(); + if (newData.size() < 6) return; + + QMap fieldValues; + fieldValues["NUCLIDE_NAME"] = newData[1]; + fieldValues["HALF_LIFE"] = newData[2]; + fieldValues["HALF_LIFE_UNCERTAINTY"] = newData[3].toDouble(); // 显式转换为REAL类型 + fieldValues["PARENT_NUCLIDE_NAME"] = newData[4]; + fieldValues["CHILD_NUCLIDE_NAME"] = newData[5]; + + int affectedRows = SqliteManager::instance().updateRow( + "nuclideLib", + fieldValues, + "ID = ?", + {newData[0]} + ); + + if (affectedRows > 0) { + QMessageBox::information(this, "成功", "核素修改成功!"); + loadNuclideData(); + } else if (affectedRows == 0) { + QMessageBox::information(this, "提示", "未修改任何数据!"); + } else { + QMessageBox::warning(this, "失败", "修改失败:" + SqliteManager::instance().lastError()); + } + } } void NuclideLibManage::on_pushButton_del_clicked() @@ -157,6 +216,7 @@ void NuclideLibManage::on_pushButton_del_clicked() QMessageBox::information(this, "提示", "请先选择要删除的核素行!"); return; } + int row = selected.first().row(); QString id = m_model->index(row, 0).data().toString(); @@ -166,7 +226,7 @@ void NuclideLibManage::on_pushButton_del_clicked() box.button(QMessageBox::No)->setText("取消"); if (box.exec() != QMessageBox::Yes) return; - bool ok = SqliteManager::instance().deleteRow("tbl_nuclide_library", "ID = ?", {id}); + bool ok = SqliteManager::instance().deleteRow("nuclideLib", "ID = ?", {id}); if (ok) { loadNuclideData(); } else { @@ -178,4 +238,10 @@ void NuclideLibManage::onButtonClicked(const QModelIndex &index) { int row = index.row(); QString nuclideId = m_model->index(row, 0).data().toString(); + QString nuclideName = m_model->index(row, 2).data().toString(); + + // 打开射线信息管理对话框 + NuclideRayListDialog dlg(nuclideId, nuclideName, this); + dlg.exec(); + } diff --git a/src/NuclideLib/NuclideLib.h b/src/NuclideLib/NuclideLib.h index 7df3fb8..20d415a 100644 --- a/src/NuclideLib/NuclideLib.h +++ b/src/NuclideLib/NuclideLib.h @@ -6,6 +6,7 @@ #include #include #include +#include "NuclideEditDialog.h" namespace Ui { class NuclideLibManage; diff --git a/src/NuclideLib/NuclideRayDialog.cpp b/src/NuclideLib/NuclideRayDialog.cpp new file mode 100644 index 0000000..f77c8f1 --- /dev/null +++ b/src/NuclideLib/NuclideRayDialog.cpp @@ -0,0 +1,133 @@ +#include "NuclideRayDialog.h" + +NuclideRayDialog::NuclideRayDialog(bool isEdit, const QStringList& rayData, QWidget *parent) + : QDialog(parent), m_isEdit(isEdit) +{ + // 编辑模式下提取ID和已有数据 + if (isEdit && !rayData.isEmpty()) { + m_id = rayData[0]; // 第一个元素是ID + } + initUI(); + + // 编辑模式:填充已有数据 + if (isEdit && rayData.size() >= 8) { + m_cmbRayType->setCurrentText(rayData[1]); + m_leRayMev->setText(rayData[2]); + m_leRayMevUnc->setText(rayData[3]); + m_leBranchRatio->setText(rayData[4]); + m_leBranchRatioUnc->setText(rayData[5]); + m_cmbMainRayIdent->setCurrentText(rayData[6]); + m_leJiahePeak->setText(rayData[7]); + } +} + +void NuclideRayDialog::initUI() +{ + setWindowTitle(m_isEdit ? "编辑射线信息" : "添加射线信息"); + setFixedSize(450, 300); + + QFormLayout *formLayout = new QFormLayout(this); + + // 射线类型下拉框(常用核素射线类型) + m_cmbRayType = new QComboBox(this); + m_cmbRayType->addItems({"γ", "β⁻", "β⁺", "α", "X射线", "中子", "电子俘获"}); + formLayout->addRow("射线类型", m_cmbRayType); + + m_leRayMev = new QLineEdit(this); + m_leRayMev->setPlaceholderText("例如:1.332 MeV"); + formLayout->addRow("射线能量", m_leRayMev); + + m_leRayMevUnc = new QLineEdit(this); + m_leRayMevUnc->setPlaceholderText("例如:0.001 MeV"); + formLayout->addRow("能量不确定度", m_leRayMevUnc); + + m_leBranchRatio = new QLineEdit(this); + m_leBranchRatio->setPlaceholderText("例如:0.9998"); + formLayout->addRow("分支比", m_leBranchRatio); + + m_leBranchRatioUnc = new QLineEdit(this); + m_leBranchRatioUnc->setPlaceholderText("例如:0.0001"); + formLayout->addRow("分支比不确定度", m_leBranchRatioUnc); + + // 主射线标识下拉框 + m_cmbMainRayIdent = new QComboBox(this); + m_cmbMainRayIdent->addItems({"是", "否"}); + formLayout->addRow("是否为主射线", m_cmbMainRayIdent); + + m_leJiahePeak = new QLineEdit(this); + m_leJiahePeak->setPlaceholderText("无则留空"); + formLayout->addRow("加和峰", m_leJiahePeak); + + // 按钮盒 + QDialogButtonBox *btnBox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, + Qt::Horizontal, this); + formLayout->addRow(btnBox); + + // 信号连接 + connect(btnBox, &QDialogButtonBox::accepted, this, [this]() { + if (validateInput()) { + accept(); + } + }); + connect(btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +bool NuclideRayDialog::validateInput() +{ + // 必填项校验 + if (m_leRayMev->text().trimmed().isEmpty()) { + QMessageBox::warning(this, "输入错误", "射线能量不能为空!"); + return false; + } + if (m_leRayMevUnc->text().trimmed().isEmpty()) { + QMessageBox::warning(this, "输入错误", "能量不确定度不能为空!"); + return false; + } + if (m_leBranchRatioUnc->text().trimmed().isEmpty()) { + QMessageBox::warning(this, "输入错误", "分支比不确定度不能为空!"); + return false; + } + + // 数字格式校验(允许带单位) + bool ok; + // 提取数字部分进行校验 + QString mevText = m_leRayMev->text().trimmed().replace(QRegExp("[^0-9.]"), ""); + mevText.toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "输入错误", "射线能量必须为有效数字!"); + return false; + } + + QString mevUncText = m_leRayMevUnc->text().trimmed().replace(QRegExp("[^0-9.]"), ""); + mevUncText.toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "输入错误", "能量不确定度必须为有效数字!"); + return false; + } + + QString ratioUncText = m_leBranchRatioUnc->text().trimmed().replace(QRegExp("[^0-9.]"), ""); + ratioUncText.toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "输入错误", "分支比不确定度必须为有效数字!"); + return false; + } + + return true; +} + +QStringList NuclideRayDialog::getRayData() const +{ + QStringList data; + if (m_isEdit) { + data << m_id; // 编辑模式返回ID + } + data << m_cmbRayType->currentText().trimmed() + << m_leRayMev->text().trimmed() + << m_leRayMevUnc->text().trimmed() + << m_leBranchRatio->text().trimmed() + << m_leBranchRatioUnc->text().trimmed() + << m_cmbMainRayIdent->currentText().trimmed() + << m_leJiahePeak->text().trimmed(); + return data; +} diff --git a/src/NuclideLib/NuclideRayDialog.h b/src/NuclideLib/NuclideRayDialog.h new file mode 100644 index 0000000..08c7413 --- /dev/null +++ b/src/NuclideLib/NuclideRayDialog.h @@ -0,0 +1,37 @@ +#ifndef NUCLIDERAYDIALOG_H +#define NUCLIDERAYDIALOG_H + +#include +#include +#include +#include +#include +#include + +class NuclideRayDialog : public QDialog +{ + Q_OBJECT +public: + explicit NuclideRayDialog(bool isEdit = false, const QStringList& rayData = {}, QWidget *parent = nullptr); + ~NuclideRayDialog() = default; + + + QStringList getRayData() const; + +private: + void initUI(); // 初始化界面 + bool validateInput(); // 输入校验 + + QComboBox *m_cmbRayType; // 射线类型(下拉选择) + QLineEdit *m_leRayMev; // 射线能量 + QLineEdit *m_leRayMevUnc; // 能量不确定度 + QLineEdit *m_leBranchRatio; // 分支比 + QLineEdit *m_leBranchRatioUnc; // 分支比不确定度 + QComboBox *m_cmbMainRayIdent; // 主射线标识 + QLineEdit *m_leJiahePeak; // 加和峰 + + bool m_isEdit; // 是否为编辑模式 + QString m_id; // 编辑模式下的射线ID +}; + +#endif // NUCLIDERAYDIALOG_H diff --git a/src/NuclideLib/NuclideRayListDialog.cpp b/src/NuclideLib/NuclideRayListDialog.cpp new file mode 100644 index 0000000..b1009c2 --- /dev/null +++ b/src/NuclideLib/NuclideRayListDialog.cpp @@ -0,0 +1,248 @@ +#include "NuclideRayListDialog.h" +#include +RayButtonDelegate::RayButtonDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void RayButtonDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (index.column() == 8) { // 第8列是操作列 + // 绘制编辑按钮 + QStyleOptionButton editBtn; + editBtn.rect = QRect(option.rect.x(), option.rect.y() + 2, 60, option.rect.height() - 4); + editBtn.text = "编辑"; + editBtn.state = QStyle::State_Enabled; + if (option.state & QStyle::State_MouseOver) + editBtn.state |= QStyle::State_MouseOver; + QApplication::style()->drawControl(QStyle::CE_PushButton, &editBtn, painter); + + // 绘制删除按钮 + QStyleOptionButton delBtn; + delBtn.rect = QRect(option.rect.x() + 70, option.rect.y() + 2, 60, option.rect.height() - 4); + delBtn.text = "删除"; + delBtn.state = QStyle::State_Enabled; + if (option.state & QStyle::State_MouseOver) + delBtn.state |= QStyle::State_MouseOver; + QApplication::style()->drawControl(QStyle::CE_PushButton, &delBtn, painter); + } else { + QStyledItemDelegate::paint(painter, option, index); + } +} + +bool RayButtonDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) +{ + if (index.column() == 8 && event->type() == QEvent::MouseButtonRelease) { + QMouseEvent *mouseEvent = static_cast(event); + QRect editRect = QRect(option.rect.x(), option.rect.y() + 2, 60, option.rect.height() - 4); + QRect delRect = QRect(option.rect.x() + 70, option.rect.y() + 2, 60, option.rect.height() - 4); + + if (editRect.contains(mouseEvent->pos())) { + emit editButtonClicked(index); + return true; + } else if (delRect.contains(mouseEvent->pos())) { + emit deleteButtonClicked(index); + return true; + } + } + return QStyledItemDelegate::editorEvent(event, model, option, index); +} + +NuclideRayListDialog::NuclideRayListDialog(const QString& nuclideId, const QString& nuclideName, QWidget *parent) + : QDialog(parent), m_nuclideId(nuclideId), m_nuclideName(nuclideName) +{ + initUI(); + loadRayData(); +} + +void NuclideRayListDialog::initUI() +{ + setWindowTitle(QString("%1 - 射线信息管理").arg(m_nuclideName)); + setFixedSize(900, 500); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // 添加按钮 + QPushButton *btnAdd = new QPushButton("添加射线", this); + connect(btnAdd, &QPushButton::clicked, this, &NuclideRayListDialog::onAddRayClicked); + mainLayout->addWidget(btnAdd, 0, Qt::AlignRight); + + // 射线列表 + m_model = new QStandardItemModel(0, 9, this); + m_model->setHorizontalHeaderLabels({ + "ID", "射线类型", "能量(MeV)", "能量不确定度", "分支比", + "分支比不确定度", "主射线", "加和峰", "操作" + }); + + QTableView *tableView = new QTableView(this); + tableView->setModel(m_model); + tableView->setColumnHidden(0, true); // 隐藏ID列 + tableView->setColumnWidth(1, 80); // 射线类型 + tableView->setColumnWidth(2, 120); // 能量 + tableView->setColumnWidth(3, 120); // 能量不确定度 + tableView->setColumnWidth(4, 100); // 分支比 + tableView->setColumnWidth(5, 120); // 分支比不确定度 + tableView->setColumnWidth(6, 80); // 主射线 + tableView->setColumnWidth(7, 100); // 加和峰 + tableView->setColumnWidth(8, 150); // 操作列 + + tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + tableView->setAlternatingRowColors(true); + tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + tableView->setFocusPolicy(Qt::NoFocus); + + // 设置委托 + m_delegate = new RayButtonDelegate(this); + tableView->setItemDelegateForColumn(8, m_delegate); + connect(m_delegate, &RayButtonDelegate::editButtonClicked, this, &NuclideRayListDialog::onEditRayClicked); + connect(m_delegate, &RayButtonDelegate::deleteButtonClicked, this, &NuclideRayListDialog::onDeleteRayClicked); + + mainLayout->addWidget(tableView); + + // 关闭按钮 + QPushButton *btnClose = new QPushButton("关闭", this); + connect(btnClose, &QPushButton::clicked, this, &QDialog::accept); + mainLayout->addWidget(btnClose, 0, Qt::AlignRight); +} + +void NuclideRayListDialog::loadRayData() +{ + // 查询当前核素的所有射线信息 + QString sql = "SELECT ID, RAY_TYPE, RAY_MEV, RAY_MEV_UNCERTAINTY, RAY_BRANCH_RATIO, " + "RAY_BRANCH_RATIO_UNCERTAINTY, MAIN_RAY_IDENT, JIAHE_PEAK " + "FROM nuclideRayInfo WHERE NUCLIDE_ID = ?;"; + auto rows = SqliteManager::instance().selectRows(sql, {m_nuclideId}); + + m_listRay.clear(); + m_model->setRowCount(0); + int row = 0; + + for (const auto& rec : rows) { + QStringList item; + item << rec["ID"].toString() + << rec["RAY_TYPE"].toString() + << rec["RAY_MEV"].toString() + << rec["RAY_MEV_UNCERTAINTY"].toString() + << rec["RAY_BRANCH_RATIO"].toString() + << rec["RAY_BRANCH_RATIO_UNCERTAINTY"].toString() + << rec["MAIN_RAY_IDENT"].toString() + << rec["JIAHE_PEAK"].toString(); + m_listRay.append(item); + + // 插入模型行 + QList rowItems; + // 第0列:ID(隐藏) + QStandardItem *idItem = new QStandardItem(item[0]); + idItem->setTextAlignment(Qt::AlignCenter); + idItem->setFlags(idItem->flags() & ~Qt::ItemIsEditable); + rowItems << idItem; + + // 第1~7列:射线数据 + for (int col = 1; col <= 7; ++col) { + QStandardItem *dataItem = new QStandardItem(item[col]); + dataItem->setTextAlignment(Qt::AlignCenter); + dataItem->setFlags(dataItem->flags() & ~Qt::ItemIsEditable); + rowItems << dataItem; + } + + // 第8列:操作列 + QStandardItem *opItem = new QStandardItem(""); + opItem->setFlags(opItem->flags() & ~Qt::ItemIsEditable); + rowItems << opItem; + + m_model->appendRow(rowItems); + ++row; + } +} + +QStringList NuclideRayListDialog::getRayData(const QString &id) +{ + for (const auto& item : m_listRay) { + if (item.at(0) == id) + return item; + } + return QStringList(); +} + +void NuclideRayListDialog::onAddRayClicked() +{ + NuclideRayDialog dlg(false, {}, this); + if (dlg.exec() == QDialog::Accepted) { + QStringList data = dlg.getRayData(); + if (data.size() < 7) return; + + QMap fieldValues; + fieldValues["NUCLIDE_ID"] = m_nuclideId.toInt(); + fieldValues["RAY_TYPE"] = data[0]; + fieldValues["RAY_MEV"] = data[1]; + fieldValues["RAY_MEV_UNCERTAINTY"] = data[2]; + fieldValues["RAY_BRANCH_RATIO"] = data[3]; + fieldValues["RAY_BRANCH_RATIO_UNCERTAINTY"] = data[4]; + fieldValues["MAIN_RAY_IDENT"] = data[5]; + fieldValues["JIAHE_PEAK"] = data[6]; + fieldValues["CREATE_TIME"] = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + + qint64 newId = SqliteManager::instance().insertRow("nuclideRayInfo", fieldValues); + if (newId > 0) { + QMessageBox::information(this, "成功", "射线信息添加成功!"); + loadRayData(); // 刷新列表 + } else { + QMessageBox::warning(this, "失败", "添加失败:" + SqliteManager::instance().lastError()); + } + } +} + +void NuclideRayListDialog::onEditRayClicked(const QModelIndex &index) +{ + int row = index.row(); + QString id = m_model->index(row, 0).data().toString(); + QStringList data = getRayData(id); + if (data.isEmpty()) return; + + NuclideRayDialog dlg(true, data, this); + if (dlg.exec() == QDialog::Accepted) { + QStringList newData = dlg.getRayData(); + if (newData.size() < 8) return; + + QMap fieldValues; + fieldValues["RAY_TYPE"] = newData[1]; + fieldValues["RAY_MEV"] = newData[2]; + fieldValues["RAY_MEV_UNCERTAINTY"] = newData[3]; + fieldValues["RAY_BRANCH_RATIO"] = newData[4]; + fieldValues["RAY_BRANCH_RATIO_UNCERTAINTY"] = newData[5]; + fieldValues["MAIN_RAY_IDENT"] = newData[6]; + fieldValues["JIAHE_PEAK"] = newData[7]; + + int affectedRows = SqliteManager::instance().updateRow( + "nuclideRayInfo", + fieldValues, + "ID = ?", + {newData[0]} + ); + + if (affectedRows > 0) { + QMessageBox::information(this, "成功", "射线信息修改成功!"); + loadRayData(); + } else if (affectedRows == 0) { + QMessageBox::information(this, "提示", "未修改任何数据!"); + } else { + QMessageBox::warning(this, "失败", "修改失败:" + SqliteManager::instance().lastError()); + } + } +} + +void NuclideRayListDialog::onDeleteRayClicked(const QModelIndex &index) +{ + int row = index.row(); + QString id = m_model->index(row, 0).data().toString(); + + QMessageBox box(QMessageBox::Question, "提示", "确定删除该射线信息吗?", + QMessageBox::Yes | QMessageBox::No, this); + box.button(QMessageBox::Yes)->setText("确认"); + box.button(QMessageBox::No)->setText("取消"); + if (box.exec() != QMessageBox::Yes) return; + + bool ok = SqliteManager::instance().deleteRow("nuclideRayInfo", "ID = ?", {id}); + if (ok) { + loadRayData(); + } else { + QMessageBox::warning(this, "错误", "删除失败:" + SqliteManager::instance().lastError()); + } +} diff --git a/src/NuclideLib/NuclideRayListDialog.h b/src/NuclideLib/NuclideRayListDialog.h new file mode 100644 index 0000000..769a725 --- /dev/null +++ b/src/NuclideLib/NuclideRayListDialog.h @@ -0,0 +1,57 @@ +#ifndef NUCLIDERAYLISTDIALOG_H +#define NUCLIDERAYLISTDIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sqlitemanager.h" +#include "NuclideRayDialog.h" + +// 射线列表操作按钮委托 +class RayButtonDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit RayButtonDelegate(QObject *parent = nullptr); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; +signals: + void editButtonClicked(const QModelIndex &index) const; + void deleteButtonClicked(const QModelIndex &index) const; +}; + +class NuclideRayListDialog : public QDialog +{ + Q_OBJECT +public: + explicit NuclideRayListDialog(const QString& nuclideId, const QString& nuclideName, QWidget *parent = nullptr); + ~NuclideRayListDialog() = default; + +private slots: + void onAddRayClicked(); // 添加射线 + void onEditRayClicked(const QModelIndex &index); // 编辑射线 + void onDeleteRayClicked(const QModelIndex &index); // 删除射线 + +private: + void initUI(); // 初始化界面 + void loadRayData(); // 加载当前核素的射线数据 + QStringList getRayData(const QString &id); // 获取指定ID的射线数据 + + QString m_nuclideId; // 当前核素ID + QString m_nuclideName; // 当前核素名称 + QStandardItemModel *m_model; // 射线列表模型 + RayButtonDelegate *m_delegate;// 按钮委托 + QVector m_listRay; // 缓存射线数据 +}; + +#endif // NUCLIDERAYLISTDIALOG_H diff --git a/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.cpp b/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.cpp index 21a7b5a..f745a6f 100644 --- a/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.cpp +++ b/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.cpp @@ -30,7 +30,9 @@ void ConformityAnalysis::SetAnalyzeDataFilename(const QMap &d { QStringList ch_count_data_name = data_files_set.keys(); int conformCount = ui->comboBox->currentIndex() + 2; - for (const QString& ch_count_data_name : ch_count_data_name) { + + for (const QString& ch_count_data_name : ch_count_data_name) + { if(conformCount == ch_count_data_name.toInt()) { setCsvFile(data_files_set[ch_count_data_name].toString()); @@ -96,6 +98,8 @@ void ConformityAnalysis::slot_ClickedBoard(int board,int channel) ui->widget->setWidgetData(board,channel,m_boardChannel[board - 1][channel - 1],data.size()); } + + void ConformityAnalysis::handleBoard() { for(auto spetruData:_spectrumDataList) diff --git a/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.h b/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.h index 4204cb3..486f7ba 100644 --- a/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.h +++ b/src/ThreeDimensionalConformityAnalysisView/ConformityAnalysis.h @@ -6,15 +6,17 @@ #include "ParticleDataStatistics.h" #include "ThreeDDisplay.h" #include "csv.h" -#include "CoincidenceSpectrumProcess.h" #include "MeasureAnalysisView.h" #include -using namespace CoincidenceSpectrum; + +//using namespace CoincidenceSpectrum; + namespace Ui { class ConformityAnalysis; } + typedef struct particleCoincidenceEvent { int eventId;//事件ID @@ -24,6 +26,13 @@ typedef struct particleCoincidenceEvent qulonglong timeCounter;//时间计数 }PARTICLECOINCIDENCEEVENT; +//// 用于存储三维数据 +//typedef struct SurfacePoint { +// float primaryEnergy; // 初级粒子能量 +// float secondaryEnergySum; // 次级粒子能量和 +// int count; // 符合事件计数 +//}SURFACEPOINT; + class ConformityAnalysis : public MeasureAnalysisView { Q_OBJECT @@ -50,7 +59,6 @@ public: private slots: void slot_InitialState(); void slot_ClickedBoard(int board,int channel); - private: //处理板卡信息 void handleBoard(); @@ -76,7 +84,7 @@ private: QString m_fileName; - std::vector m_CoincidenceEventVector;//所有的能谱符合处理 +// std::vector m_CoincidenceEventVector;//所有的能谱符合处理 int m_boardChannel[MAX_BOARD][MAX_CHANNEL]; diff --git a/src/ThreeDimensionalConformityAnalysisView/ParticleDataStatistics.h b/src/ThreeDimensionalConformityAnalysisView/ParticleDataStatistics.h index 53eff6e..4511305 100644 --- a/src/ThreeDimensionalConformityAnalysisView/ParticleDataStatistics.h +++ b/src/ThreeDimensionalConformityAnalysisView/ParticleDataStatistics.h @@ -3,14 +3,35 @@ #include #include "DetectorStatusSummary.h" -#include "CoincidenceSpectrumProcess.h" +//#include "CoincidenceSpectrumProcess.h" #include -using namespace CoincidenceSpectrum; +//using namespace CoincidenceSpectrum; namespace Ui { class ParticleDataStatistics; } +// 二次符合事件处理 +namespace F2t9Order { + + // 能谱数据结构 + struct SpectrumData { + int board_id; // 板卡号 + int channel_id; // 通道号 + double energy; // 能量 + unsigned long long timestamp; // 时间戳(纳秒) + }; + + // 符合事件结果结构 + struct CoincidenceEvent { + int coincidence_order; // 符合次数(2-9) + std::vector events; // 符合的事件集合 + unsigned int time_window; // 使用的时间窗口(秒) + }; +} +// 定义板卡和通道的最大数量 +static constexpr int MAX_BOARD = 8; +static constexpr int MAX_CHANNEL = 4; class ParticleDataStatistics : public QWidget { Q_OBJECT diff --git a/src/ThreeDimensionalConformityAnalysisView/ThreeDDisplay.h b/src/ThreeDimensionalConformityAnalysisView/ThreeDDisplay.h index c0e0170..aec4793 100644 --- a/src/ThreeDimensionalConformityAnalysisView/ThreeDDisplay.h +++ b/src/ThreeDimensionalConformityAnalysisView/ThreeDDisplay.h @@ -5,15 +5,15 @@ #include #include -#include "CoincidenceSpectrumProcess.h" +//#include "CoincidenceSpectrumProcess.h" using namespace QtDataVisualization; -//// 用于存储三维数据 -//typedef struct SurfacePoint { -// float primaryEnergy; // 初级粒子能量 -// float secondaryEnergySum; // 次级粒子能量和 -// int count; // 符合事件计数 -//}SURFACEPOINT; +// 用于存储三维数据 +typedef struct SurfacePoint { + float primaryEnergy; // 初级粒子能量 + float secondaryEnergySum; // 次级粒子能量和 + int count; // 符合事件计数 +}SURFACEPOINT; namespace Ui { class ThreeDDisplay; diff --git a/src/src.pro b/src/src.pro index 0b39fa0..44f3290 100644 --- a/src/src.pro +++ b/src/src.pro @@ -84,6 +84,9 @@ SOURCES += \ MeasureAnalysisHistoryForm/MeasureAnalysisHistoryForm.cpp \ MeasureDeviceParamsConfigView/DeviceParamsTableForm.cpp \ MeasureDeviceParamsConfigView/MeasureDeviceParamsConfigView.cpp \ + NuclideLib/NuclideEditDialog.cpp \ + NuclideLib/NuclideRayDialog.cpp \ + NuclideLib/NuclideRayListDialog.cpp \ NuclideLib/sqlitemanager.cpp \ ParticleCountPlotView/BatchEnergyScaleDialog.cpp \ ParticleCountPlotView/FindPeaksResultDialog.cpp \ @@ -132,6 +135,9 @@ HEADERS += \ MeasureAnalysisHistoryForm/MeasureAnalysisHistoryForm.h \ MeasureDeviceParamsConfigView/DeviceParamsTableForm.h \ MeasureDeviceParamsConfigView/MeasureDeviceParamsConfigView.h \ + NuclideLib/NuclideEditDialog.h \ + NuclideLib/NuclideRayDialog.h \ + NuclideLib/NuclideRayListDialog.h \ NuclideLib/sqlitemanager.h \ ParticleCountPlotView/BatchEnergyScaleDialog.h \ ParticleCountPlotView/FindPeaksResultDialog.h \