Bladeren bron

feat: 改为工程数据封装导出

main
suyu 3 weken geleden
bovenliggende
commit
e8f7cc9cd6
11 gewijzigde bestanden met toevoegingen van 537 en 237 verwijderingen
  1. +2
    -0
      app/integrated_platform.pro
  2. +234
    -0
      app/src/infrastructure/runtime_project_bundle.cpp
  3. +50
    -0
      app/src/infrastructure/runtime_project_bundle.h
  4. +36
    -1
      app/src/main.cpp
  5. +94
    -194
      app/src/ui/main_window.cpp
  6. +2
    -20
      app/src/ui/main_window.h
  7. +19
    -22
      app/src/ui/main_window.ui
  8. +84
    -0
      app/tests/runtime_project_bundle_tests.cpp
  9. +13
    -0
      app/tests/runtime_project_bundle_tests.pro
  10. +2
    -0
      app/tests/tests.pro
  11. +1
    -0
      scripts/run_qt_tests.ps1

+ 2
- 0
app/integrated_platform.pro Bestand weergeven

@@ -59,6 +59,7 @@ SOURCES += \
src/infrastructure/plc_communication_service.cpp \
src/infrastructure/plc_discovery_service.cpp \
src/infrastructure/application_settings_loader.cpp \
src/infrastructure/runtime_project_bundle.cpp \
src/infrastructure/json_project_storage.cpp \
src/ui/hmi_editor_widget.cpp \
src/ui/logic_editor_widget.cpp
@@ -113,6 +114,7 @@ HEADERS += \
src/infrastructure/plc_communication_service.h \
src/infrastructure/plc_discovery_service.h \
src/infrastructure/application_settings_loader.h \
src/infrastructure/runtime_project_bundle.h \
src/infrastructure/json_project_storage.h \
src/ui/hmi_editor_widget.h \
src/ui/logic_editor_widget.h


+ 234
- 0
app/src/infrastructure/runtime_project_bundle.cpp Bestand weergeven

@@ -0,0 +1,234 @@
/**
* @file runtime_project_bundle.cpp
* @brief 实现用户运行程序工程数据封装服务
* @version 1.0.0
* @author suyu
* @date 2026-08-26
*/

#include "runtime_project_bundle.h"

#include <QCryptographicHash>
#include <QFile>
#include <QFileInfo>
#include <QObject>

#include <limits>

namespace {

constexpr int kMagicSize = 16;
constexpr int kFooterSize = kMagicSize + 4 + 8 + 32;
constexpr quint32 kFormatVersion = 1;
constexpr qint64 kCopyBufferSize = 64 * 1024;
const QByteArray kMagic = QByteArrayLiteral("QTPROX_RUNTIME_B");

static_assert(sizeof("QTPROX_RUNTIME_B") - 1 == kMagicSize,
"runtime bundle magic must have a fixed length");

void appendUint32LittleEndian(QByteArray *bytes, quint32 value)
{
for (int shift = 0; shift < 32; shift += 8)
{
bytes->append(static_cast<char>((value >> shift) & 0xff));
}
}

void appendUint64LittleEndian(QByteArray *bytes, quint64 value)
{
for (int shift = 0; shift < 64; shift += 8)
{
bytes->append(static_cast<char>((value >> shift) & 0xff));
}
}

quint32 readUint32LittleEndian(const char *bytes)
{
quint32 value = 0;
for (int shift = 0; shift < 32; shift += 8)
{
value |= static_cast<quint32>(
static_cast<unsigned char>(bytes[shift / 8]))
<< shift;
}
return value;
}

quint64 readUint64LittleEndian(const char *bytes)
{
quint64 value = 0;
for (int shift = 0; shift < 64; shift += 8)
{
value |= static_cast<quint64>(
static_cast<unsigned char>(bytes[shift / 8]))
<< shift;
}
return value;
}

QByteArray makeFooter(quint64 project_size, const QByteArray &hash)
{
QByteArray footer;
footer.reserve(kFooterSize);
footer.append(kMagic);
appendUint32LittleEndian(&footer, kFormatVersion);
appendUint64LittleEndian(&footer, project_size);
footer.append(hash);
return footer;
}

RuntimeProjectBundleLoadResult invalidBundle(const QString &message)
{
return {RuntimeProjectBundleStatus::Invalid, {}, message};
}

} // namespace

