fix(ui): 审查修复 loadFailed接线/控制器重入守卫+ddCode降级/散点越界/点异常/Tab deleteLater/makeLines

- main.cpp: DatasetDetailController::loadFailed 连接到 statusBar,5s 显示失败消息
- DatasetDetailController: 加 busy_ 重入守卫;ddCode 非 dd_inversion_data 时直接 loadFailed 降级
- DatasetChartView::showScatter: 按 x/y/v 三者最短长度循环,防越界;加 #include <algorithm>
- DatasetChartView::rebuildAnomalyItems: 先处理 markType==1 点异常(小方块),再走原折线/多边形分支
- DatasetChartView::clearChart: 加注释说明 scene_->clear() + anomalyItems_.clear() 不变式
- DatasetDetailPanel: tabCloseRequested 改用 deleteLater,避免在事件处理期间同步析构
- ContourBands: buildContourBands 按 opt.makeLines 决定是否开 GenerateContourEdgesOn 及提取等值线
This commit is contained in:
gaozheng 2026-06-11 12:42:04 +08:00
parent 548895bdcf
commit 82b654176e
6 changed files with 50 additions and 17 deletions

View File

@ -508,6 +508,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
detailPanel, [detailPanel](const QString& dsId) { detailPanel, [detailPanel](const QString& dsId) {
detailPanel->focusDataset(dsId); detailPanel->focusDataset(dsId);
}); });
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, &window,
[&window](const QString& dsId, const QString& msg) {
window.statusBar()->showMessage(
QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000);
});
// ── 详情面板切 Tab → 反向高亮数据集列表对应行 ── // ── 详情面板切 Tab → 反向高亮数据集列表对应行 ──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged, QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged,

View File

