PySide2 dynamic / static loading UI and program release

Python is now a "household name" term. It may be used in many industries, including the most powerful artificial intelligence (AI) and big data. Today we are going to introduce the implementation of Python graphical interface (GUI). Remember that when Python first came out, it was very inconvenient to develop a powerful and beautiful GUI. In the earliest days, Python's own GUI module library was called Tkinter. Of course, the functions and functions of this module are much richer than before. In addition, wxpthon, pyGtk, Jython, Pywin32, PyQt PySide\PySide2, etc. Among so many graphics development tools, I prefer PySide2. The following focuses on how to package the developed program into an executable file and publish it, which can be used on other people's computers (only on Windows computers).

Introduction to PySide2

PySide2 is actually a python version of Qt, which is officially called "Qt for Python". In this way, you should understand that it is actually Qt's various graphical libraries and function module libraries that call Python development and import a module in Python. I believe everyone knows "import", so it's so simple to use PySide2. What we need to learn is the use of API function interfaces related to "Qt for Python". It is divided into many APIs according to modules, such as graphic control API, network communication API, serial port API, Web related API, etc. we can just look at these APIs if we need any API. That's easy!

In addition, let's explain that PySide2 is the son of Qt. For example, kotlin language is a language prepared by google for Android APP development (at first, Android APP development can only be developed in java language). Now we find that many Android apps are developed by kotlin, and the functional support is getting better and better. After all, it is their own, So the same is true for PySide2. Who is the adopter? PyQt, which I mentioned above, is a special company that develops Python related API s for Qt. Of course, it was born earlier than PySide2.

On the issue of version, PySide2 is developed based on Qt5, and there is another PySide in front of her, which is an early version. It is also the earliest version based on Qt4. Now almost no one uses it, because the function is not perfect. The latest one is PySide6. Does it feel that it jumps a little fast from 2 to 6? It may follow Lao Tzu's pace. QT has now developed to version 6.0, so it is PySide6 directly. However, because many API s in version 6.0 are not fully supported, this article introduces PySide2. It is almost synchronized with Qt5, and she can almost realize some functions of Qt5.
(PyQt - > PyQt5 - > PyQt6 this is the development chart of the adopted version)

Packing artifact Pyinstaller

There are many tools to package python, except for this, such as py2exe and Cx_ Free is also a good tool.

1. Install Pyinstaller packaging tool

Microsoft Windows [Version 10.0.19041.867]
(c) 2020 Microsoft Corporation. All rights reserved.

C:\Users\Gary>pip install -i https://pypi.douban.com/simple pyinstaller
Looking in indexes: https://pypi.douban.com/simple
Collecting pyinstaller
  Downloading https://pypi.doubanio.com/packages/b4/83/9f6ff034650abe9778c9a4f86bcead63f89a62acf02b1b47fc2bfc6bf8dd/pyinstaller-4.2.tar.gz (3.6 MB)
     |████████████████████████████████| 3.6 MB 46 kB/s
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Collecting altgraph
  Downloading https://pypi.doubanio.com/packages/ee/3d/bfca21174b162f6ce674953f1b7a640c1498357fa6184776029557c25399/altgraph-0.17-py2.py3-none-any.whl (21 kB)
Collecting pyinstaller-hooks-contrib>=2020.6
  Downloading https://pypi.doubanio.com/packages/27/c7/58a634d861e4744ac62dca4a4992ace8def8b05dab91e6b25e5043e79acf/pyinstaller_hooks_contrib-2021.1-py2.py3-none-any.whl (181 kB)
     |████████████████████████████████| 181 kB ...
Requirement already satisfied: setuptools in d:\users\python\python39\lib\site-packages (from pyinstaller) (49.2.1)
Collecting pefile>=2017.8.1; sys_platform == "win32"
  Downloading https://pypi.doubanio.com/packages/36/58/acf7f35859d541985f0a6ea3c34baaefbfaee23642cf11e85fe36453ae77/pefile-2019.4.18.tar.gz (62 kB)
     |████████████████████████████████| 62 kB 408 kB/s