RuntimeProjectBundleWriteResult RuntimeProjectBundleService::write(
const QString &template_executable,
const QString &project_file,
const QString &destination_executable)
{
if (!QFileInfo::exists(template_executable))
{
return {false,
QObject::tr("未找到用户运行程序模板:%1")
.arg(template_executable)};
}
if (!QFileInfo::exists(project_file))
{
return {false,
QObject::tr("未找到待封装的工程文件:%1").arg(project_file)};
}
if (QFileInfo::exists(destination_executable))
{
return {false,
QObject::tr("导出 exe 已经存在:%1")
.arg(destination_executable)};
}

const QString temporary_executable = destination_executable
+ QStringLiteral(".runtime-bundle-tmp");
QFile::remove(temporary_executable);
if (!QFile::copy(template_executable, temporary_executable))
{
return {false,
QObject::tr("复制用户运行程序模板失败:%1")
.arg(template_executable)};
}

QFile output(temporary_executable);
QFile project(project_file);
if (!output.open(QIODevice::ReadWrite | QIODevice::Append)
|| !project.open(QIODevice::ReadOnly))
{
output.close();
project.close();
QFile::remove(temporary_executable);
return {false, QObject::tr("无法打开用户运行程序封装文件")};
}

QCryptographicHash hash(QCryptographicHash::Sha256);
quint64 project_size = 0;
bool succeeded = true;
while (!project.atEnd())
{
const QByteArray chunk = project.read(kCopyBufferSize);
if (chunk.isEmpty() && project.error() != QFileDevice::NoError)
{
succeeded = false;
break;
}
if (chunk.isEmpty())
{
break;
}
if (output.write(chunk) != chunk.size())
{
succeeded = false;
break;
}
hash.addData(chunk);
project_size += static_cast<quint64>(chunk.size());
}

if (succeeded)
{
const QByteArray footer = makeFooter(project_size, hash.result());
succeeded = output.write(footer) == footer.size();
}
output.flush();
output.close();
project.close();
if (!succeeded || !QFile::rename(temporary_executable, destination_executable))
{
QFile::remove(temporary_executable);
return {false, QObject::tr("写入用户运行程序工程数据失败")};
}
return {true, {}};
}

RuntimeProjectBundleLoadResult RuntimeProjectBundleService::load(
const QString &executable_path)
{
QFile executable(executable_path);
if (!executable.open(QIODevice::ReadOnly))
{
return invalidBundle(
QObject::tr("无法读取用户运行程序:%1").arg(executable_path));
}

const qint64 executable_size = executable.size();
if (executable_size < kFooterSize)
{
return {RuntimeProjectBundleStatus::NotFound, {}, {}};
}
if (!executable.seek(executable_size - kFooterSize))
{
return invalidBundle(QObject::tr("无法定位用户运行程序工程数据"));
}
const QByteArray footer = executable.read(kFooterSize);
if (footer.size() != kFooterSize)
{
return invalidBundle(QObject::tr("用户运行程序工程数据不完整"));
}
if (footer.left(kMagicSize) != kMagic)
{
return {RuntimeProjectBundleStatus::NotFound, {}, {}};
}

const quint32 version = readUint32LittleEndian(footer.constData() + kMagicSize);
const quint64 project_size = readUint64LittleEndian(
footer.constData() + kMagicSize + 4);
if (version != kFormatVersion)
{
return invalidBundle(QObject::tr("不支持的用户运行程序工程数据版本"));
}
if (project_size > static_cast<quint64>(
executable_size - static_cast<qint64>(kFooterSize))
|| project_size > static_cast<quint64>(std::numeric_limits<qint64>::max()))
{
return invalidBundle(QObject::tr("用户运行程序工程数据长度无效"));
}

const qint64 project_offset = executable_size - kFooterSize
- static_cast<qint64>(project_size);
if (!executable.seek(project_offset))
{
return invalidBundle(QObject::tr("无法读取用户运行程序工程数据"));
}
const QByteArray project_data = executable.read(
static_cast<qint64>(project_size));
if (project_data.size() != static_cast<qint64>(project_size))
{
return invalidBundle(QObject::tr("用户运行程序工程数据不完整"));
}

const QByteArray expected_hash = footer.right(32);
if (QCryptographicHash::hash(project_data, QCryptographicHash::Sha256)
!= expected_hash)
{
return invalidBundle(QObject::tr("用户运行程序工程数据校验失败"));
}
return {RuntimeProjectBundleStatus::Loaded, project_data, {}};
}

