| @@ -2,7 +2,9 @@ | |||
| #include <QGraphicsItem> | |||
| #include <QGraphicsScene> | |||
| #include <QGraphicsTextItem> | |||
| #include <QPainter> | |||
| #include <QPainterPath> | |||
| #include <QResizeEvent> | |||
| #include <QStyleOptionGraphicsItem> | |||
| @@ -15,9 +17,20 @@ namespace { | |||
| constexpr qreal kMinimumSceneWidth = 1280.0; | |||
| constexpr qreal kLeftRailX = 72.0; | |||
| constexpr qreal kStageWidth = 184.0; | |||
| constexpr qreal kNodeWidth = 118.0; | |||
| constexpr qreal kBranchSpacing = 58.0; | |||
| constexpr qreal kRungGap = 30.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) | |||
| { | |||
| @@ -33,21 +46,46 @@ QString comparisonText(ComparisonOperator comparison) | |||
| } | |||
| } | |||
| QString nodeAddressText(const LogicNodeConfig &config) | |||
| 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<decltype(value)>; | |||
| if constexpr (std::is_same_v<Config, ContactNodeConfig> | |||
| || std::is_same_v<Config, CoilNodeConfig>) | |||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | |||
| { | |||
| 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<Config, CoilNodeConfig>) | |||
| { | |||
| return QStringLiteral("M%1").arg(value.address.index()); | |||
| 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 QStringLiteral("D%1 %2 %3") | |||
| .arg(value.address.index()) | |||
| return LogicEditorWidget::tr("比较条件:%1 %2 %3") | |||
| .arg(registerAddressText(value.address)) | |||
| .arg(comparisonText(value.comparison)) | |||
| .arg(value.value); | |||
| } | |||
| @@ -62,7 +100,30 @@ qreal rungHeight(const LadderRung &rung) | |||
| { | |||
| maximum_branches = std::max(maximum_branches, stage.branches.size()); | |||
| } | |||
| return 100.0 + static_cast<qreal>(maximum_branches - 1U) * kBranchSpacing; | |||
| return kRungBaseHeight | |||
| + static_cast<qreal>(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 | |||
| @@ -82,11 +143,13 @@ public: | |||
| { | |||
| setPos(center); | |||
| setFlag(ItemIsSelectable, true); | |||
| setZValue(1.0); | |||
| setToolTip(nodeToolTip(config_)); | |||
| } | |||
| QRectF boundingRect() const override | |||
| { | |||
| return {-kNodeWidth / 2.0, -27.0, kNodeWidth, 54.0}; | |||
| return {-kNodeWidth / 2.0, -46.0, kNodeWidth, 92.0}; | |||
| } | |||
| void paint( | |||
| @@ -98,27 +161,45 @@ public: | |||
| const bool selected = (option->state & QStyle::State_Selected) != 0; | |||
| if (selected) | |||
| { | |||
| painter->fillRect( | |||
| boundingRect(), QColor(QStringLiteral("#dceef7"))); | |||
| painter->fillRect(boundingRect(), kSelectionColor); | |||
| } | |||
| painter->setPen(QPen( | |||
| selected ? QColor(QStringLiteral("#1677a8")) | |||
| : QColor(QStringLiteral("#1f2b33")), | |||
| selected ? 2.0 : 1.6)); | |||
| 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<ContactNodeConfig>(&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, | |||
| registerAddressText(contact->address)); | |||
| } | |||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_)) | |||
| { | |||
| painter->drawArc(QRectF(-29, -20, 30, 40), -80 * 16, 160 * 16); | |||
| painter->drawArc(QRectF(-1, -20, 30, 40), 100 * 16, 160 * 16); | |||
| 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( | |||
| @@ -127,16 +208,36 @@ public: | |||
| coil->mode == CoilMode::Set ? QStringLiteral("S") | |||
| : QStringLiteral("R")); | |||
| } | |||
| painter->setPen(kLadderColor); | |||
| painter->drawText( | |||
| QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), | |||
| Qt::AlignCenter, | |||
| registerAddressText(coil->address)); | |||
| } | |||
| else | |||
| else if (const auto *comparison = std::get_if<CompareNodeConfig>(&config_)) | |||
| { | |||
| painter->drawRect(QRectF(-43, -18, 86, 36)); | |||
| 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, | |||
| registerAddressText(comparison->address)); | |||
| painter->drawText( | |||
| QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), | |||
| Qt::AlignCenter, | |||
| QString::number(comparison->value)); | |||
| } | |||
| painter->setPen(QColor(QStringLiteral("#263640"))); | |||
| painter->drawText( | |||
| QRectF(-kNodeWidth / 2.0, -42, kNodeWidth, 18), | |||
| Qt::AlignCenter, | |||
| nodeAddressText(config_)); | |||
| } | |||
| const std::string &nodeId() const { return node_id_; } | |||
| @@ -186,10 +287,15 @@ public: | |||
| 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, | |||
| QStringLiteral("%1 %2").arg(number_).arg(name_)); | |||
| title); | |||
| painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); | |||
| painter->drawLine( | |||
| QPointF(28, height_ - 1), QPointF(scene_width_ - 28, height_ - 1)); | |||
| @@ -267,14 +373,14 @@ void LogicEditorWidget::reloadLogic() | |||
| { | |||
| const qreal height = rungHeight(rung); | |||
| scene_->addItem(new RungItem(rung, rung_number, top, height, scene_width)); | |||
| const qreal main_y = top + 62.0; | |||
| const qreal main_y = top + kMainLineOffset; | |||
| scene_->addLine( | |||
| kLeftRailX, top + 32.0, kLeftRailX, top + height - 18.0, | |||
| QPen(QColor(QStringLiteral("#202a31")), 3)); | |||
| kLeftRailX, top + 38.0, kLeftRailX, top + height - 16.0, | |||
| QPen(kLadderColor, 2.4)); | |||
| scene_->addLine( | |||
| right_rail_x, top + 32.0, right_rail_x, top + height - 18.0, | |||
| QPen(QColor(QStringLiteral("#202a31")), 3)); | |||
| 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) | |||
| @@ -284,27 +390,27 @@ void LogicEditorWidget::reloadLogic() | |||
| const qreal right_join = next_x - 18.0; | |||
| scene_->addLine( | |||
| cursor_x, main_y, left_join, main_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| QPen(kLadderColor, kLadderLineWidth)); | |||
| scene_->addLine( | |||
| right_join, main_y, next_x, main_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| QPen(kLadderColor, kLadderLineWidth)); | |||
| if (stage.branches.size() > 1U) | |||
| { | |||
| const qreal bottom_y = main_y | |||
| + static_cast<qreal>(stage.branches.size() - 1U) * kBranchSpacing; | |||
| scene_->addLine( | |||
| left_join, main_y, left_join, bottom_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| QPen(kLadderColor, kLadderLineWidth)); | |||
| scene_->addLine( | |||
| right_join, main_y, right_join, bottom_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| QPen(kLadderColor, kLadderLineWidth)); | |||
| } | |||
| for (std::size_t branch = 0; branch < stage.branches.size(); ++branch) | |||
| { | |||
| const qreal branch_y = main_y + static_cast<qreal>(branch) * kBranchSpacing; | |||
| scene_->addLine( | |||
| left_join, branch_y, right_join, branch_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| QPen(kLadderColor, kLadderLineWidth)); | |||
| scene_->addItem(new NodeItem( | |||
| stage.branches.at(branch), | |||
| rung.id, | |||
| @@ -314,32 +420,58 @@ void LogicEditorWidget::reloadLogic() | |||
| cursor_x = next_x; | |||
| } | |||
| const qreal output_x = right_rail_x - 116.0; | |||
| scene_->addLine( | |||
| cursor_x, main_y, output_x, main_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| scene_->addLine( | |||
| output_x, main_y, right_rail_x, main_y, | |||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||
| 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_x + 50.0, main_y))); | |||
| QPointF(output_center_x, main_y))); | |||
| } | |||
| else | |||
| { | |||
| auto *placeholder = scene_->addText(QStringLiteral("< 输出线圈 >")); | |||
| placeholder->setDefaultTextColor(QColor(QStringLiteral("#87949c"))); | |||
| placeholder->setPos(output_x + 4.0, main_y - 14.0); | |||
| } | |||
| if (rung.stages.empty()) | |||
| { | |||
| auto *placeholder = scene_->addText(QStringLiteral("< 添加条件 >")); | |||
| placeholder->setDefaultTextColor(QColor(QStringLiteral("#87949c"))); | |||
| placeholder->setPos(kLeftRailX + 32.0, main_y - 14.0); | |||
| 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; | |||
| @@ -450,7 +582,7 @@ LogicEditorResult LogicEditorWidget::addParallelCondition( | |||
| { | |||
| result = {false, | |||
| LogicEditorError::InvalidOperation, | |||
| "select a condition before adding a parallel branch", | |||
| "请先选择要添加并联分支的触点或比较条件", | |||
| {}}; | |||
| } | |||
| else | |||
| @@ -501,7 +633,8 @@ LogicEditorResult LogicEditorWidget::deleteSelected() | |||
| const std::string rung_id = selectedRungId(); | |||
| if (rung_id.empty()) | |||
| { | |||
| return {false, LogicEditorError::InvalidOperation, "no ladder object is selected", {}}; | |||
| return {false, LogicEditorError::InvalidOperation, | |||
| "请先选择要删除的逻辑节点或网络", {}}; | |||
| } | |||
| result = editor_service_.removeRung(logic_id_, rung_id); | |||
| if (result.succeeded && current_rung_id_ == rung_id) | |||