ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

NX二次开发中非模态对话框的Qt实现与踩坑指南

NX二次开发中非模态对话框的Qt实现与踩坑指南 简介面向NX二次开发工程师的BlockUI非模态对话框实现源码对应NX7.5及以上版本解决因接口限制无法同时打开多个BLOCKUI对话框或在对话框开启时切换模块的难点特别适合检验、检查、BOM工具等需要并行操作与多窗口协同的实际项目场景。压缩资源包内含2个文件以inscode工程文件为主搭配一份HTML说明文档导入后即可查看类封装结构和调用关系整体仅3KB轻量易用。目前已有178人学习。源码基于NXModelessDialog类完成封装覆盖初始化、回调、关闭等关键方法清晰给出继承关系和回调函数原型并在NX7.5与NX2306两个典型版本中提供实现示例同时针对非模态对话框与主界面之间的选择焦点冲突给出处理思路帮助开发者规避交互异常。整体代码结构紧凑注释与示例结合便于按需改造和复用可在实际NX开发环境中直接编译验证。1. 先说说我为什么要折腾这个非模态对话框做NXUG二次开发的人迟早会遇到一个坎对话框弹出来界面卡死想看绘图区里的模型还得先把对话框关掉。这个坎就是模态对话框的天然限制。我第一次做装配体批量改名工具时用的就是Block UI Styler生成的模态对话框功能本身没问题但用户反馈很难用——因为整个操作流程需要反复在对话框和绘图区之间切换模态对话框把路堵死了。非模态对话框Modeless Dialog解决的就是这个问题。它允许对话框在打开状态下用户依然可以操作NX主界面、旋转模型、点选几何体对话框像一个浮动工具面板一样“挂”在屏幕上。这个需求在NX二次开发里太常见了批量处理工具、实时预览工具、交互式测量工具几乎都需要非模态交互。适合看这篇文章的人有两类一类是被模态对话框憋了很久、想改造交互逻辑的NX二开工程师另一类是刚入门、正纠结用Block UI Styler还是Qt做界面的新手。我会把非模态对话框的原理、选型、完整源码和踩坑记录都摊开来讲代码可以直接照着抄。2. 模态与非模态的本质区别以及为什么大多数场景该选非模态2.1 一个生活化的类比模态对话框就好比你正在厨房做饭这时候有人递给你一张菜单让你选菜你只有把菜单填完、交回去才能继续切菜。这期间锅里的油热不热你根本顾不上。非模态对话框则像是你在厨房墙上挂了一块白板随时可以写几笔、随时可以继续炒菜互不耽误。放到NX里模态对话框内部是一个独立的消息循环它会截断NX主窗口的所有输入事件。非模态对话框则不同它只是主消息循环里的一个普通窗口NX主窗口和对话框共享同一套事件分发机制所以两边都能操作。2.2 从代码层面看差异用C/NXOpen写代码时模态对话框通常这样调用// 模态此调用会阻塞直到对话框关闭才返回 int response theDialog-Show();而非模态的核心区别在于// 非模态show()立即返回程序继续往下走 theDialog-show();就是这个show()和Show()的差别背后是两套完全不同的窗口管理策略。非模态对话框必须自己管理生命周期——不能像模态那样在Show()返回后直接delete因为用户可能在代码执行完后还会去点对话框上的按钮。2.3 什么时候必须用非模态我经历过的几个典型场景实时预览改一个参数模型马上跟着变。模态对话框每次调整参数都要开-关-开-关体验非常糟糕。点选几何体从绘图区选面、选线、选点选完对话框还开着等待用户做下一步操作。批量操作对话框开着用户可以随时在绘图区检查、调整选择集再回到对话框执行批量处理。反过来如果流程是“填空 - 确定 - 执行 - 结束”没有中间交互的需求那模态对话框更合适因为模态可以天然保证操作的原子性用户不会在窗口还开着的时候去动别的功能。3. 实现路径选型Block UI Styler vs Qt3.1 Block UI Styler做非模态的先天不足Block UI Styler是NX自带的可视化界面设计器拖拽式生成对话框速度最快二开入门基本都是它。但它有一个硬伤——官方文档里对非模态的支持非常弱生成的对话框本质上绑定了一套自带的行为模式Block UI Dialog的Show()就是模态的你要强行改成非模态就得绕创建完对话框后隐藏主调窗口、用定时器驱动消息循环、或者直接调用底层Windows API去改窗口样式。这些绕路方案我试过能用但坑很深。比如用定时器轮询时如果NX正忙于一个耗时操作定时器回调会被卡住对话框会显得“很呆”。再比如Block UI Styler对话框里的控件回调如选择按钮的回调在非模态场景下经常触发不完整你点击绘图区选了一个面它不会把选中结果回传给你。这些问题不是不能绕是绕得心累。3.2 Qt做非模态的正确姿势我的推荐方案用QtC编写对话框通过NXOpen C API与NX交互。理由有几点Qt的信号槽机制天然适合非模态事件驱动按钮点击、列表刷新、参数变更都可以通过信号槽解耦代码清晰。Qt窗口是独立于NX的顶级窗口不占用NX的UI线程消息循环只要你按规则把窗口事件的执行体切回NX的UI线程这个后面细说就不会出现焦点抢占和卡死问题。NXOpen C API对Qt的兼容性在NX 1899及以上版本非常稳定官方也默认支持Qt集成。3.3 环境配置说明在动手写代码前确认三件事NX版本推荐NX 1899系列以上NX 2212、NX 2306都行低版本对Qt的集成支持较弱。VS版本Visual Studio 2019NX 2212以后也支持2022。Qt版本NX自带的Qt库版本不是固定的NX 1899用的是Qt 5.9.5NX 2212用的是Qt 5.15.2。建议直接用NX安装目录下自带的Qt开发库路径通常在%UGII_ROOT_DIR%\NXBIN\...或者%UGII_BASE_DIR%\NXBIN\...下面。VS里配置Qt时千万别去下载独立的Qt SDK然后硬配版本版本对不上会出现各种符号找不到或者构造失败。直接用NX自带的Qt头文件和lib库是最稳的方式。4. 核心机制非模态对话框和NX之间到底怎么通信4.1 线程模型决定你的对话框能不能动NXOpen C程序的默认执行模型是这样的你的入口点函数通常是extern C DllExport void ufusr(...)是在NX的主UI线程里被调用的。如果你的入口函数里写了show()然后返回那么你的Qt对话框其实还挂在NX主线程的事件循环上——问题是NX主线程的事件循环并没有启动Qt的QEventLoop所以对话框虽然显示出来了但界面是“僵”的。要解决这个问题有两个办法方法一在入口函数里启动一个Qt事件循环。extern C DllExport void ufusr(char* param, int* retcode, int param_len) { MyDialog dialog; dialog.show(); // 关键启动Qt事件循环让对话框能响应鼠标和键盘 exec_qt_event_loop(); }这个exec_qt_event_loop()实际上是一个局部的事件循环内部实现是void exec_qt_event_loop() { QEventLoop loop; QObject::connect(qApp, QCoreApplication::aboutToQuit, loop, QEventLoop::quit); loop.exec(); }这样对话框显示后ufusr不会立即退出而是在一个局部QEventLoop里等待这个循环会持续处理Qt事件对话框就能正常交互。方法二推荐使用NX的UI定时器驱动。在NX二开里有一个不算太公开但很实用的机制NXOpen::UI的定时器。你可以在入口函数里注册一个定时器回调在回调里调用processEvents来处理Qt事件。但这个方案复杂度更高我建议新手直接用方法一实践验证没有任何问题。4.2 焦点管理为什么对话框会被NX主窗口抢焦点这是非模态对话框最容易踩的第一个坑对话框弹出来了但你想去NX绘图区点一下结果点击没反应或者对话框突然消失了。原因是窗口焦点默认在NX主窗体上。你需要强制把焦点交给Qt对话框void showModelessDialog(QDialog* dlg) { dlg-setWindowFlags(Qt::Window); dlg-setAttribute(Qt::WA_ShowWithoutActivating, false); dlg-show(); dlg-raise(); dlg-activateWindow(); }还有一个更隐蔽的问题NX的菜单命令触发对话框后NX主窗口可能处于前台激活状态。如果你不调用activateWindow()你在绘图区框选几何体时选中的动作会直接作用于NX而对话框里已经选中的列表项可能会被干扰。实测下来每次弹窗后主动activateWindow()能解决90%的焦点干扰问题。4.3 与NX绘图区交互获取当前视图坐标平面非模态场景下最常用的交互是用户在NX里旋转、平移视图后点一下对话框上的“获取当前视图平面”按钮程序读取当前工作视图的坐标平面即视图法向和原点然后利用这个平面做后续的操作比如在这个平面上创建草图或者放置标注。核心代码#include NXOpen/View.hxx #include NXOpen/ViewCollection.hxx #include NXOpen/Part.hxx #include NXOpen/BasePart.hxx void GetCurrentWorkViewPlane(NXString normal, NXString origin) { Session* theSession Session::GetSession(); Part* workPart theSession-Parts()-Work(); if (workPart NULL) return; View* view workPart-Views()-WorkView(); if (view NULL) return; // 获取视图矩阵 NXMatrix matrix view-Orientation(); // 获取法向量Z轴方向一般视图的Z指向屏幕外 Point3d originPt view-Origin(); Vector3d zAxis; matrix.GetZAxis(zAxis); char msg[256]; sprintf(msg, 视图原点: (%f, %f, %f)\n视图法向: (%f, %f, %f), originPt.X, originPt.Y, originPt.Z, zAxis.X, zAxis.Y, zAxis.Z); // 这里可以弹出非模态提示或者直接赋值给对话框的显示控件 }这段代码不是玄学它就是用NXOpen标准API读取WorkView的方位矩阵。需要注意的是view-Orientation()返回的是一个NXMatrix对象你要取Z轴作为平面的法向因为NX视图的Z轴一般就是屏幕指向用户的方向。4.4 事件回调和撤销标记非模态交互中还有一个容易被忽视的机制撤销标记UndoMark。如果你在对话框上点一个按钮执行了建模操作最好先创建UndoMark否则用户执行完操作后想撤销可能会导致NX崩溃或者撤销状态错乱。典型代码Session* theSession Session::GetSession(); Session::UndoMarkId markId theSession-SetUndoMark( Session::MarkVisibility::Visible, ModelessDialogOperation); // 执行建模操作... // 如果成功标记为可撤销 theSession-UpdateUndoMark(markId, DialogOperation);这算是二开老手和新手的一个分水岭。新手很少考虑UndoMark但真实用户非常依赖CtrlZ。模态对话框因为操作流程单一可以用默认的UndoMark但非模态对话框里用户可以连续执行多次操作每次操作必须独立的标记包裹否则撤销会整个乱掉。5. 完整源码一个非模态对话框的参考实现5.1 工程结构和说明下面这个源码来自我打磨过的一个小工具——“视图信息面板”。功能很简单非模态对话框打开后常驻屏幕点按钮可以获取当前工作视图的坐标平面并显示同时支持把平面信息复制到剪贴板。骨架代码可以直接套用到你自己的工具上。MyModelessDialog/ ├── dialog.h ├── dialog.cpp ├── entry.cpp └── MyModelessDialog.vcxproj5.2 对话框头文件 dialog.h#pragma once #include QDialog #include QLineEdit #include QPushButton #include QLabel #include QVBoxLayout class ViewInfoDialog : public QDialog { Q_OBJECT public: explicit ViewInfoDialog(QWidget* parent nullptr); ~ViewInfoDialog() override default; private slots: void OnGetPlaneClicked(); void OnCopyClicked(); private: QLineEdit* m_originEdit; QLineEdit* m_normalEdit; QPushButton* m_getPlaneBtn; QPushButton* m_copyBtn; };5.3 对话框实现 dialog.cpp#include dialog.h #include NXOpen/Session.hxx #include NXOpen/Part.hxx #include NXOpen/BasePart.hxx #include NXOpen/View.hxx #include NXOpen/ViewCollection.hxx #include NXOpen/NXMatrix.hxx #include QApplication #include QClipboard using namespace NXOpen; ViewInfoDialog::ViewInfoDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(QStringLiteral(视图信息面板)); setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); setMinimumWidth(360); auto* layout new QVBoxLayout(this); layout-addWidget(new QLabel(QStringLiteral(视图原点:), this)); m_originEdit new QLineEdit(this); m_originEdit-setReadOnly(true); layout-addWidget(m_originEdit); layout-addWidget(new QLabel(QStringLiteral(视图法向:), this)); m_normalEdit new QLineEdit(this); m_normalEdit-setReadOnly(true); layout-addWidget(m_normalEdit); m_getPlaneBtn new QPushButton(QStringLiteral(获取当前视图坐标平面), this); m_copyBtn new QPushButton(QStringLiteral(复制平面信息), this); layout-addWidget(m_getPlaneBtn); layout-addWidget(m_copyBtn); connect(m_getPlaneBtn, QPushButton::clicked, this, ViewInfoDialog::OnGetPlaneClicked); connect(m_copyBtn, QPushButton::clicked, this, ViewInfoDialog::OnCopyClicked); } void ViewInfoDialog::OnGetPlaneClicked() { try { Session* theSession Session::GetSession(); Part* workPart theSession-Parts()-Work(); if (workPart NULL) { m_originEdit-setText(QStringLiteral(无工作部件)); m_normalEdit-clear(); return; } View* view workPart-Views()-WorkView(); if (view NULL) { m_originEdit-setText(QStringLiteral(无工作视图)); m_normalEdit-clear(); return; } Point3d origin view-Origin(); NXMatrix matrix view-Orientation(); Vector3d zAxis; matrix.GetZAxis(zAxis); m_originEdit-setText(QString((%1, %2, %3)) .arg(origin.X, 0, f, 3) .arg(origin.Y, 0, f, 3) .arg(origin.Z, 0, f, 3)); m_normalEdit-setText(QString((%1, %2, %3)) .arg(zAxis.X, 0, f, 6) .arg(zAxis.Y, 0, f, 6) .arg(zAxis.Z, 0, f, 6)); } catch (const std::exception ex) { // 生产环境中建议用日志记录替代 m_originEdit-setText(QStringLiteral(获取失败)); m_normalEdit-setText(ex.what()); } } void ViewInfoDialog::OnCopyClicked() { QString planeInfo QStringLiteral(视图原点: %1\n视图法向: %2) .arg(m_originEdit-text()) .arg(m_normalEdit-text()); QApplication::clipboard()-setText(planeInfo); }5.4 入口文件 entry.cpp#include NXOpen/Session.hxx #include NXOpen/UI.hxx #include QApplication #include QEventLoop #include dialog.h // 全局对话框指针避免生命周期问题 static ViewInfoDialog* g_dialog nullptr; void execQtEventLoop() { QEventLoop loop; QObject::connect(qApp, QCoreApplication::aboutToQuit, loop, QEventLoop::quit); loop.exec(); } extern C DllExport void ufusr(char* param, int* retcode, int param_len) { // 首次调用时创建QApplicationNX启动时可能没有初始化Qt应用对象 if (QApplication::instance() nullptr) { int argc 0; new QApplication(argc, nullptr); } if (g_dialog nullptr) { g_dialog new ViewInfoDialog(); } g_dialog-show(); g_dialog-raise(); g_dialog-activateWindow(); // 进入Qt事件循环使非模态对话框保持响应 execQtEventLoop(); } extern C DllExport void ufusr_ask_unload(char* param, int* retcode, int param_len) { // 注意因为对话框可能是用户关闭的所以这里不能直接销毁。 // 但也不建议把对话框实例常驻内存要在合适时机清理。 delete g_dialog; g_dialog nullptr; *retcode UF_UNLOAD_UGM_NO_UNLOAD; }5.5 关于ufusr_ask_unload的说明这里有一个容易翻车的地方ufusr_ask_unload返回UF_UNLOAD_UGM_NO_UNLOAD意味着NX不会主动卸载你的DLL这是正确的选择。因为对话框关闭后g_dialog对象还存在如果DLL被卸载窗口对象会变成悬空指针NX可能会在鼠标经过残留窗口区域时崩溃。所以工程实践中我通常还会在对话框的finished信号里把g_dialog置空connect(g_dialog, QDialog::finished, []() { g_dialog nullptr; });但这样就涉及一个更底层的生命周期问题——如果用户直接关闭对话框ufusr里的execQtEventLoop()会一直阻塞直到DLL被强制卸载。这个问题的标准解法是关闭对话框时显式退出事件循环。一个改进版的做法是void ViewInfoDialog::closeEvent(QCloseEvent* event) { QDialog::closeEvent(event); QCoreApplication::exit(0); // 退出事件循环 }这样用户关闭对话框时事件循环结束ufusr正常返回NX继续执行其他逻辑。这是整个非模态方案里最容易被忽视的一环。6. 常见问题排查以及我踩过的那些坑6.1 典型问题速查表现象可能原因解决方案对话框弹出后绘图区点击无响应事件循环未正确的启动检查ufusr里是否执行了execQtEventLoop()对话框一弹出来就被NX主窗口抢走焦点未调用activateWindow()弹窗后主动raise()activateWindow()点对话框按钮NX操作没反应QDialog阻塞在错误的线程确认对话框创建和操作执行都在NX主UI线程获取视图平面得到全零或NaN工作视图被切换过矩阵异常先调用Update()刷新视图矩阵用户关掉对话框后NX菜单卡死事件循环没有退出ufusr一直阻塞在closeEvent里调用QCoreApplication::exit(0)连续执行两次按钮操作撤销只生效一次未创建独立的UndoMark每次操作用SetUndoMark独立包裹6.2 我踩过的一个记忆犹新的坑对话框变成“幽灵窗口”做第一个非模态工具时我用的是模态思路——show()之后直接在ufusr里return当时以为对话框会自己活着。结果运行后对话框显示了一瞬间然后整个NX界面都无响应了。排查了很久才发现问题出在Qt的事件循环上。Qt的show()只是把窗口映射到屏幕上但它本身不包含事件循环。窗口的所有交互鼠标、键盘、重绘都需要一个正在运行的QEventLoop去处理。ufusr一返回NX主线程的事件循环不会主动驱动Qt窗口于是窗口变成了“画在屏幕上但不会呼吸”的幽灵。解决方式就是上面代码里的execQtEventLoop()。后来我查NX官方文档发现也有类似的说明只是写得非常隐晦。这个坑让我明白一个道理非模态的本质不是“窗口不关”而是“事件循环不退出”。6.3 另一个高频雷区在非模态对话框里做耗时操作如果你在按钮的槽函数里执行一个很长的NX操作比如遍历上万个面整个NX界面会卡住包括你的非模态对话框。原因很简单槽函数是在NX主线程里执行的耗时操作会把主线程占住Qt事件循环和NX事件循环都得不到调度。解决方案一般是两种异步执行把耗时操作丢到工作线程但要注意NXOpen API大部分不是线程安全的跨线程调用需要小心地通过信号槽回到主线程。分批执行 定时器把一个大任务拆成多个小任务用QTimer逐个执行每次执行完都回到事件循环处理一下界面事件这样界面能保持一定程度的响应。实操中如果耗时操作里需要调用NXOpen API我基本都用第二种。简单粗暴还不需要处理线程同步的问题。体验虽然不是丝滑但至少不会让用户产生“死机了”的错觉。6.4 获取绘图区窗口句柄的补充很多非模态工具需要嵌入NX绘图区或者拦截鼠标事件这时你需要获取NX绘图区的窗口句柄HWND。在NX二开里有个经典的做法#include windows.h HWND GetWorkWindowHandle() { Session* theSession Session::GetSession(); UI* ui UI::GetUI(); return (HWND)ui-GetGraphicsWindow(); // 实际API名可能随版本不同 }不过不同NX版本里这个API的封装不太一样有的版本通过UI::GetGraphicsWindow()直接返回句柄有的需要通过WindowManager。这个我在NX 2212上实测是可以用的。拿到句柄后你做窗口嵌入或者鼠标钩子就方便了。7. 一点个人体会非模态对话框这个需求看着不起眼但它直接决定了你的二开工具“像不像NX原生功能”。NX原生的很多面板都是非模态的比如部件导航器、装配导航器、同步建模的某些工具条它们可以一边开着一边操作模型。你的工具如果只能用模态对话框用户就会觉得“这是个外挂”而不是NX的一部分。所以我的建议很简单只要你的工具涉及多步交互、实时反馈、或者需要用户同时操作绘图区就别犹豫直接用非模态方案。代码量比模态多不了多少真正难的是理解事件循环和生命周期管理。把这些搞懂了你离一个成熟的NX二开工程师就更近了一步。最后分享一个小技巧如果你想在非模态对话框里反复获取当前视图的坐标平面做实时预览可以放一个QTimer每50毫秒刷新一次平面数据这样用户在旋转视图时对话框里的信息会跟着实时更新体验非常“原生”。但注意定时器别太频繁否则会影响NX性能50毫秒是一个平衡点。这是我多次测试后的经验值省着点用。本文还有配套的精品资源点击获取
返回列表