├── 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 |
43 |
44 |
45 |
46 |
47 | QMainWindowExt
48 | QMainWindow
49 |
50 | 1
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Minimum Qt for Python Desktop Application Demo
2 |
3 | 
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 |
--------------------------------------------------------------------------------