强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Qt 与 GTK 图形框架教程 / 10 - Python 绑定 / Python Bindings

Python 绑定 / Python Bindings

掌握 Qt 与 GTK 的 Python 绑定:PyQt6、PySide6 和 PyGObject。 Master Python bindings for Qt and GTK: PyQt6, PySide6, and PyGObject.


10.1 Python 绑定对比 / Python Binding Comparison

特性 / FeaturePyQt6PySide6PyGObject (GTK)
绑定框架sipshibokenGObject Introspection
许可证GPLv3 / 商业LGPLv3LGPLv2.1+
API 兼容Qt6 完整Qt6 完整GTK4 + libadwaita
类型提示✅ 完整✅ 完整⚠️ 部分
IDE 支持优秀优秀中等
文档优秀优秀GTK 文档
C++ 扩展sip 模块shibokenPyCapsule
官方维护Riverbank ComputingQt CompanyGNOME
学习资源丰富丰富中等

许可证选择指南 / License Selection Guide

项目类型 / Project Type推荐 / Recommended
开源项目 (GPL)PyQt6 或 PySide6
开源项目 (宽松许可)PySide6 (LGPL)
商业闭源项目PySide6 (LGPL) 或 PyQt6 商业版
GNOME 生态项目PyGObject

⚠️ PyQt6 的 GPLv3 限制

PyQt6 开源版使用 GPLv3,你的项目必须也是 GPLv3 兼容。 商业闭源项目必须购买 PyQt6 商业许可。 PySide6 使用 LGPLv3,无此限制。


10.2 PySide6 完整示例 / PySide6 Complete Example

项目结构 / Project Structure

myapp/
├── main.py
├── mainwindow.py
├── models/
│   └── user_model.py
├── views/
│   └── user_view.py
├── resources/
│   └── icons/
├── requirements.txt
└── pyproject.toml

main.py

#!/usr/bin/env python3
"""PySide6 应用入口"""

import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt, QTranslator, QLocale
from PySide6.QtGui import QIcon
from mainwindow import MainWindow


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("MyApp")
    app.setOrganizationName("MyCompany")
    app.setApplicationVersion("1.0.0")

    # 国际化
    translator = QTranslator()
    locale = QLocale.system().name()
    if translator.load(f"translations/myapp_{locale}"):
        app.installTranslator(translator)

    # 全局样式
    app.setStyleSheet("""
        QMainWindow {
            background-color: #f5f5f5;
        }
        QPushButton {
            border-radius: 6px;
            padding: 8px 16px;
            font-weight: bold;
        }
    """)

    window = MainWindow()
    window.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

mainwindow.py

"""主窗口模块"""

from PySide6.QtWidgets import (
    QMainWindow, QTabWidget, QStatusBar, QMenuBar,
    QVBoxLayout, QWidget, QLabel
)
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtCore import Slot
from views.user_view import UserView


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MyApp - PySide6 示例")
        self.resize(900, 600)
        self._setup_menus()
        self._setup_ui()
        self._setup_statusbar()

    def _setup_menus(self):
        menu_bar = self.menuBar()

        # 文件菜单
        file_menu = menu_bar.addMenu("文件(&F)")

        new_action = QAction("新建(&N)", self)
        new_action.setShortcut(QKeySequence.StandardKey.New)
        file_menu.addAction(new_action)

        file_menu.addSeparator()

        quit_action = QAction("退出(&Q)", self)
        quit_action.setShortcut(QKeySequence.StandardKey.Quit)
        quit_action.triggered.connect(self.close)
        file_menu.addAction(quit_action)

        # 视图菜单
        view_menu = menu_bar.addMenu("视图(&V)")

        # 帮助菜单
        help_menu = menu_bar.addMenu("帮助(&H)")
        about_action = QAction("关于(&A)", self)
        about_action.triggered.connect(self._show_about)
        help_menu.addAction(about_action)

    def _setup_ui(self):
        tabs = QTabWidget()
        tabs.addTab(UserView(), "用户管理")
        tabs.addTab(self._create_dashboard_tab(), "仪表盘")
        self.setCentralWidget(tabs)

    def _create_dashboard_tab(self) -> QWidget:
        widget = QWidget()
        layout = QVBoxLayout(widget)
        layout.addWidget(QLabel("仪表盘(开发中)"))
        return widget

    def _setup_statusbar(self):
        self.statusBar().showMessage("就绪", 3000)

    def _show_about(self):
        from PySide6.QtWidgets import QMessageBox
        QMessageBox.about(self, "关于",
            "MyApp v1.0.0\n\n基于 PySide6 构建的桌面应用示例")

