qt-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Qt Application Architecture

Qt应用架构

Entry-Point Pattern

入口点模式

Every Qt application requires exactly one
QApplication
(widgets) or
QGuiApplication
(QML-only) instance. Create it before any widgets.
Python/PySide6 canonical entry point:
python
undefined
每个Qt应用都需要恰好一个
QApplication
(用于组件)或
QGuiApplication
(仅用于QML)实例。需在创建任何组件之前初始化它。
Python/PySide6标准入口点:
python
undefined

src/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
src
layout to prevent accidental imports from the project root:
my-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 config
Keep
ui/
,
models/
, and
services/
separate. UI code should never contain business logic.
使用
src
目录布局可避免从项目根目录意外导入模块:
my-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/
models/
services/
目录分离。UI代码中绝不应包含业务逻辑。

QMainWindow Structure

QMainWindow结构

python
undefined
python
undefined

src/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
undefined
MVP(模型-视图-表示器)——适用于可测试的Qt应用:
  • 模型:纯Python类,无Qt导入。存储数据并实现业务逻辑。
  • 视图:QWidget子类。为用户操作发出信号;接收数据并展示。
  • 表示器:作为模型与视图之间的中介。包含决策逻辑。无需Qt即可测试。
python
undefined

Presenter 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
qt-test-suite
compatibility:
json
{
  "project_type": "python",
  "app_entry": "src/myapp/__main__.py",
  "test_dir": "tests/",
  "coverage_source": ["src/myapp"]
}
为了兼容
qt-test-suite
,请务必在项目根目录创建该文件:
json
{
  "project_type": "python",
  "app_entry": "src/myapp/__main__.py",
  "test_dir": "tests/",
  "coverage_source": ["src/myapp"]
}

Critical Constraints

关键约束

  • One
    QApplication
    per process — never create it twice or inside a function that may be called multiple times
  • All widget creation must happen after
    QApplication
    is constructed
  • Widgets created without a parent become top-level windows; always pass
    parent
    to avoid orphaned widgets
  • Never store Qt objects (QWidget, QObject) in module-level globals — deferred destruction causes segfaults
  • app.exec()
    blocks until the last window closes; all application logic runs via signals/slots within this loop
  • 每个进程只能有一个
    QApplication
    实例——绝不能创建多个,也不能在可能被多次调用的函数内部创建它
  • 所有组件的创建必须在
    QApplication
    实例化之后进行
  • 未指定父组件的组件会成为顶级窗口;请始终传入
    parent
    参数以避免出现孤立组件
  • 绝不要将Qt对象(QWidget、QObject)存储在模块级全局变量中——延迟销毁会导致段错误
  • app.exec()
    会阻塞直到最后一个窗口关闭;所有应用逻辑都通过该循环内的信号/槽运行