qtuitools.cpp 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. // Copyright (C) 2018 The Qt Company Ltd.
  2. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. // @snippet uitools-loadui
  4. /*
  5. * Based on code provided by:
  6. * Antonio Valentino <antonio.valentino at tiscali.it>
  7. * Frédéric <frederic.mantegazza at gbiloba.org>
  8. */
  9. #include <sbkpython.h>
  10. #include <sbkconverter.h>
  11. #include <QtUiTools/QUiLoader>
  12. #include <QtWidgets/QWidget>
  13. #include <QtCore/QFile>
  14. static void createChildrenNameAttributes(PyObject *root, QObject *object)
  15. {
  16. for (auto *child : object->children()) {
  17. const QByteArray name = child->objectName().toLocal8Bit();
  18. if (!name.isEmpty() && !name.startsWith("_") && !name.startsWith("qt_")) {
  19. Shiboken::AutoDecRef attrName(Py_BuildValue("s", name.constData()));
  20. if (!PyObject_HasAttr(root, attrName)) {
  21. Shiboken::AutoDecRef pyChild(%CONVERTTOPYTHON[QObject *](child));
  22. PyObject_SetAttr(root, attrName, pyChild);
  23. }
  24. createChildrenNameAttributes(root, child);
  25. }
  26. createChildrenNameAttributes(root, child);
  27. }
  28. }
  29. static PyObject *QUiLoadedLoadUiFromDevice(QUiLoader *self, QIODevice *dev, QWidget *parent)
  30. {
  31. QWidget *wdg = self->load(dev, parent);
  32. if (wdg) {
  33. PyObject *pyWdg = %CONVERTTOPYTHON[QWidget *](wdg);
  34. createChildrenNameAttributes(pyWdg, wdg);
  35. if (parent) {
  36. Shiboken::AutoDecRef pyParent(%CONVERTTOPYTHON[QWidget *](parent));
  37. Shiboken::Object::setParent(pyParent, pyWdg);
  38. }
  39. return pyWdg;
  40. }
  41. if (!PyErr_Occurred())
  42. PyErr_Format(PyExc_RuntimeError, "Unable to open/read ui device");
  43. return nullptr;
  44. }
  45. static PyObject *QUiLoaderLoadUiFromFileName(QUiLoader *self, const QString &uiFile, QWidget *parent)
  46. {
  47. QFile fd(uiFile);
  48. return QUiLoadedLoadUiFromDevice(self, &fd, parent);
  49. }
  50. // @snippet uitools-loadui
  51. // @snippet quiloader
  52. Q_IMPORT_PLUGIN(PyCustomWidgets);
  53. // @snippet quiloader
  54. // @snippet quiloader-registercustomwidget
  55. registerCustomWidget(%PYARG_1);
  56. %CPPSELF.addPluginPath(QString{}); // force reload widgets
  57. // @snippet quiloader-registercustomwidget
  58. // @snippet quiloader-load-1
  59. // Avoid calling the original function: %CPPSELF.%FUNCTION_NAME()
  60. %PYARG_0 = QUiLoadedLoadUiFromDevice(%CPPSELF, %1, %2);
  61. // @snippet quiloader-load-1
  62. // @snippet quiloader-load-2
  63. // Avoid calling the original function: %CPPSELF.%FUNCTION_NAME()
  64. auto str = PySide::pyPathToQString(%1);
  65. %PYARG_0 = QUiLoaderLoadUiFromFileName(%CPPSELF, str, %2);
  66. // @snippet quiloader-load-2
  67. // @snippet loaduitype
  68. /*
  69. Arguments:
  70. %PYARG_1 (uifile)
  71. */
  72. // 1. Generate the Python code from the UI file
  73. PyObject *strObj = PyUnicode_AsUTF8String(%PYARG_1);
  74. char *arg1 = PyBytes_AsString(strObj);
  75. QByteArray uiFileName(arg1);
  76. Py_DECREF(strObj);
  77. if (uiFileName.isEmpty()) {
  78. qCritical() << "Error converting the UI filename to QByteArray";
  79. Py_RETURN_NONE;
  80. }
  81. QFile uiFile(QString::fromUtf8(uiFileName));
  82. if (!uiFile.exists()) {
  83. qCritical().noquote() << "File" << uiFileName << "does not exist";
  84. Py_RETURN_NONE;
  85. }
  86. // Use the 'pyside6-uic' wrapper instead of 'uic'
  87. // This approach is better than rely on 'uic' since installing
  88. // the wheels cover this case.
  89. QString uicBin(QStringLiteral("pyside6-uic"));
  90. QStringList uicArgs = {QString::fromUtf8(uiFileName)};
  91. QProcess uicProcess;
  92. uicProcess.start(uicBin, uicArgs);
  93. if (!uicProcess.waitForStarted()) {
  94. qCritical().noquote() << "Cannot run '" << uicBin << "': "
  95. << uicProcess.errorString() << " - Check if 'pyside6-uic' is in PATH";
  96. Py_RETURN_NONE;
  97. }
  98. if (!uicProcess.waitForFinished()
  99. || uicProcess.exitStatus() != QProcess::NormalExit
  100. || uicProcess.exitCode() != 0) {
  101. qCritical().noquote() << '\'' << uicBin << "' failed: "
  102. << uicProcess.errorString() << " - Exit status " << uicProcess.exitStatus()
  103. << " (" << uicProcess.exitCode() << ")\n";
  104. Py_RETURN_NONE;
  105. }
  106. QByteArray uiFileContent = uicProcess.readAllStandardOutput();
  107. QByteArray errorOutput = uicProcess.readAllStandardError();
  108. if (!errorOutput.isEmpty()) {
  109. qCritical().noquote() << '\'' << uicBin << "' failed: " << errorOutput;
  110. Py_RETURN_NONE;
  111. }
  112. // 2. Obtain the 'classname' and the Qt base class.
  113. QByteArray className;
  114. QByteArray baseClassName;
  115. // Problem
  116. // The generated Python file doesn't have the Qt Base class information.
  117. // Solution
  118. // Use the XML file
  119. if (!uiFile.open(QIODevice::ReadOnly))
  120. Py_RETURN_NONE;
  121. // This will look for the first <widget> tag, e.g.:
  122. // <widget class="QWidget" name="ThemeWidgetForm">
  123. // and then extract the information from "class", and "name",
  124. // to get the baseClassName and className respectively
  125. QXmlStreamReader reader(&uiFile);
  126. while (!reader.atEnd() && baseClassName.isEmpty() && className.isEmpty()) {
  127. auto token = reader.readNext();
  128. if (token == QXmlStreamReader::StartElement && reader.name() == u"widget") {
  129. baseClassName = reader.attributes().value(QLatin1StringView("class")).toUtf8();
  130. className = reader.attributes().value(QLatin1StringView("name")).toUtf8();
  131. }
  132. }
  133. uiFile.close();
  134. if (className.isEmpty() || baseClassName.isEmpty() || reader.hasError()) {
  135. qCritical() << "An error occurred when parsing the UI file while looking for the class info "
  136. << reader.errorString();
  137. Py_RETURN_NONE;
  138. }
  139. QByteArray pyClassName("Ui_"+className);
  140. PyObject *module = PyImport_ImportModule("__main__");
  141. PyObject *loc = PyModule_GetDict(module);
  142. // 3. exec() the code so the class exists in the context: exec(uiFileContent)
  143. // The context of PyRun_SimpleString is __main__.
  144. // 'Py_file_input' is the equivalent to using exec(), since it will execute
  145. // the code, without returning anything.
  146. Shiboken::AutoDecRef codeUi(Py_CompileString(uiFileContent.constData(), "<stdin>", Py_file_input));
  147. if (codeUi.isNull()) {
  148. qCritical() << "Error while compiling the generated Python file";
  149. Py_RETURN_NONE;
  150. }
  151. PyObject *uiObj = PyEval_EvalCode(codeUi, loc, loc);
  152. if (uiObj == nullptr) {
  153. qCritical() << "Error while running exec() on the generated code";
  154. Py_RETURN_NONE;
  155. }
  156. // 4. eval() the name of the class on a variable to return
  157. // 'Py_eval_input' is the equivalent to using eval(), since it will just
  158. // evaluate an expression.
  159. Shiboken::AutoDecRef codeClass(Py_CompileString(pyClassName.constData(),"<stdin>", Py_eval_input));
  160. if (codeClass.isNull()) {
  161. qCritical() << "Error while compiling the Python class";
  162. Py_RETURN_NONE;
  163. }
  164. Shiboken::AutoDecRef codeBaseClass(Py_CompileString(baseClassName.constData(), "<stdin>", Py_eval_input));
  165. if (codeBaseClass.isNull()) {
  166. qCritical() << "Error while compiling the base class";
  167. Py_RETURN_NONE;
  168. }
  169. PyObject *classObj = PyEval_EvalCode(codeClass, loc, loc);
  170. PyObject *baseClassObj = PyEval_EvalCode(codeBaseClass, loc, loc);
  171. %PYARG_0 = PyTuple_New(2);
  172. if (%PYARG_0 == nullptr) {
  173. qCritical() << "Error while creating the return Tuple";
  174. Py_RETURN_NONE;
  175. }
  176. PyTuple_SetItem(%PYARG_0, 0, classObj);
  177. PyTuple_SetItem(%PYARG_0, 1, baseClassObj);
  178. // @snippet loaduitype