feat/object-selection-panels #4
|
|
@ -1,6 +1,7 @@
|
|||
#include "panels/DynamicFormView.hpp"
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -9,7 +10,67 @@
|
|||
|
||||
namespace geopro::app {
|
||||
|
||||
DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent) {
|
||||
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 列
|
||||
|
||||
// 字段标签(次要色,右侧留点呼吸,顶对齐以配合值换行)。
|
||||
QLabel* makeLabel(const QString& text)
|
||||
{
|
||||
auto* k = new QLabel(text);
|
||||
k->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 分组标题带:横跨整行的淡底强调条,半粗次要色,给表单清晰的层级。
|
||||
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;")
|
||||
.arg(geopro::app::type::kWeightSemibold)
|
||||
.arg(geopro::app::scaledPx(geopro::app::type::kBody))
|
||||
.arg(geopro::app::radius::kSm));
|
||||
return title;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent)
|
||||
{
|
||||
auto* outer = new QVBoxLayout(this);
|
||||
outer->setContentsMargins(0, 0, 0, 0);
|
||||
outer->setSpacing(0);
|
||||
|
|
@ -17,65 +78,119 @@ DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent) {
|
|||
auto* scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
// 滚动内容宿主:仅承载表单卡片,四周留出与面板一致的内边距,让卡片浮于面板底上。
|
||||
auto* host = new QWidget();
|
||||
content_ = new QVBoxLayout(host);
|
||||
content_->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMd,
|
||||
geopro::app::space::kLg, geopro::app::space::kMd);
|
||||
content_->setSpacing(geopro::app::space::kSm);
|
||||
content_->addStretch();
|
||||
auto* hostLayout = new QVBoxLayout(host);
|
||||
hostLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg,
|
||||
geopro::app::space::kLg, geopro::app::space::kLg);
|
||||
hostLayout->setSpacing(0);
|
||||
|
||||
// 表单卡片:浅一档底色 + 1px 边框 + 中圆角,从面板底上读出独立「表单」面。
|
||||
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);
|
||||
|
||||
hostLayout->addWidget(card_);
|
||||
hostLayout->addStretch();
|
||||
scroll->setWidget(host);
|
||||
outer->addWidget(scroll);
|
||||
|
||||
showMessage(QStringLiteral("(选中后显示属性详情)"));
|
||||
}
|
||||
|
||||
void DynamicFormView::clear() {
|
||||
while (content_->count() > 0) {
|
||||
QLayoutItem* it = content_->takeAt(0);
|
||||
void DynamicFormView::clear()
|
||||
{
|
||||
while (cardLayout_->count() > 0) {
|
||||
QLayoutItem* it = cardLayout_->takeAt(0);
|
||||
if (it->widget()) it->widget()->deleteLater();
|
||||
if (it->layout()) {
|
||||
// 嵌套网格:先回收其子控件,再删布局,避免残留控件泄漏/重叠。
|
||||
QLayout* sub = it->layout();
|
||||
while (sub->count() > 0) {
|
||||
QLayoutItem* sit = sub->takeAt(0);
|
||||
if (sit->widget()) sit->widget()->deleteLater();
|
||||
delete sit;
|
||||
}
|
||||
delete sub;
|
||||
}
|
||||
delete it;
|
||||
}
|
||||
}
|
||||
|
||||
void DynamicFormView::showMessage(const QString& message) {
|
||||
clear();
|
||||
void DynamicFormView::showCardMessage(const QString& message)
|
||||
{
|
||||
auto* hint = new QLabel(message);
|
||||
hint->setAlignment(Qt::AlignCenter);
|
||||
geopro::app::applyTokenizedStyleSheet(hint,
|
||||
QStringLiteral("color:{{text/disabled}}; padding:16px;"));
|
||||
content_->addWidget(hint);
|
||||
content_->addStretch();
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
hint, QStringLiteral("color:{{text/disabled}}; background:transparent; padding:%1px;")
|
||||
.arg(geopro::app::space::kXl));
|
||||
cardLayout_->addWidget(hint);
|
||||
}
|
||||
|
||||
void DynamicFormView::setForm(const geopro::data::DynamicForm& form) {
|
||||
void DynamicFormView::showMessage(const QString& message)
|
||||
{
|
||||
clear();
|
||||
showCardMessage(message);
|
||||
cardLayout_->addStretch();
|
||||
}
|
||||
|
||||
void DynamicFormView::setForm(const geopro::data::DynamicForm& form)
|
||||
{
|
||||
clear();
|
||||
if (form.groups.empty()) {
|
||||
showMessage(QStringLiteral("(暂无属性)"));
|
||||
showCardMessage(QStringLiteral("(暂无属性)"));
|
||||
cardLayout_->addStretch();
|
||||
return;
|
||||
}
|
||||
for (const auto& group : form.groups) {
|
||||
auto* title = new QLabel(QString::fromStdString(group.name));
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
title, QStringLiteral("color:{{text/secondary}}; font-weight:%1; padding-top:6px;")
|
||||
.arg(geopro::app::type::kWeightSemibold));
|
||||
content_->addWidget(title);
|
||||
|
||||
auto* form_w = new QWidget();
|
||||
auto* fl = new QFormLayout(form_w);
|
||||
fl->setContentsMargins(0, 0, 0, 0);
|
||||
fl->setLabelAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
fl->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||||
for (const auto& f : group.fields) {
|
||||
auto* k = new QLabel(QString::fromStdString(f.name));
|
||||
geopro::app::applyTokenizedStyleSheet(k, QStringLiteral("color:{{text/secondary}};"));
|
||||
auto* v = new QLabel(QString::fromStdString(f.value));
|
||||
v->setWordWrap(true);
|
||||
geopro::app::applyTokenizedStyleSheet(v, QStringLiteral("color:{{text/primary}};"));
|
||||
fl->addRow(k, v);
|
||||
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);
|
||||
}
|
||||
content_->addWidget(form_w);
|
||||
// 字段两两成对,每对一行:偶数下标落左对(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;
|
||||
}
|
||||
content_->addStretch();
|
||||
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();
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@
|
|||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
class QVBoxLayout;
|
||||
class QFrame;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 被动:渲染 DynamicForm(分组键值)。对象属性 / 数据集属性两面板共用。
|
||||
// 被动:渲染 DynamicForm(分组键值)为「两列卡片式属性表单」。对象属性 / 数据集属性两面板共用。
|
||||
// 视觉:外层滚动区内嵌一张带边框/底色/圆角的表单卡片;每组一个分组标题带(横跨整行),
|
||||
// 组内字段两列排布(labelA|valueA labelB|valueB),行间细分隔线。颜色全走主题令牌。
|
||||
class DynamicFormView : public QWidget {
|
||||
public:
|
||||
explicit DynamicFormView(QWidget* parent = nullptr);
|
||||
|
|
@ -14,8 +17,11 @@ public:
|
|||
void showMessage(const QString& message); // 空/错占位
|
||||
|
||||
private:
|
||||
void clear();
|
||||
QVBoxLayout* content_ = nullptr; // 滚动区内容布局
|
||||
void clear(); // 拆掉卡片内全部内容(含分隔线/标题/字段)
|
||||
void showCardMessage(const QString& message); // 卡片内居中淡色提示
|
||||
|
||||
QFrame* card_ = nullptr; // 表单卡片(objectName=attrForm)
|
||||
QVBoxLayout* cardLayout_ = nullptr; // 卡片内纵向布局(容纳各分组网格 / 占位提示)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
Loading…
Reference in New Issue