user_view.py (CRUD 视图)

"""用户管理视图 - 完整 CRUD 示例"""

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
    QLineEdit, QSpinBox, QPushButton, QTableView,
    QMessageBox, QHeaderView, QGroupBox
)
from PySide6.QtCore import Qt, Slot, QSortFilterProxyModel
from PySide6.QtGui import QStandardItemModel, QStandardItem


class UserView(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._setup_ui()
        self._load_sample_data()

    def _setup_ui(self):
        layout = QHBoxLayout(self)

        # 左侧:表单
        form_group = QGroupBox("用户信息 / User Info")
        form = QFormLayout(form_group)

        self._name_edit = QLineEdit()
        self._name_edit.setPlaceholderText("请输入姓名")
        form.addRow("姓名:", self._name_edit)

        self._email_edit = QLineEdit()
        self._email_edit.setPlaceholderText("请输入邮箱")
        form.addRow("邮箱:", self._email_edit)

        self._age_spin = QSpinBox()
        self._age_spin.setRange(0, 150)
        form.addRow("年龄:", self._age_spin)

        # 按钮
        btn_layout = QHBoxLayout()
        self._add_btn = QPushButton("添加")
        self._add_btn.clicked.connect(self._on_add)

        self._update_btn = QPushButton("更新")
        self._update_btn.clicked.connect(self._on_update)

        self._delete_btn = QPushButton("删除")
        self._delete_btn.clicked.connect(self._on_delete)

        self._clear_btn = QPushButton("清空")
        self._clear_btn.clicked.connect(self._on_clear)

        btn_layout.addWidget(self._add_btn)
        btn_layout.addWidget(self._update_btn)
        btn_layout.addWidget(self._delete_btn)
        btn_layout.addWidget(self._clear_btn)
        form.addRow("", btn_layout)

        form_group.setFixedWidth(280)
        layout.addWidget(form_group)

        # 右侧:表格
        right_layout = QVBoxLayout()

        # 搜索
        self._search_edit = QLineEdit()
        self._search_edit.setPlaceholderText("搜索...")
        self._search_edit.textChanged.connect(self._on_search)
        right_layout.addWidget(self._search_edit)

        # 表格
        self._table = QTableView()
        self._model = QStandardItemModel()
        self._model.setHorizontalHeaderLabels(["ID", "姓名", "邮箱", "年龄"])

        self._proxy = QSortFilterProxyModel()
        self._proxy.setSourceModel(self._model)
        self._proxy.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        self._proxy.setFilterKeyColumn(-1)

        self._table.setModel(self._proxy)
        self._table.setSelectionBehavior(
            QTableView.SelectionBehavior.SelectRows)
        self._table.setAlternatingRowColors(True)
        self._table.horizontalHeader().setStretchLastSection(True)
        self._table.sortByColumn(0, Qt.SortOrder.AscendingOrder)
        self._table.clicked.connect(self._on_row_selected)

        right_layout.addWidget(self._table)
        layout.addLayout(right_layout)

        self._next_id = 1
        self._selected_row = -1

    def _load_sample_data(self):
        """加载示例数据"""
        users = [
            ("张三", "zhangsan@example.com", 28),
            ("李四", "lisi@example.com", 35),
            ("王五", "wangwu@example.com", 22),
        ]
        for name, email, age in users:
            self._add_user(name, email, age)

    def _add_user(self, name: str, email: str, age: int):
        """添加用户到模型"""
        row = [
            QStandardItem(str(self._next_id)),
            QStandardItem(name),
            QStandardItem(email),
            QStandardItem(str(age))
        ]
        self._model.appendRow(row)
        self._next_id += 1

    @Slot()
    def _on_add(self):
        name = self._name_edit.text().strip()
        email = self._email_edit.text().strip()
        age = self._age_spin.value()

        if not name or not email:
            QMessageBox.warning(self, "提示", "请填写姓名和邮箱")
            return

        self._add_user(name, email, age)
        self._on_clear()
        self.statusBar().showMessage("用户已添加", 3000) if hasattr(self, 'statusBar') else None

    @Slot()
    def _on_update(self):
        if self._selected_row < 0:
            QMessageBox.warning(self, "提示", "请先选择一行")
            return

        source_idx = self._proxy.mapToSource(
            self._proxy.index(self._selected_row, 0))
        row = source_idx.row()

        self._model.item(row, 1).setText(self._name_edit.text())
        self._model.item(row, 2).setText(self._email_edit.text())
        self._model.item(row, 3).setText(str(self._age_spin.value()))

    @Slot()
    def _on_delete(self):
        if self._selected_row < 0:
            QMessageBox.warning(self, "提示", "请先选择一行")
            return

        reply = QMessageBox.question(self, "确认", "确定要删除吗?",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)

        if reply == QMessageBox.StandardButton.Yes:
            source_idx = self._proxy.mapToSource(
                self._proxy.index(self._selected_row, 0))
            self._model.removeRow(source_idx.row())
            self._on_clear()

    @Slot()
    def _on_clear(self):
        self._name_edit.clear()
        self._email_edit.clear()
        self._age_spin.setValue(0)
        self._selected_row = -1

    @Slot()
    def _on_row_selected(self, index):
        self._selected_row = index.row()
        self._name_edit.setText(index.sibling(index.row(), 1).data())
        self._email_edit.setText(index.sibling(index.row(), 2).data())
        self._age_spin.setValue(int(index.sibling(index.row(), 3).data() or 0))

    @Slot(str)
    def _on_search(self, text: str):
        self._proxy.setFilterFixedString(text)

10.3 PyQt6 差异 / PyQt6 Differences

PySide6 和 PyQt6 API 几乎相同,主要差异:

差异 / DifferencePyQt6PySide6
模块导入from PyQt6.QtWidgets import ...from PySide6.QtWidgets import ...
枚举前缀Qt.AlignmentFlag.AlignCenterQt.AlignmentFlag.AlignCenter
exec() 方法app.exec()app.exec()
信号定义pyqtSignalSignal
槽装饰器@pyqtSlot@Slot
翻译加载相同相同
# PyQt6 对应代码差异
from PyQt6.QtWidgets import QApplication, QMainWindow
from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot, Qt

# 信号定义
class MyWidget(QWidget):
    my_signal = Signal(str, int)  # 同 PySide6

    @Slot(str, int)
    def my_slot(self, text, number):
        pass

10.4 PyGObject 详解 / PyGObject Details

GObject Introspection 工作原理

C 源码 (.h/.c)
     │
     ▼
GObject Introspection (.gir)
     │
     ▼
Typelib (.typelib)
     │
     ▼
PyGObject (gi.repository)
     │
     ▼
Python import

PyGObject 信号系统 / Signal System

"""PyGObject 信号详解"""

import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, GObject


class MyWidget(Gtk.Box):
    """自定义 GTK4 控件"""
    __gtype_name__ = "MyWidget"

    # 自定义信号
    __gsignals__ = {
        "data-changed": (GObject.SignalFlags.RUN_LAST, None, (str, int)),
        "item-selected": (GObject.SignalFlags.RUN_LAST, None, (object,)),
    }

    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        self._data = ""

        # 内部按钮
        btn = Gtk.Button(label="触发信号")
        btn.connect("clicked", self._on_clicked)
        self.append(btn)

    def _on_clicked(self, button):
        self._data = "Hello"
        # 发射自定义信号
        self.emit("data-changed", self._data, 42)

    @GObject.Property(type=str)
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        self._data = value


# 使用
def on_data_changed(widget, text, number):
    print(f"Data changed: {text}, {number}")

widget = MyWidget()
widget.connect("data-changed", on_data_changed)

类型提示(PyGObject / Pyright)

"""使用类型提示改善 PyGObject 开发体验"""
from __future__ import annotations
from typing import Optional
import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw, Gio, GLib

# 使用 TYPE_CHECKING 进行类型提示
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from gi.repository import Gtk as GtkType


class MyApp(Adw.Application):
    def __init__(self) -> None:
        super().__init__(
            application_id="com.example.myapp",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )
        self.connect("activate", self.on_activate)

    def on_activate(self, app: Adw.Application) -> None:
        window: Adw.ApplicationWindow = Adw.ApplicationWindow(application=app)
        window.set_title("Type Hinted App")
        window.set_default_size(400, 300)
        window.present()


def main() -> int:
    app = MyApp()
    return app.run(None)

10.5 PySide6 与 QML / PySide6 + QML

"""PySide6 + QML 混合开发"""

import sys
from PySide6.QtCore import QObject, Signal, Slot, Property
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine


class Backend(QObject):
    """QML 可调用的后端对象"""

    username_changed = Signal()
    greeting_changed = Signal()

    def __init__(self):
        super().__init__()
        self._username = "Guest"

    @Property(str, notify=username_changed)
    def username(self):
        return self._username

    @username.setter
    def username(self, value):
        if self._username != value:
            self._username = value
            self.username_changed.emit()
            self.greeting_changed.emit()

    @Property(str, notify=greeting_changed)
    def greeting(self):
        return f"你好, {self._username}!"

    @Slot(str, result=str)
    def format_name(self, name: str) -> str:
        return name.strip().title()


def main():
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    backend = Backend()
    engine.rootContext().setContextProperty("backend", backend)

    engine.loadFromModule("MyApp", "Main")

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())
// Main.qml
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 400; height: 300
    visible: true
    title: backend.greeting

    Column {
        anchors.centerIn: parent
        spacing: 16

        Text {
            text: backend.greeting
            font.pixelSize: 24
            anchors.horizontalCenter: parent.horizontalCenter
        }

        TextField {
            id: nameField
            placeholderText: "输入姓名..."
            text: backend.username
            onTextChanged: backend.username = text
        }
    }
}