Collecting pywin32-ctypes>=0.2.0; sys_platform == "win32"
  Downloading https://pypi.doubanio.com/packages/9e/4b/3ab2720f1fa4b4bc924ef1932b842edf10007e4547ea8157b0b9fc78599a/pywin32_ctypes-0.2.0-py2.py3-none-any.whl (28 kB)
Collecting future
  Downloading https://pypi.doubanio.com/packages/45/0b/38b06fd9b92dc2b68d58b75f900e97884c45bedd2ff83203d933cf5851c9/future-0.18.2.tar.gz (829 kB)
     |████████████████████████████████| 829 kB 819 kB/s
Using legacy 'setup.py install' for pefile, since package 'wheel' is not installed.
Using legacy 'setup.py install' for future, since package 'wheel' is not installed.
Building wheels for collected packages: pyinstaller
  Building wheel for pyinstaller (PEP 517) ... done
  Created wheel for pyinstaller: filename=pyinstaller-4.2-py3-none-any.whl size=2413076 sha256=fa9cdc6ec88e2d0c3df74e7aec0f71330feb1b2b6fb751f188bb498631837d06
  Stored in directory: c:\users\gary\appdata\local\pip\cache\wheels\af\ea\26\5812f58861dc385ff5573249b18dfe1624c5f84ea67fa76397
Successfully built pyinstaller
Installing collected packages: altgraph, pyinstaller-hooks-contrib, future, pefile, pywin32-ctypes, pyinstaller
    Running setup.py install for future ... done
    Running setup.py install for pefile ... done
Successfully installed altgraph-0.17 future-0.18.2 pefile-2019.4.18 pyinstaller-4.2 pyinstaller-hooks-contrib-2021.1 pywin32-ctypes-0.2.0
WARNING: You are using pip version 20.2.3; however, version 21.0.1 is available.
You should consider upgrading via the 'd:\users\python\python39\python.exe -m pip install --upgrade pip' command.
C:\Users\Gary>

You can check the directory of the Python third-party package and find two more: (Pywin32 will be installed at the same time)

2. Common instruction parameters of pyinstaller

Here are some common parameters:

D:\py\MFGTool>pyinstaller --help
usage: pyinstaller [-h] [-v] [-D] [-F] [--specpath DIR] [-n NAME] [--add-data <SRC;DEST or SRC:DEST>]
                   [--add-binary <SRC;DEST or SRC:DEST>] [-p DIR] [--hidden-import MODULENAME]
                   [--additional-hooks-dir HOOKSPATH] [--runtime-hook RUNTIME_HOOKS] [--exclude-module EXCLUDES]
                   [--key KEY] [-d {all,imports,bootloader,noarchive}] [-s] [--noupx] [--upx-exclude FILE] [-c] [-w]
                   [-i <FILE.ico or FILE.exe,ID or FILE.icns or "NONE">] [--version-file FILE] [-m <FILE or XML>]
                   [-r RESOURCE] [--uac-admin] [--uac-uiaccess] [--win-private-assemblies] [--win-no-prefer-redirects]
                   [--osx-bundle-identifier BUNDLE_IDENTIFIER] [--runtime-tmpdir PATH] [--bootloader-ignore-signals]
                   [--distpath DIR] [--workpath WORKPATH] [-y] [--upx-dir UPX_DIR] [-a] [--clean] [--log-level LEVEL]
                   scriptname [scriptname ...]

positional arguments:
  scriptname            name of scriptfiles to be processed or exactly one .spec-file. If a .spec-file is specified,
                        most options are unnecessary and are ignored.

