#include "logic_editor_widget.h" #include #include #include #include #include #include #include #include #include #include namespace { constexpr qreal kMinimumSceneWidth = 1280.0; constexpr qreal kLeftRailX = 72.0; constexpr qreal kStageWidth = 184.0; constexpr qreal kNodeWidth = 128.0; constexpr qreal kNodeTerminalX = kNodeWidth / 2.0; constexpr qreal kBranchSpacing = 96.0; constexpr qreal kRungGap = 18.0; constexpr qreal kRungBaseHeight = 132.0; constexpr qreal kMainLineOffset = 78.0; constexpr qreal kLadderLineWidth = 1.6; constexpr qreal kPlaceholderWidth = 112.0; constexpr qreal kPlaceholderHeight = 30.0; const QColor kLadderColor(QStringLiteral("#26343d")); const QColor kSelectionColor(QStringLiteral("#dcecf4")); const QColor kSelectionBorderColor(QStringLiteral("#1677a8")); const QColor kPlaceholderColor(QStringLiteral("#82919b")); QString comparisonText(ComparisonOperator comparison) { switch (comparison) { case ComparisonOperator::Equal: return QStringLiteral("="); case ComparisonOperator::NotEqual: return QStringLiteral("<>"); case ComparisonOperator::LessThan: return QStringLiteral("<"); case ComparisonOperator::LessThanOrEqual: return QStringLiteral("<="); case ComparisonOperator::GreaterThan: return QStringLiteral(">"); case ComparisonOperator::GreaterThanOrEqual: return QStringLiteral(">="); default: return QStringLiteral("?"); } } QString registerAddressText(const RegisterAddress &address) { return QStringLiteral("%1%2") .arg(address.area() == RegisterArea::M ? QStringLiteral("M") : QStringLiteral("D")) .arg(address.index()); } QString nodeToolTip(const LogicNodeConfig &config) { return std::visit( [](const auto &value) -> QString { using Config = std::decay_t; if constexpr (std::is_same_v) { return value.mode == ContactMode::NormallyOpen ? LogicEditorWidget::tr("常开触点 %1").arg( registerAddressText(value.address)) : LogicEditorWidget::tr("常闭触点 %1").arg( registerAddressText(value.address)); } else if constexpr (std::is_same_v) { QString mode = LogicEditorWidget::tr("普通线圈"); if (value.mode == CoilMode::Set) { mode = LogicEditorWidget::tr("置位线圈"); } else if (value.mode == CoilMode::Reset) { mode = LogicEditorWidget::tr("复位线圈"); } return QStringLiteral("%1 %2").arg( mode, registerAddressText(value.address)); } else { return LogicEditorWidget::tr("比较条件:%1 %2 %3") .arg(registerAddressText(value.address)) .arg(comparisonText(value.comparison)) .arg(value.value); } }, config); } QString nodeAddressText(const RegisterAddress &address, bool configured) { return configured ? registerAddressText(address) : LogicEditorWidget::tr("< M 地址 >"); } qreal rungHeight(const LadderRung &rung) { std::size_t maximum_branches = 1U; for (const LadderStage &stage : rung.stages) { maximum_branches = std::max(maximum_branches, stage.branches.size()); } return kRungBaseHeight + static_cast(maximum_branches - 1U) * kBranchSpacing; } void addPlaceholder( QGraphicsScene &scene, const QPointF ¢er, const QString &text, qreal width = kPlaceholderWidth) { const QRectF bounds( center.x() - width / 2.0, center.y() - kPlaceholderHeight / 2.0, width, kPlaceholderHeight); scene.addRect( bounds, QPen(kPlaceholderColor, 1.2, Qt::DashLine), QBrush(Qt::white)); QGraphicsTextItem *label = scene.addText(text); label->setDefaultTextColor(kPlaceholderColor); label->setPos( center.x() - label->boundingRect().width() / 2.0, center.y() - label->boundingRect().height() / 2.0); } } // namespace class LogicEditorWidget::NodeItem final : public QGraphicsItem { public: NodeItem( const LogicNode &node, const std::string &rung_id, const std::string &stage_id, const QPointF ¢er) : node_id_(node.id), rung_id_(rung_id), stage_id_(stage_id), config_(node.config), configured_(node.configured) { setPos(center); setFlag(ItemIsSelectable, true); setZValue(1.0); setToolTip(configured_ ? nodeToolTip(config_) : LogicEditorWidget::tr( "待配置:请在属性区设置地址和参数")); } QRectF boundingRect() const override { return {-kNodeWidth / 2.0, -46.0, kNodeWidth, 92.0}; } void paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { painter->setRenderHint(QPainter::Antialiasing, true); const bool selected = (option->state & QStyle::State_Selected) != 0; if (selected) { painter->fillRect(boundingRect(), kSelectionColor); } const QColor symbol_color = selected ? kSelectionBorderColor : kLadderColor; painter->setPen(QPen(symbol_color, selected ? 2.0 : kLadderLineWidth)); QFont text_font = painter->font(); text_font.setPointSizeF(9.5); painter->setFont(text_font); if (const auto *contact = std::get_if(&config_)) { painter->fillRect(QRectF(-24, -21, 48, 42), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0)); painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0)); painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); painter->drawLine(QPointF(18, -15), QPointF(18, 15)); if (contact->mode == ContactMode::NormallyClosed) { painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); } painter->setPen(kLadderColor); painter->drawText( QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, nodeAddressText(contact->address, configured_)); } else if (const auto *coil = std::get_if(&config_)) { painter->fillRect(QRectF(-34, -23, 68, 46), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-16, 0)); painter->drawLine(QPointF(16, 0), QPointF(kNodeTerminalX, 0)); QPainterPath left_coil; left_coil.moveTo(-2, -20); left_coil.cubicTo(-24, -16, -24, 16, -2, 20); painter->drawPath(left_coil); QPainterPath right_coil; right_coil.moveTo(2, -20); right_coil.cubicTo(24, -16, 24, 16, 2, 20); painter->drawPath(right_coil); if (coil->mode == CoilMode::Set || coil->mode == CoilMode::Reset) { painter->drawText( QRectF(-12, -14, 24, 28), Qt::AlignCenter, coil->mode == CoilMode::Set ? QStringLiteral("S") : QStringLiteral("R")); } painter->setPen(kLadderColor); painter->drawText( QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, nodeAddressText(coil->address, configured_)); } else if (const auto *comparison = std::get_if(&config_)) { constexpr qreal box_half_width = 47.0; const QRectF instruction_box(-box_half_width, -17, 94, 34); painter->fillRect(instruction_box.adjusted(-1, -1, 1, 1), Qt::white); painter->drawLine( QPointF(-kNodeTerminalX, 0), QPointF(-box_half_width, 0)); painter->drawLine( QPointF(box_half_width, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(instruction_box); painter->drawText( instruction_box, Qt::AlignCenter, QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); painter->setPen(kLadderColor); painter->drawText( QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(comparison->address) : tr("< D 地址 >")); painter->drawText( QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), Qt::AlignCenter, configured_ ? QString::number(comparison->value) : LogicEditorWidget::tr("< 常量 >")); } } const std::string &nodeId() const { return node_id_; } const std::string &rungId() const { return rung_id_; } const std::string &stageId() const { return stage_id_; } private: std::string node_id_; std::string rung_id_; std::string stage_id_; LogicNodeConfig config_; bool configured_ = true; }; class LogicEditorWidget::RungItem final : public QGraphicsItem { public: RungItem( const LadderRung &rung, int number, qreal top, qreal height, qreal scene_width) : rung_id_(rung.id), name_(QString::fromUtf8(rung.name.data(), static_cast(rung.name.size()))), number_(number), height_(height), scene_width_(scene_width) { setPos(0, top); setFlag(ItemIsSelectable, true); setZValue(-2.0); } QRectF boundingRect() const override { return {20, 0, scene_width_ - 40, height_}; } void paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { if ((option->state & QStyle::State_Selected) != 0) { painter->fillRect( boundingRect(), QColor(QStringLiteral("#f0f7fa"))); } painter->setPen(QColor(QStringLiteral("#62717b"))); QString title = QStringLiteral("网络 %1").arg(number_); if (!name_.isEmpty() && !name_.startsWith(QStringLiteral("网络 "))) { title += QStringLiteral(":") + name_; } painter->drawText( QRectF(28, 6, 260, 22), Qt::AlignLeft | Qt::AlignVCenter, title); painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); painter->drawLine( QPointF(28, height_ - 1), QPointF(scene_width_ - 28, height_ - 1)); } const std::string &rungId() const { return rung_id_; } private: std::string rung_id_; QString name_; int number_ = 0; qreal height_ = 0; qreal scene_width_ = 0; }; LogicEditorWidget::LogicEditorWidget( LogicEditorService &editor_service, QWidget *parent) : QGraphicsView(parent), editor_service_(editor_service) { scene_ = new QGraphicsScene(this); setScene(scene_); setRenderHint(QPainter::Antialiasing, true); setBackgroundBrush(QColor(QStringLiteral("#ffffff"))); setDragMode(QGraphicsView::RubberBandDrag); setAlignment(Qt::AlignLeft | Qt::AlignTop); connect(scene_, &QGraphicsScene::selectionChanged, this, &LogicEditorWidget::handleSelectionChanged); } void LogicEditorWidget::setLogicId(const std::string &logic_id) { if (logic_id_ == logic_id) { return; } logic_id_ = logic_id; current_rung_id_.clear(); reloadLogic(); } void LogicEditorWidget::setEditingEnabled(bool enabled) { editing_enabled_ = enabled; setInteractive(enabled); } void LogicEditorWidget::reloadLogic() { scene_->clear(); const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) { scene_->setSceneRect(0, 0, kMinimumSceneWidth, 400); return; } std::size_t maximum_stage_count = 0U; for (const LadderRung &rung : logic->rungs) { maximum_stage_count = std::max(maximum_stage_count, rung.stages.size()); } const qreal scene_width = std::max( kMinimumSceneWidth, kLeftRailX + static_cast(maximum_stage_count) * kStageWidth + 300.0); const qreal right_rail_x = scene_width - 72.0; if (current_rung_id_.empty() && !logic->rungs.empty()) { current_rung_id_ = logic->rungs.front().id; } qreal top = 24.0; int rung_number = 1; for (const LadderRung &rung : logic->rungs) { const qreal height = rungHeight(rung); scene_->addItem(new RungItem(rung, rung_number, top, height, scene_width)); const qreal main_y = top + kMainLineOffset; scene_->addLine( kLeftRailX, top + 38.0, kLeftRailX, top + height - 16.0, QPen(kLadderColor, 2.4)); scene_->addLine( right_rail_x, top + 38.0, right_rail_x, top + height - 16.0, QPen(kLadderColor, 2.4)); qreal cursor_x = kLeftRailX; for (const LadderStage &stage : rung.stages) { const qreal next_x = cursor_x + kStageWidth; const qreal left_join = cursor_x + 18.0; const qreal right_join = next_x - 18.0; scene_->addLine( cursor_x, main_y, left_join, main_y, QPen(kLadderColor, kLadderLineWidth)); scene_->addLine( right_join, main_y, next_x, main_y, QPen(kLadderColor, kLadderLineWidth)); if (stage.branches.size() > 1U) { const qreal bottom_y = main_y + static_cast(stage.branches.size() - 1U) * kBranchSpacing; scene_->addLine( left_join, main_y, left_join, bottom_y, QPen(kLadderColor, kLadderLineWidth)); scene_->addLine( right_join, main_y, right_join, bottom_y, QPen(kLadderColor, kLadderLineWidth)); } for (std::size_t branch = 0; branch < stage.branches.size(); ++branch) { const qreal branch_y = main_y + static_cast(branch) * kBranchSpacing; scene_->addLine( left_join, branch_y, right_join, branch_y, QPen(kLadderColor, kLadderLineWidth)); scene_->addItem(new NodeItem( stage.branches.at(branch), rung.id, stage.id, QPointF((left_join + right_join) / 2.0, branch_y))); } cursor_x = next_x; } const qreal output_center_x = right_rail_x - 68.0; if (rung.stages.empty()) { const qreal condition_center_x = kLeftRailX + kStageWidth / 2.0; scene_->addLine( kLeftRailX, main_y, condition_center_x - kPlaceholderWidth / 2.0, main_y, QPen(kLadderColor, kLadderLineWidth)); scene_->addLine( condition_center_x + kPlaceholderWidth / 2.0, main_y, output_center_x, main_y, QPen(kLadderColor, kLadderLineWidth)); addPlaceholder( *scene_, QPointF(condition_center_x, main_y), tr("添加条件")); } else { scene_->addLine( cursor_x, main_y, output_center_x, main_y, QPen(kLadderColor, kLadderLineWidth)); } if (rung.output.has_value()) { scene_->addLine( output_center_x, main_y, right_rail_x, main_y, QPen(kLadderColor, kLadderLineWidth)); scene_->addItem(new NodeItem( *rung.output, rung.id, {}, QPointF(output_center_x, main_y))); } else { constexpr qreal output_placeholder_width = 96.0; scene_->addLine( output_center_x + output_placeholder_width / 2.0, main_y, right_rail_x, main_y, QPen(kLadderColor, kLadderLineWidth)); addPlaceholder( *scene_, QPointF(output_center_x, main_y), tr("输出线圈"), output_placeholder_width); } top += height + kRungGap; ++rung_number; } scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + 20.0)); } void LogicEditorWidget::selectNode(const std::string &node_id) { for (QGraphicsItem *item : scene_->items()) { NodeItem *node = dynamic_cast(item); if (node != nullptr) { node->setSelected(node->nodeId() == node_id); if (node->nodeId() == node_id) { ensureVisible(node); } } } } std::string LogicEditorWidget::selectedNodeId() const { for (QGraphicsItem *item : scene_->selectedItems()) { const NodeItem *node = dynamic_cast(item); if (node != nullptr) { return node->nodeId(); } } return {}; } std::string LogicEditorWidget::selectedRungId() const { for (QGraphicsItem *item : scene_->selectedItems()) { const NodeItem *node = dynamic_cast(item); if (node != nullptr) { return node->rungId(); } const RungItem *rung = dynamic_cast(item); if (rung != nullptr) { return rung->rungId(); } } return {}; } std::string LogicEditorWidget::selectedStageId() const { for (QGraphicsItem *item : scene_->selectedItems()) { const NodeItem *node = dynamic_cast(item); if (node != nullptr) { return node->stageId(); } } return {}; } LogicEditorResult LogicEditorWidget::addRung() { const LogicEditorResult result = editor_service_.addRung(logic_id_); if (result.succeeded) { current_rung_id_ = result.id; reloadLogic(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::appendCondition(const LogicNodeConfig &config) { const LogicEditorResult result = editor_service_.appendCondition( logic_id_, currentRungId(), config); if (result.succeeded) { reloadLogic(); selectNode(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addParallelCondition( const LogicNodeConfig &config) { const std::string rung_id = selectedRungId(); const std::string stage_id = selectedStageId(); LogicEditorResult result; if (rung_id.empty() || stage_id.empty()) { result = {false, LogicEditorError::InvalidOperation, "请先选择要添加并联分支的触点或比较条件", {}}; } else { result = editor_service_.addParallelCondition( logic_id_, rung_id, stage_id, config); } if (result.succeeded) { reloadLogic(); selectNode(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::setOutput(const LogicNodeConfig &config) { const LogicEditorResult result = editor_service_.setOutput( logic_id_, currentRungId(), config); if (result.succeeded) { reloadLogic(); selectNode(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::deleteSelected() { const std::string node_id = selectedNodeId(); LogicEditorResult result; if (!node_id.empty()) { result = editor_service_.removeNode(logic_id_, node_id); } else { const std::string rung_id = selectedRungId(); if (rung_id.empty()) { return {false, LogicEditorError::InvalidOperation, "请先选择要删除的逻辑节点或网络", {}}; } result = editor_service_.removeRung(logic_id_, rung_id); if (result.succeeded && current_rung_id_ == rung_id) { current_rung_id_ = editor_service_.firstRungId(logic_id_); } } if (result.succeeded) { reloadLogic(); emit nodeSelected({}); emit graphChanged(); } else { reportFailure(result); } return result; } void LogicEditorWidget::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); } void LogicEditorWidget::handleSelectionChanged() { const std::string rung_id = selectedRungId(); if (!rung_id.empty()) { current_rung_id_ = rung_id; } emit nodeSelected(QString::fromStdString(selectedNodeId())); } void LogicEditorWidget::reportFailure(const LogicEditorResult &result) { emit editorError(QString::fromStdString(result.message)); } std::string LogicEditorWidget::currentRungId() const { const std::string selected = selectedRungId(); if (!selected.empty()) { return selected; } if (!current_rung_id_.empty()) { return current_rung_id_; } return editor_service_.firstRungId(logic_id_); }