+ 50
- 0
app/src/infrastructure/runtime_project_bundle.h Bestand weergeven

@@ -0,0 +1,50 @@
/**
* @file runtime_project_bundle.h
* @brief 定义用户运行程序工程数据封装服务
* @version 1.0.0
* @author suyu
* @date 2026-08-26
*/

#pragma once

#include <QByteArray>
#include <QString>

/** 用户运行程序工程数据的读取状态 */
enum class RuntimeProjectBundleStatus
{
NotFound,
Loaded,
Invalid
};

/** 从用户运行程序中读取的工程数据 */
struct RuntimeProjectBundleLoadResult
{
RuntimeProjectBundleStatus status = RuntimeProjectBundleStatus::NotFound;
QByteArray project_data;
QString message;
};

/** 创建用户运行程序的结果 */
struct RuntimeProjectBundleWriteResult
{
bool succeeded = false;
QString message;
};

/** 将工程数据追加到已编译运行程序并负责校验读取 */
class RuntimeProjectBundleService final
{
public:
/** 将工程文件追加到运行程序模板末尾 */
static RuntimeProjectBundleWriteResult write(
const QString &template_executable,
const QString &project_file,
const QString &destination_executable);

/** 从当前 exe 末尾读取工程数据并校验完整性 */
static RuntimeProjectBundleLoadResult load(
const QString &executable_path);
};

+ 36
- 1
app/src/main.cpp Bestand weergeven

@@ -17,6 +17,7 @@
#include "infrastructure/plc_communication_service.h"
#include "infrastructure/plc_discovery_service.h"
#include "infrastructure/plc_register_repository.h"
#include "infrastructure/runtime_project_bundle.h"
#include "domain/active_register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/hmi_editor_service.h"
@@ -53,6 +54,8 @@ int main(int argc, char *argv[])
runtime_project_path = arguments.at(project_option + 1);
}
const QFileInfo executable_info(application.applicationFilePath());
const RuntimeProjectBundleLoadResult bundled_project_result =
RuntimeProjectBundleService::load(executable_info.absoluteFilePath());
const QString bundled_project = executable_info.absolutePath()
+ QStringLiteral("/runtime-project.json");
if (runtime_project_path.isEmpty()
@@ -63,8 +66,21 @@ int main(int argc, char *argv[])
}
const bool embedded_runtime_project = QFile::exists(
QStringLiteral(":/runtime-project.json"));
if (bundled_project_result.status == RuntimeProjectBundleStatus::Invalid
&& runtime_project_path.isEmpty()
&& !embedded_runtime_project)
{
QMessageBox::critical(
nullptr,
QObject::tr("运行程序启动失败"),
bundled_project_result.message);
return 2;
}
const bool bundled_runtime_project = bundled_project_result.status
== RuntimeProjectBundleStatus::Loaded;
const bool user_runtime_mode = !runtime_project_path.isEmpty()
|| embedded_runtime_project;
|| embedded_runtime_project
|| bundled_runtime_project;

