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
| 特性 / Feature | PyQt6 | PySide6 | PyGObject (GTK) |
|---|---|---|---|
| 绑定框架 | sip | shiboken | GObject Introspection |
| 许可证 | GPLv3 / 商业 | LGPLv3 | LGPLv2.1+ |
| API 兼容 | Qt6 完整 | Qt6 完整 | GTK4 + libadwaita |
| 类型提示 | ✅ 完整 | ✅ 完整 | ⚠️ 部分 |
| IDE 支持 | 优秀 | 优秀 | 中等 |
| 文档 | 优秀 | 优秀 | GTK 文档 |
| C++ 扩展 | sip 模块 | shiboken | PyCapsule |
| 官方维护 | Riverbank Computing | Qt Company | GNOME |
| 学习资源 | 丰富 | 丰富 | 中等 |
许可证选择指南 / 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 = [
("张三", "[email protected]", 28),
("李四", "[email protected]", 35),
("王五", "[email protected]", 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 几乎相同,主要差异:
| 差异 / Difference | PyQt6 | PySide6 |
|---|---|---|
| 模块导入 | from PyQt6.QtWidgets import ... | from PySide6.QtWidgets import ... |
| 枚举前缀 | Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignCenter |
| exec() 方法 | app.exec() | app.exec() |
| 信号定义 | pyqtSignal | Signal |
| 槽装饰器 | @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 提示找不到 typelib | sudo apt install gir1.2-gtk-4.0 |
| PySide6 信号连接报 TypeError | 检查参数类型匹配 / Check param types |
| PyQt6 枚举值变化 | 使用 Qt.AlignmentFlag 而非 Qt.AlignCenter |
| PyGObject 类型提示不完整 | 使用 # type: ignore 或 TYPE_CHECKING |
| QML 找不到 C++ 模块 | 检查 QML_IMPORT_PATH 和 qmldir |
| 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: ignore或Any类型。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 - 跨平台开发 →