├── tests └── __init__.py ├── src ├── minimum_qt_for_python_desktop_application_demo_desktop │ ├── __init__.py │ ├── ui │ │ ├── __init__.py │ │ ├── main_window.py │ │ └── main_window.ui │ ├── ui_model │ │ ├── __init__.py │ │ └── main_window_model.py │ └── main.py └── minimum_qt_for_python_desktop_application_demo │ ├── __init__.py │ └── log.py ├── docs └── screenshot.png ├── pyproject.toml ├── README.md ├── pdm.lock └── .gitignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .main_window import QMainWindowExt 2 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/ui_model/__init__.py: -------------------------------------------------------------------------------- 1 | from .main_window_model import MainWindowModel 2 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjohannchang/minimum-qt-for-python-desktop-application-demo/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | from .log import get_logger, init_logger 4 | 5 | VERSION: str = importlib.metadata.version("minimum-qt-for-python-desktop-application-demo") 6 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/ui_model/main_window_model.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QObject 2 | 3 | 4 | class MainWindowModel(QObject): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | 8 | self.title: str = "" 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "minimum-qt-for-python-desktop-application-demo" 3 | version = "1.0.0" 4 | description = "Minimum Qt for Python Desktop Application Demo" 5 | authors = [ 6 | {name = "Johann Chang", email = "johann.chang@outlook.com"}, 7 | ] 8 | dependencies = [ 9 | "PySide6>=6.6.1", 10 | ] 11 | requires-python = ">=3.11, <3.13" 12 | readme = "README.md" 13 | license = {text = "MPLv2"} 14 | 15 | [project.scripts] 16 | desktop-app = "minimum_qt_for_python_desktop_application_demo_desktop.main:main" 17 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo/log.py: -------------------------------------------------------------------------------- 1 | import logging.handlers 2 | import sys 3 | 4 | inited: bool = False 5 | 6 | 7 | def init_logger() -> None: 8 | global inited 9 | if inited: 10 | return 11 | 12 | logger: logging.Logger = logging.getLogger() 13 | formatter: logging.Formatter = logging.Formatter( 14 | '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s') 15 | 16 | handler = logging.StreamHandler() 17 | handler.setFormatter(formatter) 18 | handler.setLevel(logging.DEBUG) 19 | 20 | logger.addHandler(handler) 21 | logger.setLevel(logging.DEBUG) 22 | inited = True 23 | 24 | 25 | def get_logger(name: str = str(__package__)) -> logging.Logger: 26 | if not inited: 27 | print('Logger is not initialized yet', file=sys.stderr) 28 | return logging.getLogger(name) 29 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/main.py: -------------------------------------------------------------------------------- 1 | import importlib.resources 2 | import pathlib 3 | import sys 4 | 5 | import minimum_qt_for_python_desktop_application_demo as sdk 6 | from PySide6.QtCore import QFile, QIODevice 7 | from PySide6.QtUiTools import QUiLoader 8 | from PySide6.QtWidgets import QApplication 9 | from . import ui 10 | 11 | 12 | def main() -> int: 13 | sdk.init_logger() 14 | 15 | app: QApplication = QApplication(sys.argv) 16 | 17 | ui_loader: QUiLoader = QUiLoader() 18 | ui_loader.registerCustomWidget(ui.QMainWindowExt) 19 | 20 | ui_path: pathlib.Path 21 | with importlib.resources.path(ui, 'main_window.ui') as ui_path: 22 | ui_file: QFile = QFile(str(ui_path)) 23 | if not ui_file.open(QIODevice.ReadOnly): 24 | raise RuntimeError(f"Cannot open {ui_path}: {ui_file.errorString()}") 25 | main_window: ui.QMainWindowExt = ui_loader.load(ui_file) 26 | ui_file.close() 27 | main_window.set_up() 28 | main_window.show() 29 | return app.exec_() 30 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/ui/main_window.py: -------------------------------------------------------------------------------- 1 | import minimum_qt_for_python_desktop_application_demo as sdk 2 | from PySide6.QtGui import QCloseEvent 3 | from PySide6.QtWidgets import QMainWindow, QPushButton 4 | from .. import ui_model 5 | 6 | 7 | class QMainWindowExt(QMainWindow): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | 11 | self.main_window_model: ui_model.MainWindowModel = ui_model.MainWindowModel() 12 | self.main_window_model.title = f"Minimum Qt for Python Desktop Application v{sdk.VERSION}" 13 | 14 | def closeEvent(self, event: QCloseEvent) -> None: 15 | sdk.get_logger().info(f"Close event: {event}") 16 | super().closeEvent(event) 17 | 18 | def set_up(self) -> None: 19 | sdk.get_logger().info("Setting up") 20 | self.setWindowTitle(self.main_window_model.title) 21 | 22 | # The following line is optional. This is only for the IDE to recognize the push_button attribute. 23 | self.push_button: QPushButton = getattr(self, 'push_button') 24 | 25 | self.push_button.clicked.connect(self.on_push_button_clicked_listener) 26 | 27 | def on_push_button_clicked_listener(self, checked: bool) -> None: 28 | sdk.get_logger().info(f"Push button clicked, checked: {checked}") 29 | -------------------------------------------------------------------------------- /src/minimum_qt_for_python_desktop_application_demo_desktop/ui/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 25 | 26 | 27 | PushButton 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 800 39 | 24 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | QMainWindowExt 48 | QMainWindow 49 |
qmainwindowext.h
50 | 1 51 |
52 |
53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimum Qt for Python Desktop Application Demo 2 | 3 | ![screenshot](docs/screenshot.png) 4 | 5 | **This is only for demonstrating and reference.** 6 | 7 | ## Usage 8 | 9 | ``` 10 | pdm run desktop-app 11 | ``` 12 | 13 | ## Installation 14 | 15 | Install with [pipx](https://pipx.pypa.io/stable/installation/) 16 | 17 | ``` 18 | pipx install git+https://github.com/mrjohannchang/minimum-qt-for-python-desktop-application-demo.git 19 | ``` 20 | 21 | ## Development 22 | 23 | ### Design concepts 24 | 25 | 1. [Object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) 26 | 2. [Model–view–viewmodel (MVVM)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) 27 | 28 | ### Environment 29 | 30 | 1. [Python 3.11+](https://www.python.org/) 31 | 2. [Qt for Python](https://doc.qt.io/qtforpython) 32 | 3. [PDM](https://pdm-project.org/) 33 | 4. [Git](https://git-scm.com/) 34 | 35 | ### Setup 36 | 37 | ``` 38 | pdm install 39 | ``` 40 | 41 | ### Step by step reference 42 | 43 | 1. Create an empty project with [Git](https://git-scm.com/). 44 | 45 | ``` 46 | git init 47 | ``` 48 | 49 | 2. Initialize the Python project with [PDM](https://pdm-project.org/). 50 | 51 | ``` 52 | pdm init 53 | ``` 54 | 55 | 3. Add [Qt for Python](https://doc.qt.io/qtforpython) as a dependent library. 56 | 57 | ``` 58 | pdm add PySide6 59 | ``` 60 | 61 | 4. Create a Main Window [Qt Designer UI file](https://doc.qt.io/qt-6/designer-ui-file-format.html) with [Qt Designer](https://doc.qt.io/qt-6/qtdesigner-manual.html). 62 | 63 | ``` 64 | mkdir src/minimum_qt_for_python_desktop_application_demo_desktop 65 | touch src/minimum_qt_for_python_desktop_application_demo_desktop/__init__.py 66 | mkdir src/minimum_qt_for_python_desktop_application_demo_desktop/ui 67 | touch src/minimum_qt_for_python_desktop_application_demo_desktop/ui/__init__.py 68 | pdm run pyside6-designer 69 | ``` 70 | 71 | 5. Create the desktop application entry point. If you chose [PEP 582 – Python local packages directory](https://peps.python.org/pep-0582/) for this project and plan to use PyCharm, at the time of writing, PyCharm does not support PEP 582 directly. Please manually set `__pypackages__//lib` and `src` as [Sources Root](https://www.jetbrains.com/help/pycharm/configuring-project-structure.html#mark-dir-project-view). 72 | 73 | 1. After configuring the script file in `pyproject.toml`, we need to install it: 74 | 75 | ``` 76 | pdm install 77 | ``` 78 | 79 | 2. Then we will be able to run it: 80 | 81 | ``` 82 | pdm run desktop-app 83 | ``` 84 | 85 | 6. Add the logging function to the custom SDK. 86 | 87 | 7. Use custom `QMainWindow` class to add the window exit hook. 88 | 89 | **Note**: This is undocumented but IMHO the most authenticated way to do it. 90 | 91 | 8. Demonstrate how to hook the UI building 92 | 93 | 9. Demonstrate how to get the component from the Qt Designer UI file 94 | 95 | 10. Demonstrate the usage of [model–view–viewmodel (MVVM)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). 96 | 97 | ## License 98 | 99 | [Mozilla Public License Version 2.0](https://www.mozilla.org/en-US/MPL/2.0/) 100 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default"] 6 | strategy = ["cross_platform"] 7 | lock_version = "4.4" 8 | content_hash = "sha256:8c2ff6b731a6d727082dec98e689799fcc2ae9a86fc904c7991d9f55ac2d2151" 9 | 10 | [[package]] 11 | name = "pyside6" 12 | version = "6.6.1" 13 | requires_python = "<3.13,>=3.8" 14 | summary = "Python bindings for the Qt cross-platform application and UI framework" 15 | dependencies = [ 16 | "PySide6-Addons==6.6.1", 17 | "PySide6-Essentials==6.6.1", 18 | "shiboken6==6.6.1", 19 | ] 20 | files = [ 21 | {file = "PySide6-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:3c348948fe3957b18164c9c7b8942fe065bdb39648b326f212bc114326679fa9"}, 22 | {file = "PySide6-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a67587c088cb80e90d4ce3023b02466ea858c93a6dc9c4e062b13314e03d464"}, 23 | {file = "PySide6-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:ed3822150f0d7a06b68bf4ceebe287515b5e8309bb256e9b49ae405afd062b18"}, 24 | {file = "PySide6-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:3593d605175e83e6952cf3b428ecc9c146af97effb36de921ecf3da2752de082"}, 25 | ] 26 | 27 | [[package]] 28 | name = "pyside6-addons" 29 | version = "6.6.1" 30 | requires_python = "<3.13,>=3.8" 31 | summary = "Python bindings for the Qt cross-platform application and UI framework (Addons)" 32 | dependencies = [ 33 | "PySide6-Essentials==6.6.1", 34 | "shiboken6==6.6.1", 35 | ] 36 | files = [ 37 | {file = "PySide6_Addons-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:7cb7af1b050c40f7ac891b0e61c758c1923863173932f5b92dc47bdfb4158b42"}, 38 | {file = "PySide6_Addons-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0982da4033319667f9df5ed6fa8eff300a88216aec103a1fff6751a172b19a0"}, 39 | {file = "PySide6_Addons-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:5a63a8a943724ce5acd2df72e5ab04982b6906963278cbabb216656b9a26ee18"}, 40 | {file = "PySide6_Addons-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a223575c81e9a13173136c044c3447e25f6d656b462b4d71fc3c6bd9c935a709"}, 41 | ] 42 | 43 | [[package]] 44 | name = "pyside6-essentials" 45 | version = "6.6.1" 46 | requires_python = "<3.13,>=3.8" 47 | summary = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" 48 | dependencies = [ 49 | "shiboken6==6.6.1", 50 | ] 51 | files = [ 52 | {file = "PySide6_Essentials-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:0c8917b15236956957178a8c9854641b12b11dad79ba0caf26147119164c30cf"}, 53 | {file = "PySide6_Essentials-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c7185616083eab6f42eaed598d97d49fac4f60ae2e7415194140d54f58c2b42c"}, 54 | {file = "PySide6_Essentials-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:a383c3d60298392cfb621ec1a0cf24b4799321e6c5bbafc021d4cc8076ea1315"}, 55 | {file = "PySide6_Essentials-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:13da926e9e9ee3e26e3f66883a9d5e43726ddee70cdabddca02a07aa1ccf9484"}, 56 | ] 57 | 58 | [[package]] 59 | name = "shiboken6" 60 | version = "6.6.1" 61 | requires_python = "<3.13,>=3.8" 62 | summary = "Python/C++ bindings helper module" 63 | files = [ 64 | {file = "shiboken6-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:d756fd1fa945b787e8eef142f2eb571da0b4c4dc2f2eec1a7c12a474a2cf84e4"}, 65 | {file = "shiboken6-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fb102e4bc210006f0cdd0ce38e1aaaaf792bd871f02a2b3f01d07922c5cf4c59"}, 66 | {file = "shiboken6-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:a605960e72af5eef915991cee7eef4cc72f5cabe63b9ae1a955ceb3d3b0a00b9"}, 67 | {file = "shiboken6-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:072c35c4fe46ec13b364d9dc47b055bb2277ee3aeaab18c23650280ec362f62a"}, 68 | ] 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm-project.org/#use-with-ide 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | --------------------------------------------------------------------------------