-
Notifications
You must be signed in to change notification settings - Fork 12
F.A.Q.
- 1. Why is MO2 throwing an exception when I try to create a type inheriting one of MO2 class?
- 2. Why are my
get_overridecalls failing withMissingImplementationExceptionwhen the Python class has the right methods? - 3. Why are
QStringandQVariantconverters not registered usingregister_qclass_converter? - 4. What is
Q_DELEGATE? And how do I use it? - 5. Why is it not possible to do
bpy::bases<QObject>? - 6. Why is the
trmethod not exposed to avoid having to declare it manually on the python side?
Note: This should probably also be in the plugin creation tutorial.
This often happens if you forget to call super().__init__() with the right arguments. Even if the list of arguments is empty (as in the example), it must be called:
class MySaveGame(mobase.ISaveGame):
def __init__(self):
super().__init__() # Mandatory!2. Why are my get_override calls failing with MissingImplementationException when the Python class has the right methods?
In order for get_override to work properly, a reference to the initial python object must exists at any time. This can be on the python side (e.g., by having an attribute), or on the C++ side. For easier plugin creation, I would recommend storing on the C++ side.
Some C++ examples are:
- The
PythonRunnerimplementation that holdsboost::python::objectfor all the created plugins. - The
IPluginGamethat holds (on the python side), adictobject containing the game features. - The
SaveGameInfoWrapper(game feature) that holds the widget (m_SaveGameWidget) but also all the saves that were created (m_SaveGames).
register_qclass_converter is used to register conversion for Qt class that have PyQt equivalent. Since
PyQt uses standard Python str instead of QString, we need to use a custom converter.
While PyQt does have QVariant, it is not very convenient since Python developer would have to manually
cast to QVariant, so instead we use a custom converter that can create a QVariant from a multitude of
python types such as int, str, List[str], etc.
Q_DELEGATE is a macro that can be used within a bpy::class_ declaration, e.g.:
bpy::class_<IDownloadManager, boost::noncopyable>("IDownloadManager", bpy::no_init)
.def("startDownloadURLs", &IDownloadManager::startDownloadURLs)
.def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile)
.def("downloadPath", &IDownloadManager::downloadPath)
Q_DELEGATE(IDownloadManager, QObject, "_object")
;In this case, we indicate that we want to expose the QObject interface for IDownloadManager. The Q_DELEGATE
macro will:
- Create a
__getattr__method that is used by Python to delegate attribute lookup toQObjectwhen the attribute is not found directly in theIDownloadManagerpython class. - Create a
_objectmethod to access the underlyingQObject.
It makes it possible to do the following in python:
dm = ... # Instance of IDownloadManager
# We can connect signals declared in the C++ class:
dm.downloadComplete.connect(lambda i: print("Download {} complete!", i))
wm = ... # Instance of ISaveGameInfoWidget
# We can call QWidget method on a ISaveGameInfoWidget object:
wm.setLayout(QtWidgets.QHBoxLayout())Most of the QObject interface has no reason to be exposed, so the only cases where you should need Q_DELEGATE would
be when:
- You need to expose Qt signals to python - this is the case for
IDownloadManagerandIModRepositoryBridge. - You need to expose a class that inherits
QWidget. If you do not useQ_DELEGATEin this case, python developers will not be able to call theQWidgetmethod on objects of this class.
I will explain the reason for this here, but you should see the FAQ item above for troubleshooting.
QObject (or any Qt class) is exposed in python using sip, while everything in MO2 is exposed using boost::python. When doing bpy::bases<QObject>, boost::python does not find the PyTypeObject that corresponds to QObject since it is not exposed through a bpy::class_ declaration (registering a converter for it is not enough). It is possible, by playing with internal boost::python stuff, to make boost::python find the PyTypeObject for QObject but... that is not sufficient.
boost::python and sip create classes using their own meta-classes. For boost::python, it is Boost.Python.class. And all classes created by boost::python inherits Boost.Python.instance which is the "top" boost class. Unfortunately, it is not possible to do inheritance between classes that have different meta-classes in python, so it is not possible to inherit both QObject and Boost.Python.instance. The only way would be to provide our own meta-class, but this is not possible with boost::python.
One issue with QObject.tr in PyQt5 is that the context is dynamic, i.e. if class B inherits class A, strings declared in class A will not be translated by an instance of B since the context is the dynamic object (not the static one like in C++), see, https://doc.bccnsoft.com/docs/PyQt5/i18n.html.
It would be quite easy to provide tr since all plugins inherit IPlugin:
.def("tr", +[](bpy::object obj, const char *str) {
std::string className = bpy::extract<std::string>(
obj.attr("__class__").attr("__name__"));
return QCoreApplication::translate(className.data(), str);
})...but the issue is the same, since className will be the name of the actual class, not the class containing the strings. It could be possible to go up the class chain to find the first available translation, but I am not sure that it is worth the hassle.