| @@ -126,3 +126,8 @@ FORMS += \ | |||||
| src/ui/free_monitor_widget.ui \ | src/ui/free_monitor_widget.ui \ | ||||
| src/ui/runtime_monitor_window.ui \ | src/ui/runtime_monitor_window.ui \ | ||||
| src/ui/runtime_monitor_widget.ui | src/ui/runtime_monitor_widget.ui | ||||
| # 专用用户运行版打包时由脚本传入生成的工程资源;普通编程器构建不包含工程 | |||||
| !isEmpty(RUNTIME_PROJECT_RESOURCE) { | |||||
| RESOURCES += $$RUNTIME_PROJECT_RESOURCE | |||||
| } | |||||
| @@ -7,6 +7,10 @@ | |||||
| */ | */ | ||||
| #include <QApplication> | #include <QApplication> | ||||
| #include <QFile> | |||||
| #include <QFileInfo> | |||||
| #include <QMessageBox> | |||||
| #include <QDir> | |||||
| #include "infrastructure/json_project_storage.h" | #include "infrastructure/json_project_storage.h" | ||||
| #include "infrastructure/application_settings_loader.h" | #include "infrastructure/application_settings_loader.h" | ||||
| @@ -41,6 +45,27 @@ int main(int argc, char *argv[]) | |||||
| // 设置组织名称,用来区分这个程序保存的 Qt 配置 | // 设置组织名称,用来区分这个程序保存的 Qt 配置 | ||||
| application.setOrganizationName(QStringLiteral("QtProXinJe")); | application.setOrganizationName(QStringLiteral("QtProXinJe")); | ||||
| QString runtime_project_path; | |||||
| const QStringList arguments = application.arguments(); | |||||
| const int project_option = arguments.indexOf(QStringLiteral("--runtime-project")); | |||||
| if (project_option >= 0 && project_option + 1 < arguments.size()) | |||||
| { | |||||
| runtime_project_path = arguments.at(project_option + 1); | |||||
| } | |||||
| const QFileInfo executable_info(application.applicationFilePath()); | |||||
| const QString bundled_project = executable_info.absolutePath() | |||||
| + QStringLiteral("/runtime-project.json"); | |||||
| if (runtime_project_path.isEmpty() | |||||
| && executable_info.completeBaseName() != QStringLiteral("integrated_platform") | |||||
| && QFileInfo::exists(bundled_project)) | |||||
| { | |||||
| runtime_project_path = bundled_project; | |||||
| } | |||||
| const bool embedded_runtime_project = QFile::exists( | |||||
| QStringLiteral(":/runtime-project.json")); | |||||
| const bool user_runtime_mode = !runtime_project_path.isEmpty() | |||||
| || embedded_runtime_project; | |||||
| const ApplicationSettingsLoadResult application_settings = | const ApplicationSettingsLoadResult application_settings = | ||||
| ApplicationSettingsLoader::load( | ApplicationSettingsLoader::load( | ||||
| ApplicationSettingsLoader::defaultFilePath()); | ApplicationSettingsLoader::defaultFilePath()); | ||||
| @@ -50,6 +75,48 @@ int main(int argc, char *argv[]) | |||||
| // 统一管理新建、打开、保存和校验工程,实际读写文件交给上面的对象 | // 统一管理新建、打开、保存和校验工程,实际读写文件交给上面的对象 | ||||
| ProjectService project_service( | ProjectService project_service( | ||||
| project_storage, application_settings.settings.projectLimits); | project_storage, application_settings.settings.projectLimits); | ||||
| if (user_runtime_mode) | |||||
| { | |||||
| ProjectOperationResult result; | |||||
| if (embedded_runtime_project && runtime_project_path.isEmpty()) | |||||
| { | |||||
| const QByteArray bytes = [] | |||||
| { | |||||
| QFile resource(QStringLiteral(":/runtime-project.json")); | |||||
| return resource.open(QIODevice::ReadOnly) | |||||
| ? resource.readAll() : QByteArray{}; | |||||
| }(); | |||||
| const QString temporary_path = QDir::tempPath() | |||||
| + QStringLiteral("/qtproxinje-runtime-project.json"); | |||||
| QFile temporary_file(temporary_path); | |||||
| if (bytes.isEmpty() || !temporary_file.open(QIODevice::WriteOnly) | |||||
| || temporary_file.write(bytes) != bytes.size()) | |||||
| { | |||||
| QMessageBox::critical( | |||||
| nullptr, | |||||
| QObject::tr("运行程序启动失败"), | |||||
| QObject::tr("无法读取内嵌项目资源")); | |||||
| return 2; | |||||
| } | |||||
| temporary_file.close(); | |||||
| result = project_service.load(temporary_path.toUtf8().toStdString()); | |||||
| QFile::remove(temporary_path); | |||||
| } | |||||
| else | |||||
| { | |||||
| result = project_service.load( | |||||
| runtime_project_path.toUtf8().toStdString()); | |||||
| } | |||||
| if (!result.succeeded) | |||||
| { | |||||
| QMessageBox::critical( | |||||
| nullptr, | |||||
| QObject::tr("运行程序启动失败"), | |||||
| QObject::tr("无法加载项目:%1") | |||||
| .arg(QString::fromUtf8(result.message.c_str()))); | |||||
| return 2; | |||||
| } | |||||
| } | |||||
| // 处理 HMI 页面和控件的添加、修改、删除、撤销与重做 | // 处理 HMI 页面和控件的添加、修改、删除、撤销与重做 | ||||
| HmiEditorService hmi_editor_service( | HmiEditorService hmi_editor_service( | ||||
| project_service, application_settings.settings.hmiDefaults); | project_service, application_settings.settings.hmiDefaults); | ||||
| @@ -111,9 +178,12 @@ int main(int argc, char *argv[]) | |||||
| register_comment_service, | register_comment_service, | ||||
| register_monitor_service, | register_monitor_service, | ||||
| plc_discovery_service, | plc_discovery_service, | ||||
| application_settings); | |||||
| // 把主窗口显示到屏幕上 | |||||
| main_window.show(); | |||||
| application_settings, | |||||
| user_runtime_mode); | |||||
| if (!user_runtime_mode) | |||||
| { | |||||
| main_window.show(); | |||||
| } | |||||
| // 启动 Qt 消息循环,持续响应鼠标、键盘、定时器和串口事件,窗口关闭后才返回 | // 启动 Qt 消息循环,持续响应鼠标、键盘、定时器和串口事件,窗口关闭后才返回 | ||||
| return application.exec(); | return application.exec(); | ||||
| @@ -411,9 +411,7 @@ const std::vector<LogicCommandSuggestion> &LogicCommandService::suggestions() | |||||
| {"RST", "复位线圈", "M 地址"}, | {"RST", "复位线圈", "M 地址"}, | ||||
| {"MOV", "数据传送", "D 地址 -> D 地址"}, | {"MOV", "数据传送", "D 地址 -> D 地址"}, | ||||
| {"ADD", "加法", "D 地址 + D 地址 -> D 地址"}, | {"ADD", "加法", "D 地址 + D 地址 -> D 地址"}, | ||||
| {"SUB", "减法", "D 地址 - D 地址 -> D 地址"}, | |||||
| {"TON", "暂不支持", "T 地址", false}, | |||||
| {"CTU", "暂不支持", "C 地址", false}}; | |||||
| {"SUB", "减法", "D 地址 - D 地址 -> D 地址"}}; | |||||
| return values; | return values; | ||||
| } | } | ||||
| @@ -137,6 +137,31 @@ ProjectOperationResult ProjectService::saveAs(const std::string &file_path) | |||||
| return {true, ProjectServiceError::None, ProjectStorageError::None, {}}; | return {true, ProjectServiceError::None, ProjectStorageError::None, {}}; | ||||
| } | } | ||||
| ProjectOperationResult ProjectService::exportAs(const std::string &file_path) const | |||||
| { | |||||
| if (isBlank(file_path)) | |||||
| { | |||||
| return {false, | |||||
| ProjectServiceError::FilePathRequired, | |||||
| ProjectStorageError::None, | |||||
| "必须指定导出文件路径"}; | |||||
| } | |||||
| std::string validation_error; | |||||
| if (!project_.validate(project_limits_, &validation_error)) | |||||
| { | |||||
| return {false, | |||||
| ProjectServiceError::InvalidProject, | |||||
| ProjectStorageError::InvalidProject, | |||||
| validation_error}; | |||||
| } | |||||
| const ProjectSaveResult result = storage_.save(project_, file_path); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| return storageFailure(result.error, result.message); | |||||
| } | |||||
| return {true, ProjectServiceError::None, ProjectStorageError::None, {}}; | |||||
| } | |||||
| ProjectOperationResult ProjectService::load(const std::string &file_path) | ProjectOperationResult ProjectService::load(const std::string &file_path) | ||||
| { | { | ||||
| if (isBlank(file_path)) | if (isBlank(file_path)) | ||||
| @@ -63,6 +63,8 @@ public: | |||||
| * @return 保存操作结果 | * @return 保存操作结果 | ||||
| */ | */ | ||||
| ProjectOperationResult saveAs(const std::string &file_path); | ProjectOperationResult saveAs(const std::string &file_path); | ||||
| /** 将当前工程导出到指定 JSON,不改变当前工程文件关联 */ | |||||
| ProjectOperationResult exportAs(const std::string &file_path) const; | |||||
| /** | /** | ||||
| * @brief 从指定文件加载工程 | * @brief 从指定文件加载工程 | ||||
| @@ -1922,17 +1922,6 @@ void LogicEditorWidget::beginCommandInput( | |||||
| command_editor_->raise(); | command_editor_->raise(); | ||||
| command_editor_->setFocus(Qt::MouseFocusReason); | command_editor_->setFocus(Qt::MouseFocusReason); | ||||
| command_editor_->selectAll(); | command_editor_->selectAll(); | ||||
| QTimer::singleShot(0, this, [this] | |||||
| { | |||||
| if (command_editor_ == nullptr || !command_editor_->isVisible() | |||||
| || command_completer_ == nullptr) | |||||
| { | |||||
| return; | |||||
| } | |||||
| command_completer_->setCompletionPrefix( | |||||
| commandCompletionPrefix(command_editor_->text())); | |||||
| command_completer_->complete(command_editor_->rect()); | |||||
| }); | |||||
| } | } | ||||
| void LogicEditorWidget::commitCommandInput() | void LogicEditorWidget::commitCommandInput() | ||||
| @@ -51,6 +51,11 @@ | |||||
| #include <QToolButton> | #include <QToolButton> | ||||
| #include <QTextEdit> | #include <QTextEdit> | ||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <QProcess> | |||||
| #include <QDir> | |||||
| #include <QFileInfo> | |||||
| #include <QProgressDialog> | |||||
| #include <QDateTime> | |||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -252,6 +257,56 @@ QToolButton *addToolbarMenu( | |||||
| return button; | return button; | ||||
| } | } | ||||
| bool copyDirectoryContents( | |||||
| const QString &source_path, const QString &destination_path, QString *error) | |||||
| { | |||||
| QDir source_directory(source_path); | |||||
| if (!source_directory.exists()) | |||||
| { | |||||
| if (error != nullptr) | |||||
| { | |||||
| *error = MainWindow::tr("未找到打包目录:%1").arg(source_path); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| QDir destination_directory(destination_path); | |||||
| if (!destination_directory.exists() | |||||
| && !QDir().mkpath(destination_path)) | |||||
| { | |||||
| if (error != nullptr) | |||||
| { | |||||
| *error = MainWindow::tr("无法创建导出目录:%1").arg(destination_path); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| const QFileInfoList entries = source_directory.entryInfoList( | |||||
| QDir::NoDotAndDotDot | QDir::AllEntries, | |||||
| QDir::DirsFirst | QDir::Name); | |||||
| for (const QFileInfo &entry : entries) | |||||
| { | |||||
| const QString destination = destination_directory.filePath(entry.fileName()); | |||||
| if (entry.isDir()) | |||||
| { | |||||
| if (!copyDirectoryContents(entry.absoluteFilePath(), destination, error)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| continue; | |||||
| } | |||||
| if (QFileInfo::exists(destination) | |||||
| || !QFile::copy(entry.absoluteFilePath(), destination)) | |||||
| { | |||||
| if (error != nullptr) | |||||
| { | |||||
| *error = MainWindow::tr("复制打包文件失败:%1").arg(destination); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| } | |||||
| return true; | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| MainWindow::MainWindow( | MainWindow::MainWindow( | ||||
| @@ -266,6 +321,7 @@ MainWindow::MainWindow( | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| PlcDiscoveryGateway &plc_discovery_gateway, | PlcDiscoveryGateway &plc_discovery_gateway, | ||||
| const ApplicationSettingsLoadResult &application_settings_result, | const ApplicationSettingsLoadResult &application_settings_result, | ||||
| bool user_runtime_mode, | |||||
| QWidget *parent) | QWidget *parent) | ||||
| : QMainWindow(parent), | : QMainWindow(parent), | ||||
| ui_(std::make_unique<Ui::MainWindow>()), | ui_(std::make_unique<Ui::MainWindow>()), | ||||
| @@ -280,6 +336,7 @@ MainWindow::MainWindow( | |||||
| register_monitor_service_(register_monitor_service), | register_monitor_service_(register_monitor_service), | ||||
| plc_discovery_gateway_(plc_discovery_gateway), | plc_discovery_gateway_(plc_discovery_gateway), | ||||
| application_settings_result_(application_settings_result), | application_settings_result_(application_settings_result), | ||||
| user_runtime_mode_(user_runtime_mode), | |||||
| owned_hmi_navigation_service_( | owned_hmi_navigation_service_( | ||||
| std::make_unique<HmiNavigationService>(project_service)), | std::make_unique<HmiNavigationService>(project_service)), | ||||
| hmi_navigation_service_(owned_hmi_navigation_service_.get()), | hmi_navigation_service_(owned_hmi_navigation_service_.get()), | ||||
| @@ -301,6 +358,7 @@ MainWindow::MainWindow( | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| PlcDiscoveryGateway &plc_discovery_gateway, | PlcDiscoveryGateway &plc_discovery_gateway, | ||||
| const ApplicationSettingsLoadResult &application_settings_result, | const ApplicationSettingsLoadResult &application_settings_result, | ||||
| bool user_runtime_mode, | |||||
| QWidget *parent) | QWidget *parent) | ||||
| : QMainWindow(parent), | : QMainWindow(parent), | ||||
| ui_(std::make_unique<Ui::MainWindow>()), | ui_(std::make_unique<Ui::MainWindow>()), | ||||
| @@ -315,6 +373,7 @@ MainWindow::MainWindow( | |||||
| register_monitor_service_(register_monitor_service), | register_monitor_service_(register_monitor_service), | ||||
| plc_discovery_gateway_(plc_discovery_gateway), | plc_discovery_gateway_(plc_discovery_gateway), | ||||
| application_settings_result_(application_settings_result), | application_settings_result_(application_settings_result), | ||||
| user_runtime_mode_(user_runtime_mode), | |||||
| hmi_navigation_service_(&hmi_navigation_service), | hmi_navigation_service_(&hmi_navigation_service), | ||||
| plc_configuration_(application_settings_result.settings.plcDefaults) | plc_configuration_(application_settings_result.settings.plcDefaults) | ||||
| { | { | ||||
| @@ -382,10 +441,112 @@ void MainWindow::initializeUi() | |||||
| QMessageBox::warning(this, tr("应用配置"), warning); | QMessageBox::warning(this, tr("应用配置"), warning); | ||||
| }); | }); | ||||
| } | } | ||||
| if (user_runtime_mode_) | |||||
| { | |||||
| initializeUserRuntime(); | |||||
| } | |||||
| } | |||||
| void MainWindow::initializeUserRuntime() | |||||
| { | |||||
| setWindowTitle(fromUtf8(project_service_.project().metadata.name)); | |||||
| runtime_panel_controller_->configureUserRuntimeMode( | |||||
| true, | |||||
| [this](int mode) | |||||
| { | |||||
| if (mode == static_cast<int>(ApplicationMode::OfflineRunning)) | |||||
| { | |||||
| requestUserRuntimeMode(ApplicationMode::OfflineRunning); | |||||
| } | |||||
| else if (mode == static_cast<int>(ApplicationMode::OnlineRunning)) | |||||
| { | |||||
| requestUserRuntimeMode(ApplicationMode::OnlineRunning); | |||||
| } | |||||
| }, | |||||
| [this] { connectPlc(); }, | |||||
| [this] { disconnectPlc(); }); | |||||
| menuBar()->setVisible(false); | |||||
| ui_->hmiToolBar->setVisible(false); | |||||
| ui_->logicToolBar->setVisible(false); | |||||
| ui_->outputDock->setVisible(false); | |||||
| ui_->dataMonitorDock->setVisible(false); | |||||
| ui_->projectDock->setVisible(false); | |||||
| ui_->propertiesDock->setVisible(false); | |||||
| QTimer::singleShot( | |||||
| 0, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| if (requestMode(ApplicationMode::OfflineRunning)) | |||||
| { | |||||
| hide(); | |||||
| } | |||||
| else | |||||
| { | |||||
| qApp->quit(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| void MainWindow::requestUserRuntimeMode(ApplicationMode requested_mode) | |||||
| { | |||||
| if (runtime_mode_service_.mode() == requested_mode) | |||||
| { | |||||
| return; | |||||
| } | |||||
| if (requested_mode == ApplicationMode::OnlineRunning | |||||
| && !runtime_mode_service_.initialPlcReadCompleted()) | |||||
| { | |||||
| // 用户运行版没有可见的编辑器主窗口,失败前不能先隐藏运行监控窗口 | |||||
| if (runtime_monitor_widget_ != nullptr) | |||||
| { | |||||
| runtime_monitor_widget_->setMode( | |||||
| runtime_mode_service_.mode(), | |||||
| runtime_mode_service_.plcConnectionState()); | |||||
| const QString message = transitionErrorText( | |||||
| ModeTransitionError::InitialPlcReadRequired) | |||||
| + tr("\n当前状态:%1").arg( | |||||
| plcStatusText(runtime_mode_service_)) | |||||
| + tr("\n请先点击“PLC 配置”连接并完成首次读取。"); | |||||
| QMessageBox::warning( | |||||
| runtime_monitor_widget_, | |||||
| tr("无法切换到真机运行"), | |||||
| message); | |||||
| } | |||||
| return; | |||||
| } | |||||
| if (runtime_mode_service_.mode() != ApplicationMode::Editing) | |||||
| { | |||||
| const ModeTransitionResult editing = runtime_mode_service_.enterEditing(); | |||||
| if (!editing.succeeded) | |||||
| { | |||||
| return; | |||||
| } | |||||
| updateModeUi(tr("正在切换运行模式")); | |||||
| QTimer::singleShot( | |||||
| 0, | |||||
| this, | |||||
| [this, requested_mode] | |||||
| { | |||||
| requestMode(requested_mode); | |||||
| }); | |||||
| return; | |||||
| } | |||||
| requestMode(requested_mode); | |||||
| } | } | ||||
| MainWindow::~MainWindow() | MainWindow::~MainWindow() | ||||
| { | { | ||||
| if (runtime_export_process_ != nullptr | |||||
| && runtime_export_process_->state() != QProcess::NotRunning) | |||||
| { | |||||
| runtime_export_process_->kill(); | |||||
| runtime_export_process_->waitForFinished(1000); | |||||
| } | |||||
| if (!runtime_export_temp_project_.isEmpty()) | |||||
| { | |||||
| QFile::remove(runtime_export_temp_project_); | |||||
| } | |||||
| qApp->removeEventFilter(this); | qApp->removeEventFilter(this); | ||||
| register_monitor_service_.setAddressesChangedCallback({}); | register_monitor_service_.setAddressesChangedCallback({}); | ||||
| runtime_mode_service_.setPlcStatusChangedCallback({}); | runtime_mode_service_.setPlcStatusChangedCallback({}); | ||||
| @@ -408,6 +569,16 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) | |||||
| void MainWindow::closeEvent(QCloseEvent *event) | void MainWindow::closeEvent(QCloseEvent *event) | ||||
| { | { | ||||
| if (runtime_export_process_ != nullptr | |||||
| && runtime_export_process_->state() != QProcess::NotRunning) | |||||
| { | |||||
| QMessageBox::information( | |||||
| this, | |||||
| tr("正在导出用户运行程序"), | |||||
| tr("导出尚未完成,请等待打包结束后再关闭编程器")); | |||||
| event->ignore(); | |||||
| return; | |||||
| } | |||||
| if (!confirmSaveBeforeDestructiveAction()) | if (!confirmSaveBeforeDestructiveAction()) | ||||
| { | { | ||||
| event->ignore(); | event->ignore(); | ||||
| @@ -449,6 +620,8 @@ void MainWindow::configureActions() | |||||
| connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); | connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); | ||||
| connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); | connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); | ||||
| connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject); | connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject); | ||||
| connect(ui_->exportRuntimeAction, &QAction::triggered, | |||||
| this, &MainWindow::exportRuntimeProgram); | |||||
| connect(ui_->undoAction, &QAction::triggered, | connect(ui_->undoAction, &QAction::triggered, | ||||
| this, &MainWindow::undoActiveEditor); | this, &MainWindow::undoActiveEditor); | ||||
| connect(ui_->redoAction, &QAction::triggered, | connect(ui_->redoAction, &QAction::triggered, | ||||
| @@ -869,7 +1042,14 @@ void MainWindow::configureRuntimeMonitor() | |||||
| }, | }, | ||||
| [this] | [this] | ||||
| { | { | ||||
| requestMode(ApplicationMode::Editing); | |||||
| if (user_runtime_mode_) | |||||
| { | |||||
| qApp->quit(); | |||||
| } | |||||
| else | |||||
| { | |||||
| requestMode(ApplicationMode::Editing); | |||||
| } | |||||
| }); | }); | ||||
| runtime_panel_controller_->configure(); | runtime_panel_controller_->configure(); | ||||
| runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | ||||
| @@ -1743,6 +1923,277 @@ void MainWindow::loadProject() | |||||
| showProjectResult(tr("加载工程"), tr("工程已加载"), true); | showProjectResult(tr("加载工程"), tr("工程已加载"), true); | ||||
| } | } | ||||
| void MainWindow::exportRuntimeProgram() | |||||
| { | |||||
| if (runtime_export_process_ != nullptr) | |||||
| { | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("已有一个导出任务正在执行,请等待完成"), | |||||
| false); | |||||
| return; | |||||
| } | |||||
| std::string validation_error; | |||||
| if (!project_service_.project().validateForRunning( | |||||
| project_service_.projectLimits(), &validation_error)) | |||||
| { | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("工程还不能运行:%1").arg(fromUtf8(validation_error)), | |||||
| false); | |||||
| return; | |||||
| } | |||||
| QString suggested_name = suggestedProjectFileName( | |||||
| fromUtf8(project_service_.project().metadata.name)); | |||||
| suggested_name.chop(QStringLiteral(".json").size()); | |||||
| const QString parent_directory = QFileDialog::getExistingDirectory( | |||||
| this, | |||||
| tr("选择用户运行程序的目标父目录"), | |||||
| QDir::homePath(), | |||||
| QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); | |||||
| if (parent_directory.isEmpty()) | |||||
| { | |||||
| return; | |||||
| } | |||||
| bool name_accepted = false; | |||||
| const QString requested_name = QInputDialog::getText( | |||||
| this, | |||||
| tr("导出用户运行程序"), | |||||
| tr("请输入导出文件夹名称"), | |||||
| QLineEdit::Normal, | |||||
| suggested_name, | |||||
| &name_accepted).trimmed(); | |||||
| if (!name_accepted || requested_name.isEmpty()) | |||||
| { | |||||
| return; | |||||
| } | |||||
| const QString output_name = suggestedProjectFileName(requested_name).chopped( | |||||
| QStringLiteral(".json").size()); | |||||
| const QString destination_directory = QDir(parent_directory).filePath(output_name); | |||||
| if (QFileInfo::exists(destination_directory)) | |||||
| { | |||||
| if (!QFileInfo(destination_directory).isDir()) | |||||
| { | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("导出目标已经存在且不是文件夹:%1").arg(destination_directory), | |||||
| false); | |||||
| return; | |||||
| } | |||||
| const QMessageBox::StandardButton answer = QMessageBox::question( | |||||
| this, | |||||
| tr("覆盖已有导出目录"), | |||||
| tr("文件夹“%1”已经存在,是否覆盖?").arg(destination_directory), | |||||
| QMessageBox::Yes | QMessageBox::No, | |||||
| QMessageBox::No); | |||||
| if (answer != QMessageBox::Yes) | |||||
| { | |||||
| return; | |||||
| } | |||||
| if (!QDir(destination_directory).removeRecursively()) | |||||
| { | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("无法清理已有导出目录:%1").arg(destination_directory), | |||||
| false); | |||||
| return; | |||||
| } | |||||
| } | |||||
| const QString temp_project = QDir::tempPath() | |||||
| + QStringLiteral("/qtproxinje-runtime-") | |||||
| + QString::number(QCoreApplication::applicationPid()) | |||||
| + QStringLiteral("-") | |||||
| + QString::number(QDateTime::currentMSecsSinceEpoch()) | |||||
| + QStringLiteral(".json"); | |||||
| const ProjectOperationResult save_result = project_service_.exportAs( | |||||
| temp_project.toUtf8().toStdString()); | |||||
| if (!save_result.succeeded) | |||||
| { | |||||
| showProjectResult(tr("导出用户运行程序"), fromUtf8(save_result.message), false); | |||||
| return; | |||||
| } | |||||
| QString script_path; | |||||
| QDir probe(QCoreApplication::applicationDirPath()); | |||||
| for (int depth = 0; depth < 5 && script_path.isEmpty(); ++depth) | |||||
| { | |||||
| const QString candidate = probe.filePath(QStringLiteral("scripts/package_qt_app.ps1")); | |||||
| if (QFileInfo::exists(candidate)) | |||||
| { | |||||
| script_path = QDir::toNativeSeparators(candidate); | |||||
| break; | |||||
| } | |||||
| if (!probe.cdUp()) | |||||
| { | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (script_path.isEmpty()) | |||||
| { | |||||
| QFile::remove(temp_project); | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("未找到打包脚本,请确认程序安装目录完整"), | |||||
| false); | |||||
| return; | |||||
| } | |||||
| runtime_export_temp_project_ = temp_project; | |||||
| runtime_export_output_name_ = output_name; | |||||
| runtime_export_destination_directory_ = destination_directory; | |||||
| runtime_export_package_directory_ = QDir(probe.filePath(QStringLiteral("build/package"))) | |||||
| .filePath(output_name); | |||||
| runtime_export_process_output_.clear(); | |||||
| runtime_export_completion_handled_ = false; | |||||
| runtime_export_progress_ = std::make_unique<QProgressDialog>( | |||||
| tr("正在准备导出用户运行程序…"), | |||||
| QString(), | |||||
| 0, | |||||
| 100, | |||||
| this); | |||||
| runtime_export_progress_->setWindowTitle(tr("导出用户运行程序")); | |||||
| runtime_export_progress_->setWindowModality(Qt::WindowModal); | |||||
| runtime_export_progress_->setAutoClose(false); | |||||
| runtime_export_progress_->setAutoReset(false); | |||||
| runtime_export_progress_->setMinimumDuration(0); | |||||
| runtime_export_progress_->setValue(5); | |||||
| runtime_export_progress_->show(); | |||||
| runtime_export_process_ = std::make_unique<QProcess>(this); | |||||
| runtime_export_process_->setProgram(QStringLiteral("pwsh")); | |||||
| runtime_export_process_->setArguments({ | |||||
| QStringLiteral("-NoLogo"), QStringLiteral("-NoProfile"), | |||||
| QStringLiteral("-File"), script_path, | |||||
| QStringLiteral("-OutputName"), output_name, | |||||
| QStringLiteral("-ProjectFile"), temp_project}); | |||||
| connect( | |||||
| runtime_export_process_.get(), | |||||
| &QProcess::readyReadStandardOutput, | |||||
| this, | |||||
| &MainWindow::handleRuntimeExportOutput); | |||||
| connect( | |||||
| runtime_export_process_.get(), | |||||
| &QProcess::readyReadStandardError, | |||||
| this, | |||||
| &MainWindow::handleRuntimeExportOutput); | |||||
| connect( | |||||
| runtime_export_process_.get(), | |||||
| qOverload<int, QProcess::ExitStatus>(&QProcess::finished), | |||||
| this, | |||||
| &MainWindow::handleRuntimeExportFinished); | |||||
| connect( | |||||
| runtime_export_process_.get(), | |||||
| &QProcess::errorOccurred, | |||||
| this, | |||||
| [this](QProcess::ProcessError error) | |||||
| { | |||||
| if (error == QProcess::FailedToStart) | |||||
| { | |||||
| handleRuntimeExportFinished(-1, QProcess::CrashExit); | |||||
| } | |||||
| }); | |||||
| ui_->exportRuntimeAction->setEnabled(false); | |||||
| statusBar()->showMessage(tr("正在构建用户运行程序,请稍候…"), 0); | |||||
| runtime_export_process_->start(); | |||||
| } | |||||
| void MainWindow::handleRuntimeExportOutput() | |||||
| { | |||||
| if (runtime_export_process_ == nullptr) | |||||
| { | |||||
| return; | |||||
| } | |||||
| runtime_export_process_output_ += QString::fromUtf8( | |||||
| runtime_export_process_->readAllStandardOutput()); | |||||
| runtime_export_process_output_ += QString::fromUtf8( | |||||
| runtime_export_process_->readAllStandardError()); | |||||
| if (runtime_export_progress_ == nullptr) | |||||
| { | |||||
| return; | |||||
| } | |||||
| if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: qmake"))) | |||||
| { | |||||
| runtime_export_progress_->setValue(20); | |||||
| runtime_export_progress_->setLabelText(tr("正在生成 Qt 构建文件…")); | |||||
| } | |||||
| if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: compile"))) | |||||
| { | |||||
| runtime_export_progress_->setValue(35); | |||||
| runtime_export_progress_->setLabelText(tr("正在编译用户运行程序,这一步可能需要较长时间…")); | |||||
| } | |||||
| if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: package"))) | |||||
| { | |||||
| runtime_export_progress_->setValue(85); | |||||
| runtime_export_progress_->setLabelText(tr("正在复制 Qt 运行库和平台插件…")); | |||||
| } | |||||
| if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: verify"))) | |||||
| { | |||||
| runtime_export_progress_->setValue(95); | |||||
| runtime_export_progress_->setLabelText(tr("正在校验导出结果…")); | |||||
| } | |||||
| } | |||||
| void MainWindow::handleRuntimeExportFinished( | |||||
| int exit_code, QProcess::ExitStatus exit_status) | |||||
| { | |||||
| if (runtime_export_completion_handled_) | |||||
| { | |||||
| return; | |||||
| } | |||||
| runtime_export_completion_handled_ = true; | |||||
| handleRuntimeExportOutput(); | |||||
| const QString temp_project = runtime_export_temp_project_; | |||||
| const QString output_name = runtime_export_output_name_; | |||||
| const QString destination_directory = runtime_export_destination_directory_; | |||||
| const QString package_directory = runtime_export_package_directory_; | |||||
| const QString process_output = runtime_export_process_output_.trimmed(); | |||||
| const bool process_succeeded = exit_status == QProcess::NormalExit && exit_code == 0; | |||||
| bool succeeded = process_succeeded; | |||||
| QString result_message; | |||||
| if (!succeeded) | |||||
| { | |||||
| result_message = process_output.isEmpty() | |||||
| ? tr("打包进程未能正常完成") | |||||
| : tr("打包失败:%1").arg(process_output); | |||||
| } | |||||
| else | |||||
| { | |||||
| runtime_export_progress_->setValue(98); | |||||
| runtime_export_progress_->setLabelText(tr("正在整理导出目录…")); | |||||
| QString copy_error; | |||||
| succeeded = copyDirectoryContents(package_directory, destination_directory, ©_error); | |||||
| result_message = succeeded | |||||
| ? tr("已导出:%1").arg(QDir(destination_directory).filePath(output_name + QStringLiteral(".exe"))) | |||||
| : copy_error; | |||||
| } | |||||
| QFile::remove(temp_project); | |||||
| if (runtime_export_progress_ != nullptr) | |||||
| { | |||||
| runtime_export_progress_->setValue(succeeded ? 100 : 0); | |||||
| runtime_export_progress_->close(); | |||||
| runtime_export_progress_.reset(); | |||||
| } | |||||
| runtime_export_process_.reset(); | |||||
| runtime_export_temp_project_.clear(); | |||||
| runtime_export_output_name_.clear(); | |||||
| runtime_export_destination_directory_.clear(); | |||||
| runtime_export_package_directory_.clear(); | |||||
| runtime_export_process_output_.clear(); | |||||
| ui_->exportRuntimeAction->setEnabled( | |||||
| runtime_mode_service_.policy().allowsProjectEditing); | |||||
| showProjectResult(tr("导出用户运行程序"), result_message, succeeded); | |||||
| } | |||||
| void MainWindow::showProjectResult( | void MainWindow::showProjectResult( | ||||
| const QString &action, const QString &message, bool succeeded) | const QString &action, const QString &message, bool succeeded) | ||||
| { | { | ||||
| @@ -1767,7 +2218,7 @@ void MainWindow::appendOutputMessage(const QString &message) | |||||
| ui_->outputList->scrollToBottom(); | ui_->outputList->scrollToBottom(); | ||||
| } | } | ||||
| void MainWindow::requestMode(ApplicationMode requested_mode) | |||||
| bool MainWindow::requestMode(ApplicationMode requested_mode) | |||||
| { | { | ||||
| // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 | // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 | ||||
| // UI 仅转发模式意图,合法性由服务层和领域状态机决定 | // UI 仅转发模式意图,合法性由服务层和领域状态机决定 | ||||
| @@ -1793,7 +2244,7 @@ void MainWindow::requestMode(ApplicationMode requested_mode) | |||||
| { | { | ||||
| restoreCurrentModeAction(); | restoreCurrentModeAction(); | ||||
| statusBar()->showMessage(tr("不支持的运行模式"), 5000); | statusBar()->showMessage(tr("不支持的运行模式"), 5000); | ||||
| return; | |||||
| return false; | |||||
| } | } | ||||
| } | } | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| @@ -1816,9 +2267,10 @@ void MainWindow::requestMode(ApplicationMode requested_mode) | |||||
| } | } | ||||
| } | } | ||||
| statusBar()->showMessage(message, 5000); | statusBar()->showMessage(message, 5000); | ||||
| return; | |||||
| return false; | |||||
| } | } | ||||
| updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode()))); | updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode()))); | ||||
| return true; | |||||
| } | } | ||||
| void MainWindow::updateModeUi(const QString &message) | void MainWindow::updateModeUi(const QString &message) | ||||
| @@ -1893,6 +2345,7 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->exportRuntimeAction->setEnabled(policy.allowsProjectEditing); | |||||
| updateProjectTreeActions(); | updateProjectTreeActions(); | ||||
| updateEditActions(); | updateEditActions(); | ||||
| @@ -15,6 +15,7 @@ | |||||
| #include "services/application_settings.h" | #include "services/application_settings.h" | ||||
| #include <QMainWindow> | #include <QMainWindow> | ||||
| #include <QProcess> | |||||
| #include <memory> | #include <memory> | ||||
| #include <variant> | #include <variant> | ||||
| @@ -28,6 +29,7 @@ class QEvent; | |||||
| class QLabel; | class QLabel; | ||||
| class QDockWidget; | class QDockWidget; | ||||
| class QTimer; | class QTimer; | ||||
| class QProgressDialog; | |||||
| namespace Ui { | namespace Ui { | ||||
| class MainWindow; | class MainWindow; | ||||
| @@ -84,6 +86,7 @@ public: | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| PlcDiscoveryGateway &plc_discovery_gateway, | PlcDiscoveryGateway &plc_discovery_gateway, | ||||
| const ApplicationSettingsLoadResult &application_settings_result, | const ApplicationSettingsLoadResult &application_settings_result, | ||||
| bool user_runtime_mode = false, | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** | /** | ||||
| * @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务 | * @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务 | ||||
| @@ -103,6 +106,7 @@ public: | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| PlcDiscoveryGateway &plc_discovery_gateway, | PlcDiscoveryGateway &plc_discovery_gateway, | ||||
| const ApplicationSettingsLoadResult &application_settings_result, | const ApplicationSettingsLoadResult &application_settings_result, | ||||
| bool user_runtime_mode = false, | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放窗口拥有的控制器和可选导航服务 */ | /** 释放窗口拥有的控制器和可选导航服务 */ | ||||
| @@ -210,6 +214,12 @@ private: | |||||
| void saveProjectAs(); | void saveProjectAs(); | ||||
| /** 从用户指定的路径加载工程 */ | /** 从用户指定的路径加载工程 */ | ||||
| void loadProject(); | void loadProject(); | ||||
| /** 将当前工程构建为用户可直接运行的专用 exe */ | |||||
| void exportRuntimeProgram(); | |||||
| /** 更新用户运行程序导出的阶段进度 */ | |||||
| void handleRuntimeExportOutput(); | |||||
| /** 完成用户运行程序导出并复制最终目录 */ | |||||
| void handleRuntimeExportFinished(int exit_code, QProcess::ExitStatus exit_status); | |||||
| /** 在状态栏和输出面板显示工程操作结果 */ | /** 在状态栏和输出面板显示工程操作结果 */ | ||||
| void showProjectResult(const QString &action, const QString &message, bool succeeded); | void showProjectResult(const QString &action, const QString &message, bool succeeded); | ||||
| /** 向输出面板追加一条消息 */ | /** 向输出面板追加一条消息 */ | ||||
| @@ -221,7 +231,7 @@ private: | |||||
| * | * | ||||
| * 失败时恢复与服务层当前状态一致的工具栏选项 | * 失败时恢复与服务层当前状态一致的工具栏选项 | ||||
| */ | */ | ||||
| void requestMode(ApplicationMode requested_mode); | |||||
| bool requestMode(ApplicationMode requested_mode); | |||||
| /** | /** | ||||
| * @brief 根据服务层模式策略更新编辑权限和状态反馈 | * @brief 根据服务层模式策略更新编辑权限和状态反馈 | ||||
| @@ -237,6 +247,10 @@ private: | |||||
| void restoreCurrentModeAction(); | void restoreCurrentModeAction(); | ||||
| /** 完成主窗口初始化和首次界面刷新 */ | /** 完成主窗口初始化和首次界面刷新 */ | ||||
| void initializeUi(); | void initializeUi(); | ||||
| /** 专用用户运行版启动后直接进入运行监控 */ | |||||
| void initializeUserRuntime(); | |||||
| /** 用户运行版允许在离线和真机之间自动经过编辑态切换 */ | |||||
| void requestUserRuntimeMode(ApplicationMode requested_mode); | |||||
| /** 刷新编辑态数据监控的模式、权限和当前值 */ | /** 刷新编辑态数据监控的模式、权限和当前值 */ | ||||
| void refreshDataMonitorUi(); | void refreshDataMonitorUi(); | ||||
| @@ -268,6 +282,8 @@ private: | |||||
| PlcDiscoveryGateway &plc_discovery_gateway_; | PlcDiscoveryGateway &plc_discovery_gateway_; | ||||
| /** 启动期配置和诊断结果,由应用入口保证生命周期 */ | /** 启动期配置和诊断结果,由应用入口保证生命周期 */ | ||||
| const ApplicationSettingsLoadResult &application_settings_result_; | const ApplicationSettingsLoadResult &application_settings_result_; | ||||
| /** 是否以专用用户运行版入口启动 */ | |||||
| bool user_runtime_mode_ = false; | |||||
| /** 当调用方未提供导航服务时由主窗口负责拥有的实例 */ | /** 当调用方未提供导航服务时由主窗口负责拥有的实例 */ | ||||
| std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | ||||
| /** 当前使用的 HMI 页面导航服务 */ | /** 当前使用的 HMI 页面导航服务 */ | ||||
| @@ -308,6 +324,15 @@ private: | |||||
| std::string current_logic_id_; | std::string current_logic_id_; | ||||
| /** 是否已经安排了待处理的 PLC 状态刷新 */ | /** 是否已经安排了待处理的 PLC 状态刷新 */ | ||||
| bool plc_status_update_pending_ = false; | bool plc_status_update_pending_ = false; | ||||
| /** 用户运行程序导出进程及其临时状态 */ | |||||
| std::unique_ptr<QProcess> runtime_export_process_; | |||||
| std::unique_ptr<QProgressDialog> runtime_export_progress_; | |||||
| QString runtime_export_temp_project_; | |||||
| QString runtime_export_output_name_; | |||||
| QString runtime_export_destination_directory_; | |||||
| QString runtime_export_package_directory_; | |||||
| QString runtime_export_process_output_; | |||||
| bool runtime_export_completion_handled_ = false; | |||||
| struct HmiClipboardData | struct HmiClipboardData | ||||
| { | { | ||||
| @@ -228,6 +228,7 @@ | |||||
| <addaction name="saveProjectAction"/> | <addaction name="saveProjectAction"/> | ||||
| <addaction name="saveAsProjectAction"/> | <addaction name="saveAsProjectAction"/> | ||||
| <addaction name="loadProjectAction"/> | <addaction name="loadProjectAction"/> | ||||
| <addaction name="exportRuntimeAction"/> | |||||
| <addaction name="separator"/> | <addaction name="separator"/> | ||||
| <addaction name="exitAction"/> | <addaction name="exitAction"/> | ||||
| </widget> | </widget> | ||||
| @@ -1003,6 +1004,10 @@ | |||||
| <string>Ctrl+O</string> | <string>Ctrl+O</string> | ||||
| </property> | </property> | ||||
| </action> | </action> | ||||
| <action name="exportRuntimeAction"> | |||||
| <property name="text"><string>导出用户运行程序</string></property> | |||||
| <property name="toolTip"><string>将当前工程导出为用户可直接运行的专用程序</string></property> | |||||
| </action> | |||||
| <action name="undoAction"> | <action name="undoAction"> | ||||
| <property name="text"><string>撤销</string></property> | <property name="text"><string>撤销</string></property> | ||||
| <property name="toolTip"><string>撤销当前编辑器的上一步操作</string></property> | <property name="toolTip"><string>撤销当前编辑器的上一步操作</string></property> | ||||
| @@ -11,6 +11,7 @@ | |||||
| #include <QSplitter> | #include <QSplitter> | ||||
| #include <QComboBox> | #include <QComboBox> | ||||
| #include <QPushButton> | |||||
| #include <QSignalBlocker> | #include <QSignalBlocker> | ||||
| #include <QToolButton> | #include <QToolButton> | ||||
| @@ -67,6 +68,9 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget")); | free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget")); | ||||
| ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | ||||
| ui_->logicNoticeLabel->setVisible(false); | ui_->logicNoticeLabel->setVisible(false); | ||||
| ui_->runtimeModeComboBox->setVisible(false); | |||||
| ui_->runtimePlcButton->setVisible(false); | |||||
| ui_->runtimeDisconnectPlcButton->setVisible(false); | |||||
| ui_->upperSplitter->setSizes({600, 600}); | ui_->upperSplitter->setSizes({600, 600}); | ||||
| ui_->verticalSplitter->setSizes({440, 260}); | ui_->verticalSplitter->setSizes({440, 260}); | ||||
| connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, | connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, | ||||
| @@ -95,6 +99,22 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| ui_->runtimeLogicComboBox->itemData(index).toString())); | ui_->runtimeLogicComboBox->itemData(index).toString())); | ||||
| } | } | ||||
| }); | }); | ||||
| connect(ui_->runtimeModeComboBox, | |||||
| QOverload<int>::of(&QComboBox::currentIndexChanged), | |||||
| this, | |||||
| [this](int index) | |||||
| { | |||||
| if (user_runtime_mode_ && index >= 0) | |||||
| { | |||||
| emit modeChangeRequested(index == 0 | |||||
| ? static_cast<int>(ApplicationMode::OfflineRunning) | |||||
| : static_cast<int>(ApplicationMode::OnlineRunning)); | |||||
| } | |||||
| }); | |||||
| connect(ui_->runtimePlcButton, &QPushButton::clicked, | |||||
| this, &RuntimeMonitorWidget::plcConfigurationRequested); | |||||
| connect(ui_->runtimeDisconnectPlcButton, &QPushButton::clicked, | |||||
| this, &RuntimeMonitorWidget::plcDisconnectRequested); | |||||
| } | } | ||||
| RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; | RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; | ||||
| @@ -157,9 +177,36 @@ void RuntimeMonitorWidget::setMode( | |||||
| offline ? tr("离线仿真 · 虚拟 M/D") | offline ? tr("离线仿真 · 虚拟 M/D") | ||||
| : online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹") | : online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹") | ||||
| : tr("当前未运行")); | : tr("当前未运行")); | ||||
| if (user_runtime_mode_) | |||||
| { | |||||
| const QSignalBlocker blocker(ui_->runtimeModeComboBox); | |||||
| ui_->runtimeModeComboBox->setCurrentIndex( | |||||
| offline ? 0 : online ? 1 : -1); | |||||
| const bool plc_configurable = plc_state == PlcConnectionState::Disconnected | |||||
| || plc_state == PlcConnectionState::Faulted; | |||||
| ui_->runtimePlcButton->setEnabled(plc_configurable); | |||||
| ui_->runtimeDisconnectPlcButton->setEnabled( | |||||
| plc_state != PlcConnectionState::Disconnected); | |||||
| } | |||||
| free_monitor_widget_->refreshValues(mode, plc_state); | free_monitor_widget_->refreshValues(mode, plc_state); | ||||
| } | } | ||||
| void RuntimeMonitorWidget::setUserRuntimeMode(bool enabled) | |||||
| { | |||||
| user_runtime_mode_ = enabled; | |||||
| ui_->runtimeModeComboBox->setVisible(enabled); | |||||
| ui_->runtimePlcButton->setVisible(enabled); | |||||
| ui_->runtimeDisconnectPlcButton->setVisible(enabled); | |||||
| ui_->exitRuntimeButton->setToolTip( | |||||
| enabled ? tr("退出运行程序") : tr("返回编辑态")); | |||||
| ui_->exitRuntimeButton->setAccessibleName( | |||||
| enabled ? tr("退出运行程序") : tr("返回编辑态")); | |||||
| if (enabled) | |||||
| { | |||||
| setMode(ApplicationMode::Editing, PlcConnectionState::Disconnected); | |||||
| } | |||||
| } | |||||
| void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) | void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) | ||||
| { | { | ||||
| hmi_view_->setRuntimeWriteEnabled(enabled); | hmi_view_->setRuntimeWriteEnabled(enabled); | ||||
| @@ -57,6 +57,8 @@ public: | |||||
| void setRuntimePage(const std::string &page_id); | void setRuntimePage(const std::string &page_id); | ||||
| /** 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 */ | /** 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 */ | ||||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | void setMode(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| /** 切换专用用户运行版的简化操作栏 */ | |||||
| void setUserRuntimeMode(bool enabled); | |||||
| /** 从寄存器仓库刷新 HMI 和自由监控中的当前值 */ | /** 从寄存器仓库刷新 HMI 和自由监控中的当前值 */ | ||||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| /** 设置 HMI 控件是否允许写入运行值 */ | /** 设置 HMI 控件是否允许写入运行值 */ | ||||
| @@ -75,6 +77,12 @@ signals: | |||||
| void navigationFailed(const QString &message); | void navigationFailed(const QString &message); | ||||
| /** 用户请求退出运行态 */ | /** 用户请求退出运行态 */ | ||||
| void exitRequested(); | void exitRequested(); | ||||
| /** 用户在专用运行版中请求切换模式 */ | |||||
| void modeChangeRequested(int mode); | |||||
| /** 用户请求打开 PLC 配置窗口 */ | |||||
| void plcConfigurationRequested(); | |||||
| /** 用户请求断开 PLC */ | |||||
| void plcDisconnectRequested(); | |||||
| private: | private: | ||||
| /** 显示指定的运行监控页面 */ | /** 显示指定的运行监控页面 */ | ||||
| @@ -102,4 +110,6 @@ private: | |||||
| std::string latest_fault_node_id_; | std::string latest_fault_node_id_; | ||||
| /** 是否已经收到过可显示的逻辑轨迹 */ | /** 是否已经收到过可显示的逻辑轨迹 */ | ||||
| bool has_logic_trace_ = false; | bool has_logic_trace_ = false; | ||||
| /** 是否显示专用用户运行版操作栏 */ | |||||
| bool user_runtime_mode_ = false; | |||||
| }; | }; | ||||
| @@ -13,6 +13,9 @@ | |||||
| <item><widget class="QLabel" name="titleLabel"><property name="text"><string>运行监控</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item> | <item><widget class="QLabel" name="titleLabel"><property name="text"><string>运行监控</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item> | ||||
| <item><spacer name="headerSpacer"><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><spacer name="headerSpacer"><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="QLabel" name="modeLabel"><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | <item><widget class="QLabel" name="modeLabel"><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | ||||
| <item><widget class="QComboBox" name="runtimeModeComboBox"><property name="minimumSize"><size><width>120</width><height>0</height></size></property><item><property name="text"><string>离线仿真</string></property></item><item><property name="text"><string>真机运行</string></property></item></widget></item> | |||||
| <item><widget class="QPushButton" name="runtimePlcButton"><property name="text"><string>PLC 配置</string></property></widget></item> | |||||
| <item><widget class="QPushButton" name="runtimeDisconnectPlcButton"><property name="text"><string>断开 PLC</string></property></widget></item> | |||||
| <item><widget class="QToolButton" name="exitRuntimeButton"><property name="toolTip"><string>返回编辑态</string></property><property name="accessibleName"><string>返回编辑态</string></property><property name="text"><string>返回编辑态</string></property><property name="toolButtonStyle"><enum>Qt::ToolButtonIconOnly</enum></property></widget></item> | <item><widget class="QToolButton" name="exitRuntimeButton"><property name="toolTip"><string>返回编辑态</string></property><property name="accessibleName"><string>返回编辑态</string></property><property name="text"><string>返回编辑态</string></property><property name="toolButtonStyle"><enum>Qt::ToolButtonIconOnly</enum></property></widget></item> | ||||
| </layout> | </layout> | ||||
| </item> | </item> | ||||
| @@ -121,6 +121,36 @@ void RuntimePanelController::configure() | |||||
| status_reporter_(message, 5000); | status_reporter_(message, 5000); | ||||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | ||||
| }); | }); | ||||
| QObject::connect(runtime_monitor_widget_, | |||||
| &RuntimeMonitorWidget::modeChangeRequested, | |||||
| &parent_, | |||||
| [this](int mode) | |||||
| { | |||||
| if (mode_requester_) | |||||
| { | |||||
| mode_requester_(mode); | |||||
| } | |||||
| }); | |||||
| QObject::connect(runtime_monitor_widget_, | |||||
| &RuntimeMonitorWidget::plcConfigurationRequested, | |||||
| &parent_, | |||||
| [this] | |||||
| { | |||||
| if (plc_configuration_requester_) | |||||
| { | |||||
| plc_configuration_requester_(); | |||||
| } | |||||
| }); | |||||
| QObject::connect(runtime_monitor_widget_, | |||||
| &RuntimeMonitorWidget::plcDisconnectRequested, | |||||
| &parent_, | |||||
| [this] | |||||
| { | |||||
| if (plc_disconnect_requester_) | |||||
| { | |||||
| plc_disconnect_requester_(); | |||||
| } | |||||
| }); | |||||
| runtime_refresh_timer_ = new QTimer(&parent_); | runtime_refresh_timer_ = new QTimer(&parent_); | ||||
| runtime_refresh_timer_->setInterval(150); | runtime_refresh_timer_->setInterval(150); | ||||
| QObject::connect(runtime_refresh_timer_, &QTimer::timeout, | QObject::connect(runtime_refresh_timer_, &QTimer::timeout, | ||||
| @@ -143,6 +173,21 @@ void RuntimePanelController::configure() | |||||
| Qt::QueuedConnection); | Qt::QueuedConnection); | ||||
| } | } | ||||
| void RuntimePanelController::configureUserRuntimeMode( | |||||
| bool enabled, | |||||
| std::function<void(int)> mode_requester, | |||||
| std::function<void()> plc_configuration_requester, | |||||
| std::function<void()> plc_disconnect_requester) | |||||
| { | |||||
| mode_requester_ = std::move(mode_requester); | |||||
| plc_configuration_requester_ = std::move(plc_configuration_requester); | |||||
| plc_disconnect_requester_ = std::move(plc_disconnect_requester); | |||||
| if (runtime_monitor_widget_ != nullptr) | |||||
| { | |||||
| runtime_monitor_widget_->setUserRuntimeMode(enabled); | |||||
| } | |||||
| } | |||||
| RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const | RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const | ||||
| { | { | ||||
| return runtime_monitor_widget_; | return runtime_monitor_widget_; | ||||
| @@ -162,6 +207,8 @@ void RuntimePanelController::enterRuntime( | |||||
| } | } | ||||
| runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses(); | runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses(); | ||||
| runtime_monitor_widget_->setMode(mode, plc_state); | runtime_monitor_widget_->setMode(mode, plc_state); | ||||
| runtime_monitor_window_->setWindowTitle( | |||||
| fromUtf8(project_service_.project().metadata.name)); | |||||
| runtime_monitor_window_->showForRuntime(); | runtime_monitor_window_->showForRuntime(); | ||||
| } | } | ||||
| @@ -70,6 +70,12 @@ public: | |||||
| /** 连接运行监控相关信号并完成初始配置 */ | /** 连接运行监控相关信号并完成初始配置 */ | ||||
| void configure(); | void configure(); | ||||
| /** 配置专用用户运行版的精简控制栏行为 */ | |||||
| void configureUserRuntimeMode( | |||||
| bool enabled, | |||||
| std::function<void(int)> mode_requester, | |||||
| std::function<void()> plc_configuration_requester, | |||||
| std::function<void()> plc_disconnect_requester); | |||||
| /** 创建或复用唯一监控窗口,并把当前工程投影到运行态 */ | /** 创建或复用唯一监控窗口,并把当前工程投影到运行态 */ | ||||
| void enterRuntime( | void enterRuntime( | ||||
| const std::string &page_id, | const std::string &page_id, | ||||
| @@ -137,4 +143,7 @@ private: | |||||
| QTimer *runtime_refresh_timer_ = nullptr; | QTimer *runtime_refresh_timer_ = nullptr; | ||||
| /** 当前是否已经进入运行监控会话 */ | /** 当前是否已经进入运行监控会话 */ | ||||
| bool runtime_session_active_ = false; | bool runtime_session_active_ = false; | ||||
| std::function<void(int)> mode_requester_; | |||||
| std::function<void()> plc_configuration_requester_; | |||||
| std::function<void()> plc_disconnect_requester_; | |||||
| }; | }; | ||||
| @@ -1,6 +1,7 @@ | |||||
| [CmdletBinding()] | [CmdletBinding()] | ||||
| param( | param( | ||||
| [string]$OutputName = 'integrated_platform-win64' | |||||
| [string]$OutputName = 'integrated_platform-win64', | |||||
| [string]$ProjectFile = '' | |||||
| ) | ) | ||||
| Set-StrictMode -Version Latest | Set-StrictMode -Version Latest | ||||
| @@ -14,12 +15,13 @@ $makePath = Join-Path $mingwBin 'mingw32-make.exe' | |||||
| $pluginDirectory = Join-Path $qtRoot 'plugins' | $pluginDirectory = Join-Path $qtRoot 'plugins' | ||||
| $platformPlugin = Join-Path $pluginDirectory 'platforms\qwindows.dll' | $platformPlugin = Join-Path $pluginDirectory 'platforms\qwindows.dll' | ||||
| $stylePlugin = Join-Path $pluginDirectory 'styles\qwindowsvistastyle.dll' | $stylePlugin = Join-Path $pluginDirectory 'styles\qwindowsvistastyle.dll' | ||||
| $projectFile = Join-Path $projectRoot 'app\integrated_platform.pro' | |||||
| $buildDirectory = Join-Path $projectRoot 'build\package-build\release' | |||||
| $qtProjectFile = Join-Path $projectRoot 'app\integrated_platform.pro' | |||||
| $runtimeBuildRoot = Join-Path $projectRoot 'build\runtime-export' | |||||
| $buildDirectory = Join-Path $runtimeBuildRoot ([guid]::NewGuid().ToString('N')) | |||||
| $executablePath = Join-Path $buildDirectory 'release\integrated_platform.exe' | $executablePath = Join-Path $buildDirectory 'release\integrated_platform.exe' | ||||
| $packageRoot = Join-Path $projectRoot 'build\package' | $packageRoot = Join-Path $projectRoot 'build\package' | ||||
| foreach ($requiredPath in @($qmakePath, $makePath, $platformPlugin, $stylePlugin, $projectFile)) { | |||||
| foreach ($requiredPath in @($qmakePath, $makePath, $platformPlugin, $stylePlugin, $qtProjectFile)) { | |||||
| if (-not (Test-Path -LiteralPath $requiredPath)) { | if (-not (Test-Path -LiteralPath $requiredPath)) { | ||||
| throw "未找到必要文件:$requiredPath" | throw "未找到必要文件:$requiredPath" | ||||
| } | } | ||||
| @@ -32,6 +34,13 @@ if ([string]::IsNullOrWhiteSpace($OutputName) -or | |||||
| throw "输出名称不是有效的 Windows 文件名:$OutputName" | throw "输出名称不是有效的 Windows 文件名:$OutputName" | ||||
| } | } | ||||
| if (-not [string]::IsNullOrWhiteSpace($ProjectFile)) { | |||||
| $resolvedProjectFile = [System.IO.Path]::GetFullPath($ProjectFile) | |||||
| if (-not (Test-Path -LiteralPath $resolvedProjectFile -PathType Leaf)) { | |||||
| throw "未找到运行版工程文件:$resolvedProjectFile" | |||||
| } | |||||
| } | |||||
| $packageDirectory = Join-Path $packageRoot $OutputName | $packageDirectory = Join-Path $packageRoot $OutputName | ||||
| $archivePath = Join-Path $packageRoot "$OutputName.zip" | $archivePath = Join-Path $packageRoot "$OutputName.zip" | ||||
| $resolvedProjectRoot = [System.IO.Path]::GetFullPath($projectRoot).TrimEnd('\') | $resolvedProjectRoot = [System.IO.Path]::GetFullPath($projectRoot).TrimEnd('\') | ||||
| @@ -45,17 +54,39 @@ if (-not $resolvedPackageRoot.StartsWith("$resolvedProjectRoot\build\", [System. | |||||
| New-Item -ItemType Directory -Path $buildDirectory -Force | Out-Null | New-Item -ItemType Directory -Path $buildDirectory -Force | Out-Null | ||||
| New-Item -ItemType Directory -Path $packageRoot -Force | Out-Null | New-Item -ItemType Directory -Path $packageRoot -Force | Out-Null | ||||
| $runtimeResourceFile = $null | |||||
| if (-not [string]::IsNullOrWhiteSpace($ProjectFile)) { | |||||
| $runtimeResourceFile = Join-Path $buildDirectory 'runtime_project.qrc' | |||||
| $escapedProjectPath = [System.Security.SecurityElement]::Escape($resolvedProjectFile) | |||||
| @( | |||||
| '<?xml version="1.0" encoding="UTF-8"?>' | |||||
| '<RCC><qresource prefix="/">' | |||||
| "<file alias=`"runtime-project.json`">$escapedProjectPath</file>" | |||||
| '</qresource></RCC>' | |||||
| ) | Set-Content -LiteralPath $runtimeResourceFile -Encoding UTF8 | |||||
| $resourceText = Get-Content -LiteralPath $runtimeResourceFile -Raw -Encoding UTF8 | |||||
| if (-not $resourceText.Contains($escapedProjectPath)) { | |||||
| throw "运行版资源未正确指向工程文件:$resolvedProjectFile" | |||||
| } | |||||
| } | |||||
| $originalPath = $env:Path | $originalPath = $env:Path | ||||
| $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | ||||
| try { | try { | ||||
| Push-Location $buildDirectory | Push-Location $buildDirectory | ||||
| try { | try { | ||||
| & $qmakePath $projectFile 'CONFIG+=release' 'CONFIG-=debug' | |||||
| Write-Host 'RUNTIME_EXPORT_STAGE: qmake' | |||||
| $qmakeArguments = @($qtProjectFile, 'CONFIG+=release', 'CONFIG-=debug') | |||||
| if ($null -ne $runtimeResourceFile) { | |||||
| $qmakeArguments += "RUNTIME_PROJECT_RESOURCE=$($runtimeResourceFile.Replace('\', '/'))" | |||||
| } | |||||
| & $qmakePath @qmakeArguments | |||||
| if ($LASTEXITCODE -ne 0) { | if ($LASTEXITCODE -ne 0) { | ||||
| throw "qmake 执行失败,退出码:$LASTEXITCODE" | throw "qmake 执行失败,退出码:$LASTEXITCODE" | ||||
| } | } | ||||
| Write-Host 'RUNTIME_EXPORT_STAGE: compile' | |||||
| & $makePath -j2 | & $makePath -j2 | ||||
| if ($LASTEXITCODE -ne 0) { | if ($LASTEXITCODE -ne 0) { | ||||
| throw "mingw32-make 执行失败,退出码:$LASTEXITCODE" | throw "mingw32-make 执行失败,退出码:$LASTEXITCODE" | ||||
| @@ -69,6 +100,17 @@ try { | |||||
| throw "未生成可执行文件:$executablePath" | throw "未生成可执行文件:$executablePath" | ||||
| } | } | ||||
| if ($null -ne $runtimeResourceFile) { | |||||
| $generatedResourceSource = Join-Path $buildDirectory 'release\qrc_runtime_project.cpp' | |||||
| if (-not (Test-Path -LiteralPath $generatedResourceSource)) { | |||||
| throw "未生成运行版工程资源:$generatedResourceSource" | |||||
| } | |||||
| $generatedResourceText = Get-Content -LiteralPath $generatedResourceSource -Raw -Encoding UTF8 | |||||
| if ($generatedResourceText -notlike '*runtime-project.json*') { | |||||
| throw '编译结果中缺少 runtime-project.json 资源' | |||||
| } | |||||
| } | |||||
| if (Test-Path -LiteralPath $packageDirectory) { | if (Test-Path -LiteralPath $packageDirectory) { | ||||
| Remove-Item -LiteralPath $packageDirectory -Recurse -Force | Remove-Item -LiteralPath $packageDirectory -Recurse -Force | ||||
| } | } | ||||
| @@ -76,10 +118,15 @@ try { | |||||
| Remove-Item -LiteralPath $archivePath -Force | Remove-Item -LiteralPath $archivePath -Force | ||||
| } | } | ||||
| Write-Host 'RUNTIME_EXPORT_STAGE: package' | |||||
| New-Item -ItemType Directory -Path $packageDirectory | Out-Null | New-Item -ItemType Directory -Path $packageDirectory | Out-Null | ||||
| Copy-Item -LiteralPath $executablePath -Destination $packageDirectory | |||||
| $packagedExecutable = Join-Path $packageDirectory 'integrated_platform.exe' | |||||
| $packagedExecutableName = if ([string]::IsNullOrWhiteSpace($ProjectFile)) { | |||||
| 'integrated_platform.exe' | |||||
| } else { | |||||
| "$OutputName.exe" | |||||
| } | |||||
| $packagedExecutable = Join-Path $packageDirectory $packagedExecutableName | |||||
| Copy-Item -LiteralPath $executablePath -Destination $packagedExecutable | |||||
| $runtimeFiles = @( | $runtimeFiles = @( | ||||
| 'Qt5Core.dll', 'Qt5Gui.dll', 'Qt5Network.dll', | 'Qt5Core.dll', 'Qt5Gui.dll', 'Qt5Network.dll', | ||||
| 'Qt5SerialBus.dll', 'Qt5SerialPort.dll', 'Qt5Widgets.dll', | 'Qt5SerialBus.dll', 'Qt5SerialPort.dll', 'Qt5Widgets.dll', | ||||
| @@ -100,6 +147,7 @@ try { | |||||
| Copy-Item -LiteralPath $platformPlugin -Destination $packagedPlatformDirectory | Copy-Item -LiteralPath $platformPlugin -Destination $packagedPlatformDirectory | ||||
| Copy-Item -LiteralPath $stylePlugin -Destination $packagedStyleDirectory | Copy-Item -LiteralPath $stylePlugin -Destination $packagedStyleDirectory | ||||
| Write-Host 'RUNTIME_EXPORT_STAGE: verify' | |||||
| foreach ($requiredOutput in @( | foreach ($requiredOutput in @( | ||||
| $packagedExecutable, | $packagedExecutable, | ||||
| (Join-Path $packageDirectory 'Qt5Core.dll'), | (Join-Path $packageDirectory 'Qt5Core.dll'), | ||||
| @@ -111,11 +159,15 @@ try { | |||||
| } | } | ||||
| } | } | ||||
| Compress-Archive -LiteralPath $packageDirectory -DestinationPath $archivePath -CompressionLevel Optimal | |||||
| if ([string]::IsNullOrWhiteSpace($ProjectFile)) { | |||||
| Compress-Archive -LiteralPath $packageDirectory -DestinationPath $archivePath -CompressionLevel Optimal | |||||
| } | |||||
| Write-Host "打包完成" | Write-Host "打包完成" | ||||
| Write-Host "运行目录:$packageDirectory" | Write-Host "运行目录:$packageDirectory" | ||||
| Write-Host "压缩包:$archivePath" | |||||
| if ([string]::IsNullOrWhiteSpace($ProjectFile)) { | |||||
| Write-Host "压缩包:$archivePath" | |||||
| } | |||||
| } | } | ||||
| finally { | finally { | ||||
| $env:Path = $originalPath | $env:Path = $originalPath | ||||