qt-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseQt Application Architecture
Qt应用架构
Entry-Point Pattern
入口点模式
Every Qt application requires exactly one (widgets) or (QML-only) instance. Create it before any widgets.
QApplicationQGuiApplicationPython/PySide6 canonical entry point:
python
undefined每个Qt应用都需要恰好一个(用于组件)或(仅用于QML)实例。需在创建任何组件之前初始化它。
QApplicationQGuiApplicationPython/PySide6标准入口点:
python
undefinedsrc/myapp/main.py
src/myapp/main.py
import sys
from PySide6.QtWidgets import QApplication
from myapp.ui.main_window import MainWindow
def main() -> None:
app = QApplication(sys.argv)
app.setApplicationName("MyApp")
app.setOrganizationName("MyOrg")
app.setOrganizationDomain("myorg.com")
window = MainWindow()
window.show()
sys.exit(app.exec())
if name == "main":
main()
Using `__main__.py` enables `python -m myapp` invocation. Set `applicationName` and `organizationName` before creating any widgets — these values seed `QSettings`.
**C++/Qt canonical main.cpp:**
```cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
app.setApplicationName("MyApp");
app.setOrganizationName("MyOrg");
MainWindow window;
window.show();
return app.exec();
}import sys
from PySide6.QtWidgets import QApplication
from myapp.ui.main_window import MainWindow
def main() -> None:
app = QApplication(sys.argv)
app.setApplicationName("MyApp")
app.setOrganizationName("MyOrg")
app.setOrganizationDomain("myorg.com")
window = MainWindow()
window.show()
sys.exit(app.exec())
if name == "main":
main()
使用`__main__.py`支持通过`python -m myapp`方式启动应用。需在创建任何组件之前设置`applicationName`和`organizationName`——这些值会作为`QSettings`的初始配置。
**C++/Qt标准main.cpp:**
```cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
app.setApplicationName("MyApp");
app.setOrganizationName("MyOrg");
MainWindow window;
window.show();
return app.exec();
}Project Layout (Python/PySide6)
项目布局(Python/PySide6)
Use layout to prevent accidental imports from the project root:
srcmy-qt-app/
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── __main__.py # Entry point
│ ├── ui/
│ │ ├── __init__.py
│ │ ├── main_window.py # QMainWindow subclass
│ │ ├── dialogs/ # QDialog subclasses
│ │ └── widgets/ # Custom QWidget subclasses
│ ├── models/ # Data models (non-Qt)
│ ├── services/ # Business logic, I/O
│ └── resources/ # .qrc compiled output
├── tests/
│ ├── conftest.py
│ └── test_*.py
├── resources/
│ ├── icons/
│ └── resources.qrc
├── pyproject.toml
└── .qt-test.json # qt-test-suite configKeep , , and separate. UI code should never contain business logic.
ui/models/services/使用目录布局可避免从项目根目录意外导入模块:
srcmy-qt-app/
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── __main__.py # 入口点
│ ├── ui/
│ │ ├── __init__.py
│ │ ├── main_window.py # QMainWindow子类
│ │ ├── dialogs/ # QDialog子类
│ │ └── widgets/ # 自定义QWidget子类
│ ├── models/ # 数据模型(非Qt)
│ ├── services/ # 业务逻辑、I/O操作
│ └── resources/ # .qrc编译输出
├── tests/
│ ├── conftest.py
│ └── test_*.py
├── resources/
│ ├── icons/
│ └── resources.qrc
├── pyproject.toml
└── .qt-test.json # qt-test-suite配置文件将、和目录分离。UI代码中绝不应包含业务逻辑。
ui/models/services/QMainWindow Structure
QMainWindow结构
python
undefinedpython
undefinedsrc/myapp/ui/main_window.py
src/myapp/ui/main_window.py
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout
from PySide6.QtCore import Qt
class MainWindow(QMainWindow):
def init(self, parent: QWidget | None = None) -> None:
super().init(parent)
self.setWindowTitle("MyApp")
self.setMinimumSize(800, 600)
self._setup_ui()
self._setup_menu()
self._connect_signals()
def _setup_ui(self) -> None:
"""Build central widget and layout."""
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# Add widgets to layout here
def _setup_menu(self) -> None:
"""Build menu bar and actions."""
pass
def _connect_signals(self) -> None:
"""Wire all signal→slot connections."""
pass
Separate `_setup_ui`, `_setup_menu`, and `_connect_signals` into distinct methods. This makes each responsibility findable and testable.from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout
from PySide6.QtCore import Qt
class MainWindow(QMainWindow):
def init(self, parent: QWidget | None = None) -> None:
super().init(parent)
self.setWindowTitle("MyApp")
self.setMinimumSize(800, 600)
self._setup_ui()
self._setup_menu()
self._connect_signals()
def _setup_ui(self) -> None:
"""构建中心组件与布局。"""
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# 在此处向布局中添加组件
def _setup_menu(self) -> None:
"""构建菜单栏与动作。"""
pass
def _connect_signals(self) -> None:
"""连接所有信号→槽。"""
pass
将`_setup_ui`、`_setup_menu`和`_connect_signals`拆分为独立方法。这会让每个职责更易查找和测试。Architectural Patterns
架构模式
MVP (Model-View-Presenter) — preferred for testable Qt applications:
- Model: Pure Python classes, no Qt imports. Holds data and business logic.
- View: QWidget subclasses. Emits signals for user actions; receives data to display.
- Presenter: Mediates between Model and View. Contains decision logic. Testable without Qt.
python
undefinedMVP(模型-视图-表示器)——适用于可测试的Qt应用:
- 模型:纯Python类,无Qt导入。存储数据并实现业务逻辑。
- 视图:QWidget子类。为用户操作发出信号;接收数据并展示。
- 表示器:作为模型与视图之间的中介。包含决策逻辑。无需Qt即可测试。
python
undefinedPresenter owns the view and model
表示器持有视图和模型
class CalculatorPresenter:
def init(self, view: CalculatorView, model: CalculatorModel) -> None:
self._view = view
self._model = model
view.calculate_requested.connect(self._on_calculate)
def _on_calculate(self, expression: str) -> None:
result = self._model.evaluate(expression)
self._view.display_result(result)
**MVC** maps less naturally to Qt's signal/slot system. MVP is the idiomatic choice.
**For simple apps**: Direct signal/slot connections are fine. Introduce MVP when you need unit-testable business logic.class CalculatorPresenter:
def init(self, view: CalculatorView, model: CalculatorModel) -> None:
self._view = view
self._model = model
view.calculate_requested.connect(self._on_calculate)
def _on_calculate(self, expression: str) -> None:
result = self._model.evaluate(expression)
self._view.display_result(result)
**MVC**与Qt的信号/槽系统适配性较差。MVP是更符合Qt习惯的选择。
**对于简单应用**:直接的信号/槽连接即可满足需求。当你需要可单元测试的业务逻辑时,再引入MVP模式。pyproject.toml Configuration
pyproject.toml配置
toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["PySide6>=6.6"]
[project.scripts]
myapp = "myapp.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["src/myapp"]
[tool.pytest.ini_options]
testpaths = ["tests"]
qt_api = "pyside6"
[tool.pyright]
pythonVersion = "3.11"
include = ["src"]toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["PySide6>=6.6"]
[project.scripts]
myapp = "myapp.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["src/myapp"]
[tool.pytest.ini_options]
testpaths = ["tests"]
qt_api = "pyside6"
[tool.pyright]
pythonVersion = "3.11"
include = ["src"]Qt Project Config (.qt-test.json)
Qt项目配置文件(.qt-test.json)
Always create this at project root for compatibility:
qt-test-suitejson
{
"project_type": "python",
"app_entry": "src/myapp/__main__.py",
"test_dir": "tests/",
"coverage_source": ["src/myapp"]
}为了兼容,请务必在项目根目录创建该文件:
qt-test-suitejson
{
"project_type": "python",
"app_entry": "src/myapp/__main__.py",
"test_dir": "tests/",
"coverage_source": ["src/myapp"]
}Critical Constraints
关键约束
- One per process — never create it twice or inside a function that may be called multiple times
QApplication - All widget creation must happen after is constructed
QApplication - Widgets created without a parent become top-level windows; always pass to avoid orphaned widgets
parent - Never store Qt objects (QWidget, QObject) in module-level globals — deferred destruction causes segfaults
- blocks until the last window closes; all application logic runs via signals/slots within this loop
app.exec()
- 每个进程只能有一个实例——绝不能创建多个,也不能在可能被多次调用的函数内部创建它
QApplication - 所有组件的创建必须在实例化之后进行
QApplication - 未指定父组件的组件会成为顶级窗口;请始终传入参数以避免出现孤立组件
parent - 绝不要将Qt对象(QWidget、QObject)存储在模块级全局变量中——延迟销毁会导致段错误
- 会阻塞直到最后一个窗口关闭;所有应用逻辑都通过该循环内的信号/槽运行
app.exec()