optional arguments:
  -h, --help            show this help message and exit
  -v, --version         Show program version info and exit.
  --distpath DIR        Where to put the bundled app (default: .\dist)Specify where to save the packaged file. The default is dist This directory
  --workpath WORKPATH   Where to put all the temporary work files, .log, .pyz and etc. (default: .\build)
  -y, --noconfirm       Replace output directory (default: SPECPATH\dist\SPECNAME) without asking for confirmation
  --upx-dir UPX_DIR     Path to UPX utility (default: search the execution path)
  -a, --ascii           Do not include unicode encoding support (default: included if available)
  --clean               Clean PyInstaller cache and remove temporary files before building.After successful packaging, the files generated in the packaging process will be cleaned up

  -D, --onedir          Create a one-folder bundle containing an executable (default) This is the default. All files related to executable files will be copied to the directory dist in
  -F, --onefile         Create a one-file bundled executable. This will only generate one exe File, the file will be large
  --hidden-import MODULENAME, --hiddenimport MODULENAME
                        Name an import not visible in the code of the script(s). This option can be used multiple
                        times.Hide some modules. These modules are usually not in the actual program, but the modules required by the compiler or packer. If they are not hidden, the packaging will fail, saying that a module is missing
  -p DIR, --paths DIR   A path to search for imports (like using PYTHONPATH). Multiple paths are allowed, separated by
                        ';', or use this option multiple times
 Specify the path of the library to be included. If there is no library to be specified, this parameter is unnecessary
  -c, --console, --nowindowed
                        Open a console window for standard i/o (default). On Windows this option will have no effect
                        if the first script is a '.pyw' file.
  -w, --windowed, --noconsole
                        Windows and Mac OS X: do not provide a console window for standard i/o. On Mac OS X this also
                        triggers building an OS X .app bundle. On Windows this option will be set if the first script
                        is a '.pyw' file. This option is ignored in *NIX systems.
above-c or-w It refers to whether to display the console after packaging, which is similar dos Such an interface

packaged applications

The GUI interface is loaded dynamically:

import os
import sys

from PySide2.QtWidgets import QApplication, QMainWindow, QMessageBox
from PySide2.QtCore import QFile, QRegExp, QByteArray, QIODevice
from PySide2.QtUiTools import QUiLoader
from PySide2.QtGui import QRegExpValidator
from PySide2 import QtSerialPort
#from ui_mfgtool import Ui_MfgTool


class MfgTool(QMainWindow):
    def __init__(self):
        super(MfgTool, self).__init__()
        self.load_ui()
        #self.ui = Ui_MfgTool() #Static loading UI
        #self.ui.setupUi(self)
        self.initUI()
        self.ConnectFun()
        self.m_ba = QByteArray(b"")

    def load_ui(self):
        loader = QUiLoader() #Dynamically load UI
        path = os.path.join(os.path.dirname(__file__), "mfgtool.ui")
        ui_file = QFile(path)
        ui_file.open(QFile.ReadOnly)
        self.ui = loader.load(ui_file, self)
        ui_file.close()
        self.ui.show()

The packaging instructions are as follows:

D:\py\MFGTool>pyinstaller mfgtool.py
171 INFO: PyInstaller: 4.2
171 INFO: Python: 3.9.2
171 INFO: Platform: Windows-10-10.0.19041-SP0
171 INFO: wrote D:\py\MFGTool\mfgtool.spec
171 INFO: UPX is not available.
188 INFO: Extending PYTHONPATH with paths
['D:\\py\\MFGTool', 'D:\\py\\MFGTool']
202 INFO: checking Analysis
327 INFO: Building because hiddenimports changed
327 INFO: Initializing module dependency graph...
327 INFO: Caching module graph hooks...
344 WARNING: Several hooks defined for module 'win32ctypes.core'. Please take care they do not conflict.
359 INFO: Analyzing base_library.zip ...
4468 INFO: Processing pre-find module path hook distutils from 'd:\\users\\python\\python39\\lib\\site-packages\\PyInstaller\\hooks\\pre_find_module_path\\hook-distutils.py'.
4468 INFO: distutils: retargeting to non-venv dir 'd:\\users\\python\\python39\\lib'
.........
23921 INFO: Bootloader d:\users\python\python39\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
23921 INFO: checking EXE
23953 INFO: Building because icon changed
23953 INFO: Building EXE from EXE-00.toc
23953 INFO: Copying icons from ['d:\\users\\python\\python39\\lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico']
24140 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
24140 INFO: Writing RT_ICON 1 resource with 3752 bytes
24140 INFO: Writing RT_ICON 2 resource with 2216 bytes
24140 INFO: Writing RT_ICON 3 resource with 1384 bytes
24140 INFO: Writing RT_ICON 4 resource with 37019 bytes
24140 INFO: Writing RT_ICON 5 resource with 9640 bytes
24140 INFO: Writing RT_ICON 6 resource with 4264 bytes
24140 INFO: Writing RT_ICON 7 resource with 1128 bytes
24156 INFO: Appending archive to EXE D:\py\MFGTool\build\mfgtool\mfgtool.exe
24327 INFO: Building EXE from EXE-00.toc completed successfully.
24327 INFO: checking COLLECT
WARNING: The output directory "D:\py\MFGTool\dist\mfgtool" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)y
On your own risk, you can use the option `--noconfirm` to get rid of this question.
36014 INFO: Removing dir D:\py\MFGTool\dist\mfgtool
36093 INFO: Building COLLECT COLLECT-00.toc
37921 INFO: Building COLLECT COLLECT-00.toc completed successfully.

