| @@ -18,6 +18,7 @@ SOURCES += \ | |||
| src/main.cpp \ | |||
| src/ui/main_window.cpp \ | |||
| src/ui/alarm_configuration_dialog.cpp \ | |||
| src/ui/register_comment_dialog.cpp \ | |||
| src/ui/plc_connection_dialog.cpp \ | |||
| src/ui/free_monitor_widget.cpp \ | |||
| src/ui/runtime_monitor_widget.cpp \ | |||
| @@ -41,6 +42,7 @@ SOURCES += \ | |||
| src/services/offline_simulation_service.cpp \ | |||
| src/services/runtime_mode_service.cpp \ | |||
| src/services/register_monitor_service.cpp \ | |||
| src/services/register_comment_service.cpp \ | |||
| src/infrastructure/plc_register_repository.cpp \ | |||
| src/infrastructure/plc_communication_error_classifier.cpp \ | |||
| src/infrastructure/plc_communication_service.cpp \ | |||
| @@ -51,6 +53,7 @@ SOURCES += \ | |||
| HEADERS += \ | |||
| src/ui/main_window.h \ | |||
| src/ui/alarm_configuration_dialog.h \ | |||
| src/ui/register_comment_dialog.h \ | |||
| src/ui/plc_connection_dialog.h \ | |||
| src/ui/free_monitor_widget.h \ | |||
| src/ui/runtime_monitor_widget.h \ | |||
| @@ -75,6 +78,7 @@ HEADERS += \ | |||
| src/services/offline_simulation_service.h \ | |||
| src/services/runtime_mode_service.h \ | |||
| src/services/register_monitor_service.h \ | |||
| src/services/register_comment_service.h \ | |||
| src/services/plc_communication_gateway.h \ | |||
| src/infrastructure/plc_register_repository.h \ | |||
| src/infrastructure/plc_communication_error_classifier.h \ | |||
| @@ -86,6 +90,7 @@ HEADERS += \ | |||
| FORMS += \ | |||
| src/ui/main_window.ui \ | |||
| src/ui/alarm_configuration_dialog.ui \ | |||
| src/ui/register_comment_dialog.ui \ | |||
| src/ui/plc_connection_dialog.ui \ | |||
| src/ui/free_monitor_widget.ui \ | |||
| src/ui/runtime_monitor_widget.ui | |||
| @@ -22,6 +22,7 @@ | |||
| #include "services/project_service.h" | |||
| #include "services/runtime_mode_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "services/register_comment_service.h" | |||
| #include "ui/main_window.h" | |||
| int main(int argc, char *argv[]) | |||
| @@ -44,6 +45,7 @@ int main(int argc, char *argv[]) | |||
| HmiRuntimeService hmi_runtime_service(active_register_repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, active_register_repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| RegisterMonitorService register_monitor_service(active_register_repository); | |||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | |||
| RuntimeModeService runtime_mode_service( | |||
| @@ -62,6 +64,7 @@ int main(int argc, char *argv[]) | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| hmi_navigation_service, | |||
| register_comment_service, | |||
| register_monitor_service); | |||
| main_window.show(); | |||
| @@ -17,9 +17,9 @@ constexpr qreal kSceneMargin = 28.0; | |||
| constexpr qreal kRailInset = 40.0; | |||
| constexpr qreal kMinimumSceneWidth = 980.0; | |||
| constexpr qreal kCellWidth = 128.0; | |||
| constexpr qreal kCellHeight = 86.0; | |||
| constexpr qreal kCellHeight = 120.0; | |||
| constexpr qreal kNodeTerminalX = 50.0; | |||
| constexpr qreal kRungHeaderHeight = 34.0; | |||
| constexpr qreal kRungHeaderHeight = 52.0; | |||
| constexpr qreal kRungGap = 18.0; | |||
| constexpr qreal kLadderLineWidth = 1.8; | |||
| constexpr int kMinimumLogicColumns = 7; | |||
| @@ -48,6 +48,29 @@ QString registerAddressText(const RegisterAddress &address) | |||
| return QString::fromStdString(address.toString()); | |||
| } | |||
| QString timerAddressText(const TimerAddress &address) | |||
| { | |||
| return QString::fromStdString(address.toString()); | |||
| } | |||
| void drawRegisterComment( | |||
| QPainter *painter, const QString &comment, qreal top = 22.0) | |||
| { | |||
| if (comment.isEmpty()) | |||
| { | |||
| return; | |||
| } | |||
| QFont font = painter->font(); | |||
| font.setPointSizeF(8.0); | |||
| painter->setFont(font); | |||
| const QString visible_comment = painter->fontMetrics().elidedText( | |||
| comment, Qt::ElideRight, static_cast<int>(kCellWidth - 8.0)); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, top, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| visible_comment); | |||
| } | |||
| QString comparisonText(ComparisonOperator comparison) | |||
| { | |||
| switch (comparison) | |||
| @@ -76,18 +99,37 @@ QString nodeToolTip(const LogicNodeConfig &config) | |||
| : LogicEditorWidget::tr("常闭")) | |||
| .arg(registerAddressText(value.address)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("%1沿触点:%2") | |||
| .arg(value.mode == EdgeMode::Rising | |||
| ? LogicEditorWidget::tr("上升") | |||
| : LogicEditorWidget::tr("下降")) | |||
| .arg(registerAddressText(value.address)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("T 触点:%1") | |||
| .arg(timerAddressText(value.address)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("输出线圈:%1") | |||
| .arg(registerAddressText(value.address)); | |||
| } | |||
| else | |||
| else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("比较条件:%1 %2 %3") | |||
| .arg(registerAddressText(value.address)) | |||
| .arg(comparisonText(value.comparison)) | |||
| .arg(value.value); | |||
| } | |||
| else | |||
| { | |||
| return LogicEditorWidget::tr("TON:%1,预设 %2 ms") | |||
| .arg(timerAddressText(value.address)) | |||
| .arg(value.presetMs); | |||
| } | |||
| }, | |||
| config); | |||
| } | |||
| @@ -187,11 +229,13 @@ public: | |||
| const LogicNode &node, | |||
| const std::string &rung_id, | |||
| const QPointF ¢er, | |||
| const QString ®ister_comment, | |||
| bool active, | |||
| bool faulted) | |||
| : node_id_(node.id), | |||
| rung_id_(rung_id), | |||
| config_(node.config), | |||
| register_comment_(register_comment), | |||
| configured_(node.configured), | |||
| active_(active), | |||
| faulted_(faulted) | |||
| @@ -199,8 +243,14 @@ public: | |||
| setPos(center); | |||
| setFlag(ItemIsSelectable, true); | |||
| setZValue(3.0); | |||
| setToolTip(configured_ ? nodeToolTip(config_) | |||
| : LogicEditorWidget::tr("待配置:请在属性区设置地址和参数")); | |||
| QString tool_tip = configured_ | |||
| ? nodeToolTip(config_) | |||
| : LogicEditorWidget::tr("待配置:请在属性区设置地址和参数"); | |||
| if (!register_comment_.isEmpty()) | |||
| { | |||
| tool_tip += LogicEditorWidget::tr("\n注释:%1").arg(register_comment_); | |||
| } | |||
| setToolTip(tool_tip); | |||
| } | |||
| QRectF boundingRect() const override | |||
| @@ -241,6 +291,41 @@ public: | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? registerAddressText(contact->address) : tr("< M 地址 >")); | |||
| drawRegisterComment(painter, register_comment_); | |||
| } | |||
| else if (const auto *edge = std::get_if<EdgeContactNodeConfig>(&config_)) | |||
| { | |||
| painter->fillRect(QRectF(-25, -22, 50, 44), 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)); | |||
| painter->drawText( | |||
| QRectF(-14, -12, 28, 24), | |||
| Qt::AlignCenter, | |||
| edge->mode == EdgeMode::Rising ? QStringLiteral("P") : QStringLiteral("N")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? registerAddressText(edge->address) : tr("< M 地址 >")); | |||
| drawRegisterComment(painter, register_comment_); | |||
| } | |||
| else if (const auto *timer_contact = | |||
| std::get_if<TimerContactNodeConfig>(&config_)) | |||
| { | |||
| painter->fillRect(QRectF(-25, -22, 50, 44), 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 (timer_contact->mode == ContactMode::NormallyClosed) | |||
| { | |||
| painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); | |||
| } | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? timerAddressText(timer_contact->address) : tr("< T 地址 >")); | |||
| } | |||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_)) | |||
| { | |||
| @@ -266,6 +351,7 @@ public: | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? registerAddressText(coil->address) : tr("< M 地址 >")); | |||
| drawRegisterComment(painter, register_comment_); | |||
| } | |||
| else if (const auto *comparison = std::get_if<CompareNodeConfig>(&config_)) | |||
| { | |||
| @@ -285,6 +371,25 @@ public: | |||
| QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? QString::number(comparison->value) : tr("< 常量 >")); | |||
| drawRegisterComment(painter, register_comment_, 40.0); | |||
| } | |||
| else if (const auto *ton = std::get_if<TonNodeConfig>(&config_)) | |||
| { | |||
| const QRectF box(-47, -18, 94, 36); | |||
| painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); | |||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); | |||
| painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); | |||
| painter->drawRect(box); | |||
| painter->drawText(box, Qt::AlignCenter, QStringLiteral("TON")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? timerAddressText(ton->address) : tr("< T 地址 >")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? QStringLiteral("PT %1 ms").arg(ton->presetMs) | |||
| : tr("< 预设时间 >")); | |||
| } | |||
| } | |||
| @@ -295,6 +400,7 @@ private: | |||
| std::string node_id_; | |||
| std::string rung_id_; | |||
| LogicNodeConfig config_; | |||
| QString register_comment_; | |||
| bool configured_ = true; | |||
| bool active_ = false; | |||
| bool faulted_ = false; | |||
| @@ -312,6 +418,7 @@ public: | |||
| qreal cursor_left) | |||
| : rung_id_(rung.id), | |||
| name_(QString::fromStdString(rung.name)), | |||
| comment_(QString::fromStdString(rung.comment)), | |||
| number_(number), | |||
| height_(height), | |||
| width_(width), | |||
| @@ -321,6 +428,10 @@ public: | |||
| setPos(0, top); | |||
| setFlag(ItemIsSelectable, true); | |||
| setZValue(-2.0); | |||
| if (!comment_.isEmpty()) | |||
| { | |||
| setToolTip(comment_); | |||
| } | |||
| } | |||
| QRectF boundingRect() const override { return {kSceneMargin, 0, width_, height_}; } | |||
| @@ -351,6 +462,16 @@ public: | |||
| QRectF(kSceneMargin + 8, 5, 320, 22), | |||
| Qt::AlignLeft | Qt::AlignVCenter, | |||
| title); | |||
| if (!comment_.isEmpty()) | |||
| { | |||
| painter->setPen(QColor(QStringLiteral("#7b8790"))); | |||
| const QString visible_comment = painter->fontMetrics().elidedText( | |||
| comment_, Qt::ElideRight, static_cast<int>(width_ - 16.0)); | |||
| painter->drawText( | |||
| QRectF(kSceneMargin + 8, 24, width_ - 16, 22), | |||
| Qt::AlignLeft | Qt::AlignVCenter, | |||
| visible_comment); | |||
| } | |||
| painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); | |||
| painter->drawLine( | |||
| QPointF(kSceneMargin + 8, height_ - 1), | |||
| @@ -362,6 +483,7 @@ public: | |||
| private: | |||
| std::string rung_id_; | |||
| QString name_; | |||
| QString comment_; | |||
| int number_ = 0; | |||
| qreal height_ = 0.0; | |||
| qreal width_ = 0.0; | |||
| @@ -373,6 +495,7 @@ namespace { | |||
| RenderResult renderExpression( | |||
| QGraphicsScene &scene, | |||
| const LogicEditorService &editor_service, | |||
| const ConditionExpression &expression, | |||
| const std::string &rung_id, | |||
| const QPointF &top_left, | |||
| @@ -399,6 +522,15 @@ RenderResult renderExpression( | |||
| *expression.node, | |||
| rung_id, | |||
| center, | |||
| [&editor_service, &expression] | |||
| { | |||
| const std::optional<RegisterAddress> address = | |||
| registerAddressForLogicNode(expression.node->config); | |||
| return address.has_value() | |||
| ? QString::fromStdString( | |||
| editor_service.registerCommentFor(*address)) | |||
| : QString{}; | |||
| }(), | |||
| node_active, | |||
| expression.node->id == fault_node_id)); | |||
| return { | |||
| @@ -416,6 +548,7 @@ RenderResult renderExpression( | |||
| const ExpressionMetrics child_metrics = measureExpression(expression.children[index]); | |||
| const RenderResult current = renderExpression( | |||
| scene, | |||
| editor_service, | |||
| expression.children[index], | |||
| rung_id, | |||
| QPointF(x, top_left.y()), | |||
| @@ -445,6 +578,7 @@ RenderResult renderExpression( | |||
| const ExpressionMetrics child_metrics = measureExpression(child); | |||
| branches.push_back(renderExpression( | |||
| scene, | |||
| editor_service, | |||
| child, | |||
| rung_id, | |||
| QPointF(top_left.x(), y), | |||
| @@ -588,6 +722,7 @@ void LogicEditorWidget::reloadLogic() | |||
| { | |||
| const RenderResult rendered = renderExpression( | |||
| *scene_, | |||
| editor_service_, | |||
| *rung.condition, | |||
| rung.id, | |||
| QPointF(left_rail_x, grid_top), | |||
| @@ -620,7 +755,24 @@ void LogicEditorWidget::reloadLogic() | |||
| *rung.output, | |||
| rung.id, | |||
| QPointF(output_left + kCellWidth / 2.0, main_y), | |||
| rung_active, | |||
| [&rung, this] | |||
| { | |||
| const std::optional<RegisterAddress> address = | |||
| registerAddressForLogicNode(rung.output->config); | |||
| return address.has_value() | |||
| ? QString::fromStdString( | |||
| editor_service_.registerCommentFor(*address)) | |||
| : QString{}; | |||
| }(), | |||
| [&rung, this, rung_active] | |||
| { | |||
| return std::holds_alternative<TonNodeConfig>(rung.output->config) | |||
| ? traceValue( | |||
| trace_, | |||
| &LogicTraceSnapshot::nodeValues, | |||
| rung.output->id) | |||
| : rung_active; | |||
| }(), | |||
| rung.output->id == fault_node_id_)); | |||
| } | |||
| else | |||
| @@ -2,6 +2,7 @@ | |||
| #include "hmi_editor_widget.h" | |||
| #include "alarm_configuration_dialog.h" | |||
| #include "register_comment_dialog.h" | |||
| #include "logic_editor_widget.h" | |||
| #include "plc_connection_dialog.h" | |||
| #include "runtime_monitor_widget.h" | |||
| @@ -15,6 +16,7 @@ | |||
| #include "services/project_service.h" | |||
| #include "services/runtime_mode_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "services/register_comment_service.h" | |||
| #include "ui_main_window.h" | |||
| #include <QActionGroup> | |||
| @@ -200,6 +202,7 @@ MainWindow::MainWindow( | |||
| HmiRuntimeService &hmi_runtime_service, | |||
| AlarmEditorService &alarm_editor_service, | |||
| AlarmService &alarm_service, | |||
| RegisterCommentService ®ister_comment_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent) | |||
| : QMainWindow(parent), | |||
| @@ -211,6 +214,7 @@ MainWindow::MainWindow( | |||
| hmi_runtime_service_(hmi_runtime_service), | |||
| alarm_editor_service_(alarm_editor_service), | |||
| alarm_service_(alarm_service), | |||
| register_comment_service_(register_comment_service), | |||
| register_monitor_service_(register_monitor_service), | |||
| owned_hmi_navigation_service_( | |||
| std::make_unique<HmiNavigationService>(project_service)), | |||
| @@ -228,6 +232,7 @@ MainWindow::MainWindow( | |||
| AlarmEditorService &alarm_editor_service, | |||
| AlarmService &alarm_service, | |||
| HmiNavigationService &hmi_navigation_service, | |||
| RegisterCommentService ®ister_comment_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent) | |||
| : QMainWindow(parent), | |||
| @@ -239,6 +244,7 @@ MainWindow::MainWindow( | |||
| hmi_runtime_service_(hmi_runtime_service), | |||
| alarm_editor_service_(alarm_editor_service), | |||
| alarm_service_(alarm_service), | |||
| register_comment_service_(register_comment_service), | |||
| register_monitor_service_(register_monitor_service), | |||
| hmi_navigation_service_(&hmi_navigation_service) | |||
| { | |||
| @@ -253,6 +259,7 @@ void MainWindow::initializeUi() | |||
| configurePropertyEditor(); | |||
| configurePlcConnection(); | |||
| configureAlarms(); | |||
| configureRegisterComments(); | |||
| configureHmiEditor(); | |||
| configureLogicEditor(); | |||
| configureRuntimeMonitor(); | |||
| @@ -332,6 +339,9 @@ void MainWindow::configureActions() | |||
| QMenu *parallel_menu = new QMenu(this); | |||
| QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点")); | |||
| QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点")); | |||
| QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点")); | |||
| QAction *parallel_falling = parallel_menu->addAction(tr("并联下降沿触点")); | |||
| QAction *parallel_timer = parallel_menu->addAction(tr("并联 T 触点")); | |||
| QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件")); | |||
| connect(parallel_open, &QAction::triggered, | |||
| this, | |||
| @@ -349,6 +359,27 @@ void MainWindow::configureActions() | |||
| RegisterAddress{RegisterArea::M, 0}, | |||
| ContactMode::NormallyClosed}); | |||
| }); | |||
| connect(parallel_rising, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicParallelBranch(EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}); | |||
| }); | |||
| connect(parallel_falling, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicParallelBranch(EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); | |||
| }); | |||
| connect(parallel_timer, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicParallelBranch(TimerContactNodeConfig{ | |||
| TimerAddress{0}, ContactMode::NormallyOpen}); | |||
| }); | |||
| connect(parallel_compare, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| @@ -380,6 +411,27 @@ void MainWindow::configureActions() | |||
| RegisterAddress{RegisterArea::M, 0}, | |||
| ContactMode::NormallyClosed}); | |||
| }); | |||
| connect(ui_->addRisingEdgeAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicCondition(EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}); | |||
| }); | |||
| connect(ui_->addFallingEdgeAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicCondition(EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); | |||
| }); | |||
| connect(ui_->addTimerContactAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicCondition(TimerContactNodeConfig{ | |||
| TimerAddress{0}, ContactMode::NormallyOpen}); | |||
| }); | |||
| connect(ui_->addNormalCoilAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| @@ -404,6 +456,12 @@ void MainWindow::configureActions() | |||
| RegisterAddress{RegisterArea::M, 0}, | |||
| CoilMode::Reset}); | |||
| }); | |||
| connect(ui_->addTonAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| setLogicOutput(TonNodeConfig{TimerAddress{0}, 1000}); | |||
| }); | |||
| connect(ui_->addCompareAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| @@ -415,6 +473,8 @@ void MainWindow::configureActions() | |||
| }); | |||
| connect(ui_->deleteLogicAction, &QAction::triggered, | |||
| this, &MainWindow::deleteSelectedLogicObject); | |||
| connect(ui_->editRungCommentAction, &QAction::triggered, | |||
| this, &MainWindow::editSelectedRungComment); | |||
| connect(ui_->editorTabWidget, &QTabWidget::currentChanged, | |||
| this, | |||
| @@ -481,10 +541,15 @@ void MainWindow::configureAppearance() | |||
| ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); | |||
| ui_->addNormallyOpenAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); | |||
| ui_->addNormallyClosedAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); | |||
| ui_->addRisingEdgeAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); | |||
| ui_->addFallingEdgeAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); | |||
| ui_->addTimerContactAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); | |||
| ui_->addNormalCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); | |||
| ui_->addSetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton)); | |||
| ui_->addResetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogNoButton)); | |||
| ui_->addTonAction->setIcon(style()->standardIcon(QStyle::SP_BrowserReload)); | |||
| ui_->addCompareAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogInfoView)); | |||
| ui_->editRungCommentAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView)); | |||
| ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | |||
| ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon)); | |||
| ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView)); | |||
| @@ -755,7 +820,22 @@ void MainWindow::configureAlarms() | |||
| AlarmConfigurationDialog dialog(alarm_editor_service_, this); | |||
| dialog.exec(); | |||
| refreshProjectUi(); | |||
| }); | |||
| }); | |||
| } | |||
| void MainWindow::configureRegisterComments() | |||
| { | |||
| connect( | |||
| ui_->configureRegisterCommentsAction, | |||
| &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| RegisterCommentDialog dialog(register_comment_service_, this); | |||
| dialog.exec(); | |||
| logic_editor_widget_->reloadLogic(); | |||
| refreshProjectUi(); | |||
| }); | |||
| } | |||
| void MainWindow::refreshProjectUi() | |||
| @@ -1300,6 +1380,7 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) | |||
| static_cast<QWidget *>(ui_->logicModeComboBox), | |||
| static_cast<QWidget *>(ui_->logicComparisonComboBox), | |||
| static_cast<QWidget *>(ui_->logicValueSpinBox), | |||
| static_cast<QWidget *>(ui_->logicPresetSpinBox), | |||
| static_cast<QWidget *>(ui_->applyLogicPropertiesButton)}) | |||
| { | |||
| widget->setEnabled(has_node); | |||
| @@ -1312,11 +1393,15 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) | |||
| ui_->logicNodeTypeLabel->clear(); | |||
| ui_->logicAddressAreaLabel->clear(); | |||
| ui_->logicModeComboBox->clear(); | |||
| ui_->logicPresetSpinBox->setValue(TonNodeConfig::kMinimumPresetMs); | |||
| return; | |||
| } | |||
| ui_->logicNodeIdEdit->setText(fromUtf8(node->id)); | |||
| ui_->logicModeComboBox->clear(); | |||
| ui_->logicComparisonComboBox->setEnabled(false); | |||
| ui_->logicValueSpinBox->setEnabled(false); | |||
| ui_->logicPresetSpinBox->setEnabled(false); | |||
| if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config)) | |||
| { | |||
| ui_->logicNodeTypeLabel->setText(tr("触点")); | |||
| @@ -1327,10 +1412,36 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) | |||
| ui_->logicModeComboBox->addItem( | |||
| tr("常闭"), static_cast<int>(ContactMode::NormallyClosed)); | |||
| ui_->logicModeComboBox->setCurrentIndex( | |||
| ui_->logicModeComboBox->findData(static_cast<int>(contact->mode))); | |||
| ui_->logicModeComboBox->findData(static_cast<int>(contact->mode))); | |||
| ui_->logicModeComboBox->setEnabled(true); | |||
| } | |||
| else if (const auto *edge = std::get_if<EdgeContactNodeConfig>(&node->config)) | |||
| { | |||
| ui_->logicNodeTypeLabel->setText(tr("边沿触点")); | |||
| ui_->logicAddressAreaLabel->setText(QStringLiteral("M")); | |||
| ui_->logicAddressSpinBox->setValue(edge->address.index()); | |||
| ui_->logicModeComboBox->addItem( | |||
| tr("上升沿"), static_cast<int>(EdgeMode::Rising)); | |||
| ui_->logicModeComboBox->addItem( | |||
| tr("下降沿"), static_cast<int>(EdgeMode::Falling)); | |||
| ui_->logicModeComboBox->setCurrentIndex( | |||
| ui_->logicModeComboBox->findData(static_cast<int>(edge->mode))); | |||
| ui_->logicModeComboBox->setEnabled(true); | |||
| } | |||
| else if (const auto *timer_contact = | |||
| std::get_if<TimerContactNodeConfig>(&node->config)) | |||
| { | |||
| ui_->logicNodeTypeLabel->setText(tr("T 触点")); | |||
| ui_->logicAddressAreaLabel->setText(QStringLiteral("T")); | |||
| ui_->logicAddressSpinBox->setValue(timer_contact->address.index()); | |||
| ui_->logicModeComboBox->addItem( | |||
| tr("常开"), static_cast<int>(ContactMode::NormallyOpen)); | |||
| ui_->logicModeComboBox->addItem( | |||
| tr("常闭"), static_cast<int>(ContactMode::NormallyClosed)); | |||
| ui_->logicModeComboBox->setCurrentIndex( | |||
| ui_->logicModeComboBox->findData( | |||
| static_cast<int>(timer_contact->mode))); | |||
| ui_->logicModeComboBox->setEnabled(true); | |||
| ui_->logicComparisonComboBox->setEnabled(false); | |||
| ui_->logicValueSpinBox->setEnabled(false); | |||
| } | |||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&node->config)) | |||
| { | |||
| @@ -1344,25 +1455,33 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) | |||
| ui_->logicModeComboBox->addItem( | |||
| tr("复位"), static_cast<int>(CoilMode::Reset)); | |||
| ui_->logicModeComboBox->setCurrentIndex( | |||
| ui_->logicModeComboBox->findData(static_cast<int>(coil->mode))); | |||
| ui_->logicModeComboBox->findData(static_cast<int>(coil->mode))); | |||
| ui_->logicModeComboBox->setEnabled(true); | |||
| ui_->logicComparisonComboBox->setEnabled(false); | |||
| ui_->logicValueSpinBox->setEnabled(false); | |||
| } | |||
| else | |||
| else if (const auto *compare = std::get_if<CompareNodeConfig>(&node->config)) | |||
| { | |||
| const auto &compare = std::get<CompareNodeConfig>(node->config); | |||
| ui_->logicNodeTypeLabel->setText(tr("D 值比较")); | |||
| ui_->logicAddressAreaLabel->setText(QStringLiteral("D")); | |||
| ui_->logicAddressSpinBox->setValue(compare.address.index()); | |||
| ui_->logicAddressSpinBox->setValue(compare->address.index()); | |||
| ui_->logicModeComboBox->addItem(tr("不适用")); | |||
| ui_->logicModeComboBox->setEnabled(false); | |||
| ui_->logicComparisonComboBox->setEnabled(true); | |||
| ui_->logicComparisonComboBox->setCurrentIndex( | |||
| ui_->logicComparisonComboBox->findData( | |||
| static_cast<int>(compare.comparison))); | |||
| static_cast<int>(compare->comparison))); | |||
| ui_->logicValueSpinBox->setEnabled(true); | |||
| ui_->logicValueSpinBox->setValue(compare.value); | |||
| ui_->logicValueSpinBox->setValue(compare->value); | |||
| } | |||
| else | |||
| { | |||
| const auto &ton = std::get<TonNodeConfig>(node->config); | |||
| ui_->logicNodeTypeLabel->setText(tr("TON")); | |||
| ui_->logicAddressAreaLabel->setText(QStringLiteral("T")); | |||
| ui_->logicAddressSpinBox->setValue(ton.address.index()); | |||
| ui_->logicModeComboBox->addItem(tr("非保持接通延时")); | |||
| ui_->logicModeComboBox->setEnabled(false); | |||
| ui_->logicPresetSpinBox->setEnabled(true); | |||
| ui_->logicPresetSpinBox->setValue(ton.presetMs); | |||
| } | |||
| if (!node->configured) | |||
| { | |||
| @@ -1500,13 +1619,13 @@ void MainWindow::setLogicOutput(const LogicNodeConfig &config) | |||
| const LogicEditorResult result = logic_editor_widget_->setOutput(config); | |||
| if (!result.succeeded) | |||
| { | |||
| showProjectResult(tr("设置输出线圈"), fromUtf8(result.message), false); | |||
| showProjectResult(tr("设置逻辑输出"), fromUtf8(result.message), false); | |||
| return; | |||
| } | |||
| selected_logic_node_id_ = result.id; | |||
| showLogicNodeProperties(result.id); | |||
| refreshProjectUi(); | |||
| statusBar()->showMessage(tr("输出线圈已设置"), 3000); | |||
| statusBar()->showMessage(tr("逻辑输出已设置"), 3000); | |||
| } | |||
| void MainWindow::addLogicRung() | |||
| @@ -1523,6 +1642,43 @@ void MainWindow::addLogicRung() | |||
| statusBar()->showMessage(tr("已新建网络"), 3000); | |||
| } | |||
| void MainWindow::editSelectedRungComment() | |||
| { | |||
| const std::string rung_id = logic_editor_widget_->selectedRungId(); | |||
| if (rung_id.empty()) | |||
| { | |||
| showProjectResult(tr("网络注释"), tr("请先选择一个梯形图网络"), false); | |||
| return; | |||
| } | |||
| const LadderRung *rung = logic_editor_service_.findRung( | |||
| current_logic_id_, rung_id); | |||
| if (rung == nullptr) | |||
| { | |||
| return; | |||
| } | |||
| bool accepted = false; | |||
| const QString comment = QInputDialog::getMultiLineText( | |||
| this, | |||
| tr("网络注释"), | |||
| tr("说明"), | |||
| fromUtf8(rung->comment), | |||
| &accepted); | |||
| if (!accepted) | |||
| { | |||
| return; | |||
| } | |||
| const LogicEditorResult result = logic_editor_service_.updateRungComment( | |||
| current_logic_id_, rung_id, toUtf8(comment)); | |||
| if (!result.succeeded) | |||
| { | |||
| showProjectResult(tr("网络注释"), fromUtf8(result.message), false); | |||
| return; | |||
| } | |||
| logic_editor_widget_->reloadLogic(); | |||
| refreshProjectUi(); | |||
| statusBar()->showMessage(tr("网络注释已更新"), 3000); | |||
| } | |||
| void MainWindow::deleteSelectedLogicObject() | |||
| { | |||
| const LogicEditorResult result = logic_editor_widget_->deleteSelected(); | |||
| @@ -1551,13 +1707,25 @@ void MainWindow::applySelectedLogicNodeProperties() | |||
| RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, | |||
| static_cast<ContactMode>(ui_->logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else if (std::holds_alternative<EdgeContactNodeConfig>(config)) | |||
| { | |||
| config = EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, | |||
| static_cast<EdgeMode>(ui_->logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else if (std::holds_alternative<TimerContactNodeConfig>(config)) | |||
| { | |||
| config = TimerContactNodeConfig{ | |||
| TimerAddress{ui_->logicAddressSpinBox->value()}, | |||
| static_cast<ContactMode>(ui_->logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else if (std::holds_alternative<CoilNodeConfig>(config)) | |||
| { | |||
| config = CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, | |||
| static_cast<CoilMode>(ui_->logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else | |||
| else if (std::holds_alternative<CompareNodeConfig>(config)) | |||
| { | |||
| config = CompareNodeConfig{ | |||
| RegisterAddress{RegisterArea::D, ui_->logicAddressSpinBox->value()}, | |||
| @@ -1565,6 +1733,12 @@ void MainWindow::applySelectedLogicNodeProperties() | |||
| ui_->logicComparisonComboBox->currentData().toInt()), | |||
| static_cast<std::int16_t>(ui_->logicValueSpinBox->value())}; | |||
| } | |||
| else | |||
| { | |||
| config = TonNodeConfig{ | |||
| TimerAddress{ui_->logicAddressSpinBox->value()}, | |||
| ui_->logicPresetSpinBox->value()}; | |||
| } | |||
| const LogicEditorResult result = logic_editor_service_.updateNodeConfig( | |||
| current_logic_id_, selected_logic_node_id_, config); | |||
| @@ -1775,13 +1949,19 @@ void MainWindow::updateModeUi(const QString &message) | |||
| ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->configureAlarmsAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->configureRegisterCommentsAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addTimerContactAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addTonAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addCompareAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->editRungCommentAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->newProjectAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | |||
| @@ -35,6 +35,7 @@ class HmiRuntimeService; | |||
| class AlarmEditorService; | |||
| class AlarmService; | |||
| class HmiNavigationService; | |||
| class RegisterCommentService; | |||
| class HmiEditorWidget; | |||
| class LogicEditorService; | |||
| class LogicEditorWidget; | |||
| @@ -67,6 +68,7 @@ public: | |||
| HmiRuntimeService &hmi_runtime_service, | |||
| AlarmEditorService &alarm_editor_service, | |||
| AlarmService &alarm_service, | |||
| RegisterCommentService ®ister_comment_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent = nullptr); | |||
| MainWindow( | |||
| @@ -78,6 +80,7 @@ public: | |||
| AlarmEditorService &alarm_editor_service, | |||
| AlarmService &alarm_service, | |||
| HmiNavigationService &hmi_navigation_service, | |||
| RegisterCommentService ®ister_comment_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent = nullptr); | |||
| @@ -101,6 +104,7 @@ private: | |||
| void configurePropertyEditor(); | |||
| void configurePlcConnection(); | |||
| void configureAlarms(); | |||
| void configureRegisterComments(); | |||
| // 刷新工程树和当前 HMI 页面信息 | |||
| void refreshProjectUi(); | |||
| void handleProjectTreeSelection(); | |||
| @@ -130,6 +134,7 @@ private: | |||
| void setLogicOutput(const LogicNodeConfig &config); | |||
| // 新增一个梯形图网络 | |||
| void addLogicRung(); | |||
| void editSelectedRungComment(); | |||
| // 删除当前选中的逻辑节点或网络 | |||
| void deleteSelectedLogicObject(); | |||
| // 将逻辑属性表单内容应用到当前选中节点 | |||
| @@ -185,6 +190,7 @@ private: | |||
| HmiRuntimeService &hmi_runtime_service_; | |||
| AlarmEditorService &alarm_editor_service_; | |||
| AlarmService &alarm_service_; | |||
| RegisterCommentService ®ister_comment_service_; | |||
| RegisterMonitorService ®ister_monitor_service_; | |||
| std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | |||
| HmiNavigationService *hmi_navigation_service_ = nullptr; | |||
| @@ -254,9 +254,10 @@ | |||
| <addaction name="disconnectPlcAction"/> | |||
| </widget> | |||
| <widget class="QMenu" name="viewMenu"> | |||
| <property name="title"> | |||
| <string>视图(&V)</string> | |||
| </property> | |||
| <property name="title"> | |||
| <string>视图(&V)</string> | |||
| </property> | |||
| <addaction name="configureRegisterCommentsAction"/> | |||
| </widget> | |||
| <addaction name="fileMenu"/> | |||
| <addaction name="runMenu"/> | |||
| @@ -326,10 +327,15 @@ | |||
| <addaction name="separator"/> | |||
| <addaction name="addNormallyOpenAction"/> | |||
| <addaction name="addNormallyClosedAction"/> | |||
| <addaction name="addRisingEdgeAction"/> | |||
| <addaction name="addFallingEdgeAction"/> | |||
| <addaction name="addTimerContactAction"/> | |||
| <addaction name="addNormalCoilAction"/> | |||
| <addaction name="addSetCoilAction"/> | |||
| <addaction name="addResetCoilAction"/> | |||
| <addaction name="addTonAction"/> | |||
| <addaction name="addCompareAction"/> | |||
| <addaction name="editRungCommentAction"/> | |||
| <addaction name="deleteLogicAction"/> | |||
| </widget> | |||
| <widget class="QDockWidget" name="projectDock"> | |||
| @@ -758,7 +764,27 @@ | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="7" column="0" colspan="2"> | |||
| <item row="7" column="0"> | |||
| <widget class="QLabel" name="logicPresetLabel"> | |||
| <property name="text"> | |||
| <string>预设时间(ms)</string> | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="7" column="1"> | |||
| <widget class="QSpinBox" name="logicPresetSpinBox"> | |||
| <property name="minimum"> | |||
| <number>1</number> | |||
| </property> | |||
| <property name="maximum"> | |||
| <number>86400000</number> | |||
| </property> | |||
| <property name="value"> | |||
| <number>1000</number> | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="8" column="0" colspan="2"> | |||
| <widget class="QPushButton" name="applyLogicPropertiesButton"> | |||
| <property name="text"> | |||
| <string>应用属性</string> | |||
| @@ -848,6 +874,14 @@ | |||
| <string>配置参数并连接真实 PLC</string> | |||
| </property> | |||
| </action> | |||
| <action name="configureRegisterCommentsAction"> | |||
| <property name="text"> | |||
| <string>软元件注释</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>维护工程级 M/D 地址注释</string> | |||
| </property> | |||
| </action> | |||
| <action name="disconnectPlcAction"> | |||
| <property name="text"> | |||
| <string>断开 PLC</string> | |||
| @@ -960,6 +994,30 @@ | |||
| <string>向当前网络添加常闭触点</string> | |||
| </property> | |||
| </action> | |||
| <action name="addRisingEdgeAction"> | |||
| <property name="text"> | |||
| <string>上升沿</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>添加一个扫描周期的上升沿触点</string> | |||
| </property> | |||
| </action> | |||
| <action name="addFallingEdgeAction"> | |||
| <property name="text"> | |||
| <string>下降沿</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>添加一个扫描周期的下降沿触点</string> | |||
| </property> | |||
| </action> | |||
| <action name="addTimerContactAction"> | |||
| <property name="text"> | |||
| <string>T 触点</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>添加离线仿真定时器触点</string> | |||
| </property> | |||
| </action> | |||
| <action name="addNormalCoilAction"> | |||
| <property name="text"> | |||
| <string>线圈</string> | |||
| @@ -984,6 +1042,14 @@ | |||
| <string>设置当前网络的复位线圈</string> | |||
| </property> | |||
| </action> | |||
| <action name="addTonAction"> | |||
| <property name="text"> | |||
| <string>TON</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>设置非保持接通延时定时器输出</string> | |||
| </property> | |||
| </action> | |||
| <action name="addCompareAction"> | |||
| <property name="text"> | |||
| <string>D 比较</string> | |||
| @@ -992,6 +1058,14 @@ | |||
| <string>向当前网络添加 D 值与常量比较</string> | |||
| </property> | |||
| </action> | |||
| <action name="editRungCommentAction"> | |||
| <property name="text"> | |||
| <string>网络注释</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>编辑当前梯形图网络说明</string> | |||
| </property> | |||
| </action> | |||
| <action name="deleteLogicAction"> | |||
| <property name="text"> | |||
| <string>删除</string> | |||
| @@ -0,0 +1,155 @@ | |||
| #include "register_comment_dialog.h" | |||
| #include "services/register_comment_service.h" | |||
| #include "ui_register_comment_dialog.h" | |||
| #include <QHeaderView> | |||
| #include <QMessageBox> | |||
| #include <QTableWidgetItem> | |||
| #include <optional> | |||
| namespace { | |||
| QString fromUtf8(const std::string &value) | |||
| { | |||
| return QString::fromUtf8(value.data(), static_cast<int>(value.size())); | |||
| } | |||
| std::string toUtf8(const QString &value) | |||
| { | |||
| const QByteArray bytes = value.toUtf8(); | |||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | |||
| } | |||
| } // namespace | |||
| RegisterCommentDialog::RegisterCommentDialog( | |||
| RegisterCommentService &service, | |||
| QWidget *parent) | |||
| : QDialog(parent), | |||
| ui_(std::make_unique<Ui::RegisterCommentDialog>()), | |||
| service_(service) | |||
| { | |||
| ui_->setupUi(this); | |||
| ui_->commentTable->horizontalHeader()->setSectionResizeMode( | |||
| 0, QHeaderView::ResizeToContents); | |||
| ui_->commentTable->horizontalHeader()->setSectionResizeMode( | |||
| 1, QHeaderView::Stretch); | |||
| ui_->areaComboBox->setItemData(0, static_cast<int>(RegisterArea::M)); | |||
| ui_->areaComboBox->setItemData(1, static_cast<int>(RegisterArea::D)); | |||
| connect(ui_->commentTable, &QTableWidget::itemSelectionChanged, | |||
| this, [this] { loadSelectedComment(); }); | |||
| connect(ui_->saveButton, &QPushButton::clicked, | |||
| this, [this] { saveComment(); }); | |||
| connect(ui_->removeButton, &QPushButton::clicked, | |||
| this, [this] { removeComment(); }); | |||
| connect(ui_->buttonBox, &QDialogButtonBox::rejected, | |||
| this, &QDialog::reject); | |||
| reloadComments(); | |||
| } | |||
| RegisterCommentDialog::~RegisterCommentDialog() = default; | |||
| void RegisterCommentDialog::reloadComments( | |||
| const RegisterAddress *selected_address) | |||
| { | |||
| const auto &comments = service_.comments(); | |||
| ui_->commentTable->setRowCount(static_cast<int>(comments.size())); | |||
| int selected_row = -1; | |||
| for (std::size_t index = 0; index < comments.size(); ++index) | |||
| { | |||
| const RegisterComment &comment = comments[index]; | |||
| const int row = static_cast<int>(index); | |||
| auto *address_item = new QTableWidgetItem( | |||
| QString::fromStdString(comment.address.toString())); | |||
| address_item->setData(Qt::UserRole, static_cast<int>(comment.address.area())); | |||
| address_item->setData(Qt::UserRole + 1, comment.address.index()); | |||
| ui_->commentTable->setItem(row, 0, address_item); | |||
| ui_->commentTable->setItem(row, 1, new QTableWidgetItem(fromUtf8(comment.text))); | |||
| if (selected_address != nullptr && comment.address == *selected_address) | |||
| { | |||
| selected_row = row; | |||
| } | |||
| } | |||
| if (selected_row >= 0) | |||
| { | |||
| ui_->commentTable->selectRow(selected_row); | |||
| } | |||
| else | |||
| { | |||
| ui_->commentTable->clearSelection(); | |||
| ui_->removeButton->setEnabled(false); | |||
| } | |||
| } | |||
| void RegisterCommentDialog::loadSelectedComment() | |||
| { | |||
| const std::optional<RegisterAddress> address = selectedAddress(); | |||
| const RegisterComment *comment = address.has_value() | |||
| ? service_.findComment(*address) : nullptr; | |||
| ui_->removeButton->setEnabled(comment != nullptr); | |||
| if (comment == nullptr) | |||
| { | |||
| return; | |||
| } | |||
| ui_->areaComboBox->setCurrentIndex( | |||
| comment->address.area() == RegisterArea::M ? 0 : 1); | |||
| ui_->indexSpinBox->setValue(comment->address.index()); | |||
| ui_->commentEdit->setText(fromUtf8(comment->text)); | |||
| } | |||
| void RegisterCommentDialog::saveComment() | |||
| { | |||
| const RegisterAddress address = addressFromInputs(); | |||
| const RegisterCommentResult result = service_.setComment( | |||
| address, toUtf8(ui_->commentEdit->text())); | |||
| if (!result.succeeded) | |||
| { | |||
| QMessageBox::warning(this, tr("保存注释"), fromUtf8(result.message)); | |||
| return; | |||
| } | |||
| reloadComments(&address); | |||
| } | |||
| void RegisterCommentDialog::removeComment() | |||
| { | |||
| const std::optional<RegisterAddress> address = selectedAddress(); | |||
| if (!address.has_value()) | |||
| { | |||
| return; | |||
| } | |||
| if (QMessageBox::question( | |||
| this, tr("删除注释"), tr("确定删除当前软元件注释吗?")) | |||
| != QMessageBox::Yes) | |||
| { | |||
| return; | |||
| } | |||
| const RegisterCommentResult result = service_.removeComment(*address); | |||
| if (!result.succeeded) | |||
| { | |||
| QMessageBox::warning(this, tr("删除注释"), fromUtf8(result.message)); | |||
| return; | |||
| } | |||
| reloadComments(); | |||
| } | |||
| RegisterAddress RegisterCommentDialog::addressFromInputs() const | |||
| { | |||
| const RegisterArea area = static_cast<RegisterArea>( | |||
| ui_->areaComboBox->currentData().toInt()); | |||
| return RegisterAddress{area, ui_->indexSpinBox->value()}; | |||
| } | |||
| std::optional<RegisterAddress> RegisterCommentDialog::selectedAddress() const | |||
| { | |||
| const int row = ui_->commentTable->currentRow(); | |||
| const QTableWidgetItem *item = row >= 0 ? ui_->commentTable->item(row, 0) : nullptr; | |||
| if (item == nullptr) | |||
| { | |||
| return std::nullopt; | |||
| } | |||
| return RegisterAddress{ | |||
| static_cast<RegisterArea>(item->data(Qt::UserRole).toInt()), | |||
| item->data(Qt::UserRole + 1).toInt()}; | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| #pragma once | |||
| #include "domain/register_address.h" | |||
| #include <QDialog> | |||
| #include <memory> | |||
| #include <optional> | |||
| namespace Ui { | |||
| class RegisterCommentDialog; | |||
| } | |||
| class RegisterCommentService; | |||
| class RegisterCommentDialog final : public QDialog | |||
| { | |||
| public: | |||
| explicit RegisterCommentDialog( | |||
| RegisterCommentService &service, | |||
| QWidget *parent = nullptr); | |||
| ~RegisterCommentDialog() override; | |||
| private: | |||
| void reloadComments(const RegisterAddress *selected_address = nullptr); | |||
| void loadSelectedComment(); | |||
| void saveComment(); | |||
| void removeComment(); | |||
| RegisterAddress addressFromInputs() const; | |||
| std::optional<RegisterAddress> selectedAddress() const; | |||
| std::unique_ptr<Ui::RegisterCommentDialog> ui_; | |||
| RegisterCommentService &service_; | |||
| }; | |||
| @@ -0,0 +1,40 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <ui version="4.0"> | |||
| <class>RegisterCommentDialog</class> | |||
| <widget class="QDialog" name="RegisterCommentDialog"> | |||
| <property name="windowTitle"><string>软元件注释</string></property> | |||
| <property name="minimumSize"><size><width>620</width><height>400</height></size></property> | |||
| <layout class="QVBoxLayout" name="verticalLayout"> | |||
| <item> | |||
| <widget class="QTableWidget" name="commentTable"> | |||
| <property name="selectionBehavior"><enum>QAbstractItemView::SelectRows</enum></property> | |||
| <property name="selectionMode"><enum>QAbstractItemView::SingleSelection</enum></property> | |||
| <property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></property> | |||
| <property name="columnCount"><number>2</number></property> | |||
| <column><property name="text"><string>地址</string></property></column> | |||
| <column><property name="text"><string>注释</string></property></column> | |||
| </widget> | |||
| </item> | |||
| <item> | |||
| <layout class="QFormLayout" name="commentForm"> | |||
| <item row="0" column="0"><widget class="QLabel" name="areaLabel"><property name="text"><string>地址区域</string></property></widget></item> | |||
| <item row="0" column="1"><widget class="QComboBox" name="areaComboBox"><item><property name="text"><string>M</string></property></item><item><property name="text"><string>D</string></property></item></widget></item> | |||
| <item row="1" column="0"><widget class="QLabel" name="indexLabel"><property name="text"><string>地址</string></property></widget></item> | |||
| <item row="1" column="1"><widget class="QSpinBox" name="indexSpinBox"><property name="maximum"><number>4000</number></property></widget></item> | |||
| <item row="2" column="0"><widget class="QLabel" name="commentLabel"><property name="text"><string>注释</string></property></widget></item> | |||
| <item row="2" column="1"><widget class="QLineEdit" name="commentEdit"><property name="placeholderText"><string>例如:启动按钮</string></property></widget></item> | |||
| </layout> | |||
| </item> | |||
| <item> | |||
| <layout class="QHBoxLayout" name="operationLayout"> | |||
| <item><widget class="QPushButton" name="saveButton"><property name="text"><string>保存</string></property></widget></item> | |||
| <item><widget class="QPushButton" name="removeButton"><property name="text"><string>删除</string></property></widget></item> | |||
| <item><spacer name="operationSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> | |||
| <item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::Close</set></property></widget></item> | |||
| </layout> | |||
| </item> | |||
| </layout> | |||
| </widget> | |||
| <resources/> | |||
| <connections/> | |||
| </ui> | |||
| @@ -11,6 +11,7 @@ | |||
| #include "services/project_service.h" | |||
| #include "services/runtime_mode_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "services/register_comment_service.h" | |||
| #include "ui/hmi_editor_widget.h" | |||
| #include "ui/free_monitor_widget.h" | |||
| #include "ui/logic_editor_widget.h" | |||
| @@ -29,9 +30,11 @@ | |||
| #include <QLabel> | |||
| #include <QLineEdit> | |||
| #include <QListWidget> | |||
| #include <QMenu> | |||
| #include <QPainter> | |||
| #include <QSpinBox> | |||
| #include <QPushButton> | |||
| #include <QStringList> | |||
| #include <QTableWidget> | |||
| #include <QTimer> | |||
| #include <QTest> | |||
| @@ -501,6 +504,7 @@ void testModeActionsControlEditingAvailability() | |||
| HmiRuntimeService runtime_service(repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| OfflineSimulationService simulation_service(repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(repository); | |||
| @@ -512,6 +516,7 @@ void testModeActionsControlEditingAvailability() | |||
| runtime_service, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| window.resize(1000, 640); | |||
| window.show(); | |||
| @@ -711,6 +716,105 @@ void testModeActionsControlEditingAvailability() | |||
| "rejected online running must restore the editing action"); | |||
| } | |||
| void testEdgeTimerPropertyEditingAndParallelMenu() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService editor_service(project_service); | |||
| LogicEditorService logic_editor_service(project_service); | |||
| VirtualRegisterRepository repository; | |||
| HmiRuntimeService runtime_service(repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| OfflineSimulationService simulation_service(repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(repository); | |||
| MainWindow window( | |||
| mode_service, | |||
| project_service, | |||
| editor_service, | |||
| logic_editor_service, | |||
| runtime_service, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| window.show(); | |||
| QApplication::processEvents(); | |||
| QAction *parallel_action = requiredChild<QAction>(window, "parallelInsertAction"); | |||
| require(parallel_action->menu() != nullptr, | |||
| "parallel insertion must expose its node type menu"); | |||
| QStringList parallel_labels; | |||
| for (QAction *action : parallel_action->menu()->actions()) | |||
| { | |||
| parallel_labels.push_back(action->text()); | |||
| } | |||
| require(parallel_labels.contains(QStringLiteral("并联上升沿触点")) | |||
| && parallel_labels.contains(QStringLiteral("并联下降沿触点")) | |||
| && parallel_labels.contains(QStringLiteral("并联 T 触点")), | |||
| "parallel insertion must include edge and T contact choices"); | |||
| QAction *rising_action = requiredChild<QAction>(window, "addRisingEdgeAction"); | |||
| QAction *falling_action = requiredChild<QAction>(window, "addFallingEdgeAction"); | |||
| QAction *timer_contact_action = requiredChild<QAction>( | |||
| window, "addTimerContactAction"); | |||
| QAction *ton_action = requiredChild<QAction>(window, "addTonAction"); | |||
| requiredChild<QAction>(window, "editRungCommentAction"); | |||
| requiredChild<QAction>(window, "configureRegisterCommentsAction"); | |||
| QSpinBox *address = requiredChild<QSpinBox>(window, "logicAddressSpinBox"); | |||
| QSpinBox *preset = requiredChild<QSpinBox>(window, "logicPresetSpinBox"); | |||
| QComboBox *mode = requiredChild<QComboBox>(window, "logicModeComboBox"); | |||
| QPushButton *apply = requiredChild<QPushButton>( | |||
| window, "applyLogicPropertiesButton"); | |||
| const std::string logic_id = logic_editor_service.firstLogicId(); | |||
| rising_action->trigger(); | |||
| address->setValue(8); | |||
| mode->setCurrentIndex(mode->findData(static_cast<int>(EdgeMode::Falling))); | |||
| apply->click(); | |||
| const LogicNode *edge = logic_editor_service.findNode(logic_id, "edge-1"); | |||
| require(edge != nullptr && edge->configured | |||
| && std::get<EdgeContactNodeConfig>(edge->config).address.index() == 8 | |||
| && std::get<EdgeContactNodeConfig>(edge->config).mode | |||
| == EdgeMode::Falling, | |||
| "edge properties must update M address and edge mode"); | |||
| timer_contact_action->trigger(); | |||
| address->setValue(9); | |||
| mode->setCurrentIndex(mode->findData( | |||
| static_cast<int>(ContactMode::NormallyClosed))); | |||
| apply->click(); | |||
| const LogicNode *timer_contact = logic_editor_service.findNode( | |||
| logic_id, "timer-contact-1"); | |||
| require(timer_contact != nullptr && timer_contact->configured | |||
| && std::get<TimerContactNodeConfig>(timer_contact->config).address | |||
| == TimerAddress{9} | |||
| && std::get<TimerContactNodeConfig>(timer_contact->config).mode | |||
| == ContactMode::NormallyClosed, | |||
| "T contact properties must update T address and contact mode"); | |||
| ton_action->trigger(); | |||
| require(preset->isEnabled() && !mode->isEnabled(), | |||
| "TON properties must enable PT and disable irrelevant mode editing"); | |||
| address->setValue(9); | |||
| preset->setValue(1234); | |||
| apply->click(); | |||
| const LogicNode *ton = logic_editor_service.findNode(logic_id, "ton-1"); | |||
| require(ton != nullptr && ton->configured | |||
| && std::get<TonNodeConfig>(ton->config).address == TimerAddress{9} | |||
| && std::get<TonNodeConfig>(ton->config).presetMs == 1234, | |||
| "TON properties must update T address and preset milliseconds"); | |||
| falling_action->trigger(); | |||
| const LogicNode *falling = logic_editor_service.findNode(logic_id, "edge-2"); | |||
| require(falling != nullptr | |||
| && std::get<EdgeContactNodeConfig>(falling->config).mode | |||
| == EdgeMode::Falling, | |||
| "falling edge action must create a falling-edge condition"); | |||
| } | |||
| void testPlcConfigurationUsesDialog() | |||
| { | |||
| PlcSerialConfiguration initial; | |||
| @@ -746,6 +850,7 @@ void testPlcConfigurationUsesDialog() | |||
| HmiRuntimeService runtime_service(repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| OfflineSimulationService simulation_service(repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(repository); | |||
| @@ -757,6 +862,7 @@ void testPlcConfigurationUsesDialog() | |||
| runtime_service, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr, | |||
| "serial configuration controls must not be embedded in the main window"); | |||
| @@ -791,6 +897,7 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||
| HmiRuntimeService runtime_service(active_repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, active_repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| OfflineSimulationService simulation_service(virtual_repository); | |||
| RepeatedStatusGateway gateway; | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| @@ -805,6 +912,7 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||
| runtime_service, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | |||
| const int initial_count = output->count(); | |||
| @@ -839,6 +947,7 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| HmiRuntimeService runtime_service(active_repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, active_repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| OfflineSimulationService simulation_service(virtual_repository); | |||
| OnlineReadyGateway gateway; | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| @@ -853,6 +962,7 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| runtime_service, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| window.resize(1200, 760); | |||
| window.show(); | |||
| @@ -965,6 +1075,7 @@ void testMultiPageAndLogicMainWindowIntegration() | |||
| HmiRuntimeService runtime_service(repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| OfflineSimulationService simulation_service(repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(repository); | |||
| @@ -978,6 +1089,7 @@ void testMultiPageAndLogicMainWindowIntegration() | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| navigation_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| window.resize(1400, 820); | |||
| window.show(); | |||
| @@ -1078,6 +1190,7 @@ int main(int argc, char *argv[]) | |||
| testRuntimePageJumpDoesNotRequireRegisterWritePermission(); | |||
| testRuntimeAlarmListInteraction(); | |||
| testModeActionsControlEditingAvailability(); | |||
| testEdgeTimerPropertyEditingAndParallelMenu(); | |||
| testPlcConfigurationUsesDialog(); | |||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | |||
| testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | |||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||
| main_window_tests.cpp \ | |||
| ../src/ui/main_window.cpp \ | |||
| ../src/ui/alarm_configuration_dialog.cpp \ | |||
| ../src/ui/register_comment_dialog.cpp \ | |||
| ../src/ui/plc_connection_dialog.cpp \ | |||
| ../src/ui/free_monitor_widget.cpp \ | |||
| ../src/ui/runtime_monitor_widget.cpp \ | |||
| @@ -36,11 +37,13 @@ SOURCES += \ | |||
| ../src/services/software_logic_executor.cpp \ | |||
| ../src/services/offline_simulation_service.cpp \ | |||
| ../src/services/runtime_mode_service.cpp \ | |||
| ../src/services/register_monitor_service.cpp | |||
| ../src/services/register_monitor_service.cpp \ | |||
| ../src/services/register_comment_service.cpp | |||
| HEADERS += \ | |||
| ../src/ui/main_window.h \ | |||
| ../src/ui/alarm_configuration_dialog.h \ | |||
| ../src/ui/register_comment_dialog.h \ | |||
| ../src/ui/plc_connection_dialog.h \ | |||
| ../src/ui/free_monitor_widget.h \ | |||
| ../src/ui/runtime_monitor_widget.h \ | |||
| @@ -66,7 +69,8 @@ HEADERS += \ | |||
| ../src/services/software_logic_executor.h \ | |||
| ../src/services/offline_simulation_service.h \ | |||
| ../src/services/runtime_mode_service.h \ | |||
| ../src/services/register_monitor_service.h | |||
| ../src/services/register_monitor_service.h \ | |||
| ../src/services/register_comment_service.h | |||
| HEADERS += \ | |||
| ../src/services/plc_communication_gateway.h | |||
| @@ -74,6 +78,7 @@ HEADERS += \ | |||
| FORMS += \ | |||
| ../src/ui/main_window.ui \ | |||
| ../src/ui/alarm_configuration_dialog.ui \ | |||
| ../src/ui/register_comment_dialog.ui \ | |||
| ../src/ui/plc_connection_dialog.ui \ | |||
| ../src/ui/free_monitor_widget.ui \ | |||
| ../src/ui/runtime_monitor_widget.ui | |||