const ApplicationSettingsLoadResult application_settings =
ApplicationSettingsLoader::load(
@@ -102,6 +118,25 @@ int main(int argc, char *argv[])
result = project_service.load(temporary_path.toUtf8().toStdString());
QFile::remove(temporary_path);
}
else if (bundled_runtime_project && runtime_project_path.isEmpty())
{
const QString temporary_path = QDir::tempPath()
+ QStringLiteral("/qtproxinje-runtime-bundle-project.json");
QFile temporary_file(temporary_path);
const QByteArray &bytes = bundled_project_result.project_data;
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(


+ 94
- 194
app/src/ui/main_window.cpp Bestand weergeven

@@ -25,6 +25,7 @@
#include "ui_main_window.h"

#include "domain/project_limits.h"
#include "infrastructure/runtime_project_bundle.h"

#include <QActionGroup>
#include <QAbstractSpinBox>
@@ -51,11 +52,9 @@
#include <QToolButton>
#include <QTextEdit>
#include <QTimer>
#include <QProcess>
#include <QDir>
#include <QFileInfo>
#include <QProgressDialog>
#include <QDateTime>

#include <algorithm>

@@ -469,7 +468,6 @@ void MainWindow::initializeUserRuntime()
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(
@@ -537,16 +535,6 @@ void MainWindow::requestUserRuntimeMode(ApplicationMode 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({});
@@ -569,16 +557,6 @@ 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();
@@ -879,6 +857,11 @@ void MainWindow::configureActions()
{
showLogicNodeProperties(selected_logic_node_id_);
}
else
{
// 数据监控页不对应 HMI 或梯形图对象
showControlProperties({});
}
if (index == 0 || index == 1)
{
refreshProjectUi();
@@ -1057,12 +1040,10 @@ void MainWindow::configureRuntimeMonitor()

void MainWindow::configureDataMonitor()
{
data_monitor_dock_ = ui_->dataMonitorDock;
data_monitor_widget_ = new FreeMonitorWidget(
register_monitor_service_, data_monitor_dock_);
register_monitor_service_, ui_->dataMonitorTab);
data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget"));
ui_->dataMonitorDockLayout->addWidget(data_monitor_widget_);
ui_->viewMenu->addAction(data_monitor_dock_->toggleViewAction());
ui_->dataMonitorLayout->addWidget(data_monitor_widget_);

connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged,
this,
@@ -1120,10 +1101,6 @@ void MainWindow::refreshDataMonitorUi()
|| online_connected);
data_monitor_widget_->refreshValues(
mode, runtime_mode_service_.plcConnectionState());
if (data_monitor_dock_ != nullptr)
{
data_monitor_dock_->setVisible(mode == ApplicationMode::Editing);
}
}

void MainWindow::configureProjectTree()
@@ -1925,15 +1902,6 @@ void MainWindow::loadProject()

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))
@@ -2006,8 +1974,6 @@ void MainWindow::exportRuntimeProgram()
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());
@@ -2017,181 +1983,115 @@ void MainWindow::exportRuntimeProgram()
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>(
QProgressDialog progress(
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);
}
});

progress.setWindowTitle(tr("导出用户运行程序"));
progress.setWindowModality(Qt::WindowModal);
progress.setAutoClose(false);
progress.setAutoReset(false);
progress.setMinimumDuration(0);
progress.setValue(5);
progress.show();
QApplication::processEvents();
ui_->exportRuntimeAction->setEnabled(false);
statusBar()->showMessage(tr("正在构建用户运行程序,请稍候…"), 0);
runtime_export_process_->start();
}

void MainWindow::handleRuntimeExportOutput()
{
if (runtime_export_process_ == nullptr)
const QString destination_executable = QDir(destination_directory).filePath(
output_name + QStringLiteral(".exe"));
const QString template_executable = QCoreApplication::applicationFilePath();
const auto failExport = [this, &progress, &temp_project, &destination_directory](
const QString &message)
{
return;
}
runtime_export_process_output_ += QString::fromUtf8(
runtime_export_process_->readAllStandardOutput());
runtime_export_process_output_ += QString::fromUtf8(
runtime_export_process_->readAllStandardError());
QFile::remove(temp_project);
QDir(destination_directory).removeRecursively();
progress.setValue(0);
progress.close();
ui_->exportRuntimeAction->setEnabled(
runtime_mode_service_.policy().allowsProjectEditing);
showProjectResult(tr("导出用户运行程序"), message, false);
};

if (runtime_export_progress_ == nullptr)
if (!QDir().mkpath(destination_directory))
{
failExport(tr("无法创建导出目录:%1").arg(destination_directory));
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_)
progress.setLabelText(tr("正在封装工程数据…"));
progress.setValue(35);
QApplication::processEvents();
const RuntimeProjectBundleWriteResult bundle_result =
RuntimeProjectBundleService::write(
template_executable,
temp_project,
destination_executable);
if (!bundle_result.succeeded)
{
failExport(bundle_result.message);
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);
progress.setLabelText(tr("正在复制 Qt 运行库和平台插件…"));
progress.setValue(65);
QApplication::processEvents();
const QString application_directory = QFileInfo(template_executable).absolutePath();
const QStringList runtime_files = {
QStringLiteral("Qt5Core.dll"),
QStringLiteral("Qt5Gui.dll"),
QStringLiteral("Qt5Network.dll"),
QStringLiteral("Qt5SerialBus.dll"),
QStringLiteral("Qt5SerialPort.dll"),
QStringLiteral("Qt5Widgets.dll"),
QStringLiteral("libgcc_s_seh-1.dll"),
QStringLiteral("libstdc++-6.dll"),
QStringLiteral("libwinpthread-1.dll")};
for (const QString &runtime_file : runtime_files)
{
const QString source = QDir(application_directory).filePath(runtime_file);
const QString destination = QDir(destination_directory).filePath(runtime_file);
if (!QFileInfo::exists(source) || !QFile::copy(source, destination))
{
failExport(tr("复制运行库失败:%1").arg(source));
return;
}
}
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;
QString copy_error;
if (!copyDirectoryContents(
QDir(application_directory).filePath(QStringLiteral("platforms")),
QDir(destination_directory).filePath(QStringLiteral("platforms")),
&copy_error)
|| !copyDirectoryContents(
QDir(application_directory).filePath(QStringLiteral("styles")),
QDir(destination_directory).filePath(QStringLiteral("styles")),
&copy_error))
{
failExport(copy_error);
return;
}