D:\py\MFGTool>

Go directly to dist\mfgtool \ directory to run the packaged mfgtool Exe, a box will pop up and report an error:

The above are all default parameters with console (DOS like interface at runtime).
Run directly from the command line:

D:\py\MFGTool\dist\mfgtool>mfgtool.exe
Traceback (most recent call last):
  File "mfgtool.py", line 8, in <module>
  File "C:\Users\Gary\AppData\Local\Temp\embedded.r2luoun3.zip\shibokensupport\__feature__.py", line 142, in _import
ImportError: could not import module 'PySide2.QtXml'
[544] Failed to execute script mfgtool

D:\py\MFGTool\dist\mfgtool>

The operation prompt is missing 'pyside2 Qtxml ', this library is not used in our actual code development, but is required during packaging, so we need to hide it with the parameter "– hidden import" mentioned above. In addition, why bring the console when packaging? This is convenient to find problems. If there is no console to run directly, you can't see where the real error is.

Now try again with this parameter:

D:\py\MFGTool>pyinstaller mfgtool.py --hidden-import PySide2.QtXml
D:\py\MFGTool\dist\mfgtool>mfgtool.exe
QIODevice::read (QFile, "mfgtool.ui"): device not open
Designer: An error has occurred while reading the UI file at line 1, column 0: Premature end of document.
Traceback (most recent call last):
  File "mfgtool.py", line 245, in <module>
  File "mfgtool.py", line 17, in __init__
  File "mfgtool.py", line 29, in load_ui
RuntimeError: Unable to open/read ui device
[440] Failed to execute script mfgtool

D:\py\MFGTool\dist\mfgtool>

After running, a new error is reported, "RuntimeError: Unable to open/read ui device", because our program loading interface is dynamic and needs to be corresponding to the interface Copy the ui file to the location of the exe program. Our program is a serial communication demonstration program, and the corresponding ui interface file is called mfgtool ui, so copy this file to mfgtool exe (packaged executable file), which is under dist\mfgtool \ by default.
After copying it, double-click the exe file directly to see the UI interface. At this time, you will see a black dos interface in the back. The way to remove it is to add a parameter "- w" and repackage it, and then run again, you will not see the back console window, as follows:

D:\py\MFGTool>pyinstaller mfgtool.py --hidden-import PySide2.QtXml -w

The program interface after dynamic loading UI operation is as follows:

In fact, one part of the program interface is not fully displayed. The following is part of the code for the initialization of the program. We can see from the code that we have set the theme title of the program, but the theme name here is "Mfgtool" (the default theme name).

    def initUI(self):
        desktop = QApplication.desktop()
        self.screenWidth = desktop.width() * 0.4
        self.screenHeight = desktop.height() * 0.6
#        print("Screen width:", self.screenWidth, "height:", self.screenHeight)
        self.setGeometry(0, 0, self.screenWidth, self.screenHeight)
        self.setWindowTitle("MfgTool V1.0.0") #The correct answer should be self ui. setWindowTitle("MfgTool V1.0.0")

