Search code examples
pythonpyqt5fbs

How can I implement an .ui file to package a PyQt5 application with fbs?


EDIT: My question might duplicate with this one: link. I was used to search through Google, haven't realized I should look through Stack search bar.

I have been trying to package a PyQt5 application with Pyinstaller (without success) and decided to give it a try using fbs. But I am struggling to rewrite my main python file to make it work with the compiler. After two weeks trying to figure out how to finally solve these issue, I would like to request help from more advanced developers.

My project is a virtual board with draggable elements organized this way:

  • a "main.py" file
  • a "board.ui" file
  • a folder with images (used while creating the UI with Qt Designer)

First version of code (before trying to convert it to fbs):

# Main Window
class App(QMainWindow):

    def __init__(self):
        super().__init__()

        self.initUI()

        # ... Several other functions called for initialization 
        # (changing language, e.g.)...

   def initUI(self):

       uic.loadUi("board.ui", self)

       self.setWindowIcon(QtGui.QIcon('images/logo.png'))
       self.setWindowTitle("My Board")

       self.show()

    # ... rest of the code ...
    # (includes functions for initialization and interaction with UI elements
    # (changing their text content, position, e.g.)
    # and a subclass of QLabel for Drag and Drop interaction)

    if __name__ == '__main__':
       app = QApplication(sys.argv)
       ex = App()
       sys.exit(app.exec_())

This code would work fine when compiled, but I could not have it packaged for distribution no matter what I tried - the console would open but the UI would never appear. So I have decided to give it a try with fbs (which, as a bonus, would force me to start thinking more about the project organization next time):

New version of code (attempting to organize the project according to the fbs guideline):

class AppContext(ApplicationContext):

    def run(self):
        window = QMainWindow()
        version = self.build_settings['version']
        window.setWindowTitle("My Board v" + version)
        self.initUI()

        # ... other functions for initialization...

        window.show()
        return self.app.exec_()

   @cached_property
   def initUI(self):
       uic.loadUi("board.ui", self)

       # I know the next line has to be rewritten, I have tried to comment it out 
       # as it is another question - one step at the time
       self.setWindowIcon(QtGui.QIcon('images/logo.png')) 

       self.setWindowTitle("My Board")

       self.show()

# ...other cached properties linked to the previous initialization functions...

# ...rest of the code (same than in the first version)

if __name__ == '__main__':
appctxt = AppContext()
exit_code = appctxt.run()
sys.exit(exit_code)

This code would not even compile, I receive this traceback:

Traceback (most recent call last):
  File "C:/Users/...Board/main.py", line 529, in <module>
    exit_code = appctxt.run()
  File "C:/Users/...Board/main.py", line 25, in run
    self.initUI()
  File "C:/Users/...Board/main.py", line 45, in initUI
    uic.loadUi("board.ui", self)
  File "C:\Users\...Board\main.py\venv\lib\site-packages\PyQt5\uic\__init__.py", line 238, in 
     loadUi
     return DynamicUILoader(package).loadUi(uifile, baseinstance, resource_suffix)
  File "C:\Users\...Board\venv\lib\site-packages\PyQt5\uic\Loader\loader.py", line 66, in 
     loadUi
    return self.parse(filename, resource_suffix)
  File "C:\Users\...Board\venv\lib\site-packages\PyQt5\uic\uiparser.py", line 1037, in parse
    actor(elem)
  File "C:\Users\...Board\venv\lib\site-packages\PyQt5\uic\uiparser.py", line 822, in 
    createUserInterface
     self.toplevelWidget = self.createToplevelWidget(cname, wname)
  File "C:\Users\...Board\venv\lib\site-packages\PyQt5\uic\Loader\loader.py", line 59, in 
    createToplevelWidget
     (type(self.toplevelInst), classname)))
  TypeError: ('Wrong base class of toplevel widget', (<class '__main__.AppContext'>, 
    'QMainWindow'))

I have tried to use the solution proposed there (for UI implementation): https://forum.learnpyqt.com/t/ui-files-with-fbs/61/2

qtCreatorFile = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "board.ui")  
Ui_MainWindow, QtBaseClass = uic.loadUiType(qtCreatorFile)

But I suppose I could not figure out how to implement it properly. About the "Wrong base class of toplevel widget" I have no idea what to do. I really would like to find out what my next steps must be and get an understanding of it.

I shall precise I am using Python 3.6 and working on PyCharm.

I am pretty new to programming and this is my very first question on Stack Overflow (which has been so useful during the past months), so please let me know if something is unclear, I will do my best to explain it more correctly.

Thank you for your insight!

EDIT:

The selected answer helped. FOr other reasons I had to change the structure a bit, though, here is the current code:

class AppContext(ApplicationContext):

    def run(self):
        self.window()
        return self.app.exec_()

    @cached_property
    def window(self):
        return App

class App(QMainWindow):

    def __init__(self):
        super().__init__()

        self.initUI()
        
        # ...Other functions called to initialize
        # ...

    def initUI(self):

        uic.loadUi("src/main/resources/base/ui/board.ui", self)
        self.setWindowTitle("Board Routine")
        self.show()

I followed the structure shown by eyllanesc and included the folder with the pictures used in Qt Designer to build the UI directly in the UI folder. Now the project runs well with the fbs run command. The executable obtained after freezing returns a "no module named main" error, but it seems to be linked to other reasons.


Solution

  • ApplicationContext is not a widget so loadUi is illogical, what you should do is use the window. Also since you do not indicate where the .ui is, then you must use the following structure:

    └── src
        ├── build
        │   └── settings
        │       ├── base.json
        │       ├── linux.json
        │       └── mac.json
        └── main
            ├── icons
            │   ├── base
            │   │   ├── 16.png
            │   │   ├── 24.png
            │   │   ├── 32.png
            │   │   ├── 48.png
            │   │   └── 64.png
            │   ├── Icon.ico
            │   ├── linux
            │   │   ├── 1024.png
            │   │   ├── 128.png
            │   │   ├── 256.png
            │   │   └── 512.png
            │   ├── mac
            │   │   ├── 1024.png
            │   │   ├── 128.png
            │   │   ├── 256.png
            │   │   └── 512.png
            │   └── README.md
            ├── python
            │   └── main.py
            └── resources
                └── base
                    └── ui
                        └── board.ui
    

    main.py

    from fbs_runtime.application_context.PyQt5 import ApplicationContext, cached_property
    from PyQt5 import QtGui, QtWidgets, uic
    
    import sys
    
    
    class AppContext(ApplicationContext):
        def run(self):
            self.initUI()
            return self.app.exec_()
    
        def initUI(self):
            uic.loadUi(self.get_resource("ui/board.ui"), self.window)
            version = self.build_settings['version']
            self.window.setWindowTitle("My Board v" + version)
            self.window.show()
    
        @cached_property
        def window(self):
            return QtWidgets.QMainWindow()
    
    
    if __name__ == '__main__':
        appctxt = AppContext()
        exit_code = appctxt.run()
        sys.exit(exit_code)