Parcourir la source

feat: 完善用户运行程序导出链路

main
suyu il y a 3 semaines
Parent
révision
51ca203ad3
15 fichiers modifiés avec 772 ajouts et 32 suppressions
  1. +5
    -0
      app/integrated_platform.pro
  2. +73
    -3
      app/src/main.cpp
  3. +1
    -3
      app/src/services/logic_command_service.cpp
  4. +25
    -0
      app/src/services/project_service.cpp
  5. +2
    -0
      app/src/services/project_service.h
  6. +0
    -11
      app/src/ui/logic_editor_widget.cpp
  7. +457
    -4
      app/src/ui/main_window.cpp
  8. +26
    -1
      app/src/ui/main_window.h
  9. +5
    -0
      app/src/ui/main_window.ui
  10. +47
    -0
      app/src/ui/runtime_monitor_widget.cpp
  11. +10
    -0
      app/src/ui/runtime_monitor_widget.h
  12. +3
    -0
      app/src/ui/runtime_monitor_widget.ui
  13. +47
    -0
      app/src/ui/runtime_panel_controller.cpp
  14. +9
    -0
      app/src/ui/runtime_panel_controller.h
  15. +62
    -10
      scripts/package_qt_app.ps1

+ 5
- 0
app/integrated_platform.pro Voir le fichier

@@ -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
}

+ 73
- 3
app/src/main.cpp Voir le fichier

@@ -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();


+ 1
- 3
app/src/services/logic_command_service.cpp Voir le fichier

@@ -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;
}



+ 25
- 0
app/src/services/project_service.cpp Voir le fichier

@@ -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))


+ 2
- 0
app/src/services/project_service.h Voir le fichier

@@ -63,6 +63,8 @@ public:
* @return 保存操作结果
*/
ProjectOperationResult saveAs(const std::string &file_path);
/** 将当前工程导出到指定 JSON,不改变当前工程文件关联 */
ProjectOperationResult exportAs(const std::string &file_path) const;

/**
* @brief 从指定文件加载工程


+ 0
- 11
app/src/ui/logic_editor_widget.cpp Voir le fichier

@@ -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()


+ 457
- 4
app/src/ui/main_window.cpp Voir le fichier

@@ -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 &register_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 &register_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, &copy_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();



+ 26
- 1
app/src/ui/main_window.h Voir le fichier

@@ -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 &register_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 &register_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
{


+ 5
- 0
app/src/ui/main_window.ui Voir le fichier

@@ -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>


+ 47
- 0
app/src/ui/runtime_monitor_widget.cpp Voir le fichier

@@ -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);


+ 10
- 0
app/src/ui/runtime_monitor_widget.h Voir le fichier

@@ -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;
};

+ 3
- 0
app/src/ui/runtime_monitor_widget.ui Voir le fichier

@@ -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>


+ 47
- 0
app/src/ui/runtime_panel_controller.cpp Voir le fichier

@@ -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();
}



+ 9
- 0
app/src/ui/runtime_panel_controller.h Voir le fichier

@@ -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_;
};

+ 62
- 10
scripts/package_qt_app.ps1 Voir le fichier

@@ -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


Chargement…
Annuler
Enregistrer