Python: How to rebase or dynamically replace a cla

2019-02-19 00:24发布

问题:

I am trying to create a common class which I want to use inside different applications.

The idea: a class which could be used as a base class for ui creations. This class will be based to a widget of the applications in it's specific gui system (PySide, or PyQt)

This would allow me to follow the same code flow to generate gui's. I like to have same structures inside my pipeline and it makes it much easier to work in different applications with the same commands.

The problem: PyQt and PySide are compiled in C++ and do not follow the Python class structure

I tried a lot of different things to get it to work, but every time I got stock at some point and doesn't get the result I want.

  1. Try (rebase):

    in this try I used __class__ to rebase the class itself.

    from PyQt4 import QtGui, QtCore
    
    class UiMainBase(object):
        PARENT = QtGui.QMainWindow
        def __init__(self, uiFile=None, parent=None):
            if parent: self.PARENT = parent
            self.__class__ = self.PARENT
    
    
    if __name__ == "__main__":
        import sys
        from PySide import QtGui as PySideGui
    
        class MyGui(UiMainBase):
            def __init__(self, uiFile):
                super(MyGui, self).__init__(uiFile, PySideGui.QMainWindow)
    
        Ui = r"C:\pythonTest\ui\test.ui"
        app = PySideGui.QApplication(sys.argv)
        win = MyGui(Ui)
        win.show()
        sys.exit(app.exec_())
    

The errors I get are reasonable and doesn't has to be explained.

    # 1st Error :
    Traceback (most recent call last):
      #[..]
      File "C:/pythonTest/ui/ui.py", line 18, in __init__
        self.__class__ = self.PARENT
    TypeError: __class__ assignment: only for heap types

    # 2nd Error (without parsing the parent
    # argument to __init__ of super in MyGui) :
    Traceback (most recent call last):
      #[..]
      File "C:/pythonTest/ui/uiTest.py", line 18, in __init__
        self.__class__ = self.PARENT
    TypeError: __class__ assignment: 'MyGui' object layout differs from 'QMainWindow'
  1. Try (rebase):

    in this try I used __bases__ to rebase the class itself.

    from PyQt4 import QtGui, QtCore
    
    class UiMainBase(object):
        PARENT = QtGui.QMainWindow
        def __init__(self, uiFile=None, parent=None):
            if parent: self.PARENT = parent
            self.__bases__= (self.PARENT,)
    
    
    if __name__ == "__main__":
        import sys
        from PySide import QtGui as PySideGui
    
        class MyGui(UiMainBase):
            def __init__(self, uiFile):
                super(MyGui, self).__init__(uiFile, PySideGui.QMainWindow)
    
        Ui = r"C:\pythonTest\ui\test.ui"
        app = PySideGui.QApplication(sys.argv)
        win = MyGui(Ui)
        print win.__bases__ # <- shows what base the object has
        win.show()
        sys.exit(app.exec_())
    

In this result, we can see that the object now get's the right base, but don't get it's methods and variables (not even if I call super after setting __bases__).

    <type 'PySide.QtGui.QMainWindow'> # <- the base
    Traceback (most recent call last):
      File "C:/pythonTest/ui/uiTest.py", line 34, in <module>
        win.show()
    AttributeError: 'MyGui' object has no attribute 'show'
  1. Try (decorator) :

    instead of rebase an object I tried to replace the class with another one that has the right base

    from PyQt4 import QtGui, QtCore
    
    
    def GetUiObject(uiClass):
        parentWidget = uiClass.PARENT # <- Gets the original value, 
                                      # not the latest, why?
        class ParentUI(parentWidget, uiClass):
            def __init__(self, *args, **kwargs):
                super(ParentUI, self).__init__()
                uiClass.__init__(self, *args, **kwargs)
                #[..]
            def __call__(self, cls):
                for func in uiClass.__dict__:
                    setattr(cls, func, uiClass.__dict__[func])
        #ParentUI = type("ParentUI", (parentWidget,),ParentUI.__dict__.copy())
        return ParentUI
    
    @GetUiObject
    class UiMainBase( object ):
        PARENT = QtGui.QMainWindow
        def __init__(self, uiFile=None, *args, **kwargs):
            """constructor.."""
            #[..]
    
    
    
    if __name__ == "__main__":
        import sys
        from PySide import QtGui as PySideGui
    
        UiMainBase.PARENT = PySideGui.QMainWindow # <- specify the application
                                                  # ui architecture as parent
    
        class MyGui(UiMainBase):
            def __init__(self, uiFile=None): 
                # This variant of base class definition doesn't allow parsing
                # the parent argument with with it
                # (__init__ will be executed after base is set)
                super(MyGui, self).__init__(uiFile=None)
                print self.menuBar () # <- check used ui architecture
    

Th e result of this variant doesn't error and prints: <PyQt4.QtGui.QMenuBar object at 0x00000000021C9D38>, but MyGui is not based to PySideGui.QMainWindow like I expected

EDIT:

Why do not defining classes with a base of PySide and one with a base of PyQt4?:

Because I want to leave it open for a genral use and later extensions. It should be free to set a parent widget, which is defined by the application (PySide or PyQt). But each application has it's own method or function to get it's MainWindow. So if you want extend some widgets of this MainWindow, or just parent a new ui to it you need to define it as the parent, or directly base from it. Some applications and it's API's do not support a free generated ui from type PySide.QtGui.QMainWindow or PyQt4.QtGui.QMainWindow. That's the main reason I was trying to do it in this way. Of course I could create a UiMainWindow class for each application and base it to it's main window (this is what I do for now), but I was trying to make it more generic and global for my pipeline API.

Example of usage:

This example is for 3DS-Max and inside a module of it's pipeline integration.

from pythonLibrary.file import Path 
from qtLibrary.ui import UiMainBase
from blurdev.gui import Window

UiMainBase.PARENT = Window
class UiWidget( UiMainBase ):
    """
    This class creates a widget which is parented
    to the PySide implementation of blur's Python
    in 3DS-Max
    """
    def __init__(self, uiFile=None):
        super(UiWidget, self).__init__()
        if uiFile: self.loadUi(uiFile, self)
        self.show()
    #[..]

class PublishUi(UiWidget):
    def __init__(self):
        uiFile = Path.Combine(__file__,self.__class__.__name__+".ui")
        super(PublishUi, self).__init__(uiFile)

    #[..]

if __name__ == "__main__":
    # This should be called from a menu entry inside 3DS-Max
    publishWindow = PublishUi()

Does anyone has a solution for this situation?

Cheers, Michael

回答1:

Make a factory function which dynamically generates the correct base class. In real life, you'd probably memoize the factory so that it always returns the same class object for a given parent, rather than creating multiple identical classes.

from PyQt4 import QtGui, QtCore

def BaseFactory(parent):

    class UiMainBase(parent):
        ....

    return UiMainBase

if __name__ == "__main__":
    import sys
    from PySide import QtGui as PySideGui

    # UiMainBase = BaseFactory(QtGui.QMainWindow)
    UiMainBase = BaseFactory(PySideGui.QMainWindow)
    class MyGui(UiMainBase):
        ...

    Ui = r"C:\pythonTest\ui\test.ui"
    app = PySideGui.QApplication(sys.argv)
    win = MyGui(Ui)