fix(ui): 创建三维体对话框 左侧源/右侧生成位置 改树型结构(4b)

- 左「已选择对象」:QListWidget 平铺 → QTreeWidget(项目/GS/TM 容器树 + 可勾选源 ds 挂各自结构归属下,
  容器按源 ds 的 structParentId 向上剪枝)
- 右「生成位置」:QComboBox 平铺 → QTreeWidget(GS/项目根/TM 层级单选);confType 据所选节点 type 推导
- ctor 改签名:传 VolumeSourceItem(含 structParentId)+ StructNode 结构 + defaultMountId;main 同步传 structParentId

构建:app 链接通过
This commit is contained in:
gaozheng 2026-06-25 19:54:44 +08:00
parent b2904c211e
commit 40c8d6ccb4
3 changed files with 134 additions and 62 deletions

View File

@ -1,17 +1,22 @@
#include "VolumeParamsDialog.hpp" #include "VolumeParamsDialog.hpp"
#include <QComboBox> #include <map>
#include <set>
#include "EmptyAwareComboBox.hpp" #include <QComboBox>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QFormLayout> #include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHash>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QListWidget>
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "EmptyAwareComboBox.hpp"
#include "FormKit.hpp" #include "FormKit.hpp"
#include "Theme.hpp" #include "Theme.hpp"
@ -23,13 +28,14 @@ constexpr double kDefCellXY = 1.0;
constexpr double kDefCellZ = 0.5; constexpr double kDefCellZ = 0.5;
constexpr double kDefPower = 2.0; constexpr double kDefPower = 2.0;
constexpr double kDefMaxDist = 4.0; constexpr double kDefMaxDist = 4.0;
constexpr int kRoleDsId = Qt::UserRole + 1; // 源列表项存 dsId constexpr int kRoleDsId = Qt::UserRole + 1; // 源树项存 dsId
constexpr int kRoleMountId = Qt::UserRole + 1; // 生成位置项存 iditemData 默认存 confType constexpr int kRoleMountId = Qt::UserRole + 1; // 生成位置树项存 id
constexpr int kRoleMountConfType = Qt::UserRole + 2; // 生成位置树项存 confType
} // namespace } // namespace
VolumeParamsDialog::VolumeParamsDialog(const QVector<QPair<QString, QString>>& sources, VolumeParamsDialog::VolumeParamsDialog(const QVector<VolumeSourceItem>& sources,
const QVector<VolumeMountTarget>& mounts, const std::vector<geopro::data::StructNode>& structure,
int defaultMountIndex, QWidget* parent) const QString& defaultMountId, QWidget* parent)
: QDialog(parent) { : QDialog(parent) {
setWindowTitle(QStringLiteral("生成三维体")); setWindowTitle(QStringLiteral("生成三维体"));
setModal(true); setModal(true);
@ -40,21 +46,58 @@ VolumeParamsDialog::VolumeParamsDialog(const QVector<QPair<QString, QString>>& s
geopro::app::applyTokenizedStyleSheet(intro, QStringLiteral("color:{{text/secondary}};")); geopro::app::applyTokenizedStyleSheet(intro, QStringLiteral("color:{{text/secondary}};"));
root->addWidget(intro); root->addWidget(intro);
// 左右两栏:左·源数据集列表(可勾选/取消,二次确认或修改) | 右·参数卡片。
auto* cols = new QHBoxLayout(); auto* cols = new QHBoxLayout();
// ── 左:源数据集列表 ── // ── 左:源数据集树(项目/GS/TM 容器 + 可勾选源 ds挂到各自结构归属下──
auto* leftCard = formkit::formCard(this); auto* leftCard = formkit::formCard(this);
auto* leftLay = formkit::cardBody(leftCard); auto* leftLay = formkit::cardBody(leftCard);
formkit::addSection(leftLay, QStringLiteral("源数据集"), leftCard, false); formkit::addSection(leftLay, QStringLiteral("源数据集"), leftCard, false);
sourceList_ = new QListWidget(); sourceTree_ = new QTreeWidget();
sourceTree_->setHeaderHidden(true);
sourceTree_->setIndentation(14);
{
std::map<std::string, const geopro::data::StructNode*> byId;
for (const auto& n : structure) byId[n.id] = &n;
// 仅保留「源 ds 结构归属」向上的祖先链容器(树剪枝,不展示无关 GS/TM
std::set<std::string> keep;
for (const auto& s : sources) { for (const auto& s : sources) {
auto* it = new QListWidgetItem(s.second.isEmpty() ? s.first : s.second, sourceList_); std::string p = s.structParentId.toStdString();
it->setData(kRoleDsId, s.first); while (!p.empty() && byId.count(p) && !keep.count(p)) {
it->setFlags(it->flags() | Qt::ItemIsUserCheckable); keep.insert(p);
it->setCheckState(Qt::Checked); // 默认全勾(=传入的当前勾选源) p = byId[p]->parentId;
} }
leftLay->addWidget(sourceList_); }
QHash<QString, QTreeWidgetItem*> cont; // 容器 id → item
for (const auto& n : structure)
if (keep.count(n.id)) {
auto* it = new QTreeWidgetItem();
it->setText(0, QString::fromStdString(n.name));
cont.insert(QString::fromStdString(n.id), it);
}
for (const auto& n : structure)
if (keep.count(n.id)) {
auto* it = cont.value(QString::fromStdString(n.id));
auto* par = cont.value(QString::fromStdString(n.parentId), nullptr);
if (par)
par->addChild(it);
else
sourceTree_->addTopLevelItem(it);
}
for (const auto& s : sources) {
auto* it = new QTreeWidgetItem();
it->setText(0, s.name.isEmpty() ? s.id : s.name);
it->setData(0, kRoleDsId, s.id);
it->setFlags(it->flags() | Qt::ItemIsUserCheckable);
it->setCheckState(0, Qt::Checked); // 默认全勾(=传入的当前勾选源)
auto* par = cont.value(s.structParentId, nullptr);
if (par)
par->addChild(it);
else
sourceTree_->addTopLevelItem(it);
}
}
sourceTree_->expandAll();
leftLay->addWidget(sourceTree_);
cols->addWidget(leftCard, 1); cols->addWidget(leftCard, 1);
// ── 右:参数 ── // ── 右:参数 ──
@ -66,18 +109,40 @@ VolumeParamsDialog::VolumeParamsDialog(const QVector<QPair<QString, QString>>& s
name_ = new QLineEdit(QStringLiteral("三维体")); name_ = new QLineEdit(QStringLiteral("三维体"));
formkit::capField(name_); formkit::capField(name_);
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_); form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
cardLay->addLayout(form);
// 生成位置下拉:项目内 GS/项目根/TMitemData 存 confTypeUserRole+1 存 id // 生成位置GS/项目根/TM 层级树(单选),替代原扁平下拉。
mount_ = new EmptyAwareComboBox(); formkit::addSection(cardLay, QStringLiteral("生成位置"), card, false);
for (const auto& m : mounts) { mountTree_ = new QTreeWidget();
mount_->addItem(m.name, m.confType); mountTree_->setHeaderHidden(true);
mount_->setItemData(mount_->count() - 1, m.id, kRoleMountId); mountTree_->setIndentation(14);
mountTree_->setMaximumHeight(geopro::app::scaledPx(160));
{
QHash<QString, QTreeWidgetItem*> mItems;
for (const auto& n : structure) {
auto* it = new QTreeWidgetItem();
it->setText(0, QString::fromStdString(n.name));
it->setData(0, kRoleMountId, QString::fromStdString(n.id));
it->setData(0, kRoleMountConfType, n.type == 2 ? 2 : 1);
mItems.insert(QString::fromStdString(n.id), it);
} }
if (defaultMountIndex >= 0 && defaultMountIndex < mounts.size()) for (const auto& n : structure) {
mount_->setCurrentIndex(defaultMountIndex); auto* it = mItems.value(QString::fromStdString(n.id));
formkit::capField(mount_); auto* par = mItems.value(QString::fromStdString(n.parentId), nullptr);
form->addRow(formkit::editLabel(QStringLiteral("生成位置")), mount_); if (par)
par->addChild(it);
else
mountTree_->addTopLevelItem(it);
}
mountTree_->expandAll();
// 默认选中:指定的 defaultMountId否则首个节点。
QTreeWidgetItem* def = mItems.value(defaultMountId, nullptr);
if (!def && mountTree_->topLevelItemCount() > 0) def = mountTree_->topLevelItem(0);
if (def) mountTree_->setCurrentItem(def);
}
cardLay->addWidget(mountTree_);
auto* form2 = formkit::makeEditForm();
model_ = new EmptyAwareComboBox(); model_ = new EmptyAwareComboBox();
model_->addItem(QStringLiteral("反距离加权 (IDW)"), model_->addItem(QStringLiteral("反距离加权 (IDW)"),
static_cast<int>(geopro::data::VolumeBuildParams::Model::Idw)); static_cast<int>(geopro::data::VolumeBuildParams::Model::Idw));
@ -88,7 +153,7 @@ VolumeParamsDialog::VolumeParamsDialog(const QVector<QPair<QString, QString>>& s
} }
model_->setCurrentIndex(0); model_->setCurrentIndex(0);
formkit::capField(model_); formkit::capField(model_);
form->addRow(formkit::editLabel(QStringLiteral("插值模型")), model_); form2->addRow(formkit::editLabel(QStringLiteral("插值模型")), model_);
auto makeSpin = [this](double val, double min, double max, double step, int decimals) { auto makeSpin = [this](double val, double min, double max, double step, int decimals) {
auto* s = new QDoubleSpinBox(); auto* s = new QDoubleSpinBox();
@ -103,12 +168,12 @@ VolumeParamsDialog::VolumeParamsDialog(const QVector<QPair<QString, QString>>& s
cellZ_ = makeSpin(kDefCellZ, 0.01, 1000.0, 0.5, 2); cellZ_ = makeSpin(kDefCellZ, 0.01, 1000.0, 0.5, 2);
power_ = makeSpin(kDefPower, 0.5, 6.0, 0.5, 1); power_ = makeSpin(kDefPower, 0.5, 6.0, 0.5, 1);
maxDist_ = makeSpin(kDefMaxDist, 0.1, 10000.0, 1.0, 2); maxDist_ = makeSpin(kDefMaxDist, 0.1, 10000.0, 1.0, 2);
form->addRow(formkit::editLabel(QStringLiteral("水平间距 (米)")), cellXY_); form2->addRow(formkit::editLabel(QStringLiteral("水平间距 (米)")), cellXY_);
form->addRow(formkit::editLabel(QStringLiteral("竖向间距 (米)")), cellZ_); form2->addRow(formkit::editLabel(QStringLiteral("竖向间距 (米)")), cellZ_);
form->addRow(formkit::editLabel(QStringLiteral("IDW 幂次")), power_); form2->addRow(formkit::editLabel(QStringLiteral("IDW 幂次")), power_);
form->addRow(formkit::editLabel(QStringLiteral("最大影响距离 (米)")), maxDist_); form2->addRow(formkit::editLabel(QStringLiteral("最大影响距离 (米)")), maxDist_);
cardLay->addLayout(form2);
cardLay->addLayout(form);
cols->addWidget(card, 1); cols->addWidget(card, 1);
root->addLayout(cols); root->addLayout(cols);
@ -132,19 +197,21 @@ geopro::data::VolumeBuildParams VolumeParamsDialog::params() const {
QStringList VolumeParamsDialog::sourceDatasetIds() const { QStringList VolumeParamsDialog::sourceDatasetIds() const {
QStringList ids; QStringList ids;
for (int i = 0; i < sourceList_->count(); ++i) { for (QTreeWidgetItemIterator it(sourceTree_); *it; ++it) {
QListWidgetItem* it = sourceList_->item(i); const QString id = (*it)->data(0, kRoleDsId).toString();
if (it->checkState() == Qt::Checked) ids << it->data(kRoleDsId).toString(); if (!id.isEmpty() && (*it)->checkState(0) == Qt::Checked) ids << id;
} }
return ids; return ids;
} }
QString VolumeParamsDialog::mountTargetId() const { QString VolumeParamsDialog::mountTargetId() const {
return mount_->currentData(kRoleMountId).toString(); auto* it = mountTree_->currentItem();
return it ? it->data(0, kRoleMountId).toString() : QString();
} }
int VolumeParamsDialog::mountConfType() const { int VolumeParamsDialog::mountConfType() const {
const int ct = mount_->currentData().toInt(); auto* it = mountTree_->currentItem();
const int ct = it ? it->data(0, kRoleMountConfType).toInt() : 0;
return ct == 0 ? 1 : ct; // 缺省按 GS/项目根 return ct == 0 ? 1 : ct; // 缺省按 GS/项目根
} }

View File

@ -1,51 +1,53 @@
#pragma once #pragma once
#include <QDialog> #include <QDialog>
#include <QPair>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QVector> #include <QVector>
#include <vector>
#include "repo/RepoTypes.hpp" // StructNode
#include "repo/VolumeBuildParams.hpp" #include "repo/VolumeBuildParams.hpp"
class QLineEdit; class QLineEdit;
class QComboBox; class QComboBox;
class QDoubleSpinBox; class QDoubleSpinBox;
class QListWidget; class QTreeWidget;
namespace geopro::app { namespace geopro::app {
// 生成位置候选spec §7/§8项目内 GS/项目根/TM。confType1=GS/项目根 2=TM // 源数据集参与生成id + 显示名 + 结构归属structParentId用于在左侧树里挂到其 GS/TM 下)
struct VolumeMountTarget { struct VolumeSourceItem {
QString id; QString id;
QString name; QString name;
int confType = 1; QString structParentId;
}; };
// 「生成三维体」对话框spec §7/§8左侧·源数据集列表(可勾选/取消,二次确认或修改 // 「生成三维体」对话框spec §7/§8左侧·源数据集**树**(项目/GS/TM/ds可勾选/取消
// 右侧·参数(名称 + 生成位置下拉 + 插值模型 + cellXY/cellZ/power/maxDist // 右侧·参数(名称 + 生成位置**树**[GS/项目根/TM 层级单选] + 插值模型 + cellXY/cellZ/power/maxDist
class VolumeParamsDialog : public QDialog { class VolumeParamsDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
// sources参与生成的源数据集 (dsId, 显示名)mounts生成位置候选defaultMountIndex默认选中位置下标。 // sources参与生成的源 ds含结构归属structure项目结构(GS/TM),左右两树共用;
VolumeParamsDialog(const QVector<QPair<QString, QString>>& sources, // defaultMountId默认选中的生成位置 id空=选首个)。
const QVector<VolumeMountTarget>& mounts, int defaultMountIndex, VolumeParamsDialog(const QVector<VolumeSourceItem>& sources,
QWidget* parent = nullptr); const std::vector<geopro::data::StructNode>& structure,
const QString& defaultMountId, QWidget* parent = nullptr);
QString volumeName() const; QString volumeName() const;
geopro::data::VolumeBuildParams params() const; geopro::data::VolumeBuildParams params() const;
QStringList sourceDatasetIds() const; // 左侧最终勾选的源 ds id QStringList sourceDatasetIds() const; // 左侧最终勾选的源 ds id
QString mountTargetId() const; // 生成位置 idstructParentId QString mountTargetId() const; // 生成位置 idstructParentId
int mountConfType() const; // 生成位置 confType1=GS/项目根 2=TM int mountConfType() const; // 生成位置 confType1=GS/项目根 2=TM
private: private:
QLineEdit* name_ = nullptr; QLineEdit* name_ = nullptr;
QComboBox* mount_ = nullptr;
QComboBox* model_ = nullptr; QComboBox* model_ = nullptr;
QDoubleSpinBox* cellXY_ = nullptr; QDoubleSpinBox* cellXY_ = nullptr;
QDoubleSpinBox* cellZ_ = nullptr; QDoubleSpinBox* cellZ_ = nullptr;
QDoubleSpinBox* power_ = nullptr; QDoubleSpinBox* power_ = nullptr;
QDoubleSpinBox* maxDist_ = nullptr; QDoubleSpinBox* maxDist_ = nullptr;
QListWidget* sourceList_ = nullptr; QTreeWidget* sourceTree_ = nullptr; // 左:源数据集树(可勾选)
QTreeWidget* mountTree_ = nullptr; // 右:生成位置树(单选)
}; };
} // namespace geopro::app } // namespace geopro::app

View File

@ -718,20 +718,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
[&window, &nav, scene3dRepo, refreshAnalysis, lastSourceRows, [&window, &nav, scene3dRepo, refreshAnalysis, lastSourceRows,
lastStructNodes](const QString& /*dsTypeCode*/, const QStringList& sourceIds) { lastStructNodes](const QString& /*dsTypeCode*/, const QStringList& sourceIds) {
if (sourceIds.isEmpty()) return; if (sourceIds.isEmpty()) return;
// 源 ds (id,名称):名称从最近拉取的行查(缺则用 id // 源 dsid,名称,结构归属):名称/structParentId 从最近拉取的行查(缺则用 id
QVector<QPair<QString, QString>> sources; QVector<geopro::app::VolumeSourceItem> sources;
for (const QString& id : sourceIds) { for (const QString& id : sourceIds) {
QString name = id; geopro::app::VolumeSourceItem s;
s.id = id;
s.name = id;
for (const auto& r : *lastSourceRows) for (const auto& r : *lastSourceRows)
if (r.id == id.toStdString()) { name = QString::fromStdString(r.dsName); break; } if (r.id == id.toStdString()) {
sources.push_back({id, name}); s.name = QString::fromStdString(r.dsName);
s.structParentId = QString::fromStdString(r.structParentId);
break;
} }
// 生成位置候选:项目内 GS/项目根/TMStructNode.type==2 → TM(2),其余 → GS/项目根(1))。 sources.push_back(std::move(s));
QVector<geopro::app::VolumeMountTarget> mounts; }
for (const auto& n : *lastStructNodes) // 左右两树共用项目结构GS/项目根/TM 层级);生成位置默认选首个节点。
mounts.push_back({QString::fromStdString(n.id), QString::fromStdString(n.name), geopro::app::VolumeParamsDialog dlg(sources, *lastStructNodes, /*defaultMountId=*/QString(),
n.type == 2 ? 2 : 1}); &window);
geopro::app::VolumeParamsDialog dlg(sources, mounts, /*defaultMountIndex=*/0, &window);
if (dlg.exec() != QDialog::Accepted) return; if (dlg.exec() != QDialog::Accepted) return;
const QStringList chosen = dlg.sourceDatasetIds(); const QStringList chosen = dlg.sourceDatasetIds();
if (chosen.isEmpty()) return; // 全取消则不生成 if (chosen.isEmpty()) return; // 全取消则不生成