| @@ -126,3 +126,8 @@ FORMS += \ | |||
| src/ui/free_monitor_widget.ui \ | |||
| src/ui/runtime_monitor_window.ui \ | |||
| src/ui/runtime_monitor_widget.ui | |||
| # 专用用户运行版打包时由脚本传入生成的工程资源;普通编程器构建不包含工程 | |||
| !isEmpty(RUNTIME_PROJECT_RESOURCE) { | |||
| RESOURCES += $$RUNTIME_PROJECT_RESOURCE | |||
| } | |||
| @@ -7,6 +7,10 @@ | |||
| */ | |||
| #include <QApplication> | |||
| #include <QFile> | |||
| #include <QFileInfo> | |||
| #include <QMessageBox> | |||
| #include <QDir> | |||
| #include "infrastructure/json_project_storage.h" | |||
| #include "infrastructure/application_settings_loader.h" | |||
| @@ -41,6 +45,27 @@ int main(int argc, char *argv[]) | |||
| // 设置组织名称,用来区分这个程序保存的 Qt 配置 | |||
| 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 = | |||
| ApplicationSettingsLoader::load( | |||
| ApplicationSettingsLoader::defaultFilePath()); | |||
| @@ -50,6 +75,48 @@ int main(int argc, char *argv[]) | |||
| // 统一管理新建、打开、保存和校验工程,实际读写文件交给上面的对象 | |||
| ProjectService project_service( | |||
| 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 页面和控件的添加、修改、删除、撤销与重做 | |||
| HmiEditorService hmi_editor_service( | |||
| project_service, application_settings.settings.hmiDefaults); | |||
| @@ -111,9 +178,12 @@ int main(int argc, char *argv[]) | |||
| register_comment_service, | |||
| register_monitor_service, | |||
| plc_discovery_service, | |||
| application_settings); | |||
| // 把主窗口显示到屏幕上 | |||
| main_window.show(); | |||
| application_settings, | |||
| user_runtime_mode); | |||
| if (!user_runtime_mode) | |||
| { | |||
| main_window.show(); | |||
| } | |||
| // 启动 Qt 消息循环,持续响应鼠标、键盘、定时器和串口事件,窗口关闭后才返回 | |||
| return application.exec(); | |||
| @@ -411,9 +411,7 @@ const std::vector<LogicCommandSuggestion> &LogicCommandService::suggestions() | |||
| {"RST", "复位线圈", "M 地址"}, | |||
| {"MOV", "数据传送", "D 地址 -> D 地址"}, | |||
| {"ADD", "加法", "D 地址 + D 地址 -> D 地址"}, | |||
| {"SUB", "减法", "D 地址 - D 地址 -> D 地址"}, | |||
| {"TON", "暂不支持", "T 地址", false}, | |||
| {"CTU", "暂不支持", "C 地址", false}}; | |||
| {"SUB", "减法", "D 地址 - D 地址 -> D 地址"}}; | |||
| return values; | |||
| } | |||
| @@ -137,6 +137,31 @@ ProjectOperationResult ProjectService::saveAs(const std::string &file_path) | |||
| 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) | |||
| { | |||
| if (isBlank(file_path)) | |||
| @@ -63,6 +63,8 @@ public: | |||
| * @return 保存操作结果 | |||
| */ | |||
| ProjectOperationResult saveAs(const std::string &file_path); | |||
| /** 将当前工程导出到指定 JSON,不改变当前工程文件关联 */ | |||
| ProjectOperationResult exportAs(const std::string &file_path) const; | |||
| /** | |||
| * @brief 从指定文件加载工程 | |||
| @@ -1922,17 +1922,6 @@ void LogicEditorWidget::beginCommandInput( | |||
| command_editor_->raise(); | |||
| command_editor_->setFocus(Qt::MouseFocusReason); | |||
| 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() | |||
| @@ -51,6 +51,11 @@ | |||
| #include <QToolButton> | |||
| #include <QTextEdit> | |||
| #include <QTimer> | |||
| #include <QProcess> | |||
| #include <QDir> | |||
| #include <QFileInfo> | |||
| #include <QProgressDialog> | |||
| #include <QDateTime> | |||
| #include <algorithm> | |||
| @@ -252,6 +257,56 @@ QToolButton *addToolbarMenu( | |||
| 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 | |||
| MainWindow::MainWindow( | |||
| @@ -266,6 +321,7 @@ MainWindow::MainWindow( | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| PlcDiscoveryGateway &plc_discovery_gateway, | |||
| const ApplicationSettingsLoadResult &application_settings_result, | |||
| bool user_runtime_mode, | |||
| QWidget *parent) | |||
| : QMainWindow(parent), | |||
| ui_(std::make_unique<Ui::MainWindow>()), | |||
| @@ -280,6 +336,7 @@ MainWindow::MainWindow( | |||
| register_monitor_service_(register_monitor_service), | |||
| plc_discovery_gateway_(plc_discovery_gateway), | |||
| application_settings_result_(application_settings_result), | |||
| user_runtime_mode_(user_runtime_mode), | |||
| owned_hmi_navigation_service_( | |||
| std::make_unique<HmiNavigationService>(project_service)), | |||
| hmi_navigation_service_(owned_hmi_navigation_service_.get()), | |||
| @@ -301,6 +358,7 @@ MainWindow::MainWindow( | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| PlcDiscoveryGateway &plc_discovery_gateway, | |||
| const ApplicationSettingsLoadResult &application_settings_result, | |||
| bool user_runtime_mode, | |||
| QWidget *parent) | |||
| : QMainWindow(parent), | |||
| ui_(std::make_unique<Ui::MainWindow>()), | |||
| @@ -315,6 +373,7 @@ MainWindow::MainWindow( | |||
| register_monitor_service_(register_monitor_service), | |||
| plc_discovery_gateway_(plc_discovery_gateway), | |||
| application_settings_result_(application_settings_result), | |||
| user_runtime_mode_(user_runtime_mode), | |||
| hmi_navigation_service_(&hmi_navigation_service), | |||
| plc_configuration_(application_settings_result.settings.plcDefaults) | |||
| { | |||
| @@ -382,10 +441,112 @@ void MainWindow::initializeUi() | |||
| 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() | |||
| { | |||
| 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); | |||
| register_monitor_service_.setAddressesChangedCallback({}); | |||
| runtime_mode_service_.setPlcStatusChangedCallback({}); | |||
| @@ -408,6 +569,16 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *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()) | |||
| { | |||
| event->ignore(); | |||
| @@ -449,6 +620,8 @@ void MainWindow::configureActions() | |||
| connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); | |||
| connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); | |||
| connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject); | |||
| connect(ui_->exportRuntimeAction, &QAction::triggered, | |||
| this, &MainWindow::exportRuntimeProgram); | |||
| connect(ui_->undoAction, &QAction::triggered, | |||
| this, &MainWindow::undoActiveEditor); | |||
| connect(ui_->redoAction, &QAction::triggered, | |||
| @@ -869,7 +1042,14 @@ void MainWindow::configureRuntimeMonitor() | |||
| }, | |||
| [this] | |||
| { | |||
| requestMode(ApplicationMode::Editing); | |||
| if (user_runtime_mode_) | |||
| { | |||
| qApp->quit(); | |||
| } | |||
| else | |||
| { | |||
| requestMode(ApplicationMode::Editing); | |||
| } | |||
| }); | |||
| runtime_panel_controller_->configure(); | |||
| runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | |||
| @@ -1743,6 +1923,277 @@ void MainWindow::loadProject() | |||
| 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( | |||
| const QString &action, const QString &message, bool succeeded) | |||
| { | |||
| @@ -1767,7 +2218,7 @@ void MainWindow::appendOutputMessage(const QString &message) | |||
| ui_->outputList->scrollToBottom(); | |||
| } | |||
| void MainWindow::requestMode(ApplicationMode requested_mode) | |||
| bool MainWindow::requestMode(ApplicationMode requested_mode) | |||
| { | |||
| // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 | |||
| // UI 仅转发模式意图,合法性由服务层和领域状态机决定 | |||
| @@ -1793,7 +2244,7 @@ void MainWindow::requestMode(ApplicationMode requested_mode) | |||
| { | |||
| restoreCurrentModeAction(); | |||
| statusBar()->showMessage(tr("不支持的运行模式"), 5000); | |||
| return; | |||
| return false; | |||
| } | |||
| } | |||
| if (!result.succeeded) | |||
| @@ -1816,9 +2267,10 @@ void MainWindow::requestMode(ApplicationMode requested_mode) | |||
| } | |||
| } | |||
| statusBar()->showMessage(message, 5000); | |||
| return; | |||
| return false; | |||
| } | |||
| updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode()))); | |||
| return true; | |||
| } | |||
| void MainWindow::updateModeUi(const QString &message) | |||
| @@ -1893,6 +2345,7 @@ void MainWindow::updateModeUi(const QString &message) | |||
| ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->exportRuntimeAction->setEnabled(policy.allowsProjectEditing); | |||
| updateProjectTreeActions(); | |||
| updateEditActions(); | |||
| @@ -15,6 +15,7 @@ | |||
| #include "services/application_settings.h" | |||
| #include <QMainWindow> | |||
| #include <QProcess> | |||
| #include <memory> | |||
| #include <variant> | |||
| @@ -28,6 +29,7 @@ class QEvent; | |||
| class QLabel; | |||
| class QDockWidget; | |||
| class QTimer; | |||
| class QProgressDialog; | |||
| namespace Ui { | |||
| class MainWindow; | |||
| @@ -84,6 +86,7 @@ public: | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| PlcDiscoveryGateway &plc_discovery_gateway, | |||
| const ApplicationSettingsLoadResult &application_settings_result, | |||
| bool user_runtime_mode = false, | |||
| QWidget *parent = nullptr); | |||
| /** | |||
| * @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务 | |||
| @@ -103,6 +106,7 @@ public: | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| PlcDiscoveryGateway &plc_discovery_gateway, | |||
| const ApplicationSettingsLoadResult &application_settings_result, | |||
| bool user_runtime_mode = false, | |||
| QWidget *parent = nullptr); | |||
| /** 释放窗口拥有的控制器和可选导航服务 */ | |||
| @@ -210,6 +214,12 @@ private: | |||
| void saveProjectAs(); | |||
| /** 从用户指定的路径加载工程 */ | |||
| 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); | |||
| /** 向输出面板追加一条消息 */ | |||
| @@ -221,7 +231,7 @@ private: | |||
| * | |||
| * 失败时恢复与服务层当前状态一致的工具栏选项 | |||
| */ | |||
| void requestMode(ApplicationMode requested_mode); | |||
| bool requestMode(ApplicationMode requested_mode); | |||
| /** | |||
| * @brief 根据服务层模式策略更新编辑权限和状态反馈 | |||
| @@ -237,6 +247,10 @@ private: | |||
| void restoreCurrentModeAction(); | |||
| /** 完成主窗口初始化和首次界面刷新 */ | |||
| void initializeUi(); | |||
| /** 专用用户运行版启动后直接进入运行监控 */ | |||
| void initializeUserRuntime(); | |||
| /** 用户运行版允许在离线和真机之间自动经过编辑态切换 */ | |||
| void requestUserRuntimeMode(ApplicationMode requested_mode); | |||
| /** 刷新编辑态数据监控的模式、权限和当前值 */ | |||
| void refreshDataMonitorUi(); | |||
| @@ -268,6 +282,8 @@ private: | |||
| PlcDiscoveryGateway &plc_discovery_gateway_; | |||
| /** 启动期配置和诊断结果,由应用入口保证生命周期 */ | |||
| const ApplicationSettingsLoadResult &application_settings_result_; | |||
| /** 是否以专用用户运行版入口启动 */ | |||
| bool user_runtime_mode_ = false; | |||
| /** 当调用方未提供导航服务时由主窗口负责拥有的实例 */ | |||
| std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | |||
| /** 当前使用的 HMI 页面导航服务 */ | |||
| @@ -308,6 +324,15 @@ private: | |||
| std::string current_logic_id_; | |||
| /** 是否已经安排了待处理的 PLC 状态刷新 */ | |||
| 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 | |||
| { | |||
| @@ -228,6 +228,7 @@ | |||
| <addaction name="saveProjectAction"/> | |||
| <addaction name="saveAsProjectAction"/> | |||
| <addaction name="loadProjectAction"/> | |||
| <addaction name="exportRuntimeAction"/> | |||
| <addaction name="separator"/> | |||
| <addaction name="exitAction"/> | |||
| </widget> | |||
| @@ -1003,6 +1004,10 @@ | |||
| <string>Ctrl+O</string> | |||
| </property> | |||
| </action> | |||
| <action name="exportRuntimeAction"> | |||
| <property name="text"><string>导出用户运行程序</string></property> | |||
| <property name="toolTip"><string>将当前工程导出为用户可直接运行的专用程序</string></property> | |||
| </action> | |||
| <action name="undoAction"> | |||
| <property name="text"><string>撤销</string></property> | |||
| <property name="toolTip"><string>撤销当前编辑器的上一步操作</string></property> | |||
| @@ -11,6 +11,7 @@ | |||
| #include <QSplitter> | |||
| #include <QComboBox> | |||
| #include <QPushButton> | |||
| #include <QSignalBlocker> | |||
| #include <QToolButton> | |||
| @@ -67,6 +68,9 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||
| free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget")); | |||
| ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | |||
| ui_->logicNoticeLabel->setVisible(false); | |||
| ui_->runtimeModeComboBox->setVisible(false); | |||
| ui_->runtimePlcButton->setVisible(false); | |||
| ui_->runtimeDisconnectPlcButton->setVisible(false); | |||
| ui_->upperSplitter->setSizes({600, 600}); | |||
| ui_->verticalSplitter->setSizes({440, 260}); | |||
| connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, | |||
| @@ -95,6 +99,22 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||
| 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; | |||
| @@ -157,9 +177,36 @@ void RuntimeMonitorWidget::setMode( | |||
| offline ? tr("离线仿真 · 虚拟 M/D") | |||
| : online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹") | |||
| : 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); | |||
| } | |||
| 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) | |||
| { | |||
| hmi_view_->setRuntimeWriteEnabled(enabled); | |||
| @@ -57,6 +57,8 @@ public: | |||
| void setRuntimePage(const std::string &page_id); | |||
| /** 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 */ | |||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | |||
| /** 切换专用用户运行版的简化操作栏 */ | |||
| void setUserRuntimeMode(bool enabled); | |||
| /** 从寄存器仓库刷新 HMI 和自由监控中的当前值 */ | |||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | |||
| /** 设置 HMI 控件是否允许写入运行值 */ | |||
| @@ -75,6 +77,12 @@ signals: | |||
| void navigationFailed(const QString &message); | |||
| /** 用户请求退出运行态 */ | |||
| void exitRequested(); | |||
| /** 用户在专用运行版中请求切换模式 */ | |||
| void modeChangeRequested(int mode); | |||
| /** 用户请求打开 PLC 配置窗口 */ | |||
| void plcConfigurationRequested(); | |||
| /** 用户请求断开 PLC */ | |||
| void plcDisconnectRequested(); | |||
| private: | |||
| /** 显示指定的运行监控页面 */ | |||
| @@ -102,4 +110,6 @@ private: | |||
| std::string latest_fault_node_id_; | |||
| /** 是否已经收到过可显示的逻辑轨迹 */ | |||
| 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><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="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> | |||
| </layout> | |||
| </item> | |||
| @@ -121,6 +121,36 @@ void RuntimePanelController::configure() | |||
| status_reporter_(message, 5000); | |||
| 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_->setInterval(150); | |||
| QObject::connect(runtime_refresh_timer_, &QTimer::timeout, | |||
| @@ -143,6 +173,21 @@ void RuntimePanelController::configure() | |||
| 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 | |||
| { | |||
| return runtime_monitor_widget_; | |||
| @@ -162,6 +207,8 @@ void RuntimePanelController::enterRuntime( | |||
| } | |||
| runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses(); | |||
| runtime_monitor_widget_->setMode(mode, plc_state); | |||
| runtime_monitor_window_->setWindowTitle( | |||
| fromUtf8(project_service_.project().metadata.name)); | |||
| runtime_monitor_window_->showForRuntime(); | |||
| } | |||
| @@ -70,6 +70,12 @@ public: | |||
| /** 连接运行监控相关信号并完成初始配置 */ | |||
| 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( | |||
| const std::string &page_id, | |||
| @@ -137,4 +143,7 @@ private: | |||
| QTimer *runtime_refresh_timer_ = nullptr; | |||
| /** 当前是否已经进入运行监控会话 */ | |||
| 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()] | |||
| param( | |||
| [string]$OutputName = 'integrated_platform-win64' | |||
| [string]$OutputName = 'integrated_platform-win64', | |||
| [string]$ProjectFile = '' | |||
| ) | |||
| Set-StrictMode -Version Latest | |||
| @@ -14,12 +15,13 @@ $makePath = Join-Path $mingwBin 'mingw32-make.exe' | |||
| $pluginDirectory = Join-Path $qtRoot 'plugins' | |||
| $platformPlugin = Join-Path $pluginDirectory 'platforms\qwindows.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' | |||
| $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)) { | |||
| throw "未找到必要文件:$requiredPath" | |||
| } | |||
| @@ -32,6 +34,13 @@ if ([string]::IsNullOrWhiteSpace($OutputName) -or | |||
| 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 | |||
| $archivePath = Join-Path $packageRoot "$OutputName.zip" | |||
| $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 $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 | |||
| $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | |||
| try { | |||
| Push-Location $buildDirectory | |||
| 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) { | |||
| throw "qmake 执行失败,退出码:$LASTEXITCODE" | |||
| } | |||
| Write-Host 'RUNTIME_EXPORT_STAGE: compile' | |||
| & $makePath -j2 | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "mingw32-make 执行失败,退出码:$LASTEXITCODE" | |||
| @@ -69,6 +100,17 @@ try { | |||
| 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) { | |||
| Remove-Item -LiteralPath $packageDirectory -Recurse -Force | |||
| } | |||
| @@ -76,10 +118,15 @@ try { | |||
| Remove-Item -LiteralPath $archivePath -Force | |||
| } | |||
| Write-Host 'RUNTIME_EXPORT_STAGE: package' | |||
| 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 = @( | |||
| 'Qt5Core.dll', 'Qt5Gui.dll', 'Qt5Network.dll', | |||
| 'Qt5SerialBus.dll', 'Qt5SerialPort.dll', 'Qt5Widgets.dll', | |||
| @@ -100,6 +147,7 @@ try { | |||
| Copy-Item -LiteralPath $platformPlugin -Destination $packagedPlatformDirectory | |||
| Copy-Item -LiteralPath $stylePlugin -Destination $packagedStyleDirectory | |||
| Write-Host 'RUNTIME_EXPORT_STAGE: verify' | |||
| foreach ($requiredOutput in @( | |||
| $packagedExecutable, | |||
| (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 "运行目录:$packageDirectory" | |||
| Write-Host "压缩包:$archivePath" | |||
| if ([string]::IsNullOrWhiteSpace($ProjectFile)) { | |||
| Write-Host "压缩包:$archivePath" | |||
| } | |||
| } | |||
| finally { | |||
| $env:Path = $originalPath | |||