10.6 常见问题与解决方案 / Common Issues & Solutions

问题 / Issue解决方案 / Solution
PyGObject 提示找不到 typelibsudo apt install gir1.2-gtk-4.0
PySide6 信号连接报 TypeError检查参数类型匹配 / Check param types
PyQt6 枚举值变化使用 Qt.AlignmentFlag 而非 Qt.AlignCenter
PyGObject 类型提示不完整使用 # type: ignoreTYPE_CHECKING
QML 找不到 C++ 模块检查 QML_IMPORT_PATHqmldir
PyGObject 多线程问题使用 GLib.idle_add() 回到主线程

线程安全 / Thread Safety

"""PyGObject 线程安全示例"""
import threading
from gi.repository import GLib, Gtk

def background_task(label):
    """后台线程"""
    import time
    time.sleep(2)
    result = "任务完成"

    # ✅ 使用 GLib.idle_add 回到主线程更新 UI
    GLib.idle_add(label.set_text, result)

def start_task(label):
    """启动后台任务"""
    thread = threading.Thread(target=background_task, args=(label,))
    thread.daemon = True
    thread.start()

注意事项 / Important Notes

⚠️ 选择 PySide6 还是 PyQt6? / PySide6 or PyQt6?

2026 年推荐 PySide6:LGPLv3 许可、Qt Company 官方维护、API 一致。 Choose PySide6 in 2026: LGPLv3, officially maintained by Qt Company.

⚠️ PyGObject 的 IDE 支持 / PyGObject IDE Support

PyGObject 的类型提示不如 PySide6 完整。推荐使用 Pyright 或 Pylance。 类型提示不完整时使用 # type: ignoreAny 类型。

PyGObject type hints are less complete. Use Pyright/Pylance.

⚠️ virtualenv 与系统包 / virtualenv & System Packages

PyGObject 通常作为系统包安装。使用 virtualenv 时:

# 创建包含系统包的虚拟环境
python3 -m venv --system-site-packages venv

扩展阅读 / Further Reading

资源 / Resource链接 / Link
PySide6 文档https://doc.qt.io/qtforpython-6/
PyQt6 文档https://www.riverbankcomputing.com/static/Docs/PyQt6/
PyGObject 文档https://pygobject.readthedocs.io/
GTK Python 教程https://python-gtk-4-tutorial.readthedocs.io/
PySide6 示例https://doc.qt.io/qtforpython-6/examples/index.html

09 - libadwaita | 11 - 跨平台开发