feat(vtk): 异常属性对话框(#4c-3, R83)-双击异常列表弹只读属性

- AnomalyPropertiesDialog:名称/类型/标记类型/归属三维体/异常体/顶点世界坐标/备注,只读
- Column3DAnalysis:留存 anomalies_,双击 itemDoubleClicked 按 id 回查发 anomalyPropertiesRequested
- main:接线打开对话框
- 截图字段:模型/端点无,不展示(保存对话框截图为mock未持久化)

编译绿(build.bat app);用户实测通过。#4 异常功能收口。
This commit is contained in:
gaozheng 2026-06-18 19:26:02 +08:00
parent f1309240a4
commit c83f63a8f5
6 changed files with 113 additions and 0 deletions

View File

@ -0,0 +1,74 @@
#include "AnomalyPropertiesDialog.hpp"
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QPlainTextEdit>
#include <QVBoxLayout>
namespace geopro::app {
namespace {
QString markTypeLabel(geopro::core::AnomalyMarkType t) {
switch (t) {
case geopro::core::AnomalyMarkType::Point: return QStringLiteral("");
case geopro::core::AnomalyMarkType::Polyline: return QStringLiteral("折线");
case geopro::core::AnomalyMarkType::Polygon: return QStringLiteral("多边形");
}
return QStringLiteral("");
}
QString orDash(const std::string& s) {
return s.empty() ? QStringLiteral("") : QString::fromStdString(s);
}
} // namespace
AnomalyPropertiesDialog::AnomalyPropertiesDialog(const geopro::core::Anomaly& a, QWidget* parent)
: QDialog(parent) {
setWindowTitle(QStringLiteral("异常属性"));
setModal(true);
auto* root = new QVBoxLayout(this);
auto* form = new QFormLayout();
form->addRow(QStringLiteral("名称"), new QLabel(orDash(a.name)));
form->addRow(QStringLiteral("类型"), new QLabel(orDash(a.typeName)));
form->addRow(QStringLiteral("标记类型"), new QLabel(markTypeLabel(a.markType)));
form->addRow(QStringLiteral("归属三维体"), new QLabel(orDash(a.volumeDsId)));
form->addRow(QStringLiteral("异常体"),
new QLabel(a.consortiumId.empty() ? QStringLiteral("(未分组)")
: QString::fromStdString(a.consortiumId)));
root->addLayout(form);
// 顶点世界坐标只读列表x/y/z 每行一个点)。
root->addWidget(new QLabel(QStringLiteral("顶点坐标(%1 个)").arg(a.worldPts.size())));
auto* pts = new QPlainTextEdit();
pts->setReadOnly(true);
pts->setFixedHeight(120);
QString text;
for (std::size_t i = 0; i < a.worldPts.size(); ++i) {
const auto& p = a.worldPts[i];
text += QStringLiteral("%1: (%2, %3, %4)\n")
.arg(i + 1)
.arg(p.x, 0, 'f', 2)
.arg(p.y, 0, 'f', 2)
.arg(p.z, 0, 'f', 2);
}
pts->setPlainText(text);
root->addWidget(pts);
// 备注(只读)。
root->addWidget(new QLabel(QStringLiteral("备注")));
auto* remark = new QPlainTextEdit();
remark->setReadOnly(true);
remark->setFixedHeight(60);
remark->setPlainText(QString::fromStdString(a.remark));
root->addWidget(remark);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
root->addWidget(buttons);
}
} // namespace geopro::app

View File

@ -0,0 +1,17 @@
#pragma once
#include <QDialog>
#include "model/Anomaly.hpp"
namespace geopro::app {
// 异常属性对话框(#4c-3需求 R83双击异常列表项弹出只读展示选中异常的
// 名称/类型/标记类型/备注/归属三维体/异常体分组/顶点世界坐标。
// 截图:模型与异常端点均无截图字段(保存对话框的截图仅为 mock 预览、未持久化),故不展示。
class AnomalyPropertiesDialog : public QDialog {
Q_OBJECT
public:
AnomalyPropertiesDialog(const geopro::core::Anomaly& a, QWidget* parent = nullptr);
};
} // namespace geopro::app

View File

@ -65,6 +65,7 @@ add_executable(geopro_desktop WIN32
ImportDatasetDialog.cpp
ExportDatasetDialog.cpp
AnomalySaveDialog.cpp
AnomalyPropertiesDialog.cpp
SettingsDialog.cpp
SliceExport.cpp
VolumeParamsDialog.cpp

View File

@ -98,6 +98,7 @@
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include "AnomalySaveDialog.hpp"
#include "AnomalyPropertiesDialog.hpp"
#include "SettingsDialog.hpp"
#include "SliceExport.hpp"
#include "TopBar.hpp"
@ -785,6 +786,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
[sceneView](const QString& id) {
sceneView->setSelectedAnomaly(id.toStdString());
});
// 双击异常 → 只读属性对话框R83名称/类型/标记/归属/坐标/备注)。
QObject::connect(ca, &geopro::app::Column3DAnalysis::anomalyPropertiesRequested, &window,
[&window](const geopro::core::Anomaly& a) {
geopro::app::AnomalyPropertiesDialog dlg(a, &window);
dlg.exec();
});
// 删除异常 → 删 mock + 刷新渲染/列表。
QObject::connect(ca, &geopro::app::Column3DAnalysis::anomalyDeleteRequested, &window,
[scene3dRepo, refreshAnomalies](const QString& id) {

View File

@ -57,6 +57,17 @@ Column3DAnalysis::Column3DAnalysis(QWidget* parent) : QWidget(parent) {
[this](QTreeWidgetItem* cur, QTreeWidgetItem*) {
if (cur != nullptr) emit anomalySelected(cur->data(0, kDsIdRole).toString());
});
// 双击异常项 → 属性对话框R83按 id 回查当前集合发出整条异常。
connect(anomalyTree_, &QTreeWidget::itemDoubleClicked, this,
[this](QTreeWidgetItem* it, int) {
if (it == nullptr) return;
const QString id = it->data(0, kDsIdRole).toString();
for (const auto& a : anomalies_)
if (QString::fromStdString(a.id) == id) {
emit anomalyPropertiesRequested(a);
return;
}
});
auto* anomGroup = new QGroupBox(QStringLiteral("异常"));
auto* gv = new QVBoxLayout(anomGroup);
@ -92,6 +103,7 @@ int Column3DAnalysis::anomalyFilterMode() const {
}
void Column3DAnalysis::setAnomalies(const std::vector<geopro::core::Anomaly>& anoms) {
anomalies_ = anoms; // 留存供双击查属性R83
QSignalBlocker block(anomalyTree_); // 填充不触发 visibilityChanged
anomalyTree_->clear();
for (const auto& a : anoms) {

View File

@ -41,6 +41,7 @@ signals:
void anomalyDisplayFilterChanged(int mode); // 过滤档位 0..3
void anomalySelected(const QString& anomalyId); // 列表选中→VTK 高亮
void anomalyDeleteRequested(const QString& anomalyId); // 右键删除
void anomalyPropertiesRequested(const geopro::core::Anomaly& a); // 双击→属性对话框(R83)
private:
void onContextMenu(const QPoint& pos);
@ -48,6 +49,7 @@ private:
QTreeWidget* tree_ = nullptr;
QTreeWidget* anomalyTree_ = nullptr;
QComboBox* anomalyFilter_ = nullptr;
std::vector<geopro::core::Anomaly> anomalies_; // 当前展示集合(双击查属性按 id 回查)
};
} // namespace geopro::app