DESKTOP_APP_WORKFLOW.md 17 KB

桌面仿真工具开发与协作工作流指南

来源:轴向磁拉力仿真 GUI 项目(Motor-CAD + PyQt5 + PyInstaller) 适用:任何需要"科学计算内核 + 桌面 GUI + 打包 exe + Git 协作"的工程项目 日期:2026-08-27


一、项目纪律(先立规矩再干活)

纪律 具体做法 为什么
运行前 commit 每次启动仿真/打包前 git add -A && git commit 仿真可能改坏模型/脚本,可随时回退
生成物不入库 output/, build/, dist/, *.log, *.spec 写入 .gitignore 二进制/临时文件膨胀仓库,diff 无意义
版本化不覆盖 报告/结果文件名带版本号 V1/V2,旧版保留 出新版可回溯对比,避免误删
时间戳记录 结果文件名含 MMDD_HHMMSS,对话/决策记入 CONVERSATION_LOG.md 跨机器复现、追溯"什么时候改了什么"
原始文件只读 原始模型/数据文件不修改,操作在时间戳副本上进行 保护输入数据完整性

.gitignore 模板

# 生成物
output/
build/
dist/
*.log
*.spec
__pycache__/
*.pyc

二、如何跑仿真(Motor-CAD + PyMotorCAD)

2.1 环境要求

要求
系统 Windows(Motor-CAD 仅 Windows)
Motor-CAD 2026R1 (v261),安装路径如 D:\Program Files\ANSYS Inc\v261\motorcad\
Python ≥ 3.10,pip install ansys-motorcad-core
许可证 FlexNet ANSYSLMD_LICENSE_FILE=1055@localhost,lmgrd + ansyslmd 都必须在跑

2.2 环境变量陷阱(AI 工具 shell 常踩)

非登录 shell(Kimi/Cursor/豆包等)可能不继承机器级环境变量,导致:

  • MOTORCAD_ACTIVEX 为空 → pymotorcad 找不到 Motor-CAD
  • ANSYSLMD_LICENSE_FILE 为空 → Motor-CAD 启动后 ~30s 静默退出,报 psutil.NoSuchProcess

解决:运行前 inline 设置,或脚本内回退:

import os
if not os.environ.get("MOTORCAD_ACTIVEX"):
    from ansys.motorcad.core import set_motorcad_exe
    set_motorcad_exe(r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe")

2.3 核心方法:AFM 轴向力的唯一数据口

Motor-CAD 对轴向磁通电机 (AFM) 的轴向力没有输出变量、没有 2D 图、帮助文档无记录。唯一入口:

mc.get_magnetic_3d_graph_point(graph_name, section, node, timestep)
# graph_name ∈ {Fr|Ft}_{Rotor|Stator}_{OL|OC}_Lumped
  • Fr(法向力)= 轴向力(AFM 2.5D 展开模型沿用径向机命名)
  • Ft = 切向力(可由 Σ(Ft×r) 交叉核对转矩)
  • OL=负载, OC=空载 —— 一次求解两者全出
  • 求解前必须开开关(默认关):

    mc.set_variable("ElectromagneticForcesCalc_Load", True)
    mc.set_variable("ElectromagneticForcesCalc_OC", True)
    
    • 节点首尾 0°/360° 重复,求和须去重:if x[-1]-x[0]==360: 丢末点
    • 净力 = 对全部去重节点求和 + 对全部径向切片求和

    2.4 .mot 参数语义陷阱(AFM 模板易错)

    参数 正确语义 常见误读
    Magnet_Length 磁钢轴向厚度 以为是长度
    Magnet_Thickness 磁钢环径向深度 = (D_out−D_in)/2 以为是厚度
    CurrentDefinition=1 RMS 口径 → 改电流设 RMSCurrent PeakCurrent 无效且不报错
    Magnet_Temperature 默认 100°C 热态 以为是常温 20°C
    Pole_Arc 某些转子类型不生效 以为是极弧
    极弧实际参数 Magnet_Arc_[ED](电角度)

    2.5 前台运行 + 窗口可见性

    mc = MotorCAD()          # 前台启动(非 /SCRIPTING 隐藏模式)
    mc.set_visible(True)     # 关键:/SCRIPTING 模式默认主窗口隐藏,任务栏有图标点不开
    

实测:/SCRIPTING 模式下 Motor-CAD 主窗口 IsWindowVisible=False,任务栏图标是坐标 (−32000,−32000) 的代理窗口。set_visible(True) 后正常显示。这是双机通用的默认行为,不是个别机器问题。

2.6 三判据验证(全过才采信结果)

  1. 作用-反作用:定/转子合力反号,偏差 < 5%
  2. 转矩交叉:Σ(Ft×r) vs 转矩图,偏差 < 10%
  3. 解析量级:F ≈ A·mean(B²)/(2μ₀) 与 FEA 同量级(解析偏高 ~1.4× 属正常)

2.7 仿真脚本标准结构

# 1. 启动 Motor-CAD(set_visible 兜底)
# 2. load_from_file(原始.mot) → save_to_file(时间戳副本.mot)
# 3. set_variable 设置工况(RMSCurrent/ShaftSpeed/Airgap/Magnet_Temperature)
# 4. 开力计算开关
# 5. do_magnetic_calculation()  (~90-150s)
# 6. 读 3D 力图 → 节点去重 → 切片求和 → 净力时间序列
# 7. 三判据计算
# 8. 输出 JSON + CSV(output/ 目录,不入库)
# 9. finally: keep_open? 保持打开 : mc.quit()

三、如何做 exe 应用(PyQt5 + PyInstaller)

3.1 技术选型

选择 理由
GUI 框架 PyQt5 成熟稳定、控件丰富、QSS 样式表强大、工业软件风
打包 PyInstaller --onefile --windowed 单 exe 双击即用、无控制台黑窗
计算核心 独立 solver.py 与 GUI 解耦,可单独测试/复用
GUI 主程序 main.py 仅负责界面 + 线程调度 + 信号槽

3.2 代码结构(关键:计算与界面分离)

project/
├── solver.py          # 计算核心:纯函数/类,无 GUI 依赖,可单元测试
├── main.py            # PyQt5 GUI:参数面板 + 日志 + 结果展示 + 线程
├── requirements.txt   # PyQt5, ansys-motorcad-core, pyinstaller
├── build.bat          # 一键打包脚本
└── dist/
    └── AppName.exe    # 生成物(不入库)

3.3 后台线程模式(避免 GUI 卡死)

仿真耗时 ~2 分钟,必须在子线程运行,否则 GUI 冻结:

class SolverWorker(QObject):
    log = pyqtSignal(str)           # 实时日志
    result = pyqtSignal(dict)       # 结果数据
    finished = pyqtSignal()
    error = pyqtSignal(str)

    def run(self):
        try:
            from solver import Solver
            s = Solver(..., log_cb=lambda m: self.log.emit(m))
            r = s.run_single()
            self.result.emit(r)
        except Exception as e:
            self.error.emit(traceback.format_exc())
        finally:
            self.finished.emit()

# GUI 中启动
self.thread = QThread()
self.worker = SolverWorker(params)
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.log.connect(self._append_log)
self.worker.result.connect(self._on_result)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()

3.4 打包命令

pyinstaller --onefile --windowed --name AppName ^
  --hidden-import solver ^
  --collect-submodules ansys ^
  main.py
  • --onefile:单 exe(启动时解压到临时目录,首次 ~3-5s)
  • --windowed:无控制台窗口
  • --hidden-import solver:solver.py 是动态导入,需显式声明
  • --collect-submodules ansys:pymotorcad 子模块多,确保全部收集

3.5 闪退防护(5 层,缺一不可)

打包成 --windowed 后,任何未捕获异常都会导致静默闪退(无控制台可看)。必须层层防护:

防护 代码
1 全局异常钩子 sys.excepthook = custom_handler(写日志文件 + 弹窗)
2 禁止末窗退出 app.setQuitOnLastWindowClosed(False)
3 数据安全访问 所有 dict 访问用 (d.get(k) or {}).get(k2),禁止链式 .get().get()
4 槽函数 try-except 每个 _on_xxx 信号槽包裹 try-except,异常写日志不抛出
5 计算对象 keepalive 全局变量持有 MotorCAD 等长生命周期对象,防止 GC 时连带关闭子进程

全局异常钩子模板

def _excepthook(exc_type, exc_value, exc_tb):
    import traceback, time
    msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
    with open(os.path.expanduser("~/app_error.log"), "a", encoding="utf-8") as f:
        f.write("\n=== %s ===\n%s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg))
    QMessageBox.critical(None, "程序异常", str(exc_value))
sys.excepthook = _excepthook

线程清理防竞态

def _on_finished(self):
    # 不要立即 self.worker = None / self.thread = None
    # 用 QTimer 延迟清理,避免与 deleteLater 竞态
    QTimer.singleShot(100, lambda: setattr(self, 'worker', None))
    QTimer.singleShot(100, lambda: setattr(self, 'thread', None))

3.6 打包后验证清单

  1. 双击 exe,8 秒内不崩溃 → 基本初始化 OK
  2. 跑一次完整仿真 → 确认不闪退、结果正常显示
  3. 测试边界情况(空结果、部分字段缺失)→ 确认优雅降级
  4. 检查 ~/app_error.log 是否有未捕获异常

四、GUI 外观风格要求(现代工业软件风)

4.1 设计原则

参考 ANSYS、JetBrains IDE、VS Code 的工业工具风格:

  • 简洁:无多余装饰,信息层级清晰
  • 比例得当:字体大小、间距、卡片尺寸协调
  • 功能优先:结果数据醒目,操作路径短
  • 无动画:工程工具不需要花哨动效

4.2 配色方案

角色 色值 用途
背景 #eef1f5 主窗口浅灰蓝
卡片/面板 #ffffff 分组容器
边框 #e2e8f0 卡片/输入框边框
强调色 #2563eb 按钮、选中态、链接
主文字 #1e293b 标题、重要数值
次文字 #64748b 标签、说明
弱文字 #94a3b8 占位符、辅助信息
成功 #16a34a 通过判据、完成状态
警告 #d97706 运行中、警告
错误 #dc2626 失败、错误
信息 #7c3aed 文件路径、解析值

4.3 字体比例

元素 字号 字重
全局基础 10pt Regular
分组标题 11pt 600
输入框/按钮 10pt 600(按钮)
结果大数字 22pt 700
结果单位 11pt Regular
结果标签 9pt 500,字间距 1px
日志 9.5pt Consolas 等宽
状态栏 9.5pt Regular

关键:结果数字用大字号(20-24pt),让用户一眼看到核心数据;标签用小字号弱化为辅助信息。

4.4 布局规范

  • 卡片圆角 6-8px,边框 1px #e2e8f0
  • 卡片内边距 14-16px
  • 控件间距 8-10px
  • 输入框最小高度 22px,圆角 6px
  • 按钮最小高度 38-42px(主操作按钮),圆角 6px
  • 左右分栏:左侧参数面板固定宽 380-400px,右侧结果+日志自适应
  • 结果区在上(优先展示),日志区在下(辅助信息)

4.5 结果卡片设计

┌─────────────────────┐
│ 空载净轴向力          │  ← 9pt 弱色标签
│ 342.9  N             │  ← 22pt 粗体数字 + 11pt 单位
└─────────────────────┘
  • 白色卡片,圆角 10px,边框 1px
  • 数字与单位水平排列,单位弱色
  • 成功/不同工况用不同颜色区分数字

4.6 日志区美化(拒绝大黑框)

  • 浅色终端风:背景 #f8fafc,文字 #334155
  • 等宽字体 Consolas/Cascadia Code,行高 1.5
  • 彩色分级(HTML 格式 QTextEdit):
    • 结论/成功 → 绿色 #16a34a
    • 错误 → 红色 #dc2626
    • 警告 → 橙色 #d97706
    • 提示 → 蓝色 #2563eb
    • 阶段标题 → 深灰加粗 #475569
    • 文件路径 → 紫色 #7c3aed
  • 圆角边框,内边距 10px

4.7 状态栏

  • 底部状态栏,深色背景 #1e293b,浅色文字
  • 左侧:运行状态(就绪 / ● 运行中 / ✓ 完成 / ✗ 出错),颜色区分
  • 右侧:总耗时等永久信息

4.8 QSS 样式表要点

app.setStyle("Fusion")   # 基础风格,再叠加 QSS
app.setStyleSheet("""
    QGroupBox { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px;
                margin-top: 16px; padding-top: 14px; font-weight: 600; font-size: 11pt; }
    QGroupBox::title { subcontrol-origin: margin; left: 14px; padding: 0 8px; background: #fff; }
    QPushButton { padding: 8px 20px; border-radius: 6px; background: #2563eb; color: #fff;
                  border: 1px solid #2563eb; font-weight: 600; }
    QPushButton:hover { background: #1d4ed8; }
    QPushButton:disabled { background: #94a3b8; border-color: #94a3b8; }
    QLineEdit, QDoubleSpinBox, QSpinBox { padding: 6px 10px; border: 1px solid #cbd5e1;
                                          border-radius: 6px; background: #f8fafc; }
    QLineEdit:focus { border-color: #2563eb; background: #fff; }
    QTextEdit#logView { background: #f8fafc; color: #334155; border: 1px solid #e2e8f0;
                        border-radius: 8px; padding: 10px; }
    QStatusBar { background: #1e293b; color: #e2e8f0; }
    QScrollBar:vertical { width: 10px; background: #f1f5f9; }
    QScrollBar::handle:vertical { background: #cbd5e1; border-radius: 5px; }
""")

五、Git 协作流程

5.1 Pull 最新程序

# 1. 先看本地状态,确保无未提交改动
git status

# 2. 拉取
git pull origin master

# 3. 如果有冲突 → 手动解决后 git add + git commit
# 4. 确认同步
git status -sb   # 应显示 ## master...origin/master(无 ahead/behind)

Pull 前检查清单

  • 工作区是否干净?有未提交改动先 commit 或 stash
  • 当前在哪个分支?git branch
  • 远程地址对不对?git remote -v

5.2 推送更新

# 1. 确保本地已 commit
git add -A
git commit -m "描述: 做了什么, 为什么"

# 2. 先 pull 避免冲突(别人可能也推了)
git pull origin master

# 3. 再 push
git push origin master

# 4. 确认
git log --oneline -3
git status -sb

5.3 Commit 信息规范

<类型>: <简明描述>

类型可选:
  新增  - 新功能/新文件
  修复  - bug 修复
  文档  - 文档/说明更新
  重构  - 代码重构,功能不变
  结果  - 仿真结果/数据
  打包  - exe 打包相关
  合并  - merge

示例:
  修复: 仿真完成后GUI闪退 - None安全访问+全局异常钩子
  新增: PyQt5桌面GUI - 参数面板+结果卡片+彩色日志
  文档: README补充exe不入库原因

5.4 常见问题

现象 原因 解决
push rejected 远程有新提交,本地落后 git pull,解决冲突后再 push
merge conflict 同一文件同一行被两边修改 手动编辑冲突文件 → git addgit commit
PowerShell 中 git push 报 exit code 1 git 把进度信息写 stderr,PS 误判为错误 看 stdout 是否有 branch -> branch,有就是成功了
大文件 push 慢/失败 exe/二进制入库了 加入 .gitignoregit rm --cached 移除追踪

5.5 二进制交付物处理

  • exe 不入库(40MB+ 二进制,git diff 无意义,仓库膨胀快)
  • 源码 + build.bat 入库,同事可自行打包
  • 交付 exe 时:直接传文件 / 放网盘 / Release 页面
  • 在 README 中明确说明"为什么仓库没有 exe"以及"如何自行打包"

六、排查速查表

现象 可能原因 排查方向
Motor-CAD 启动后 30s 退出 许可证 ansyslmd 未运行 检查 ANSYS License Management Center
pymotorcad 报 NoSuchProcess 同上,或环境变量未继承 inline export 两个环境变量
力结果全为 0/空 力计算开关未开 确认 ElectromagneticForcesCalc_Load/OC=True
改电流无效 CurrentDefinition=1 时改了 PeakCurrent RMSCurrent
力值比预期小很多 磁钢温度 100°C vs 报告 20°C F∝Br²,按温度系数折算
GUI 点运行后卡死 仿真在主线程跑了 必须用 QThread 子线程
exe 双击闪退 未捕获异常 + windowed 模式 加全局 excepthook,查 ~/app_error.log
exe 运行正常但结果不显示 结果处理函数中 None 访问异常 所有 dict 访问加 or {} 安全兜底
Motor-CAD 窗口看不见 /SCRIPTING 模式默认隐藏 mc.set_visible(True)
打包后缺模块 动态导入未被 PyInstaller 发现 --hidden-import / --collect-submodules

七、新项目快速启动清单

  1. 初始化 git 仓库,写 .gitignore
  2. AGENTS.md / README.md(项目说明 + 纪律 + 快速开始)
  3. 确认计算环境(软件版本、许可证、Python 包)
  4. 先写计算核心脚本(无 GUI),跑通并验证数值
  5. 建立结果验证判据(三判据或等效方法)
  6. 提取计算逻辑为 solver.py(可独立调用)
  7. 写 PyQt5 GUI(参数面板 + 日志 + 结果卡片 + 子线程)
  8. 加 5 层闪退防护
  9. 开发模式测试 → 边界测试 → 打包 exe 测试
  10. build.bat + requirements.txt + 使用说明
  11. commit → 同事 review → push
  12. 对话/决策记录入 CONVERSATION_LOG.md