@ -4,7 +4,7 @@ namespace geopro::app {
DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) { DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) {
setTabsClosable(true); setTabsClosable(true);
connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { delete widget(i); }); connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { widget(i)->deleteLater(); });
connect(this, &QTabWidget::currentChanged, this, [this](int i) { connect(this, &QTabWidget::currentChanged, this, [this](int i) {
if (auto* p = qobject_cast<DatasetDetailPage*>(widget(i))) if (auto* p = qobject_cast<DatasetDetailPage*>(widget(i)))
emit activeDatasetChanged(p->dsId()); emit activeDatasetChanged(p->dsId());

View File

@ -1,4 +1,5 @@
#include "panels/chart/DatasetChartView.hpp" #include "panels/chart/DatasetChartView.hpp"
#include <algorithm>
#include <QGraphicsScene> #include <QGraphicsScene>
#include <QGraphicsPathItem> #include <QGraphicsPathItem>
#include <QGraphicsRectItem> #include <QGraphicsRectItem>
@ -24,13 +25,18 @@ DatasetChartView::DatasetChartView(QWidget* parent)
} }
void DatasetChartView::clearChart() { void DatasetChartView::clearChart() {
// scene_->clear() 删除场景中的所有 item含 anomalyItems_ 所指向的对象);
// anomalyItems_.clear() 随后清空向量中的悬空指针。
// 二者必须成对、顺序不可颠倒:若先 clear() 后仍持有指针rebuildAnomalyItems
// 中的 delete 会对已释放对象 double-free若先清向量、后继续操作旧指针同样 UB。
scene_->clear(); anomalyItems_.clear(); scene_->clear(); anomalyItems_.clear();
} }
void DatasetChartView::showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs) { void DatasetChartView::showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs) {
clearChart(); clearChart();
const double sz = 0.6; // 方块边长(数据单位,近似 web 方点) const double sz = 0.6; // 方块边长(数据单位,近似 web 方点)
for (size_t i = 0; i < f.v.size(); ++i) { const size_t n = std::min({f.x.size(), f.y.size(), f.v.size()});
for (size_t i = 0; i < n; ++i) {
auto* r = scene_->addRect(f.x[i] - sz / 2, f.y[i] - sz / 2, sz, sz, auto* r = scene_->addRect(f.x[i] - sz / 2, f.y[i] - sz / 2, sz, sz,
QPen(Qt::white, 0), QBrush(toQ(cs.colorAt(f.v[i])))); QPen(Qt::white, 0), QBrush(toQ(cs.colorAt(f.v[i]))));
r->setPen(QPen(Qt::white, 0)); // 白描边 r->setPen(QPen(Qt::white, 0)); // 白描边
@ -81,6 +87,16 @@ void DatasetChartView::rebuildAnomalyItems() {
for (int i = 0; i < static_cast<int>(anomalies_.size()); ++i) { for (int i = 0; i < static_cast<int>(anomalies_.size()); ++i) {
if (hidden_.count(i)) continue; if (hidden_.count(i)) continue;
const auto& a = anomalies_[i]; const auto& a = anomalies_[i];
if (a.localPts.empty()) continue;
if (static_cast<int>(a.markType) == 1) { // 点:小方块标记
const double s = 0.8;
auto* dot = scene_->addRect(a.localPts[0].x - s/2, a.localPts[0].y - s/2, s, s,
QPen(QColor(QString::fromStdString(a.lineColor)), 0),
QBrush(QColor(QString::fromStdString(a.lineColor))));
dot->setZValue(2);
anomalyItems_.push_back(dot);
continue;
}
if (a.localPts.size() < 2) continue; if (a.localPts.size() < 2) continue;
QPainterPath path; path.moveTo(a.localPts[0].x, a.localPts[0].y); QPainterPath path; path.moveTo(a.localPts[0].x, a.localPts[0].y);
for (size_t k = 1; k < a.localPts.size(); ++k) path.lineTo(a.localPts[k].x, a.localPts[k].y); for (size_t k = 1; k < a.localPts.size(); ++k) path.lineTo(a.localPts[k].x, a.localPts[k].y);

View File

@ -7,6 +7,13 @@ DatasetDetailController::DatasetDetailController(data::IDatasetRepository& repo,
: QObject(parent), repo_(repo) {} : QObject(parent), repo_(repo) {}
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) { void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) {
if (busy_) return; // 防重入(同步网络期间 QEventLoop 可重入)
busy_ = true;
if (ddCode != QLatin1String("dd_inversion_data")) { // 首版仅支持 ERT 反演
busy_ = false;
emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
return;
}
const std::string id = dsId.toStdString(); const std::string id = dsId.toStdString();
try { try {
ChartData d; ChartData d;
@ -17,8 +24,10 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd
d.grid = repo_.loadGrid(id); d.grid = repo_.loadGrid(id);
d.gridScale = repo_.loadColorScale(id); d.gridScale = repo_.loadColorScale(id);
d.anomalies = repo_.loadAnomalies(id); d.anomalies = repo_.loadAnomalies(id);
busy_ = false;
emit chartReady(d); emit chartReady(d);
} catch (const std::exception& e) { } catch (const std::exception& e) {
busy_ = false;
emit loadFailed(dsId, QString::fromStdString(e.what())); emit loadFailed(dsId, QString::fromStdString(e.what()));
} }
} }

View File

@ -31,5 +31,6 @@ signals:
void loadFailed(const QString& dsId, const QString& message); void loadFailed(const QString& dsId, const QString& message);
private: private:
data::IDatasetRepository& repo_; data::IDatasetRepository& repo_;
bool busy_ = false;
}; };
} // namespace geopro::controller } // namespace geopro::controller

View File

@ -130,7 +130,7 @@ ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const
banded->SetInputConnection(surf->GetOutputPort()); banded->SetInputConnection(surf->GetOutputPort());
banded->SetNumberOfContours(static_cast<int>(stops.size())); banded->SetNumberOfContours(static_cast<int>(stops.size()));
for (int i = 0; i < static_cast<int>(stops.size()); ++i) banded->SetValue(i, stops[i]); for (int i = 0; i < static_cast<int>(stops.size()); ++i) banded->SetValue(i, stops[i]);
banded->GenerateContourEdgesOn(); if (opt.makeLines) banded->GenerateContourEdgesOn();
banded->SetScalarModeToValue(); banded->SetScalarModeToValue();
banded->Update(); banded->Update();
@ -152,21 +152,23 @@ ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const
out.bands.push_back(std::move(bp)); out.bands.push_back(std::move(bp));
} }
// port1等值线polylines+ DP 简化。 // port1等值线polylines+ DP 简化。仅 makeLines=true 时生成(否则不提取)。
vtkPolyData* edges = banded->GetContourEdgesOutput(); if (opt.makeLines) {
if (edges) { vtkPolyData* edges = banded->GetContourEdgesOutput();
edges->BuildCells(); if (edges) {
const vtkIdType nLines = edges->GetNumberOfCells(); edges->BuildCells();
for (vtkIdType c = 0; c < nLines; ++c) { const vtkIdType nLines = edges->GetNumberOfCells();
vtkCell* cell = edges->GetCell(c); for (vtkIdType c = 0; c < nLines; ++c) {
vtkPoints* cp = cell->GetPoints(); vtkCell* cell = edges->GetCell(c);
ContourLine cl; cl.level = 0.0; vtkPoints* cp = cell->GetPoints();
for (vtkIdType p = 0; p < cp->GetNumberOfPoints(); ++p) { ContourLine cl; cl.level = 0.0;
double xyz[3]; cp->GetPoint(p, xyz); for (vtkIdType p = 0; p < cp->GetNumberOfPoints(); ++p) {
cl.pts.push_back(Vec2{xyz[0], xyz[1]}); double xyz[3]; cp->GetPoint(p, xyz);
cl.pts.push_back(Vec2{xyz[0], xyz[1]});
}
simplifyInPlace(cl.pts, opt.simplifyTol);
if (cl.pts.size() >= 2) out.lines.push_back(std::move(cl));
} }
simplifyInPlace(cl.pts, opt.simplifyTol);
if (cl.pts.size() >= 2) out.lines.push_back(std::move(cl));
} }
} }
return out; return out;