在nvidia_omniverse_app中加载c++_qt_qml插件

2025/12/20

Tags: Omniverse Qt Plugin

Table of Contents

接上文

好久没写博客了, 5月份-11月份一直在研究react native , ts和cpp hybrid的mvvm

从一开始的app单个工程,到后面的npm模块化, 在11月份勉强画上句号

国庆前经恒星和银狼介绍,投了他们仿真团伙UI岗位,原计划qt6 + vulkan写一个仿真软件.

(感谢他们,新的工作环境比以前好得多, 新的工作也是每天在探索无人区,充满新鲜感

​ 希望新的公司可以干趴其他竞争对手,也希望自己可以在新的行业有所成长

入职前狠狠的研究了一把qt6 + vulkan

封装了vulkan 的qml控件;把我的prism for qt mvvm框架从qt5切到qt6 修复了热重载 / native无边框窗口

不过计划总是赶不上变化,入职后变成基于nvidia omniverse的二开了

就像在岸上走的人突然掉入水中, 第一件事就是努力爬到岸上去

所以我当时也试图把qt搬到omniverse app 的python世界里, 随着目标越来越清晰, qt方案已经放弃了

但是那段时间积累了一些经验, 还是觉得需要记录一下 ,防止未来还会遇到在python中调用qt c++ plugin 的场景

创建qt qml c++ plugin

本文不介绍如何封装自定义接口的qt 插件,

简而言之就是定义一个接口, 然后在动态库中定义,实现继承接口的类 , 在app中实例化类以调用

安装qt和pyside版本

从license 方面来看 pyqt的开源协议为具有左传染的GPL, pyside 是LGPL, pyside胜

从兼容方面来看, pyqt的qobject 和c++ qt 的qobject不兼容, pyside 则是兼容的, 还是pyside胜

所以选择pyside

我安装的qt 6是 6.8.3, 所以我在Python环境中也安装6.8.3的pyside6, 这样就可以兼容了

1
2
#把pyside6 安装到pip_install目录
python -m pip install pyside6 == 6.8.3  -t c:\path\to\pip_install

从python加载qt插件

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import sys
#设置pyside pip安装的位置,这样加载 c++ 插件时,会自动到这里找到相关的dll/so和pyd
extra_libs = [
    r"c:\path\to\pip_install\PySide6"
]
os.environ["PATH"] = ";".join(extra_libs) + ";" + os.environ["PATH"]
#或
#sys.path.insert(0, r"c:\path\to\pip_install\PySide6") 

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QPluginLoader, QObject , QLibraryInfo
from PySide6.QtCore import qInstallMessageHandler, QtMsgType

# 安装message hook, 让qDebug 日志输出到python console
def message_handler(msg_type, context, message):
    if msg_type == QtMsgType.QtDebugMsg:
        prefix = "[Debug]"
    elif msg_type == QtMsgType.QtWarningMsg:
        prefix = "[Warning]"
    elif msg_type == QtMsgType.QtCriticalMsg:
        prefix = "[Critical]"
    elif msg_type == QtMsgType.QtFatalMsg:
        prefix = "[Fatal]"
    else:
        prefix = "[Info]"
    print(f"{prefix} {message}")
qInstallMessageHandler(message_handler)


if __name__ == "__main__":
    print("sys.argv:", sys.argv)
    
    # 在python中创建app 和qml engine的实例
    app = QGuiApplication(sys.argv)  #传递app args
    engine = QQmlApplicationEngine()

    # 这三个是我的 c++/qml hybrid 的plugin, 每个plugin里有独立的c++逻辑和qml ui
    plugin_path = ["C:/Users/CNF2025581067/source/repos/qt6-soclution/bin/AMD64/prism_qt_core.dll",
                   "C:/Users/CNF2025581067/source/repos/qt6-soclution/bin/AMD64/prism_qt_ui.dll",
                   "C:/Users/CNF2025581067/source/repos/qt6-soclution/bin/AMD64/rs_common.dll"]
    
    plugin_instances = []
    loaders = []

    # qt 插件实加载,实例化
    for path in plugin_path:
        print(path)
        loaders.append(QPluginLoader(path))
        loader =loaders[-1]
        if loader.load():
            plugin_instance = loader.instance()
            if plugin_instance:
                plugin_instances.append(plugin_instance)
                print("Loaded:", plugin_instance.metaObject())
            else:
                print("Failed to get plugin instance:", loader.errorString())
        else:
            print("Failed to load plugin:", loader.errorString())


    inject_app_engin = False

    #遍历所有插件,调用接口方法 register_types 注册qt类型以及 ioc
    for instance in plugin_instances:
        #在第一个插件调用之前,把app,和qml engine注入 plugin
        if not inject_app_engin :
            inject_app_engin = True
            if hasattr(instance, "attach_app") :
                instance.attach_app(app)
            if hasattr(instance, "attach_engine") :
                instance.attach_engine(engine)

        if hasattr(instance, "register_types"):
            ok = instance.register_types()
            print("register_types result:",ok)
        else:
            print("C++ plugin instance does not have 'register_types' method.")

    #遍历所有插件,调用接口方法 init 
    for instance in plugin_instances:
        if hasattr(instance, "init"):
            ok = instance.init()
            print("init result:",ok)
        else:
            print("C++ plugin instance does not have 'init' method.")

    #遍历所有插件,调用接口方法 install 
    for instance in plugin_instances:
        if hasattr(instance, "install"):
            ok = instance.init()
            print("init install:",ok)
        else:
            print("C++ plugin instance does not have 'install' method.")




    #加载插件中的qml ui, 弹出窗口
    qml_file = "qrc:/rs_common/views/layouts/windows/MainWin.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    #执行ui线程的消息循环
    result = app.exec()



    #退出程序(主线程消息循环后), 反安装 反初始化
    for instance in plugin_instances:
        if hasattr(instance, "uninstall"):
            ok = instance.uninstall()
            print("uninstall result:",ok)
        else:
            print("C++ plugin instance does not have 'uninstall' method.")

    for instance in plugin_instances:
        if hasattr(instance, "uninit"):
            ok = instance.init()
            print("uninit result:",ok)
        else:
            print("C++ plugin instance does not have 'uninit' method.")

    #python进程退出
    sys.exit(result)

从omniverse 加载qt 插件

omniverse kit是nvidia的 一套用于开发 app的sdk, 可以很方便的开发 usd / simulation app

在omniverse kit中,万物皆插件, 开发语言是python , 同时也内置了omni ui模块, 可以用python + omni.ui 在开发过程中实时热载python代码,快速开发ui

关于omniverse kit的插件开发,可以参考 kit-app-template

omni ui 是基于imgui 开发的立即式ui框架 ,不同于qt事件响应式的ui框架, omni ui每一帧都在重绘, 所以qt 在主线程中 开启消息循环,会导致整个 omniverse app ui 挂起.

所以要么把qt ui thread 创建在后台线程, 要么qt 不执行消息循环, 而是手动泵 ui消息

消息循环处理

我选择把 qt ui 消息串到 omni ui的update 中,这样就不必考虑ui交互的同步问题了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import carb
import carb.events
import omni.ext
import omni.kit.app

g_app, g_engine = get_app_engine()

#  订阅omni ui 的update_stream的Update事件, omni ui每更新一帧,执行一个qt ui消息处理
def on_update(e: carb.events.IEvent):
    g_app.processEvents()

update_stream = omni.kit.app.get_app().get_update_event_stream()
g_sub = update_stream.create_subscription_to_pop(on_update, name="My Subscription Name")

win下dll依赖定位失败

但是在windows上出现了qt c++ 插件加载失败的问题, 即使已经设置了

1
2
3
4
5
6
import sys
#设置pyside pip安装的位置,这样加载 c++ 插件时,会自动到这里找到相关的dll/so和pyd
extra_libs = [r"c:\path\to\pip_install\PySide6"]
os.environ["PATH"] = ";".join(extra_libs) + ";" + os.environ["PATH"]
#或
#sys.path.insert(0, extra_libs[0]) 

这个问题的原因暂时不清楚, (gcc,clang上有rpath,runpath, msvc上好像没有类似的东西)

但看到open usd的文档上也记载了这个问题

在windows上改成用 os.add_dll_directory 设置dll目录就可以了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dynamicPaths = [
    r"c:\path\to\pip_install\PySide6",
]
for path in dynamicPaths:
    print(f"Adding {path} to DLL search path list")
    if sys.platform == "win32":
        os.add_dll_directory(path)
    elif sys.platform == "linux":
        os.environ["LD_LIBRARY_PATH"] = path + ":" + os.environ.get("LD_LIBRARY_PATH", "")
    elif sys.platform == "darwin":
        os.environ["DYLD_LIBRARY_PATH"] = path + ":" + os.environ.get("DYLD_LIBRARY_PATH", "")

传递参数到qt 中

由于我们的qt plugin是在omniverse extenstion中创建的,所以由kit app传递到python中, 再通过python代码传给qt app

1
2
3
    print("sys.argv:", sys.argv)
    # 在python中创建app 和qml engine的实例
    app = QGuiApplication(sys.argv)  #传递app args

这涉及到怎么启动kit 进程

通常我们启动kit app都是用kit executable 启动一个.kit文件

1
.\kit.exe   xxx.kit  

如果要传递command parameter, 使用– args 即可

1
.\kit.exe   xxx.kit  -- qml_live

解决掉上面这些问题后, 就可以使用qt c++ 开发gui, 然后在omniverse中加载了,

如果你有一个成熟的qt c++写的工具,这也许是一个好方式, 但90%的情况,可能omni.ui才是应该选择的

后继我将基于omni.kit.phsyX 的ui做一些定制开发,应该还会发表一些相送的博客 ^..^

>> Home

Comments