4. PyQt与PySide的兼容性封装

任务目标

编写一个轻量抽象层,让同一份代码无需修改即可在 PyQt5 和 PySide6 下运行,并验证窗口能够正常创建与显示。

核心实现

两种 Qt 绑定的 API 高度相似,主要差异集中在:exec() 与 exec_()、Signal 与 pyqtSignal、Slot 与 pyqtSlot、枚举类型访问方式。以下封装层统一处理这些差异:

# qt_compat.py
import sys

# --- 绑定选择与导入 ---
try:
    from PySide6 import QtCore, QtWidgets
    from PySide6.QtCore import Signal, Slot
    QT_BINDING = "PySide6"
    # PySide6 中 exec() 可直接使用,无需别名
except ImportError:
    try:
        from PyQt5 import QtCore, QtWidgets
        from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
        QT_BINDING = "PyQt5"
        # PyQt5 中 exec() 在部分版本不可用,需提供别名
        if not hasattr(QtCore.QCoreApplication, "exec"):
            QtCore.QCoreApplication.exec = QtCore.QCoreApplication.exec_
    except ImportError:
        raise ImportError("需要安装 PySide6 或 PyQt5")

# --- 统一枚举访问 ---
# PyQt5 使用 QFlag 包装,PySide6 直接暴露 IntEnum
def enum_value(enum_class, name):
    """跨绑定获取枚举值,如 enum_value(Qt.AlignmentFlag, 'AlignCenter')"""
    if QT_BINDING == "PySide6":
        return getattr(enum_class, name)
    else:
        # PyQt5 中枚举值通过 __getattr__ 直接访问
        return getattr(enum_class, name)

# --- 统一应用执行入口 ---
def run_app(app):
    """兼容两种绑定的事件循环启动"""
    if QT_BINDING == "PySide6":
        return app.exec()
    else:
        return app.exec_()

# --- 统一信号定义辅助 ---
# 用法:class MyWidget(QtWidgets.QWidget):
#           value_changed = qt_signal(int)
def qt_signal(*types):
    """返回当前绑定对应的信号类"""
    return Signal(*types)

# --- 统一槽装饰器 ---
# 用法:@qt_slot(int)
def qt_slot(*types):
    """返回当前绑定对应的槽装饰器"""
    return Slot(*types)

最小验证程序

# demo.py
from qt_compat import QtWidgets, QT_BINDING, run_app, enum_value

def main():
    app = QtWidgets.QApplication([])

    window = QtWidgets.QWidget()
    window.setWindowTitle(f"兼容层验证 - {QT_BINDING}")
    window.resize(320, 200)

    layout = QtWidgets.QVBoxLayout(window)
    label = QtWidgets.QLabel(f"当前绑定: {QT_BINDING}")
    layout.addWidget(label)

    # 验证枚举访问兼容性
    align = enum_value(QtCore.Qt.AlignmentFlag, "AlignCenter")
    label.setAlignment(align)

    window.show()
    return run_app(app)

if __name__ == "__main__":
    sys.exit(main())

注意:demo.py 中使用了 QtCore,需在文件顶部补充 from qt_compat import QtCore 或直接通过 qt_compat 暴露。为保持简洁,可将 QtCore 一并导入:

# 修正后的导入行
from qt_compat import QtCore, QtWidgets, QT_BINDING, run_app, enum_value

运行与验证

  1. 安装依赖(任选其一):
    pip install PySide6 或 pip install PyQt5
  2. 运行程序:
    python demo.py
  3. 通过条件:
    窗口标题显示当前实际绑定的名称(如“兼容层验证 - PySide6”),标签文字居中显示,窗口可正常关闭。分别在两种绑定环境下运行同一份代码,均无报错即验证通过。

验证要点

  • 若先安装 PySide6 再安装 PyQt5,qt_compat.py 会优先选择 PySide6;反之亦然。可通过 QT_BINDING 变量确认当前生效的绑定。
  • PyQt5 下若遇到 QApplication.exec() 不存在的情况,封装层已自动回退到 exec_()。
  • 枚举访问统一通过 enum_value() 完成,避免直接使用 Qt.AlignCenter 这类在 PySide6 中已废弃的写法。