从 P0 到 P5:SAM 插件界面开发流程 / SAM Plugin Interface Development from P0 to P5
📅 创建时间:2026-07-23
🏷️ 标签:#SAM #Qt插件 #Toolset #P0到P5
📚 前置知识:00-overview-and-evidence
📖 系列导航:总目录 · 上一篇:00-overview-and-evidence · 下一篇:02-pyd-registration-and-fragments
第2部分:从 P0 到 P5 的插件开发流程
D:\wx702\OpenOLTranSim-main\7231 将开发过程拆成了清晰的阶段:
P0 原始骨架
↓
P1 编辑菜单栏
↓
P2 菜单信号槽
↓
P3 生成参数界面
↓
P4 调用模块接口
↓
P5 调用 Part 核心数据接口2.1 P0:建立双工程
顶层工程至少包含:
src/
├── Example1/ # 输出 Example1.pyd
└── Example1Toolset/ # 输出 SAM.Pre.Example1Toolset.dll顶层 CMake 要区分:
set(LIBS_SAM_ROOT "D:/Program Files/SAM")
set(LIBS_SAMSDK_ROOT "D:/SAMDevelopment/SAMSDK")
set(LIBS_PYTHON_ROOT ${LIBS_SAM_ROOT}/Python27)
7231手册文字同时出现 Visual C++ 2017 和 VS2015 x64,示例 Qt 路径为 VC14。实际工程必须以老师提供的完整 SDK ABI 为准,不能仅凭手册中的单一版本文字自行选择工具链。
2.2 P1:创建并注册菜单
Toolset GUI 继承:
class Example1ToolsetGui : public SAMToolsetGui,
public omiSingleton<Example1ToolsetGui>
{
// ...
};菜单创建:
SAMMenu* menu = new SAMMenu(this, tr("&Test"));
SAMMenuCommand* action = new SAMMenuCommand(
this, menu, tr("&Example"));
menu->addAction(action);Toolset 插件继承 SAMToolsetGuiInterface,并通过 Qt 元数据实现自动发现:
class Example1ToolsetPlugin : public QObject,
public SAMToolsetGuiInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID SAMToolsetGuiPlugin_iid)
Q_INTERFACES(SAMToolsetGuiInterface)
};向 Part 模块注册:
SAMApp* app = SAMApp::getSAMApp();
SAMMainWindow* window = app->getSAMMainWindow();
if (window->getModule("Part")) {
window->getModule("Part")->registerToolset(
&Example1ToolsetGui::Instance(),
GUI_IN_MENUBAR | GUI_IN_TOOLBAR);
}2.3 P2:把菜单动作连接到功能
connect(action,
SIGNAL(triggered(bool)),
this,
SLOT(createUI()));2.4 P3:显示参数界面
void Example1ToolsetGui::createUI()
{
Example1Form* form = new Example1Form(this);
form->onCmdActivate(this, 0, 0);
}界面通常使用:
SAMForm
SAMDataDialog
QLineEdit / QComboBox / QValidator
edtGSelectNodeWidget / 其他 SAM 视口选择控件2.5 P4:GUI 调用 Python 模块接口
模块方法注册:
static omuInterfaceObj::methodTable methods[] = {
{"calcArea",
(omuInterfaceObj::methodFunc)&Example1PytModule::calcArea},
{0, 0}
};参数读取:
omuPrimitive* Example1PytModule::calcArea(omuArguments& args)
{
double length = 0.0;
double width = 0.0;
args.Begin();
args.Get(length, "Length");
args.Get(width, "Width");
args.End();
return new omuPrimNumber(length * width);
}这里必须注意 7231 的资料存在一处源码与手册差异:
SAM二次开发手册.doc给出的完整实现是return new omuPrimNumber(area);;P4/P5/Example1PytModule.cpp的随附教学源码最后却写成return nullptr;。
后者只能完成“参数进入 C++ 方法”的演示,Python 端拿不到面积结果。需要返回数值时,应采用手册中的 omuPrimNumber 写法,并在目标 SAM 版本中做一次实际调用验证。
GUI 调用:
omuArguments args(2);
args.Put(length, "Length");
args.Put(width, "Width");
omuMethodCall call("Example1", "calcArea", args);
cmdGCommandDeliveryRole::Instance().SendCommand("import Example1");
cmdGCommandDeliveryRole::Instance().SendCommand(call);2.6 P5:把方法挂载到当前 Part
继承 ptsKPartFragment:
class SAMExample1Fragment : public ptsKPartFragment
{
public:
omuPrimitive* getNodeInfo(omuArguments& args);
};注册方法:
static omuInterfaceObj::methodTable methods[] = {
{"getNodeInfo",
(omuInterfaceObj::methodFunc)&SAMExample1Fragment::getNodeInfo},
{0, 0}
};
SAMExample1Fragment::SAMExample1Fragment()
: ptsKPartFragment()
{
omuInterfaceObj::DescribeType(
"SAMExample1Fragment", methods, members);
}Python 2 侧就可以调用:
import Example1
mdb.models['Model-1'].parts['Part-1'].getNodeInfo(NodeID=1)P5 的 getNodeInfo 同样只是接口挂载骨架:源码取得坐标后仍然 return nullptr,而且把 NodeID 直接交给 GetNodalCoord,其语义更接近网格内部索引。正式工具若接收用户看到的节点标签,应采用吊耳源码已经示范的转换:
int meshIndex = nodeData.GetMeshNodeIndex(userLabel);
nodeData.GetNodalCoord(meshIndex, x, y, z);因此,P5 能证明 Part Fragment 的注册与访问路径,但不能直接证明其示例返回值和节点标签语义已经生产可用。