#include "nmWxGeometryLayerDlg.h" #include "nmLayerGeometryDataSource.h" #include "nmWxLayerGeometryPreview.h" #include "iUnitHelper.h" #include "iUnitGroup.h" #include "ZxBaseUtil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // 表格的高度只决定可见行数,行高由字体和主题确定;支持复制只读顶底深度。 class LayerTable : public QTableWidget { public: explicit LayerTable(QWidget* parent) : QTableWidget(parent) {} virtual QSize sizeHint() const { // 按典型数值文本和七行内容估算初始尺寸;这只是推荐值,不限制层数和窗口拉伸。 const int width = fontMetrics().width("-0000000.0000") * 4 + verticalHeader()->sizeHint().width() + 48; return QSize(width, horizontalHeader()->sizeHint().height() + verticalHeader()->defaultSectionSize() * 7 + frameWidth() * 2); } protected: virtual void keyPressEvent(QKeyEvent* event) { if (event->matches(QKeySequence::Copy)) { // 使用制表符分列、换行分行,复制后可直接粘贴到表格软件;只读单元格也能复制。 QStringList lines; // 剪贴板中的逐行文本,每行内部用制表符分隔列。 const QList ranges = selectedRanges(); // 当前复制范围,含只读列。 foreach (const QTableWidgetSelectionRange& range, ranges) { for (int row = range.topRow(); row <= range.bottomRow(); ++row) { QStringList cells; // 当前行选中单元格的显示文本,复制不改变米制数据。 for (int column = range.leftColumn(); column <= range.rightColumn(); ++column) cells.append(item(row, column) ? item(row, column)->text() : QString()); lines.append(cells.join("\t")); } } QApplication::clipboard()->setText(lines.join("\n")); event->accept(); return; } QTableWidget::keyPressEvent(event); } }; } // 使用文本编辑器,避免默认浮点微调框的范围和精度截断。 // 编辑起始值取完整精度;没有改动就不 setData,不触发显示值反写。 class nmLayerCellDelegate : public QStyledItemDelegate { public: explicit nmLayerCellDelegate(QObject* parent) : QStyledItemDelegate(parent) {} virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem&, const QModelIndex& index) const { m_pActiveEditor = new QLineEdit(parent); m_oEditorIndex = index; return m_pActiveEditor; } virtual void setEditorData(QWidget* editor, const QModelIndex& index) const { QLineEdit* line = static_cast(editor); // 视图拥有的编辑器,仅在本次回调中借用。 // UserRole 保存编辑文本,DisplayRole 仅供格式化显示;非法输入也保留原文供再次修正。 const QString text = index.data(Qt::UserRole).toString(); // 完整精度的初始文本,或待修正的原始输入。 line->setText(text); line->setProperty("initialText", text); line->selectAll(); } virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { // initialText 区分“进入编辑后直接离开”和真正修改,防止未操作时进行数值往返换算。 QLineEdit* line = static_cast(editor); // 用户当前编辑内容的来源,不拥有该对象。 if (line->text() != line->property("initialText").toString()) { model->setData(index, line->text(), Qt::EditRole); line->setProperty("initialText", line->text()); } } void finish() { // Qt 4 的编辑器在关闭后延迟删除;不要再次提交已经隐藏的旧编辑器。 if (!m_pActiveEditor || !m_pActiveEditor->isVisible() || !m_oEditorIndex.isValid()) return; QLineEdit* editor = m_pActiveEditor; // 当前有效编辑器,交给视图提交及关闭,不在此处释放。 // 先通知表格写入并触发输入检查,再关闭编辑器;关闭时不自动跳到其他单元格。 emit commitData(editor); emit closeEditor(editor, QAbstractItemDelegate::NoHint); m_pActiveEditor = 0; } private: // 编辑器由视图创建/销毁,此处只弱引用;持久索引可在行结构变化后检测原单元格是否仍有效。 mutable QPointer m_pActiveEditor; // 当前活动编辑器的弱引用,视图销毁它后自动置空。 mutable QPersistentModelIndex m_oEditorIndex; // 活动编辑器对应的单元格,结构变化后用于判断索引是否有效。 }; nmWxGeometryLayerDlg::nmWxGeometryLayerDlg(nmLayerGeometryDataSource* source, QWidget* parent) : iDlgBase(parent), m_pDataSource(source), m_pLengthUnitGroup(iUnitHelper::getUnitGroupByUnit("m")), m_sDisplayUnit("m"), m_bRefreshing(false), m_bInitialized(false), m_bCommitBlocked(false) { setWindowTitle(tr("Layer Setting")); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); initUI(); } void nmWxGeometryLayerDlg::initUI() { // 普通字段采用主题推荐高度,纵向剩余空间交给表格与预览;整窗不固定像素尺寸。 QVBoxLayout* main = new QVBoxLayout(this); // 根布局,按字段、操作、表格、统计、底栏顺序排列。 main->setContentsMargins(12, 10, 12, 0); main->setSpacing(10); QGridLayout* fields = new QGridLayout; // 首层顶深及统一长度单位,加入根布局后由 Qt 管理。 fields->setHorizontalSpacing(6); fields->addWidget(new QLabel(tr("First layer top"), this), 0, 0); m_pFirstTopEdit = new QLineEdit(this); m_pFirstTopEdit->setObjectName("layerFirstTop"); m_pFirstTopEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); fields->addWidget(m_pFirstTopEdit, 0, 1); m_pLengthUnitComboBox = new QComboBox(this); m_pLengthUnitComboBox->setObjectName("layerLengthUnit"); m_pLengthUnitComboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); m_pLengthUnitComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // “m”只作为框架长度单位组的识别入口;可选单位列表和换算规则均由框架维护。 if (m_pLengthUnitGroup) m_pLengthUnitComboBox->addItems(m_pLengthUnitGroup->getAllUnitNames()); m_pLengthUnitComboBox->setCurrentIndex(m_pLengthUnitComboBox->findText(m_sDisplayUnit)); fields->addWidget(m_pLengthUnitComboBox, 0, 2); // 单位下拉框紧随顶深输入框,不另设单位标签;两者沿用已统一对话框的 8:5 伸展比例。 fields->setColumnStretch(1, 8); fields->setColumnStretch(2, 5); main->addLayout(fields); QHBoxLayout* actions = new QHBoxLayout; // 表格上方的分层编辑操作区。 m_pAddButton = new QPushButton(tr("Add"), this); m_pInsertButton = new QPushButton(tr("Insert"), this); m_pDeleteButton = new QPushButton(tr("Delete"), this); // 仅借用三个层结构操作按钮,以同一规则设置非默认按钮属性和排列顺序。 QPushButton* buttons[] = { m_pAddButton, m_pInsertButton, m_pDeleteButton }; for (int i = 0; i < 3; ++i) { buttons[i]->setAutoDefault(false); actions->addWidget(buttons[i]); } actions->addStretch(); main->addLayout(actions); m_pLayerTable = new LayerTable(this); m_pLayerTable->setObjectName("layerGeometryTable"); m_pLayerTable->setColumnCount(4); m_pLayerTable->setSelectionBehavior(QAbstractItemView::SelectItems); m_pLayerTable->setSelectionMode(QAbstractItemView::ContiguousSelection); // 行顺序就是实际上下层顺序,排序或拖放都会破坏几何含义,因此只提供显式插入/删除。 m_pLayerTable->setSortingEnabled(false); m_pLayerTable->setDragEnabled(false); m_pLayerTable->setAcceptDrops(false); m_pLayerTable->setDropIndicatorShown(false); m_pLayerTable->setAlternatingRowColors(true); m_pLayerTable->setStyleSheet( "QTableWidget { background-color: palette(base);" "alternate-background-color: rgb(232,232,232);" "gridline-color: palette(midlight); border: 1px solid palette(mid); }" "QHeaderView::section { background-color: palette(button); color: palette(button-text);" "border: none; border-right: 1px solid palette(mid); border-bottom: 1px solid palette(mid); }" "QTableCornerButton::section { background-color: palette(base); border: none;" "border-right: 1px solid palette(mid); border-bottom: 1px solid palette(mid); }"); m_pLayerTable->horizontalHeader()->setMovable(false); m_pLayerTable->horizontalHeader()->setResizeMode(QHeaderView::Stretch); m_pLayerTable->verticalHeader()->setMovable(false); m_pLayerTable->verticalHeader()->setResizeMode(QHeaderView::Fixed); m_pLayerTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); // 行高取实际输入控件的推荐高度,适应字体/DPI,纵向拉伸时不拉长每个输入格。 m_pLayerTable->verticalHeader()->setDefaultSectionSize(m_pFirstTopEdit->sizeHint().height() + 4); m_pLayerDelegate = new nmLayerCellDelegate(m_pLayerTable); m_pLayerTable->setItemDelegate(m_pLayerDelegate); m_pLayerPreview = new nmWxLayerGeometryPreview(this); // 预览只负责绘制/命中查询;窗口接管双击并复用统一的选色流程。 m_pLayerPreview->installEventFilter(this); QHBoxLayout* body = new QHBoxLayout; // 接收纵向余量的主体区,横向按 5:1 分配表格和预览。 // 主要宽度用于数值表格,侧边预览同步伸展;纵向增大时增加可见行,而非拉高输入格。 body->addWidget(m_pLayerTable, 5); body->addWidget(m_pLayerPreview, 1); main->addLayout(body, 1); QGridLayout* summary = new QGridLayout; // 层数与总厚度统计区,不承担几何编辑。 // 统计使用只读输入框以支持复制;内容来自米制副本,不从表格显示字符串累计。 summary->setHorizontalSpacing(6); summary->addWidget(new QLabel(tr("Layer count"), this), 0, 0); m_pLayerCountEdit = new QLineEdit(this); m_pLayerCountEdit->setReadOnly(true); m_pLayerCountEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); summary->addWidget(m_pLayerCountEdit, 0, 1); summary->addWidget(new QLabel(tr("Total thickness"), this), 0, 2); m_pTotalThicknessEdit = new QLineEdit(this); m_pTotalThicknessEdit->setReadOnly(true); m_pTotalThicknessEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); summary->addWidget(m_pTotalThicknessEdit, 0, 3); summary->setColumnStretch(1, 1); summary->setColumnStretch(3, 1); main->addLayout(summary); QWidget* footer = new QWidget(this); // 底部操作区,随主题采用推荐高度,由对话框负责释放。 footer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); QHBoxLayout* footerLayout = new QHBoxLayout(footer); // 左侧重置,右侧标准确定/取消按钮。 footerLayout->setContentsMargins(4, 5, 4, 5); m_pResetButton = new QPushButton(tr("Reset from analytical"), footer); m_pResetButton->setAutoDefault(false); footerLayout->addWidget(m_pResetButton); footerLayout->addStretch(); // 标准按钮容器拥有确定/取消按钮,统一转发接受和拒绝操作。 QDialogButtonBox* box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, footer); m_pOkButton = box->button(QDialogButtonBox::Ok); m_pOkButton->setText(tr("OK")); m_pOkButton->setIcon(zxLoadIcon("OK")); m_pOkButton->setDefault(true); box->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); box->button(QDialogButtonBox::Cancel)->setIcon(zxLoadIcon("Cancel")); footerLayout->addWidget(box); main->addWidget(footer); connect(m_pLayerTable, SIGNAL(itemChanged(QTableWidgetItem*)), this, SLOT(onItemChanged(QTableWidgetItem*))); connect(m_pLayerTable, SIGNAL(itemSelectionChanged()), this, SLOT(onSelectionChanged())); connect(m_pLengthUnitComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onUnitChanged(int))); connect(m_pFirstTopEdit, SIGNAL(textEdited(QString)), this, SLOT(onTopEdited())); connect(m_pFirstTopEdit, SIGNAL(editingFinished()), this, SLOT(onTopFinished())); connect(m_pAddButton, SIGNAL(clicked()), this, SLOT(onAdd())); connect(m_pInsertButton, SIGNAL(clicked()), this, SLOT(onInsert())); connect(m_pDeleteButton, SIGNAL(clicked()), this, SLOT(onDelete())); connect(m_pResetButton, SIGNAL(clicked()), this, SLOT(onReset())); connect(box, SIGNAL(accepted()), this, SLOT(accept())); connect(box, SIGNAL(rejected()), this, SLOT(reject())); updateActions(); } bool nmWxGeometryLayerDlg::initialize() { // 单位服务与初始副本同时准备成功才开放操作;构造窗口本身不会触发工程数据写入。 if (!m_pDataSource || !m_pLengthUnitGroup || m_pLengthUnitComboBox->currentIndex() < 0 || !m_pDataSource->load(m_oGeometry) || !canDisplayUnit(m_sDisplayUnit)) return false; m_bInitialized = true; refreshTable(0); adjustSize(); return true; } QString nmWxGeometryLayerDlg::displayValue(double meters, bool fullPrecision) const { double value = meters; // 框架换算的输出缓冲;成功后为当前显示单位下的数值。 int digits = -1; // 框架返回的目标单位小数位;负数表示未配置,采用有效数字格式。 if (!m_pLengthUnitGroup || !m_pLengthUnitGroup->convert("m", meters, m_sDisplayUnit, value, digits) || !qIsFinite(value)) return QString(); // 完整精度用于编辑起始值;普通显示遵循框架小数位设置,两者始终共用同一米制原值。 if (fullPrecision) return QString::number(value, 'g', 17); // 极小的正厚度不能被显示成零;计算始终使用米制原值,显示精度不参与回写。 const QString text = digits >= 0 ? QString::number(value, 'f', qMin(digits, 15)) : QString::number(value, 'g', 12); return value != 0.0 && text.toDouble() == 0.0 ? QString::number(value, 'g', 12) : text; } bool nmWxGeometryLayerDlg::toMeters(const QString& text, double& value) const { // 文本解析成功不代表可用:NaN/无穷大和换算后的溢出都必须在写回前排除。 bool ok = false; // 仅表示文本解析成功,不代替有限值及单位换算检查。 const double input = text.trimmed().toDouble(&ok); // 当前显示单位的输入数值,尚未换算为米。 int digits = -1; // 接收框架附带的精度信息;回写米制值时不按该精度舍入。 return ok && qIsFinite(input) && m_pLengthUnitGroup && m_pLengthUnitGroup->convert(m_sDisplayUnit, input, "m", value, digits) && qIsFinite(value); } bool nmWxGeometryLayerDlg::canDisplayUnit(const QString& unit) const { // 不仅检查单位名称存在,还验证当前各值能在该单位下表达;过程中不改 m_sDisplayUnit 和副本。 if (!m_pLengthUnitGroup || m_oGeometry.m_vecLayers.isEmpty()) return false; for (int i = 0; i < m_oGeometry.m_vecLayers.size(); ++i) { const nmLayerGeometryRow& row = m_oGeometry.m_vecLayers.at(i); // 三项均为米制原值,顺序为顶深、底深、厚度;第 2 项额外检查正厚度换算后没有下溢。 const double lengths[] = { row.m_dTopDepth, row.m_dBottomDepth, row.m_dThickness }; for (int j = 0; j < 3; ++j) { double converted = 0.0; // 候选显示单位下的值,仅用于可表示性预检。 int digits = -1; // 框架接口的精度输出,此处不生成显示文本。 if (!m_pLengthUnitGroup->convert("m", lengths[j], unit, converted, digits) || !qIsFinite(converted) || (j == 2 && row.m_dThickness > 0.0 && converted <= 0.0)) return false; } } double total = 0.0; // 候选单位下的累计层厚度,与单层值分开检查换算溢出。 int digits = -1; // 框架接口的精度输出,不影响累计值本身。 return m_pLengthUnitGroup->convert("m", m_oGeometry.totalThickness(), unit, total, digits) && qIsFinite(total); } void nmWxGeometryLayerDlg::refreshTable(int selectedRow) { // 批量填表必须屏蔽 itemChanged,避免格式化文本被当作用户编辑再次换算写入。 // 此函数会重建厚度显示并清空错误行,仅用于编辑已收齐或用户明确放弃旧输入的场景。 m_bRefreshing = true; m_pLayerTable->blockSignals(true); m_pLayerTable->setRowCount(m_oGeometry.m_vecLayers.size()); m_pLayerTable->setHorizontalHeaderLabels(QStringList() << tr("Layer name") << tr("Thickness") + " (" + m_sDisplayUnit + ")" << tr("Top depth") + " (" + m_sDisplayUnit + ")" << tr("Bottom depth") + " (" + m_sDisplayUnit + ")"); for (int row = 0; row < m_oGeometry.m_vecLayers.size(); ++row) { const nmLayerGeometryRow& layer = m_oGeometry.m_vecLayers.at(row); for (int column = 0; column < 4; ++column) { QTableWidgetItem* item = m_pLayerTable->item(row, column); // 表格拥有的单元格,优先复用已有项。 if (!item) { item = new QTableWidgetItem; m_pLayerTable->setItem(row, column, item); } item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | (column < 2 ? Qt::ItemIsEditable : Qt::NoItemFlags)); item->setTextAlignment(column == 0 ? Qt::AlignLeft | Qt::AlignVCenter : Qt::AlignRight | Qt::AlignVCenter); } QTableWidgetItem* name = m_pLayerTable->item(row, 0); // 名称列单元格,同时通过 DecorationRole 显示层色块。 name->setText(layer.m_sName.isEmpty() ? QString("Layer%1").arg(row + 1) : layer.m_sName); name->setData(Qt::UserRole, name->text()); name->setData(Qt::DecorationRole, layer.m_clrLayer); m_pLayerTable->item(row, 1)->setText(displayValue(layer.m_dThickness)); // 同一格分别保存“给用户看的精度”和“进入编辑时的精度”,显示舍入不影响底层数据。 m_pLayerTable->item(row, 1)->setData(Qt::UserRole, displayValue(layer.m_dThickness, true)); } m_setInvalidThicknessRows.clear(); m_sFirstTopDisplayText = m_oGeometry.m_vecLayers.isEmpty() ? QString() : displayValue(m_oGeometry.m_vecLayers.first().m_dTopDepth); m_pFirstTopEdit->setText(m_sFirstTopDisplayText); if (selectedRow >= 0 && selectedRow < m_pLayerTable->rowCount()) m_pLayerTable->setCurrentCell(selectedRow, 0); m_pLayerTable->blockSignals(false); m_bRefreshing = false; refreshDerived(); } void nmWxGeometryLayerDlg::refreshDerived() { // 首层顶深尚未提交时暂不展示旧边界,避免输入值与剖面出现矛盾。 const bool valid = m_setInvalidThicknessRows.isEmpty() && m_oGeometry.isValid() && canDisplayUnit(m_sDisplayUnit) && m_pFirstTopEdit->text() == m_sFirstTopDisplayText && topIsValid(); m_pLayerTable->blockSignals(true); for (int row = 0; row < m_oGeometry.m_vecLayers.size(); ++row) { m_pLayerTable->item(row, 2)->setText(valid ? displayValue(m_oGeometry.m_vecLayers.at(row).m_dTopDepth) : QString()); m_pLayerTable->item(row, 3)->setText(valid ? displayValue(m_oGeometry.m_vecLayers.at(row).m_dBottomDepth) : QString()); } m_pLayerTable->blockSignals(false); m_pLayerCountEdit->setText(QString::number(m_oGeometry.m_vecLayers.size())); m_pTotalThicknessEdit->setText(valid ? displayValue(m_oGeometry.totalThickness()) : QString()); m_pLayerPreview->setGeometryData(valid ? m_oGeometry : nmLayerGeometry(), m_pLayerTable->currentRow()); updateActions(); } bool nmWxGeometryLayerDlg::topIsValid() const { // 文本没有变化时使用原值;文本改变时在候选副本上试算,按钮状态检查不能提前提交。 if (m_pFirstTopEdit->text() == m_sFirstTopDisplayText) return !m_oGeometry.m_vecLayers.isEmpty() && qIsFinite(m_oGeometry.m_vecLayers.first().m_dTopDepth); double top = 0.0; // 文本换算后的米制首层顶深;只有 toMeters 成功后才使用。 nmLayerGeometry candidate(m_oGeometry); // 仅用于合法性试算,不能替换当前编辑副本。 return toMeters(m_pFirstTopEdit->text(), top) && candidate.setFirstTop(top); } void nmWxGeometryLayerDlg::updateActions() { const bool valid = m_bInitialized && m_setInvalidThicknessRows.isEmpty() && m_oGeometry.isValid() && canDisplayUnit(m_sDisplayUnit) && topIsValid(); const bool selected = m_pLayerTable->currentRow() >= 0; // 是否存在当前层,不表示整行都被选中。 m_pOkButton->setEnabled(valid && !m_bCommitBlocked); m_pAddButton->setEnabled(valid); m_pInsertButton->setEnabled(valid && selected); m_pDeleteButton->setEnabled(m_bInitialized && selected && m_oGeometry.m_vecLayers.size() > 1 && topIsValid()); m_pResetButton->setEnabled(m_bInitialized); } bool nmWxGeometryLayerDlg::applyTop() { if (m_pFirstTopEdit->text() == m_sFirstTopDisplayText) return topIsValid(); double top = 0.0; // 本次实际准备应用的首层顶深,已通过 toMeters 转换时单位为 m。 if (!toMeters(m_pFirstTopEdit->text(), top) || !m_oGeometry.setFirstTop(top)) return false; // 只有应用成功才更新文本基线,失败时保留用户原输入以便修正。 m_sFirstTopDisplayText = m_pFirstTopEdit->text(); m_bCommitBlocked = false; return true; } void nmWxGeometryLayerDlg::onTopEdited() { m_bCommitBlocked = false; refreshDerived(); } void nmWxGeometryLayerDlg::onTopFinished() { if (m_bRefreshing || !m_bInitialized) return; applyTop(); refreshDerived(); } bool nmWxGeometryLayerDlg::finishEditing() { // 委托提交会同步触发 onItemChanged,随后再判断错误行集合,不能提前用旧副本判断成功。 m_pLayerDelegate->finish(); const bool valid = applyTop() && m_setInvalidThicknessRows.isEmpty() && m_oGeometry.isValid() && canDisplayUnit(m_sDisplayUnit); refreshDerived(); return valid; } void nmWxGeometryLayerDlg::onItemChanged(QTableWidgetItem* item) { if (m_bRefreshing || !m_bInitialized || !item) return; const int row = item->row(); // 表格与几何副本共用的零起始层索引。 if (row < 0 || row >= m_oGeometry.m_vecLayers.size()) return; m_bCommitBlocked = false; if (item->column() == 0) { // 层名是显示属性,直接更新副本即可,不触发顶底深度重排。 m_oGeometry.m_vecLayers[row].m_sName = item->text(); m_pLayerTable->blockSignals(true); item->setData(Qt::UserRole, item->text()); m_pLayerTable->blockSignals(false); } else if (item->column() == 1) { double thickness = 0.0; // 换算后的米制厚度候选值,合法时才用于重排副本。 const bool valid = toMeters(item->text(), thickness) && m_oGeometry.setThickness(row, thickness); // 几何函数失败时副本保持原值,但错误标记必须阻止保存,不能悄悄提交旧厚度。 if (valid) m_setInvalidThicknessRows.remove(row); else m_setInvalidThicknessRows.insert(row); m_pLayerTable->blockSignals(true); // 非法文本留在单元格和再次编辑的起始值中,禁止确定,不用弹窗打断用户。 item->setData(Qt::UserRole, valid ? displayValue(m_oGeometry.m_vecLayers.at(row).m_dThickness, true) : item->text()); if (valid) item->setText(displayValue(m_oGeometry.m_vecLayers.at(row).m_dThickness)); m_pLayerTable->blockSignals(false); } refreshDerived(); } void nmWxGeometryLayerDlg::onUnitChanged(int index) { if (m_bRefreshing || !m_bInitialized || index < 0) return; const QString nextUnit = m_pLengthUnitComboBox->itemText(index); // 候选单位,通过输入检查后才成为当前单位。 if (nextUnit == m_sDisplayUnit) return; // 下拉框此时已显示新选项,但活动文本仍属于旧单位;先收齐输入再改 m_sDisplayUnit。 // 失败时仅恢复选项,不全量刷新表格,否则会覆盖用户尚待修正的非法文本。 if (!finishEditing() || !canDisplayUnit(nextUnit)) { m_pLengthUnitComboBox->blockSignals(true); m_pLengthUnitComboBox->setCurrentIndex(m_pLengthUnitComboBox->findText(m_sDisplayUnit)); m_pLengthUnitComboBox->blockSignals(false); return; } m_sDisplayUnit = nextUnit; refreshTable(m_pLayerTable->currentRow()); } void nmWxGeometryLayerDlg::onSelectionChanged() { if (!m_bRefreshing && m_bInitialized) refreshDerived(); } QString nmWxGeometryLayerDlg::nextLayerName() const { // 从现有名称查重,删除中间层后可复用空闲编号,但不能重命名其他层。 QStringList names; // 当前层的实际显示名称,包含旧工程空名称对应的默认 LayerN。 for (int i = 0; i < m_oGeometry.m_vecLayers.size(); ++i) names.append(m_oGeometry.m_vecLayers.at(i).m_sName.isEmpty() ? QString("Layer%1").arg(i + 1) : m_oGeometry.m_vecLayers.at(i).m_sName); int suffix = 1; // 从 1 起查找未占用的 LayerN 编号,不等同于新层的行号。 while (names.contains(QString("Layer%1").arg(suffix))) ++suffix; return QString("Layer%1").arg(suffix); } void nmWxGeometryLayerDlg::insertAt(int row) { if (!finishEditing()) return; // 新层使用当前层厚度作为起始值,不再引入与分析数据无关的固定厚度。 const int reference = qBound(0, m_pLayerTable->currentRow(), m_oGeometry.m_vecLayers.size() - 1); // 参考层索引。 const double thickness = m_oGeometry.m_vecLayers.at(reference).m_dThickness; // 新层采用的初始米制厚度。 if (m_oGeometry.insertLayer(row, nextLayerName(), thickness, QColor::fromHsv((105 + m_oGeometry.m_vecLayers.size() * 47) % 360, 90, 210))) { m_bCommitBlocked = false; refreshTable(row); } } void nmWxGeometryLayerDlg::onAdd() { insertAt(m_oGeometry.m_vecLayers.size()); } void nmWxGeometryLayerDlg::onInsert() { insertAt(m_pLayerTable->currentRow()); } void nmWxGeometryLayerDlg::onDelete() { m_pLayerDelegate->finish(); if (!applyTop()) return; const int row = m_pLayerTable->currentRow(); // 允许删除含非法厚度文本的当前行,但不能丢弃其他行尚未修正的输入。 QSet remaining = m_setInvalidThicknessRows; // 临时错误行集合,排除被删行后判断是否还有其他未修正输入。 remaining.remove(row); if (!remaining.isEmpty() || !m_oGeometry.removeLayer(row)) return; m_bCommitBlocked = false; refreshTable(qMin(row, m_oGeometry.m_vecLayers.size() - 1)); } void nmWxGeometryLayerDlg::chooseLayerColor(int nRow) { if (nRow < 0 || nRow >= m_oGeometry.m_vecLayers.size()) return; // 选色结果只写入编辑副本;取消选色返回无效 QColor,保持原有层颜色。 const QColor color = QColorDialog::getColor(m_oGeometry.m_vecLayers.at(nRow).m_clrLayer, this, tr("Layer color")); if (!color.isValid()) return; m_oGeometry.m_vecLayers[nRow].m_clrLayer = color; m_bCommitBlocked = false; refreshTable(nRow); } bool nmWxGeometryLayerDlg::eventFilter(QObject* pWatched, QEvent* pEvent) { if (pWatched == m_pLayerPreview && pEvent->type() == QEvent::MouseButtonDblClick) { QMouseEvent* pMouseEvent = static_cast(pEvent); // 借用本次鼠标事件,坐标属于右侧预览。 if (pMouseEvent->button() == Qt::LeftButton) { // 先完成可能尚未失焦的厚度/顶深编辑,让命中计算基于最新有效边界。 if (m_bInitialized && finishEditing()) { const int nRow = m_pLayerPreview->layerAtPoint(pMouseEvent->pos()); // -1 表示点在间隙或边距。 if (nRow >= 0) { m_pLayerTable->setCurrentCell(nRow, 0); chooseLayerColor(nRow); } } pMouseEvent->accept(); return true; } } return iDlgBase::eventFilter(pWatched, pEvent); } void nmWxGeometryLayerDlg::onReset() { // 重置是显式放弃当前层编辑,非法厚度也允许直接用所属分析重新初始化。 m_pLayerDelegate->finish(); applyTop(); // 顶深输入合法时沿用新值;非法时沿用副本中上一次合法顶深,重置不采用非法文本。 const double top = m_oGeometry.m_vecLayers.isEmpty() ? 6000.0 : m_oGeometry.m_vecLayers.first().m_dTopDepth; nmLayerGeometry reset; // 从分析读取的候选副本,完整读取并校验后才替换当前编辑内容。 if (!m_pDataSource || !m_pDataSource->loadFromAnalysis(top, reset) || !reset.isValid()) return; m_oGeometry = reset; m_bCommitBlocked = false; refreshTable(0); } void nmWxGeometryLayerDlg::accept() { // 包括键盘回车和活动单元格尚未失焦的情况,都必须先收齐输入。 if (!m_bInitialized || !finishEditing()) return; if (!m_pDataSource || !m_pDataSource->commit(m_oGeometry)) { // 数据源拒绝提交时保留完整编辑现场,不把窗口关闭伪装成保存成功。 m_bCommitBlocked = true; updateActions(); return; } iDlgBase::accept(); }