const RuntimeProjectBundleLoadResult verification =
RuntimeProjectBundleService::load(destination_executable);
if (verification.status != RuntimeProjectBundleStatus::Loaded)
{
failExport(verification.message.isEmpty()
? tr("导出结果校验失败")
: verification.message);
return;
}
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();
progress.setLabelText(tr("导出完成"));
progress.setValue(100);
QApplication::processEvents();
progress.close();
ui_->exportRuntimeAction->setEnabled(
runtime_mode_service_.policy().allowsProjectEditing);
showProjectResult(tr("导出用户运行程序"), result_message, succeeded);
showProjectResult(
tr("导出用户运行程序"),
tr("已导出:%1").arg(destination_executable),
true);
}

void MainWindow::showProjectResult(


+ 2
- 20
app/src/ui/main_window.h Bestand weergeven

@@ -15,7 +15,6 @@
#include "services/application_settings.h"

#include <QMainWindow>
#include <QProcess>

#include <memory>
#include <variant>
@@ -27,9 +26,7 @@ class QActionGroup;
class QCloseEvent;
class QEvent;
class QLabel;
class QDockWidget;
class QTimer;
class QProgressDialog;

namespace Ui {
class MainWindow;
@@ -134,7 +131,7 @@ private:
void configureLogicEditor();
/** 创建并连接运行监控面板 */
void configureRuntimeMonitor();
/** 创建并连接编辑态数据监控面板 */
/** 创建并连接数据监控编辑页 */
void configureDataMonitor();
/** 创建并连接工程树 */
void configureProjectTree();
@@ -216,10 +213,6 @@ private:
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);
/** 向输出面板追加一条消息 */
@@ -302,8 +295,7 @@ private:
LogicEditorWidget *logic_editor_widget_ = nullptr;
/** 唯一的运行监控控件 */
RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr;
/** 主窗口中的数据监控面板 */
QDockWidget *data_monitor_dock_ = nullptr;
/** 主窗口中的数据监控编辑页 */
FreeMonitorWidget *data_monitor_widget_ = nullptr;
QTimer *data_monitor_refresh_timer_ = nullptr;
/** 显示当前运行模式的状态标签 */
@@ -324,16 +316,6 @@ 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
{
std::vector<HmiControl> controls;


+ 19
- 22
app/src/ui/main_window.ui Bestand weergeven

@@ -207,7 +207,26 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="dataMonitorTab">
<attribute name="title">
<string>数据监控</string>
</attribute>
<layout class="QVBoxLayout" name="dataMonitorLayout">
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>8</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>8</number>
</property>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
@@ -906,28 +925,6 @@
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dataMonitorDock">
<property name="minimumSize">
<size>
<width>640</width>
<height>220</height>
</size>
</property>
<property name="windowTitle">
<string>数据监控</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dataMonitorDockContents">
<layout class="QVBoxLayout" name="dataMonitorDockLayout">
<property name="leftMargin"><number>6</number></property>
<property name="topMargin"><number>6</number></property>
<property name="rightMargin"><number>6</number></property>
<property name="bottomMargin"><number>6</number></property>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="outputDock">
<property name="minimumSize">
<size>


+ 84
- 0
app/tests/runtime_project_bundle_tests.cpp Bestand weergeven

@@ -0,0 +1,84 @@
#include "infrastructure/runtime_project_bundle.h"
#include "support/test_support.h"

#include <QFile>
#include <QTemporaryDir>

#include <iostream>
#include <stdexcept>

namespace {

using TestSupport::require;

void writeFile(const QString &path, const QByteArray &bytes)
{
QFile file(path);
require(file.open(QIODevice::WriteOnly), "test file must open for writing");
require(file.write(bytes) == bytes.size(), "test file must be written fully");
}

void testBundleRoundTripAndValidation()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be available");

const QString template_path = directory.filePath(QStringLiteral("template.exe"));
const QString project_path = directory.filePath(QStringLiteral("project.json"));
const QString bundle_path = directory.filePath(QStringLiteral("motor.exe"));
const QByteArray template_bytes("MZ-template-binary");
const QByteArray project_bytes(
"{\"formatVersion\":\"1.0\",\"name\":\"motor\"}");
writeFile(template_path, template_bytes);
writeFile(project_path, project_bytes);

const RuntimeProjectBundleWriteResult write_result =
RuntimeProjectBundleService::write(
template_path, project_path, bundle_path);
require(write_result.succeeded, "runtime bundle must be written");

const RuntimeProjectBundleLoadResult loaded =
RuntimeProjectBundleService::load(bundle_path);
require(loaded.status == RuntimeProjectBundleStatus::Loaded,
"runtime bundle must be recognized");
require(loaded.project_data == project_bytes,
"runtime bundle must preserve project bytes");

const RuntimeProjectBundleLoadResult ordinary =
RuntimeProjectBundleService::load(template_path);
require(ordinary.status == RuntimeProjectBundleStatus::NotFound,
"ordinary executable must not be treated as a runtime bundle");

QFile corrupted(bundle_path);
require(corrupted.open(QIODevice::ReadWrite),
"bundle must open for corruption regression");
require(corrupted.seek(corrupted.size() - 1),
"bundle footer must be seekable");
const char invalid_byte = static_cast<char>(0xff);
require(corrupted.write(&invalid_byte, 1) == 1,
"bundle corruption must be writable");
corrupted.close();
const RuntimeProjectBundleLoadResult invalid =
RuntimeProjectBundleService::load(bundle_path);
require(invalid.status == RuntimeProjectBundleStatus::Invalid,
"corrupted runtime bundle must be rejected");
}

} // namespace

int main()
{
try
{
testBundleRoundTripAndValidation();
}
catch (const std::exception &error)
{
std::cerr << "runtime project bundle tests failed: "
<< error.what() << '\n';
return 1;
}

std::cout << "runtime project bundle tests passed\n";
return 0;
}

+ 13
- 0
app/tests/runtime_project_bundle_tests.pro Bestand weergeven

@@ -0,0 +1,13 @@
include(pri/test_defaults.pri)

TARGET = runtime_project_bundle_tests
QT += core
CONFIG += testcase

SOURCES += \
runtime_project_bundle_tests.cpp \
../src/infrastructure/runtime_project_bundle.cpp

HEADERS += \
../src/infrastructure/runtime_project_bundle.h \
$$TEST_SUPPORT_HEADERS

+ 2
- 0
app/tests/tests.pro Bestand weergeven

@@ -11,6 +11,7 @@ SUBDIRS += \
offline_simulation \
project_management \
register_monitor \
runtime_project_bundle \
runtime_mode \
runtime_panel_controller \
plc_connection_dialog \
@@ -24,6 +25,7 @@ logic_editor_service.file = logic_editor_service_tests.pro
offline_simulation.file = offline_simulation_service_tests.pro
project_management.file = project_management_tests.pro
register_monitor.file = register_monitor_service_tests.pro
runtime_project_bundle.file = runtime_project_bundle_tests.pro
runtime_mode.file = runtime_mode_service_tests.pro
runtime_panel_controller.file = runtime_panel_controller_tests.pro
plc_connection_dialog.file = plc_connection_dialog_tests.pro


+ 1
- 0
scripts/run_qt_tests.ps1 Bestand weergeven

@@ -33,6 +33,7 @@ $functionalTargets = @(
'offline_simulation_service_tests',
'project_management_tests',
'register_monitor_service_tests',
'runtime_project_bundle_tests',
'runtime_mode_service_tests',
'runtime_panel_controller_tests',
'plc_connection_dialog_tests',


Laden…
Annuleren
Opslaan