Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PySide2 equivalent of PyQt5's loadUiType() to dynamically mix in UI designs

TL;DR: I want a drop-in replacement for PyQt5's loadUiType() function from its uic module that works with PySide2 and Python 3.6+.


I want to migrate a PyQt5 application to PySide2. A common pattern I use is, I create the user-interface design in Qt Designer and dynamically load the resulting .ui file as a mix-in class extending a Qt widget in the Python code, such as the main window itself:

from PyQt5 import QtWidgets, uic

class Window(QtWidgets.QMainWindow, uic.loadUiType('design.ui')[0]):

    def __init__(self):
        super().__init__()
        self.setupUi(self)
        print(self.label.text())

app = QtWidgets.QApplication([])
window = Window()
window.show()
app.exec_()

This means I can forgo compiling the .ui design to a .py Python module on the command line. More importantly, the mix-in pattern lets me access all Qt widgets defined in the design via self.name within the scope of the importing widgets, where name is assigned as such within Qt Designer.

For the sake of providing a reproducible example, here is a minimal Qt design file to go along with the above Python code, in which it is referenced as design.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
  <class>MainWindow</class>
  <widget class="QMainWindow" name="MainWindow">
    <widget class="QWidget" name="centralwidget">
      <widget class="QLabel" name="label">
        <property name="text">
          <string>Hi, Qt.</string>
        </property>
      </widget>
    </widget>
  </widget>
</ui>

I would like to accomplish the same, but with PySide2, and with the fewest code changes possible. The problem is, PySide2 does not provide an equivalent of PyQt5's uic.loadUiType() function, which, importantly, returns the design's form class to be used as the mix-in.

There is a related question, "PyQt5 to PySide2, loading UI-Files in different classes", but its premise is that the loaded objects be usable from a separate class, which is not my concern per se. Plus, the (currently) only answer to it is not the solution I am looking for. Other questions and their answers (1, 2) establish that design files can be dynamically loaded in PySide2, via QtUiTools.QUiLoader().load('design.ui'), but that method returns the widget object, not the required form class.

The latter approach, without mixing in the imported class, would require me to change many lines of code for the migration, as it results in a different object hierarchy of the Python instance variables. In the above example, self.label would then have to be renamed to something like self.ui.label throughout the code base.

What I want is a drop-in replacement for PyQt5's loadUiType(design) function from its uic module that works with PySide2 and Python 3.6+, wherein design designates the path to a .ui file.

This answer, from 2013, perfectly demonstrates that, but for PySide (based on Qt4) and (legacy) Python 2. How do I adapt that code to PySide2 (based on Qt5) running on (modern) Python?

like image 988
john-hen Avatar asked Jun 18 '19 23:06

john-hen


2 Answers

The following is an adaptation for Python 3.6 or newer and PySide2 5.13 or older (see note at the end) of the solution presented in the above-cited, earlier answer:

from PySide2 import QtWidgets
from pyside2uic import compileUi
from xml.etree import ElementTree
from io import StringIO

def loadUiType(design):
    """
    PySide2 equivalent of PyQt5's `uic.loadUiType()` function.

    Compiles the given `.ui` design file in-memory and executes the
    resulting Python code. Returns form and base class.
    """
    parsed_xml   = ElementTree.parse(design)
    widget_class = parsed_xml.find('widget').get('class')
    form_class   = parsed_xml.find('class').text
    with open(design) as input:
        output = StringIO()
        compileUi(input, output, indent=0)
        source_code = output.getvalue()
        syntax_tree = compile(source_code, filename='<string>', mode='exec')
        scope = {}
        exec(syntax_tree, scope)
        form_class = scope[f'Ui_{form_class}']
        base_class = eval(f'QtWidgets.{widget_class}')
    return (form_class, base_class)

If saved as uic.py alongside the main Python module, only the import statements have to be changed in order to migrate the example in the question from PyQt5 to PySide2:

from PySide2 import QtWidgets
import uic

Tested with Python 3.7.3 and PySide2 5.12.3 on Windows 10 (installed via pip install pyside2) and Manjaro Linux 18.0.4 (via the pacman-packages pyside2 and pyside2-tools).


Note: The pyside2uic module used in the above solution was removed from the PySide2 code base as of version 5.14.0 (December 2019). However, a request was then filed on the PySide2 issue tracker to "bring back loadUiType". As of version 5.14.2.2 (May 2020), loadUiType can be imported from the QtUiTools module and works just like it does in PyQt5. This renders the problem presented in the question obsolete.

like image 55
john-hen Avatar answered Nov 05 '22 01:11

john-hen


PySide2 brought back loadUiType in May 2020. So if you upgrade, you can get a drop-in replacement. The only difference is the import:

from PySide2.QtUiTools import loadUiType

Syntax is the same (you will use loadUiType(<file>)[0] )

like image 41
bfris Avatar answered Nov 05 '22 01:11

bfris