fredrik
fredrik

Reputation: 10291

How to use custom widget and uic.loadUi without importing custom widget class package?

Using PyQt4's uic.loadUi, I’d like to load a .ui file and use a custom widget in it. This means using the third package argument of uic.loadUi which will import the package with the custom widget's class inside.

However, I wish to define the custom widget's class in the same file as where I'm calling uic.loadUi. I’m trying to achieve this like so:

class MyCustomClass(QtWidgets.QPushButton):
    """ This is my custom class for my custom widget """
    def __init__(self, *args):
        QtWidgets.QPushButton.__init__(self, *args)

...

sys.modules['mycustompackage'] = MyCustomClass
uic.loadUi('my_ui.ui', self, 'mycustompackage')  # Loads .ui file which contains the MyCustomWidget widget

However, this returns the following error:

AttributeError: type object 'MyCustomClass' has no attribute 'MyCustomWidget'

Is there anything I could do to make this actually work? I suspect that MyCustomClass isn't defined in the manner uic.loadUi expects it.

In Qt Designer, I've promoted MyCustomWidget:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="MyCustomWidget" name="customWidget">
    <property name="geometry">
     <rect>
      <x>50</x>
      <y>70</y>
      <width>113</width>
      <height>32</height>
     </rect>
    </property>
    <property name="text">
     <string>PushButton</string>
    </property>
   </widget>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>22</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>MyCustomWidget</class>
   <extends>QPushButton</extends>
   <header>MyCustomClass</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

Solution

I solved it using the above .ui file like this:

class MyCustomClasses(object):
    class MyCustomWidget(QtWidgets.QPushButton):
        def __init__(self, *args):
            QtWidgets.QPushButton.__init__(self, *args)

...

sys.modules['MyCustomClasses'] = MyCustomClasses
uic.loadUi('my_ui.ui', self)  # Loads .ui file which contains MyCustomWidget

Upvotes: 3

Views: 3158

Answers (2)

EzR1d3r
EzR1d3r

Reputation: 194

The are three possible ways. For example, you have the module QtCustomWidgets.widgets.mybutton It is a files QtCustomWidgets/widgets/mybutton.py and QtCustomWidgets/python/mybuttonplugin.py in you project with MyButton class in it.

First way define includeFile method from QtCustomWidgets/python/mybuttonplugin.py as:

def includeFile(self):
    return "QtCustomWidgets.widgets.mybutton"

Second way is to use uic.loadUi with packadge path: uic.loadUi('my_ui.ui', self, packadge='QtCustomWidgets.widgets')

but you have to use the dot in you module names (includeFile is the method from QtCustomWidgets/python/mybuttonplugin.py ):

def includeFile(self):
    return ".mybutton"

, so in the header it must look like this:

<customwidgets>
  <customwidget>
   <class>MyButton</class>
   <extends>QPushButton</extends>
   <header>.mybutton</header>
  </customwidget>
 </customwidgets>

And result way still will be "QtCustomWidgets.widgets" + ".mybutton"

There is the source PyQt code custom widget loader (qobjectcreator.py), you may find it yourself:

class _CustomWidgetLoader(object):
    def __init__(self, package):
        # should it stay this way?
        if '.' not in sys.path:
            sys.path.append('.')

        self._widgets = {}
        self._modules = {}
        self._package = package

    def addCustomWidget(self, widgetClass, baseClass, module):
        assert widgetClass not in self._widgets
        self._widgets[widgetClass] = module

    def search(self, cls):
        module_name = self._widgets.get(cls)
        if module_name is None:
            return None

        module = self._modules.get(module_name)
        if module is None:
            if module_name.startswith('.'):
                if self._package == '':
                    raise ImportError(
                            "relative import of %s without base package specified" % module_name)

                if self._package.startswith('.'):
                    raise ImportError(
                            "base package %s is relative" % self._package)

                mname = self._package + module_name
            else:
                mname = module_name

            try:
                module = __import__(mname, {}, {}, (cls,))
            except ValueError:
                # Raise a more helpful exception.
                raise ImportError("unable to import module %s" % mname)

            self._modules[module_name] = module

        return getattr(module, cls)

Third way: To add path to your widgets in sys.path (you have to be import sys):

sys.path.append( "./QtCustomWidgets/widgets" )
uic.loadUi('my_ui.ui', self)

Upvotes: 1

ekhumoro
ekhumoro

Reputation: 120768

To quote from the documentation you linked to, the third argument of loadUi is:

the optional package that is the base package for any relative imports of custom widgets [emphasis added]

The actual module name that the custom class will be imported from must be specified in the ui file itself. In Qt Designer, this is achieved by setting the "Header file" to the appropriate value, and it will be stored in the <header> tags inside the ui file. Note that this value can be the fully qualified package path of the module (e.g. "pkg.mymodule") - in which case, it would not be necessary to use the third argument of loadUi. There should never be any need for sys.module hacks.

The loadUi function is quite simple. It just generates a python module in exactly the same way that the command-line tool does, and then loads it using exec.

Upvotes: 2

Related Questions