feat/vtk-3d-view #7
|
|
@ -414,7 +414,7 @@
|
|||
| 元素 | 规范 |
|
||||
|---|---|
|
||||
| 标签位置 | **左侧标签列**(默认,密集专业风),标签文字**右对齐**贴近字段;字段名过长或窄单列对话框可改顶部标签 |
|
||||
| 标签列宽 | 默认 `96–120px`,同一表单内**等宽对齐**(纯只读键值表用 §6.4 的 `72px`) |
|
||||
| 标签列宽 | 可编辑表单**固定 `100px`**(`space::kFormLabelCol`,右对齐,跨表单等宽);纯只读键值表**固定 `72px`**(`space::kDetailKeyCol`,§6.4)。**不取区间**——区间会让不同实现各选一值而漂移 |
|
||||
| 标签 ↔ 字段间距 | `space/md`(12) |
|
||||
| 字段控件高 | `28px`(§7.1);行与行垂直间距 `space/sm`(8) |
|
||||
| 字段宽度 | 宽面板/对话框中**不要拉满**,单字段最大宽约 `360px`(多行/长文本可更宽);窄属性面板中填充可用宽度 |
|
||||
|
|
@ -479,6 +479,18 @@
|
|||
- **不以颜色为唯一信息**:必填除 `*` 外,校验失败有文字说明;错误态有文字。
|
||||
- 可点控件最小命中区 ≥ `24×24px`(§12)。
|
||||
|
||||
#### 7.0.10 实现纪律(单一实现 · 禁止手搭)
|
||||
|
||||
> **本节是 §7.0 能落地的关键。** 文档约束管不住代码——同类表单分散在各文件里手搭 `QFormLayout`/`QLabel`、各填各的边距与列宽,必然漂移(曾出现:三维体/切片/异常详情用裸 `QFormLayout`,与属性面板天差地别;同为下拉框,设置页无箭头、对象属性偏矮带箭头)。一致性**只能由代码复用强制**。
|
||||
|
||||
- **唯一实现(必须经此产出,不得另起炉灶)**:
|
||||
- 只读键值详情 → `DynamicFormView`(§6.4 渲染器)。对话框用 `formkit::DetailForm`(链式 `group()/row()`)构模型,`formkit::buildDetailDialog()` 铺骨架。
|
||||
- 可编辑表单 → `formkit::makeEditForm()` + `formkit::editLabel()` + `formkit::capField()` + `formkit::addSection()`。`DynamicFormEditor` 与各参数对话框(如「生成三维体」)**都走同一组**,确保标签列宽/行距/分组/字段上限逐像素一致。
|
||||
- **禁止**:业务表单/对话框直接 `new QFormLayout` + `new QLabel("名",值)` 手搭键值;禁止在表单里逐处写死边距/列宽/字号(用 `space::*`/`type::*` 令牌)。
|
||||
- **精确常量(单一来源 `Theme.hpp`,禁止区间/魔数)**:可编辑标签列 `space::kFormLabelCol=100`;只读键列 `space::kDetailKeyCol=72`;字段最大宽 `space::kFormFieldMax=360`;行距 `space::kMd`;分组上间距 `space::kLg`。
|
||||
- **控件构造一致性**:下拉框统一用不可编辑 `QComboBox`(无候选项的「选择」字段退化为 `QLineEdit` 自由文本,**不得**用「可编辑下拉框」——其几何/高度与不可编辑款不一致)。全局 QSS **不覆写** `QComboBox::drop-down`/`::down-arrow`,保留 Fusion 原生箭头随调色板自适应(覆写却不提供箭头图会致箭头消失)。`QComboBox` 的 `min-height`/`padding` 与 `QLineEdit` 完全对齐 → 同高。
|
||||
- **新增表单的验收**:截图与既有「对象属性 / 数据详情」并排,标签列、行高、分组标题、下拉框外观应**无法区分**;做不到即说明绕开了上述唯一实现。
|
||||
|
||||
---
|
||||
|
||||
### 7.1 输入框(Text Input)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#include "AnomalyPropertiesDialog.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
|
|
@ -26,25 +27,27 @@ QString orDash(const std::string& s) {
|
|||
AnomalyPropertiesDialog::AnomalyPropertiesDialog(const geopro::core::Anomaly& a, QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setWindowTitle(QStringLiteral("异常属性"));
|
||||
setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
formkit::DetailForm form;
|
||||
form.group(QStringLiteral("异常"))
|
||||
.row(QStringLiteral("名称"), orDash(a.name))
|
||||
.row(QStringLiteral("类型"), orDash(a.typeName))
|
||||
.row(QStringLiteral("标记类型"), markTypeLabel(a.markType))
|
||||
.row(QStringLiteral("归属三维体"), orDash(a.volumeDsId))
|
||||
.row(QStringLiteral("异常体"), a.consortiumId.empty()
|
||||
? QStringLiteral("(未分组)")
|
||||
: QString::fromStdString(a.consortiumId));
|
||||
|
||||
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();
|
||||
// 顶点世界坐标(只读列表,x/y/z 每行一个点)—— 作为附加只读区,复用统一分组标题样式。
|
||||
auto* vertexBox = new QWidget(this);
|
||||
auto* vlay = new QVBoxLayout(vertexBox);
|
||||
vlay->setContentsMargins(0, 0, 0, 0);
|
||||
vlay->setSpacing(geopro::app::space::kSm);
|
||||
formkit::addSection(vlay, QStringLiteral("顶点坐标(%1 个)").arg(a.worldPts.size()), vertexBox,
|
||||
false);
|
||||
auto* pts = new QPlainTextEdit(vertexBox);
|
||||
pts->setReadOnly(true);
|
||||
pts->setFixedHeight(120);
|
||||
pts->setFixedHeight(geopro::app::scaledPx(120));
|
||||
QString text;
|
||||
for (std::size_t i = 0; i < a.worldPts.size(); ++i) {
|
||||
const auto& p = a.worldPts[i];
|
||||
|
|
@ -55,20 +58,21 @@ AnomalyPropertiesDialog::AnomalyPropertiesDialog(const geopro::core::Anomaly& a,
|
|||
.arg(p.z, 0, 'f', 2);
|
||||
}
|
||||
pts->setPlainText(text);
|
||||
root->addWidget(pts);
|
||||
vlay->addWidget(pts);
|
||||
|
||||
// 备注(只读)。
|
||||
root->addWidget(new QLabel(QStringLiteral("备注")));
|
||||
auto* remark = new QPlainTextEdit();
|
||||
auto* remarkBox = new QWidget(this);
|
||||
auto* rlay = new QVBoxLayout(remarkBox);
|
||||
rlay->setContentsMargins(0, 0, 0, 0);
|
||||
rlay->setSpacing(geopro::app::space::kSm);
|
||||
formkit::addSection(rlay, QStringLiteral("备注"), remarkBox, false);
|
||||
auto* remark = new QPlainTextEdit(remarkBox);
|
||||
remark->setReadOnly(true);
|
||||
remark->setFixedHeight(60);
|
||||
remark->setFixedHeight(geopro::app::scaledPx(60));
|
||||
remark->setPlainText(QString::fromStdString(a.remark));
|
||||
root->addWidget(remark);
|
||||
rlay->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);
|
||||
formkit::buildDetailDialog(this, form.build(), {vertexBox, remarkBox});
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebEngineQuick)
|
|||
add_executable(geopro_desktop WIN32
|
||||
main.cpp
|
||||
Theme.cpp
|
||||
FormKit.cpp
|
||||
TopBar.cpp
|
||||
ToastOverlay.cpp
|
||||
Glyphs.cpp
|
||||
|
|
@ -30,6 +31,7 @@ add_executable(geopro_desktop WIN32
|
|||
panels/AnomalyListPanel.cpp
|
||||
panels/DatasetListPanel.cpp
|
||||
panels/ObjectTreePanel.cpp
|
||||
panels/KeyValueView.cpp
|
||||
panels/DynamicFormView.cpp
|
||||
panels/DynamicFormEditor.cpp
|
||||
panels/ObjectAttrPanel.cpp
|
||||
|
|
@ -46,6 +48,7 @@ add_executable(geopro_desktop WIN32
|
|||
panels/chart/DetailViewFactory.cpp
|
||||
resources/map/map.qrc
|
||||
resources/keys.qrc
|
||||
resources/icons.qrc
|
||||
panels/chart/ChartTheme.cpp
|
||||
panels/chart/ColorMapService.cpp
|
||||
panels/chart/ColorBarWidget.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
#include "FormKit.hpp"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "Theme.hpp"
|
||||
#include "panels/KeyValueView.hpp"
|
||||
|
||||
namespace geopro::app::formkit {
|
||||
|
||||
DetailForm& DetailForm::group(const QString& name) {
|
||||
geopro::data::DynamicFormGroup g;
|
||||
g.name = name.toStdString();
|
||||
form_.groups.push_back(std::move(g));
|
||||
return *this;
|
||||
}
|
||||
|
||||
DetailForm& DetailForm::row(const QString& key, const QString& value) {
|
||||
if (form_.groups.empty()) group(QString()); // 无显式分组时落入匿名组
|
||||
geopro::data::DynamicFormField f;
|
||||
f.name = key.toStdString();
|
||||
f.value = value.toStdString();
|
||||
form_.groups.back().fields.push_back(std::move(f));
|
||||
return *this;
|
||||
}
|
||||
|
||||
void buildDetailDialog(QDialog* dlg, const geopro::data::DynamicForm& form,
|
||||
const QList<QWidget*>& extras) {
|
||||
dlg->setModal(true);
|
||||
auto* root = new QVBoxLayout(dlg);
|
||||
root->setContentsMargins(space::kLg, space::kLg, space::kLg, space::kLg);
|
||||
root->setSpacing(space::kMd);
|
||||
|
||||
// 卡片:与属性面板同款(bg/panel-subtle + 1px 边框 + 中圆角),内嵌唯一只读渲染器
|
||||
// KeyValueView——对话框随内容自适应大小,无嵌套滚动/留空。
|
||||
auto* card = new QFrame(dlg);
|
||||
card->setObjectName(QStringLiteral("detailCard"));
|
||||
applyTokenizedStyleSheet(
|
||||
card, QStringLiteral("#detailCard { background:{{bg/panel-subtle}};"
|
||||
"border:1px solid {{border/default}}; border-radius:%1px; }")
|
||||
.arg(radius::kMd));
|
||||
auto* cardLay = new QVBoxLayout(card);
|
||||
cardLay->setContentsMargins(space::kLg, space::kLg, space::kLg, space::kLg);
|
||||
cardLay->setSpacing(space::kMd);
|
||||
|
||||
auto* kv = new KeyValueView(card);
|
||||
kv->setForm(form);
|
||||
cardLay->addWidget(kv);
|
||||
|
||||
for (QWidget* w : extras)
|
||||
if (w) cardLay->addWidget(w);
|
||||
|
||||
root->addWidget(card);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, dlg);
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
|
||||
root->addWidget(buttons);
|
||||
|
||||
const int minW = scaledPx(440);
|
||||
if (dlg->minimumWidth() < minW) dlg->setMinimumWidth(minW);
|
||||
}
|
||||
|
||||
QFormLayout* makeEditForm() {
|
||||
auto* fl = new QFormLayout();
|
||||
fl->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
fl->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||||
fl->setHorizontalSpacing(space::kLg); // 标签↔字段
|
||||
fl->setVerticalSpacing(space::kMd); // 行距
|
||||
return fl;
|
||||
}
|
||||
|
||||
QLabel* editLabel(const QString& text, QWidget* parent, bool richText) {
|
||||
auto* lbl = new QLabel(text, parent);
|
||||
if (richText) lbl->setTextFormat(Qt::RichText); // 允许必填星号红色 span
|
||||
lbl->setFixedWidth(scaledPx(space::kFormLabelCol)); // 定宽右标签列,跨表单对齐
|
||||
return lbl;
|
||||
}
|
||||
|
||||
void capField(QWidget* field) {
|
||||
if (field) field->setMaximumWidth(scaledPx(space::kFormFieldMax)); // §7.0.2「不要拉满」
|
||||
}
|
||||
|
||||
void addSection(QBoxLayout* into, const QString& title, QWidget* parent, bool topGap) {
|
||||
if (topGap) into->addSpacing(space::kLg);
|
||||
// 唯一的分组标题样式(编辑态与只读态共用):kTitle 半粗 + 次级色 + 标题下 1px divider。
|
||||
// 字号走 scaledPx 以随系统字号缩放。
|
||||
auto* sec = new QLabel(title, parent);
|
||||
applyTokenizedStyleSheet(
|
||||
sec, QStringLiteral("color:{{text/secondary}};font-size:%1px;font-weight:%2;"
|
||||
"padding-bottom:%3px;")
|
||||
.arg(scaledPx(type::kTitle))
|
||||
.arg(type::kWeightSemibold)
|
||||
.arg(space::kXs));
|
||||
into->addWidget(sec);
|
||||
auto* rule = new QFrame(parent);
|
||||
rule->setFrameShape(QFrame::HLine);
|
||||
rule->setFixedHeight(1);
|
||||
applyTokenizedStyleSheet(rule, QStringLiteral("background:{{divider}};border:none;"));
|
||||
into->addWidget(rule);
|
||||
}
|
||||
|
||||
} // namespace geopro::app::formkit
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
// FormKit —— 表单渲染的「单一真相」层。
|
||||
//
|
||||
// 历史问题:每个表单/对话框各自手搭布局(裸 QFormLayout + 裸 QLabel、各写边距/标签列宽),
|
||||
// 规范文档(§6.4/§7.0)只是文字约束、无法强制,于是同类控件在不同位置长得不一样。
|
||||
// 本模块把「只读键值详情」与「可编辑表单」的视觉度量收敛到唯一实现,所有表单必须经此产出,
|
||||
// 一致性由「代码复用」强制,而非「人工遵守文档」。
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
class QBoxLayout;
|
||||
class QDialog;
|
||||
class QFormLayout;
|
||||
class QLabel;
|
||||
class QWidget;
|
||||
|
||||
namespace geopro::app::formkit {
|
||||
|
||||
// ── 只读详情:唯一键值模型构建器 ─────────────────────────────────────────────
|
||||
// 链式 group()/row() 产出 data::DynamicForm,喂给唯一的 §6.4 渲染器 DynamicFormView。
|
||||
// 三维体/切片/异常等「数据详情」对话框共用,杜绝裸 QFormLayout 漂移。
|
||||
class DetailForm {
|
||||
public:
|
||||
DetailForm& group(const QString& name); // 开新分组
|
||||
DetailForm& row(const QString& key, const QString& value); // 向当前组追加键值行
|
||||
const geopro::data::DynamicForm& build() const { return form_; }
|
||||
|
||||
private:
|
||||
geopro::data::DynamicForm form_;
|
||||
};
|
||||
|
||||
// 给只读详情对话框铺设标准骨架:DynamicFormView(§6.4 卡片)+ 底部 Close + 统一边距。
|
||||
// extras 追加在卡片下方、按钮上方(异常详情的顶点坐标/备注走此口),与主键值表共享节奏。
|
||||
void buildDetailDialog(QDialog* dlg, const geopro::data::DynamicForm& form,
|
||||
const QList<QWidget*>& extras = {});
|
||||
|
||||
// ── 可编辑表单:§7.0 统一度量(DynamicFormEditor 与各参数对话框共用,单一真相)──────
|
||||
QFormLayout* makeEditForm(); // 右对齐标签 + 标准行距/列距
|
||||
QLabel* editLabel(const QString& text, QWidget* parent = nullptr,
|
||||
bool richText = false); // 定宽右标签列(kFormLabelCol)
|
||||
void capField(QWidget* field); // 字段最大宽上限(kFormFieldMax)
|
||||
// 分组标题(heading 字号 + 半粗 + 次级色)+ 标题下 1px divider;topGap=true 时上方留 space/lg。
|
||||
void addSection(QBoxLayout* into, const QString& title, QWidget* parent, bool topGap);
|
||||
|
||||
} // namespace geopro::app::formkit
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
#include "SlicePropertiesDialog.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
|
|
@ -34,31 +31,22 @@ SlicePropertiesDialog::SlicePropertiesDialog(const QString& name, const SliceSpe
|
|||
QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setWindowTitle(QStringLiteral("切片属性"));
|
||||
setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
auto* form = new QFormLayout();
|
||||
formkit::DetailForm form;
|
||||
form.group(QStringLiteral("切片"))
|
||||
.row(QStringLiteral("名称"), name.isEmpty() ? QStringLiteral("—") : name)
|
||||
.row(QStringLiteral("所属三维体"), spec.volumeDsId.empty()
|
||||
? QStringLiteral("—")
|
||||
: QString::fromStdString(spec.volumeDsId))
|
||||
.row(QStringLiteral("轴向"), axisLabel(spec.axis))
|
||||
.row(QStringLiteral("Origin"), pointLabel(spec.origin))
|
||||
.row(QStringLiteral("Point1"), pointLabel(spec.point1))
|
||||
.row(QStringLiteral("Point2"), pointLabel(spec.point2))
|
||||
.row(QStringLiteral("色阶来源"),
|
||||
spec.colorScaleId.empty() ? QStringLiteral("首个源数据集")
|
||||
: QString::fromStdString(spec.colorScaleId));
|
||||
|
||||
form->addRow(QStringLiteral("名称"),
|
||||
new QLabel(name.isEmpty() ? QStringLiteral("—") : name));
|
||||
form->addRow(QStringLiteral("所属三维体"),
|
||||
new QLabel(spec.volumeDsId.empty() ? QStringLiteral("—")
|
||||
: QString::fromStdString(spec.volumeDsId)));
|
||||
form->addRow(QStringLiteral("轴向"), new QLabel(axisLabel(spec.axis)));
|
||||
form->addRow(QStringLiteral("Origin"), new QLabel(pointLabel(spec.origin)));
|
||||
form->addRow(QStringLiteral("Point1"), new QLabel(pointLabel(spec.point1)));
|
||||
form->addRow(QStringLiteral("Point2"), new QLabel(pointLabel(spec.point2)));
|
||||
form->addRow(QStringLiteral("色阶来源"),
|
||||
new QLabel(spec.colorScaleId.empty()
|
||||
? QStringLiteral("首个源数据集")
|
||||
: QString::fromStdString(spec.colorScaleId)));
|
||||
|
||||
root->addLayout(form);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
root->addWidget(buttons);
|
||||
formkit::buildDetailDialog(this, form.build());
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -375,8 +375,8 @@ QComboBox {
|
|||
color: {{text/primary}};
|
||||
border: 1px solid {{border/default}};
|
||||
border-radius: 4px; /* radius/sm */
|
||||
padding: 6px 10px;
|
||||
min-height: 16px;
|
||||
padding: 6px 8px; /* 与 QLineEdit 完全一致 */
|
||||
min-height: 16px; /* 与 QLineEdit 完全一致 → 同高(可编辑/不可编辑均如此)*/
|
||||
}
|
||||
QComboBox:hover {
|
||||
border-color: {{border/strong}};
|
||||
|
|
@ -384,9 +384,42 @@ QComboBox:hover {
|
|||
QComboBox:focus {
|
||||
border-color: {{border/focus}};
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
QComboBox:disabled {
|
||||
background: {{bg/app}};
|
||||
color: {{text/disabled}};
|
||||
}
|
||||
/* 日期/时间编辑器:与输入框/下拉框同款外观(box 一致,避免「布设日期」与下拉框不一致)。 */
|
||||
QDateEdit, QTimeEdit, QDateTimeEdit {
|
||||
background: {{bg/panel}};
|
||||
color: {{text/primary}};
|
||||
border: 1px solid {{border/default}};
|
||||
border-radius: 4px; /* radius/sm */
|
||||
padding: 6px 8px;
|
||||
min-height: 16px;
|
||||
}
|
||||
QDateEdit:hover, QTimeEdit:hover, QDateTimeEdit:hover {
|
||||
border-color: {{border/strong}};
|
||||
}
|
||||
QDateEdit:focus, QTimeEdit:focus, QDateTimeEdit:focus {
|
||||
border-color: {{border/focus}};
|
||||
}
|
||||
QDateEdit:disabled, QTimeEdit:disabled, QDateTimeEdit:disabled {
|
||||
background: {{bg/app}};
|
||||
color: {{text/disabled}};
|
||||
}
|
||||
/* 下拉按钮平面化(去 Fusion 原生斜角/分隔)+ 统一的扁平 chevron 箭头(qrc 内嵌 SVG)。
|
||||
覆写 ::drop-down 必须同时提供 ::down-arrow 图,否则箭头消失(历史坑)。 */
|
||||
QComboBox::drop-down, QDateEdit::drop-down, QTimeEdit::drop-down, QDateTimeEdit::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
width: 20px;
|
||||
border: none;
|
||||
width: 22px;
|
||||
background: transparent;
|
||||
}
|
||||
QComboBox::down-arrow, QDateEdit::down-arrow, QTimeEdit::down-arrow, QDateTimeEdit::down-arrow {
|
||||
image: url(:/icons/chevron-down.svg);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background: {{bg/panel}};
|
||||
|
|
|
|||
|
|
@ -77,11 +77,14 @@ inline constexpr int kXl = 16; // 区块内边距
|
|||
inline constexpr int kXxl = 24; // 区块间距、表单纵向边距
|
||||
inline constexpr int kXxxl = 32; // 页面级留白(登录窗左右边距)
|
||||
|
||||
// 可编辑表单标签列宽(规范 §7.0.2:默认 96–120px,同表单内等宽对齐右标签)。
|
||||
// 唯一事实来源——DynamicFormEditor / ObjectAttrPanel / ObjectFormDialog 的左标签列统一引此值。
|
||||
// 注:纯只读键值表(§6.4)另用 72px(见 DynamicFormView::kKeyColWidth),不复用此档。
|
||||
// 可编辑表单标签列宽(规范 §7.0.2:固定 100px,同表单内等宽对齐右标签)。
|
||||
// 唯一事实来源——DynamicFormEditor / 各参数对话框经 formkit::editLabel 统一引此值。
|
||||
inline constexpr int kFormLabelCol = 100;
|
||||
|
||||
// 只读键值表(§6.4)键列定宽。与可编辑标签列分档(只读两列布局更紧凑)——
|
||||
// 唯一事实来源,DynamicFormView 引此值;规范 §7.0.10 列为精确常量,禁止区间/魔数。
|
||||
inline constexpr int kDetailKeyCol = 72;
|
||||
|
||||
// 可编辑字段最大宽(规范 §7.0.2:宽对话框中「不要拉满」,单字段最大约 360px)。
|
||||
// 窄属性面板里该上限大于面板宽,故字段仍填满——符合规范。多行/长文本不受此限。
|
||||
inline constexpr int kFormFieldMax = 360;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
#include <QStandardItemModel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
|
|
@ -24,13 +27,19 @@ VolumeParamsDialog::VolumeParamsDialog(int sourceCount, QWidget* parent) : QDial
|
|||
setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->addWidget(new QLabel(
|
||||
QStringLiteral("由 %1 个源数据集插值生成三维体").arg(sourceCount)));
|
||||
root->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg,
|
||||
geopro::app::space::kLg, geopro::app::space::kLg);
|
||||
root->setSpacing(geopro::app::space::kMd);
|
||||
|
||||
auto* form = new QFormLayout();
|
||||
auto* intro = new QLabel(QStringLiteral("由 %1 个源数据集插值生成三维体").arg(sourceCount));
|
||||
geopro::app::applyTokenizedStyleSheet(intro, QStringLiteral("color:{{text/secondary}};"));
|
||||
root->addWidget(intro);
|
||||
|
||||
auto* form = formkit::makeEditForm();
|
||||
|
||||
name_ = new QLineEdit(QStringLiteral("三维体"));
|
||||
form->addRow(QStringLiteral("名称"), name_);
|
||||
formkit::capField(name_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
|
||||
|
||||
model_ = new QComboBox();
|
||||
model_->addItem(QStringLiteral("反距离加权 (IDW)"),
|
||||
|
|
@ -42,7 +51,8 @@ VolumeParamsDialog::VolumeParamsDialog(int sourceCount, QWidget* parent) : QDial
|
|||
if (auto* it = m->item(1)) it->setEnabled(false);
|
||||
}
|
||||
model_->setCurrentIndex(0);
|
||||
form->addRow(QStringLiteral("插值模型"), model_);
|
||||
formkit::capField(model_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("插值模型")), model_);
|
||||
|
||||
auto makeSpin = [this](double val, double min, double max, double step, int decimals) {
|
||||
auto* s = new QDoubleSpinBox();
|
||||
|
|
@ -50,16 +60,17 @@ VolumeParamsDialog::VolumeParamsDialog(int sourceCount, QWidget* parent) : QDial
|
|||
s->setSingleStep(step);
|
||||
s->setDecimals(decimals);
|
||||
s->setValue(val);
|
||||
formkit::capField(s);
|
||||
return s;
|
||||
};
|
||||
cellXY_ = makeSpin(kDefCellXY, 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);
|
||||
maxDist_ = makeSpin(kDefMaxDist, 0.1, 10000.0, 1.0, 2);
|
||||
form->addRow(QStringLiteral("水平间距 (米)"), cellXY_);
|
||||
form->addRow(QStringLiteral("竖向间距 (米)"), cellZ_);
|
||||
form->addRow(QStringLiteral("IDW 幂次"), power_);
|
||||
form->addRow(QStringLiteral("最大影响距离 (米)"), maxDist_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("水平间距 (米)")), cellXY_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("竖向间距 (米)")), cellZ_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("IDW 幂次")), power_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("最大影响距离 (米)")), maxDist_);
|
||||
|
||||
root->addLayout(form);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
#include "VolumePropertiesDialog.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QStringList>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
|
|
@ -32,53 +30,38 @@ VolumePropertiesDialog::VolumePropertiesDialog(const QString& name, const Volume
|
|||
QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setWindowTitle(QStringLiteral("三维体属性"));
|
||||
setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
auto* form = new QFormLayout();
|
||||
|
||||
formkit::DetailForm form;
|
||||
// ── 参数(随时可取)─────────────────────────────────────────────
|
||||
form->addRow(QStringLiteral("名称"),
|
||||
new QLabel(name.isEmpty() ? QStringLiteral("—") : name));
|
||||
form->addRow(QStringLiteral("源数据集"), new QLabel(joinSources(info.params.sourceDatasetIds)));
|
||||
form->addRow(QStringLiteral("插值模型"), new QLabel(modelLabel(info.params)));
|
||||
form->addRow(QStringLiteral("网格间距"),
|
||||
new QLabel(QStringLiteral("XY=%1 m Z=%2 m")
|
||||
.arg(info.params.cellXY, 0, 'f', 2)
|
||||
.arg(info.params.cellZ, 0, 'f', 2)));
|
||||
form->addRow(QStringLiteral("超距"),
|
||||
new QLabel(QStringLiteral("%1 m").arg(info.params.maxDist, 0, 'f', 2)));
|
||||
form->addRow(QStringLiteral("色阶来源"),
|
||||
new QLabel(info.params.colorScaleId.empty()
|
||||
? QStringLiteral("首个源数据集")
|
||||
: QString::fromStdString(info.params.colorScaleId)));
|
||||
form.group(QStringLiteral("参数"))
|
||||
.row(QStringLiteral("名称"), name.isEmpty() ? QStringLiteral("—") : name)
|
||||
.row(QStringLiteral("源数据集"), joinSources(info.params.sourceDatasetIds))
|
||||
.row(QStringLiteral("插值模型"), modelLabel(info.params))
|
||||
.row(QStringLiteral("网格间距"), QStringLiteral("XY=%1 m Z=%2 m")
|
||||
.arg(info.params.cellXY, 0, 'f', 2)
|
||||
.arg(info.params.cellZ, 0, 'f', 2))
|
||||
.row(QStringLiteral("超距"), QStringLiteral("%1 m").arg(info.params.maxDist, 0, 'f', 2))
|
||||
.row(QStringLiteral("色阶来源"),
|
||||
info.params.colorScaleId.empty() ? QStringLiteral("首个源数据集")
|
||||
: QString::fromStdString(info.params.colorScaleId));
|
||||
|
||||
// ── 统计(仅 loaded 时有效)──────────────────────────────────────
|
||||
if (info.loaded) {
|
||||
form->addRow(QStringLiteral("值域"), new QLabel(QStringLiteral("%1 ~ %2")
|
||||
.arg(info.vmin, 0, 'f', 2)
|
||||
.arg(info.vmax, 0, 'f', 2)));
|
||||
form->addRow(QStringLiteral("网格"), new QLabel(QStringLiteral("%1 × %2 × %3")
|
||||
.arg(info.nx)
|
||||
.arg(info.ny)
|
||||
.arg(info.nz)));
|
||||
form->addRow(QStringLiteral("测点数"),
|
||||
new QLabel(QString::number(static_cast<qulonglong>(info.pointCount))));
|
||||
form->addRow(QStringLiteral("范围"),
|
||||
new QLabel(QStringLiteral("%1 × %2 × %3 m")
|
||||
.arg(info.nx * info.dx, 0, 'f', 1)
|
||||
.arg(info.ny * info.dy, 0, 'f', 1)
|
||||
.arg(info.nz * info.dz, 0, 'f', 1)));
|
||||
form.group(QStringLiteral("统计"))
|
||||
.row(QStringLiteral("值域"),
|
||||
QStringLiteral("%1 ~ %2").arg(info.vmin, 0, 'f', 2).arg(info.vmax, 0, 'f', 2))
|
||||
.row(QStringLiteral("网格"),
|
||||
QStringLiteral("%1 × %2 × %3").arg(info.nx).arg(info.ny).arg(info.nz))
|
||||
.row(QStringLiteral("测点数"), QString::number(static_cast<qulonglong>(info.pointCount)))
|
||||
.row(QStringLiteral("范围"), QStringLiteral("%1 × %2 × %3 m")
|
||||
.arg(info.nx * info.dx, 0, 'f', 1)
|
||||
.arg(info.ny * info.dy, 0, 'f', 1)
|
||||
.arg(info.nz * info.dz, 0, 'f', 1));
|
||||
} else {
|
||||
form->addRow(QStringLiteral("统计"), new QLabel(QString::fromUtf8(kPending)));
|
||||
form.group(QStringLiteral("统计")).row(QStringLiteral("统计"), QString::fromUtf8(kPending));
|
||||
}
|
||||
|
||||
root->addLayout(form);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
root->addWidget(buttons);
|
||||
formkit::buildDetailDialog(this, form.build());
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <QTimeEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
|
@ -92,15 +93,18 @@ QWidget* buildWidget(const data::EditField& f) {
|
|||
}
|
||||
case kCompSelect:
|
||||
case kCompTreeSelect: {
|
||||
auto* cb = new QComboBox();
|
||||
// 无候选项:退化为自由文本输入(QLineEdit),而非「可编辑下拉框」。
|
||||
// 可编辑下拉框与不可编辑下拉框在 QSS 下几何/高度不一致,是表单观感分裂的来源之一。
|
||||
if (f.options.empty()) {
|
||||
cb->setEditable(true);
|
||||
cb->setCurrentText(val);
|
||||
} else {
|
||||
flattenOptions(f.options, cb);
|
||||
const int idx = cb->findData(val);
|
||||
if (idx >= 0) cb->setCurrentIndex(idx);
|
||||
auto* le = new QLineEdit();
|
||||
le->setText(val);
|
||||
if (ro) le->setEnabled(false);
|
||||
return le;
|
||||
}
|
||||
auto* cb = new QComboBox();
|
||||
flattenOptions(f.options, cb);
|
||||
const int idx = cb->findData(val);
|
||||
if (idx >= 0) cb->setCurrentIndex(idx);
|
||||
if (ro) cb->setEnabled(false);
|
||||
return cb;
|
||||
}
|
||||
|
|
@ -178,6 +182,7 @@ QString readWidget(int comp, QWidget* w) {
|
|||
const QVariant d = cb->currentData();
|
||||
return d.isValid() ? d.toString() : cb->currentText();
|
||||
}
|
||||
if (auto* le = qobject_cast<QLineEdit*>(w)) return le->text(); // 无候选项时的自由文本退化
|
||||
return {};
|
||||
case kCompDate:
|
||||
if (auto* de = qobject_cast<QDateEdit*>(w))
|
||||
|
|
@ -247,6 +252,8 @@ void DynamicFormEditor::setForm(const data::EditableForm& form,
|
|||
case kCompTreeSelect:
|
||||
if (auto* x = qobject_cast<QComboBox*>(w))
|
||||
connect(x, &QComboBox::currentTextChanged, this, [this] { emit changed(); });
|
||||
else if (auto* le = qobject_cast<QLineEdit*>(w)) // 无候选项时的自由文本退化
|
||||
connect(le, &QLineEdit::textEdited, this, [this] { emit changed(); });
|
||||
break;
|
||||
case kCompDate:
|
||||
if (auto* x = qobject_cast<QDateEdit*>(w))
|
||||
|
|
@ -292,36 +299,16 @@ void DynamicFormEditor::setForm(const data::EditableForm& form,
|
|||
if (visible.empty()) continue; // 整组被隐藏 → 不渲染空标题
|
||||
|
||||
if (form.groups.size() > 1 || !g.name.empty()) {
|
||||
// 分组标题(规范 §7.0.3 / §6.4):text/heading 字号 + 加粗 + 次级色;非首组上留 space/lg。
|
||||
if (renderedGroup) outer->addSpacing(geopro::app::space::kLg);
|
||||
auto* sec = new QLabel(QString::fromStdString(g.name), body_);
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
sec, QStringLiteral("color:{{text/secondary}};font-size:%1px;font-weight:%2;")
|
||||
.arg(geopro::app::type::kHeading)
|
||||
.arg(geopro::app::type::kWeightSemibold));
|
||||
outer->addWidget(sec);
|
||||
// 标题下 1px divider 贯通(规范 §7.0.3)。
|
||||
auto* rule = new QFrame(body_);
|
||||
rule->setFrameShape(QFrame::HLine);
|
||||
rule->setFixedHeight(1);
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
rule, QStringLiteral("background:{{divider}};border:none;"));
|
||||
outer->addWidget(rule);
|
||||
// 分组标题 + 标题下 1px divider(规范 §7.0.3 / §6.4),统一走 formkit(单一真相)。
|
||||
formkit::addSection(outer, QString::fromStdString(g.name), body_, renderedGroup);
|
||||
}
|
||||
renderedGroup = true;
|
||||
auto* fl = new QFormLayout();
|
||||
fl->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
fl->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||||
fl->setHorizontalSpacing(geopro::app::space::kLg); // 标签↔字段 space/md(12)=项目 kLg
|
||||
fl->setVerticalSpacing(geopro::app::space::kMd); // 行距 ≈8px(项目 kMd)
|
||||
auto* fl = formkit::makeEditForm();
|
||||
for (const data::EditField* fp : visible) {
|
||||
const data::EditField& f = *fp;
|
||||
QWidget* w = buildWidget(f);
|
||||
capFieldWidth(f.comp, w); // 字段最大宽上限(§7.0.2 不要拉满)
|
||||
auto* lbl = new QLabel(labelText(f), body_);
|
||||
lbl->setTextFormat(Qt::RichText); // 允许 * 的红色 span
|
||||
lbl->setFixedWidth(geopro::app::scaledPx(geopro::app::space::kFormLabelCol)); // 等宽右标签列
|
||||
fl->addRow(lbl, w);
|
||||
capFieldWidth(f.comp, w); // 字段最大宽上限(§7.0.2 不要拉满;多行除外,故仍用本地版)
|
||||
fl->addRow(formkit::editLabel(labelText(f), body_, true), w);
|
||||
Entry e;
|
||||
e.code = QString::fromStdString(f.code);
|
||||
e.name = QString::fromStdString(f.name);
|
||||
|
|
|
|||
|
|
@ -1,114 +1,16 @@
|
|||
#include "panels/DynamicFormView.hpp"
|
||||
|
||||
#include <QFont>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Theme.hpp"
|
||||
#include "panels/KeyValueView.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
|
||||
// 两列字段网格的逻辑列:label/value 各两份,value 列吸收伸展、label 列贴合内容。
|
||||
constexpr int kColLabelA = 0;
|
||||
constexpr int kColValueA = 1;
|
||||
constexpr int kColLabelB = 2;
|
||||
constexpr int kColValueB = 3;
|
||||
constexpr int kColSpanAll = 4; // 分组标题带横跨全部 4 列
|
||||
|
||||
constexpr int kKeyColWidth = 72; // 键列定宽(§6.4「定宽约 72px」),保证多组键列对齐
|
||||
constexpr int kRowMinHeight = 28; // 字段行高(§6.4「行高 28px」)
|
||||
|
||||
// 判断值是否为「数值类」(坐标/数值/带单位/编号/日期):去掉常见数字标点、单位与
|
||||
// 坐标符号后,剩余字符多为数字则判定为数值类——这类值改用等宽字族逐列对齐(§2.1/§6.4)。
|
||||
// 纯文字(如名称、说明)不命中,保持默认字族。
|
||||
bool isNumericValue(const QString& text)
|
||||
{
|
||||
const QString trimmed = text.trimmed();
|
||||
if (trimmed.isEmpty()) return false;
|
||||
int digits = 0;
|
||||
int letters = 0;
|
||||
for (const QChar c : trimmed) {
|
||||
if (c.isDigit()) {
|
||||
++digits;
|
||||
} else if (c.isLetter()) {
|
||||
// 允许少量单位/坐标字母(E/N/W/S/m/z/Ω 等),但成片字母视为文字。
|
||||
++letters;
|
||||
}
|
||||
}
|
||||
// 至少含一位数字,且数字不少于字母:坐标「103.85°E·36.72°N」「140m」「z=1.0x」命中,
|
||||
// 「华亭测区」「正常」这类纯文字不命中。
|
||||
return digits > 0 && digits >= letters;
|
||||
}
|
||||
|
||||
// 字段标签(次要色,右侧留点呼吸,顶对齐以配合值换行)。键列定宽以与各组对齐(§6.4)。
|
||||
QLabel* makeLabel(const QString& text)
|
||||
{
|
||||
auto* k = new QLabel(text);
|
||||
k->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
k->setFixedWidth(geopro::app::scaledPx(kKeyColWidth)); // 定宽 72px,跨组对齐
|
||||
k->setMinimumHeight(geopro::app::scaledPx(kRowMinHeight)); // 行高 28px
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
k, QStringLiteral("color:{{text/secondary}}; background:transparent; padding:2px 0;"));
|
||||
return k;
|
||||
}
|
||||
|
||||
// 字段值(主色、可换行、可选中复制)。数值/坐标/带单位值改用等宽字族(§6.4)。
|
||||
QLabel* makeValue(const QString& text)
|
||||
{
|
||||
auto* v = new QLabel(text);
|
||||
v->setWordWrap(true);
|
||||
v->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
v->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
v->setMinimumHeight(geopro::app::scaledPx(kRowMinHeight)); // 行高 28px
|
||||
if (isNumericValue(text)) {
|
||||
// 仅切字族(保留 QSS 给的颜色/字号):等宽保证逐列对齐。
|
||||
QFont f = v->font();
|
||||
// 必须用 setFamilies(列表):setFamily 把整串逗号名当成单一字族找不到→静默回退。
|
||||
f.setFamilies(QString::fromLatin1(geopro::app::type::kMonoFamily).split(QStringLiteral(", ")));
|
||||
v->setFont(f);
|
||||
}
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
v, QStringLiteral("color:{{text/primary}}; background:transparent; padding:2px 0;"));
|
||||
return v;
|
||||
}
|
||||
|
||||
// 行间横向分隔线(1px,divider 令牌,随主题重着色)。
|
||||
QFrame* makeRowDivider()
|
||||
{
|
||||
auto* line = new QFrame();
|
||||
line->setFrameShape(QFrame::HLine);
|
||||
line->setFrameShadow(QFrame::Plain);
|
||||
line->setFixedHeight(1);
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
line, QStringLiteral("background:{{divider}}; border:none;"));
|
||||
return line;
|
||||
}
|
||||
|
||||
// 分组标题带:横跨整行的淡底强调条,标题级字号 + 半粗,给表单清晰的层级(§6.4
|
||||
// 「分组标题 text/heading,上留 space/md」)。上外边距用 space/md 与上一组拉开。
|
||||
QLabel* makeGroupHeader(const QString& name)
|
||||
{
|
||||
auto* title = new QLabel(name);
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
title, QStringLiteral("color:{{text/secondary}}; background:{{bg/hover}};"
|
||||
"font-weight:%1; font-size:%2px;"
|
||||
"border-radius:%3px; padding:5px 10px; margin-top:%4px;")
|
||||
.arg(geopro::app::type::kWeightSemibold)
|
||||
.arg(geopro::app::scaledPx(geopro::app::type::kHeading))
|
||||
.arg(geopro::app::radius::kSm)
|
||||
.arg(geopro::app::space::kMd));
|
||||
return title;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent)
|
||||
{
|
||||
DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent) {
|
||||
auto* outer = new QVBoxLayout(this);
|
||||
outer->setContentsMargins(0, 0, 0, 0);
|
||||
outer->setSpacing(0);
|
||||
|
|
@ -125,18 +27,31 @@ DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent)
|
|||
hostLayout->setSpacing(0);
|
||||
|
||||
// 表单卡片:浅一档底色 + 1px 边框 + 中圆角,从面板底上读出独立「表单」面。
|
||||
card_ = new QFrame();
|
||||
card_->setObjectName(QStringLiteral("attrForm"));
|
||||
auto* card = new QFrame();
|
||||
card->setObjectName(QStringLiteral("attrForm"));
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
card_, QStringLiteral("#attrForm { background:{{bg/panel-subtle}};"
|
||||
"border:1px solid {{border/default}}; border-radius:%1px; }")
|
||||
.arg(geopro::app::radius::kMd));
|
||||
cardLayout_ = new QVBoxLayout(card_);
|
||||
cardLayout_->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg,
|
||||
geopro::app::space::kLg, geopro::app::space::kLg);
|
||||
cardLayout_->setSpacing(geopro::app::space::kMd);
|
||||
card, QStringLiteral("#attrForm { background:{{bg/panel-subtle}};"
|
||||
"border:1px solid {{border/default}}; border-radius:%1px; }")
|
||||
.arg(geopro::app::radius::kMd));
|
||||
auto* cardLayout = new QVBoxLayout(card);
|
||||
cardLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg,
|
||||
geopro::app::space::kLg, geopro::app::space::kLg);
|
||||
cardLayout->setSpacing(geopro::app::space::kMd);
|
||||
|
||||
hostLayout->addWidget(card_);
|
||||
// 空/错占位(与键值视图互斥显示)。
|
||||
placeholder_ = new QLabel(card);
|
||||
placeholder_->setAlignment(Qt::AlignCenter);
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
placeholder_,
|
||||
QStringLiteral("color:{{text/disabled}}; background:transparent; padding:%1px;")
|
||||
.arg(geopro::app::space::kXl));
|
||||
cardLayout->addWidget(placeholder_);
|
||||
|
||||
// 唯一只读渲染器(§6.4)。详情对话框直接复用 KeyValueView,保证处处一致。
|
||||
kv_ = new KeyValueView(card);
|
||||
cardLayout->addWidget(kv_);
|
||||
|
||||
hostLayout->addWidget(card);
|
||||
hostLayout->addStretch();
|
||||
scroll->setWidget(host);
|
||||
outer->addWidget(scroll);
|
||||
|
|
@ -144,91 +59,21 @@ DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent)
|
|||
showMessage(QStringLiteral("(选中后显示属性详情)"));
|
||||
}
|
||||
|
||||
void DynamicFormView::clear()
|
||||
{
|
||||
while (cardLayout_->count() > 0) {
|
||||
QLayoutItem* it = cardLayout_->takeAt(0);
|
||||
if (it->widget()) it->widget()->deleteLater();
|
||||
if (QLayout* sub = it->layout()) {
|
||||
// 嵌套网格:先回收其子控件(控件归属 card_,删布局不会连带删),再让下方
|
||||
// delete it 释放该嵌套布局本身——注意 it == it->layout()(QLayout 即 QLayoutItem),
|
||||
// 故此处不可再 delete sub,否则与 delete it 重复释放导致崩溃。
|
||||
while (sub->count() > 0) {
|
||||
QLayoutItem* sit = sub->takeAt(0);
|
||||
if (sit->widget()) sit->widget()->deleteLater();
|
||||
delete sit;
|
||||
}
|
||||
}
|
||||
delete it;
|
||||
}
|
||||
}
|
||||
|
||||
void DynamicFormView::showCardMessage(const QString& message)
|
||||
{
|
||||
auto* hint = new QLabel(message);
|
||||
hint->setAlignment(Qt::AlignCenter);
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
hint, QStringLiteral("color:{{text/disabled}}; background:transparent; padding:%1px;")
|
||||
.arg(geopro::app::space::kXl));
|
||||
cardLayout_->addWidget(hint);
|
||||
}
|
||||
|
||||
void DynamicFormView::showMessage(const QString& message)
|
||||
{
|
||||
clear();
|
||||
showCardMessage(message);
|
||||
cardLayout_->addStretch();
|
||||
}
|
||||
|
||||
void DynamicFormView::setForm(const geopro::data::DynamicForm& form)
|
||||
{
|
||||
clear();
|
||||
void DynamicFormView::setForm(const geopro::data::DynamicForm& form) {
|
||||
if (form.groups.empty()) {
|
||||
showCardMessage(QStringLiteral("(暂无属性)"));
|
||||
cardLayout_->addStretch();
|
||||
showMessage(QStringLiteral("(暂无属性)"));
|
||||
return;
|
||||
}
|
||||
placeholder_->hide();
|
||||
kv_->show();
|
||||
kv_->setForm(form);
|
||||
}
|
||||
|
||||
for (const auto& group : form.groups) {
|
||||
// 每组一个独立网格:分组标题带横跨 4 列,字段两列自上而下、自左而右铺排。
|
||||
auto* grid = new QGridLayout();
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setHorizontalSpacing(geopro::app::space::kLg);
|
||||
grid->setVerticalSpacing(geopro::app::space::kXs);
|
||||
grid->setColumnStretch(kColValueA, 1);
|
||||
grid->setColumnStretch(kColValueB, 1);
|
||||
|
||||
// gridRow 线性递增:标题占一行,之后「分隔线行 + 字段对行」交替。
|
||||
int gridRow = 0;
|
||||
grid->addWidget(makeGroupHeader(QString::fromStdString(group.name)), gridRow, 0, 1,
|
||||
kColSpanAll);
|
||||
++gridRow;
|
||||
|
||||
const int n = static_cast<int>(group.fields.size());
|
||||
if (n == 0) {
|
||||
grid->addWidget(makeLabel(QStringLiteral("(本组暂无字段)")), gridRow, 0, 1,
|
||||
kColSpanAll);
|
||||
}
|
||||
// 字段两两成对,每对一行:偶数下标落左对(labelA|valueA),奇数落右对(labelB|valueB)。
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const auto& f = group.fields[static_cast<size_t>(i)];
|
||||
const bool left = (i % 2 == 0);
|
||||
if (left) {
|
||||
// 每新起一行前先放一条横向分隔线(铺满 4 列),再让字段对落到下一行。
|
||||
grid->addWidget(makeRowDivider(), gridRow, 0, 1, kColSpanAll);
|
||||
++gridRow;
|
||||
}
|
||||
const int labelCol = left ? kColLabelA : kColLabelB;
|
||||
const int valueCol = left ? kColValueA : kColValueB;
|
||||
grid->addWidget(makeLabel(QString::fromStdString(f.name)), gridRow, labelCol);
|
||||
grid->addWidget(makeValue(QString::fromStdString(f.value)), gridRow, valueCol);
|
||||
if (!left) ++gridRow; // 右对填满,行完结;左对则等右对或循环结束
|
||||
}
|
||||
|
||||
cardLayout_->addLayout(grid);
|
||||
}
|
||||
|
||||
cardLayout_->addStretch();
|
||||
void DynamicFormView::showMessage(const QString& message) {
|
||||
kv_->clear();
|
||||
kv_->hide();
|
||||
placeholder_->setText(message);
|
||||
placeholder_->show();
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
#include <QWidget>
|
||||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
class QVBoxLayout;
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 被动:渲染 DynamicForm(分组键值)为「两列卡片式属性表单」。对象属性 / 数据集属性两面板共用。
|
||||
// 视觉:外层滚动区内嵌一张带边框/底色/圆角的表单卡片;每组一个分组标题带(横跨整行),
|
||||
// 组内字段两列排布(labelA|valueA labelB|valueB),行间细分隔线。颜色全走主题令牌。
|
||||
class KeyValueView;
|
||||
|
||||
// 属性面板用的只读表单容器:滚动区 + 卡片,内嵌唯一渲染器 KeyValueView(§6.4)。
|
||||
// 对象属性 / 数据集属性面板共用。数据详情对话框不经本类,直接用 KeyValueView
|
||||
// (见 formkit::buildDetailDialog),但二者共享同一渲染器,外观一致。
|
||||
class DynamicFormView : public QWidget {
|
||||
public:
|
||||
explicit DynamicFormView(QWidget* parent = nullptr);
|
||||
|
|
@ -17,11 +18,8 @@ public:
|
|||
void showMessage(const QString& message); // 空/错占位
|
||||
|
||||
private:
|
||||
void clear(); // 拆掉卡片内全部内容(含分隔线/标题/字段)
|
||||
void showCardMessage(const QString& message); // 卡片内居中淡色提示
|
||||
|
||||
QFrame* card_ = nullptr; // 表单卡片(objectName=attrForm)
|
||||
QVBoxLayout* cardLayout_ = nullptr; // 卡片内纵向布局(容纳各分组网格 / 占位提示)
|
||||
KeyValueView* kv_ = nullptr;
|
||||
QLabel* placeholder_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
#include "panels/KeyValueView.hpp"
|
||||
|
||||
#include <QFont>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kColLabelA = 0;
|
||||
constexpr int kColValueA = 1;
|
||||
constexpr int kColLabelB = 2;
|
||||
constexpr int kColValueB = 3;
|
||||
|
||||
constexpr int kRowMinHeight = 28; // 字段行高(§6.4)
|
||||
|
||||
// 数值/坐标类值用等宽字族逐列对齐(§2.1/§6.4);纯文字保持默认字族。
|
||||
bool isNumericValue(const QString& text) {
|
||||
const QString trimmed = text.trimmed();
|
||||
if (trimmed.isEmpty()) return false;
|
||||
int digits = 0;
|
||||
int letters = 0;
|
||||
for (const QChar c : trimmed) {
|
||||
if (c.isDigit())
|
||||
++digits;
|
||||
else if (c.isLetter())
|
||||
++letters;
|
||||
}
|
||||
return digits > 0 && digits >= letters;
|
||||
}
|
||||
|
||||
QLabel* makeKey(const QString& text) {
|
||||
auto* k = new QLabel(text);
|
||||
k->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
k->setFixedWidth(geopro::app::scaledPx(geopro::app::space::kDetailKeyCol)); // 定宽,跨组对齐
|
||||
k->setMinimumHeight(geopro::app::scaledPx(kRowMinHeight));
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
k, QStringLiteral("color:{{text/secondary}}; background:transparent; padding:2px 0;"));
|
||||
return k;
|
||||
}
|
||||
|
||||
QLabel* makeValue(const QString& text) {
|
||||
auto* v = new QLabel(text);
|
||||
v->setWordWrap(true);
|
||||
v->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
v->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
v->setMinimumHeight(geopro::app::scaledPx(kRowMinHeight));
|
||||
if (isNumericValue(text)) {
|
||||
QFont f = v->font();
|
||||
// 必须 setFamilies(列表):setFamily 把整串逗号名当单一字族找不到→静默回退。
|
||||
f.setFamilies(QString::fromLatin1(geopro::app::type::kMonoFamily).split(QStringLiteral(", ")));
|
||||
v->setFont(f);
|
||||
}
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
v, QStringLiteral("color:{{text/primary}}; background:transparent; padding:2px 0;"));
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
KeyValueView::KeyValueView(QWidget* parent) : QWidget(parent) {
|
||||
lay_ = new QVBoxLayout(this);
|
||||
lay_->setContentsMargins(0, 0, 0, 0);
|
||||
lay_->setSpacing(geopro::app::space::kMd);
|
||||
}
|
||||
|
||||
void KeyValueView::clear() {
|
||||
QLayoutItem* it = nullptr;
|
||||
while ((it = lay_->takeAt(0)) != nullptr) {
|
||||
if (it->widget()) it->widget()->deleteLater();
|
||||
if (QLayout* sub = it->layout()) {
|
||||
QLayoutItem* sit = nullptr;
|
||||
while ((sit = sub->takeAt(0)) != nullptr) {
|
||||
if (sit->widget()) sit->widget()->deleteLater();
|
||||
delete sit;
|
||||
}
|
||||
}
|
||||
delete it;
|
||||
}
|
||||
}
|
||||
|
||||
void KeyValueView::setForm(const geopro::data::DynamicForm& form) {
|
||||
clear();
|
||||
bool renderedGroup = false;
|
||||
for (const auto& group : form.groups) {
|
||||
const QString gname = QString::fromStdString(group.name);
|
||||
if (!gname.isEmpty() || form.groups.size() > 1) {
|
||||
formkit::addSection(lay_, gname, this, renderedGroup);
|
||||
renderedGroup = true;
|
||||
}
|
||||
|
||||
// 两列网格:label/value 各两份,value 列吸收伸展、label 列贴合内容。
|
||||
auto* grid = new QGridLayout();
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setHorizontalSpacing(geopro::app::space::kLg);
|
||||
grid->setVerticalSpacing(geopro::app::space::kXs);
|
||||
grid->setColumnStretch(kColValueA, 1);
|
||||
grid->setColumnStretch(kColValueB, 1);
|
||||
|
||||
const int n = static_cast<int>(group.fields.size());
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const auto& f = group.fields[static_cast<size_t>(i)];
|
||||
const bool left = (i % 2 == 0);
|
||||
const int gridRow = i / 2;
|
||||
const int labelCol = left ? kColLabelA : kColLabelB;
|
||||
const int valueCol = left ? kColValueA : kColValueB;
|
||||
grid->addWidget(makeKey(QString::fromStdString(f.name)), gridRow, labelCol);
|
||||
grid->addWidget(makeValue(QString::fromStdString(f.value)), gridRow, valueCol);
|
||||
}
|
||||
lay_->addLayout(grid);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
#include <QWidget>
|
||||
|
||||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
class QVBoxLayout;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 唯一的「只读键值表」渲染器(规范 §6.4 / §7.0.10):分组标题(formkit::addSection)+
|
||||
// 两列键值(数值/坐标等宽对齐)。**不含**滚动区/卡片外壳——容器由调用方决定:
|
||||
// · 属性面板:DynamicFormView 在滚动卡片内嵌本视图;
|
||||
// · 数据详情对话框:formkit::buildDetailDialog 直接内嵌本视图(随内容自适应大小)。
|
||||
// 属性面板与各详情对话框共用本类,保证只读表单处处一致。
|
||||
class KeyValueView : public QWidget {
|
||||
public:
|
||||
explicit KeyValueView(QWidget* parent = nullptr);
|
||||
void setForm(const geopro::data::DynamicForm& form);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
QVBoxLayout* lay_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<!-- QSS 用的内嵌图标(下拉箭头等)。运行时经 :/icons/... 读取,平面现代风,
|
||||
跨明暗主题用中性灰,避免覆写 ::drop-down 后 Fusion 原生斜角箭头的复古观感。 -->
|
||||
<qresource prefix="/icons">
|
||||
<file alias="chevron-down.svg">icons/chevron-down.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
||||
<path d="M3 4.5 L6 7.5 L9 4.5" fill="none" stroke="#6B7785" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 229 B |
Loading…
Reference in New Issue