The above problem is actually a deficiency of dynamically loading the UI. This may be a problem in the design of PySide2, which may be improved in the future. The following is the scenario of this problem:

  1. The UI loading process is placed in a custom class

  2. Create an instance of the above class in the main function

  3. The code program is as follows:

class MfgTool(QMainWindow):
    def __init__(self):
        super(MfgTool, self).__init__()
        self.load_ui()

    def load_ui(self):
        loader = QUiLoader()
        path = os.path.join(os.path.dirname(__file__), "mfgtool.ui")
        ui_file = QFile(path)
        ui_file.open(QFile.ReadOnly)
        self.ui = loader.load(ui_file, self)
        ui_file.close()
        self.ui.show()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainUI = MfgTool()
    sys.exit(app.exec_())

Problems with dynamic loading UI scenarios above:

  1. self is our custom class, not ui, so the above theme setting is invalid

  2. 1 is not a big problem. The most fatal problem is that our main window QMainWindow and our ui are independent of each other, that is, the ui will not be loaded in the main window QMainWindow. Therefore, the interface after the above code is run is as shown in the figure above, but careful people may find that there is no program icon in my win10 (my development host is win10) taskbar. Why not? Because the icon is the main program, and the main program QMainWindow of the above code is not shown at all. Let's show it below, You need to add a line in Main:

mainUI.show()

Or, directly in load_ Adding a line at the end of the UI function has the same effect:

self.show()


How can there be two displayed interfaces? Indeed, there are two. The empty one is the main program of QMainWindow, and the win10 taskbar also has a main program icon (python icon by default). This display result also explains the problem in 2 above. The main program and our UI are two independent interfaces.

  1. In addition, question 2 above will also bring a series of problems, such as the event that will not trigger QMainWindow, and so on. Of course, to receive window events, you need to rewrite the corresponding event interface, and the rewriting effect is not the best, so I won't explain it in detail here. Others have encountered the same problem. Here is a post written by a foreigner. The problem is roughly the same as that I encountered above.

https://stackoverflow.com/questions/53828666/pyside2-qmainwindow-loaded-from-ui-file-not-triggering-window-events

Summary:

  1. Seeing this, I wonder if I'm a little disappointed with PySide2's development of python GUI. Don't worry. It's just a design of PySide2. If you study PyQt5, you'll find that it doesn't have this problem. It treats the ui as a part of the main interface QMainWindow, that is, the parent-child relationship in the class.
    In addition, according to the official practice of Qt, the action of loading ui is directly placed in main, so there will be no above problems. The following is an example of direct copy:
# File: main.py
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication
from PySide2.QtCore import QFile, QIODevice

if __name__ == "__main__":
    app = QApplication(sys.argv)

    ui_file_name = "mainwindow.ui"
    ui_file = QFile(ui_file_name)
    if not ui_file.open(QIODevice.ReadOnly):
        print("Cannot open {}: {}".format(ui_file_name, ui_file.errorString()))
        sys.exit(-1)
    loader = QUiLoader()
    window = loader.load(ui_file)
    ui_file.close()
    if not window:
        print(loader.errorString())
        sys.exit(-1)
    window.show()

    sys.exit(app.exec_())
  1. The best UI calling method is static loading, so that events can be overwritten, which is very similar to using Qt IDE development tools directly.

The GUI interface is loaded statically:
First, PySide2 provides a tool to convert ui into py files:

pyside2-uic mfgtool.ui > ui_mfgtool.py

If you are familiar with Qt development, the converted py file is very similar to the converted UI interface file by Qt Creator. Yes, in fact, their principles are similar. They all build the UI content into a class, and then call this class directly. The following is the partially converted code:

## Created by: Qt User Interface Compiler version 5.15.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


class Ui_MfgTool(object):
    def setupUi(self, MfgTool):
        if not MfgTool.objectName():
            MfgTool.setObjectName(u"MfgTool")
        MfgTool.resize(800, 600)
        MfgTool.setStyleSheet(u"QLabel{\n"
"font:20px;\n"
"}\n"
"QPushButton{\n"
"font:25px;\n"
"}\n"
"QComboBox,QLineEdit{\n"
"font:18px;\n"
"}")
        self.centralwidget = QWidget(MfgTool)
        self.centralwidget.setObjectName(u"centralwidget")
        self.label_port = QLabel(self.centralwidget)
        self.label_port.setObjectName(u"label_port")
        self.label_port.setGeometry(QRect(139, 30, 141, 31))
        self.label_port.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
