-
Notifications
You must be signed in to change notification settings - Fork 9
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_override
calls failing withMissingImplementationException
when the Python class has the right methods? - 3. Why are
QString
andQVariant
converters 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
tr
method 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
PythonRunner
implementation that holdsboost::python::object
for all the created plugins. - The
IPluginGame
that holds (on the python side), adict
object 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 toQObject
when the attribute is not found directly in theIDownloadManager
python class. - Create a
_object
method 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
IDownloadManager
andIModRepositoryBridge
. - You need to expose a class that inherits
QWidget
. If you do not useQ_DELEGATE
in this case, python developers will not be able to call theQWidget
method 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.