......
        MfgTool.setStatusBar(self.statusbar)

        self.retranslateUi(MfgTool)

        QMetaObject.connectSlotsByName(MfgTool)
    # setupUi

    def retranslateUi(self, MfgTool):
        MfgTool.setWindowTitle(QCoreApplication.translate("MfgTool", u"MfgTool", None))
        self.label_port.setText(QCoreApplication.translate("MfgTool", u"Serial Port:", None))
        self.pushButton_refresh.setText(QCoreApplication.translate("MfgTool", u"Refresh", None))
        self.pushButton_open.setText(QCoreApplication.translate("MfgTool", u"Open", None))
        self.pushButton_read.setText(QCoreApplication.translate("MfgTool", u"Read", None))
        self.pushButton_write.setText(QCoreApplication.translate("MfgTool", u"Write", None))
        self.label_type.setText(QCoreApplication.translate("MfgTool", u"Device Type:", None))
        self.label_sn.setText(QCoreApplication.translate("MfgTool", u"Device S/N:", None))
        self.label_mfgsn.setText(QCoreApplication.translate("MfgTool", u"MFG S/N:", None))
        self.label_pdate.setText(QCoreApplication.translate("MfgTool", u"PDate:", None))
        self.label_hwver.setText(QCoreApplication.translate("MfgTool", u"HW Version:", None))
    # retranslateUi

Let's look at the main program:

from ui_mfgtool import Ui_MfgTool


class MfgTool(QMainWindow):
    def __init__(self):
        super(MfgTool, self).__init__()
#        self.load_ui()
        self.ui = Ui_MfgTool()
        self.ui.setupUi(self)
        self.initUI()
        self.ConnectFun()

........

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainUI = MfgTool()
    mainUI.show()
    sys.exit(app.exec_())

In this way, we first run it in the command line mode, and the interface is as follows:

It can be seen from the above figure that the theme title of the program is also displayed normally, the win10 taskbar also has the main program icon, and there are no two interfaces (QMainWindow and ui). This is the advantage of static loading. It has the same effect as the development under Qt Creator. It is perfect!

Finally, let's demonstrate that the main program window and Ui have been perfectly docked, which is completely the relationship between parent and child classes.

    def initUI(self):
        desktop = QApplication.desktop()
        self.screenWidth = desktop.width() * 0.4
        self.screenHeight = desktop.height() * 0.6
#        print("Screen width:", self.screenWidth, "height:", self.screenHeight)
        self.setGeometry(0, 0, self.screenWidth, self.screenHeight)
        self.ui.setWindowTitle("MfgTool V1.0.0")#Static loading should be written as self setWindowTitle("MfgTool V1.0.0")

If you set the title in the above code to be written in the dynamic loading mode, you will encounter the following error:

D:\py\MFGTool>python mfgtool.py
Traceback (most recent call last):
  File "D:\py\MFGTool\mfgtool.py", line 245, in <module>
    mainUI = MfgTool()
  File "D:\py\MFGTool\mfgtool.py", line 20, in __init__
    self.initUI()
  File "D:\py\MFGTool\mfgtool.py", line 39, in initUI
    self.ui.setWindowTitle("MfgTool V1.0.0")
AttributeError: 'Ui_MfgTool' object has no attribute 'setWindowTitle'

Now I've finished introducing the static loading UI. To package it into exe, the method is the same as the instructions used for dynamic packaging. It doesn't repeat. It means that you don't have to copy the UI file to the packaging folder.

In addition, if you only want to package into an exe executable file without other associated libraries and other files, you can use the "- F" parameter, which will only package into an exe file, but the exe file is a little large.

Keywords: Python

Added by DaveLinger on Sat, 22 Jan 2022 02:46:39 +0200