QMetaObject.connectSlotsByName()的简单研究

        QMetaObject.connectSlotsByName()是 Qt 框架中 自动关联信号与槽(Slot) 的核心机制,作用是:根据控件名称和槽函数名称的约定,自动将 MainWindow 及其子控件的信号连接到对应的槽函数,无需手动调用 connect() 函数。


使用范例:

QMetaObject.connectSlotsByName(MainWindow)

  1. 作用对象MainWindow 是目标窗口(或容器控件),会递归扫描其所有子控件(如按钮、输入框等)。
  2. 名称约定规则(关键):槽函数必须遵循固定命名格式:on_<控件对象名>_<信号名>例:
    • 控件对象名:pushButton_ok(一个按钮)
    • 信号名:clicked()(按钮的点击信号)
    • 对应的槽函数:on_pushButton_ok_clicked()(无需手动 connect)
  3. 自动连接过程:Qt 会遍历 MainWindow 的元对象(QMetaObject)中所有符合上述命名规则的槽函数,再找到对应的 “控件对象名 + 信号名”,自动完成 connect(控件, 信号, MainWindow, 槽函数) 的逻辑。

    核心用途:

  • 简化代码:避免手动编写大量重复的 connect() 语句,尤其适合 UI 控件较多的场景。
  • 降低维护成本:UI 控件名称或信号变更时,只需同步修改槽函数名称,无需调整连接逻辑。

前提条件:

  1. 槽函数所在的类(如 MainWindow)必须继承 QObject,且声明 Q_OBJECT 宏(启用 Qt 元对象系统)。
  2. 控件必须设置有效的 objectName(在 UI 设计器中设置或通过 setObjectName() 代码设置)。
  3. 槽函数需声明在 public slots/protected slots/private slots 区域(Qt 5+ 也支持用 Q_SLOT 宏)。

demo代码:

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button1 = QPushButton(self)
        self.button1.setObjectName("pressButton1")  # 必需的
        layout = QHBoxLayout()
        layout.addWidget(self.button1)
        self.setLayout(layout)

        QtCore.QMetaObject.connectSlotsByName(self)


    @QtCore.Slot()   # pyside6必需
    def on_pressButton1_pressed(self):
        print(f"Button1 is pressed")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

        需要注意的是,如果使用pyside6,必须有@QtCore.Slot() 这一行声明,否则不能正确连接到槽函数,pyqt5则不需要。


使用中的注意点:

1、程序变量名与ObjectName的区别

上面的代码中:

self.button1 = QPushButton(self)
self.button1.setObjectName("pressButton1")

self.button1是程序中的变量名,那么如果在程序中定义和使用变量就使用这个名称,比如:

self.button1.setCheckable(True)

或者

self.button1.setText("Button1")

所以,如果需要通过connect()显式连接槽函数的时候也用这个:

self.button1.pressed.connect(self.on_Button1_pressed)

        而ObjectName("pressButton1"),则是Qt 为每个 QObject 子类(所有控件都是 QObject 子类)提供的内置属性,是 Qt 框架识别控件的 “内部身份证号”。所以,connectSlotsByName()函数执行后连接到的槽函数也是用ObjectName命名的函数,这一点必需区分清楚。比如上例中按钮的ObjectName是"pressButton1",那么通过connectSlotsByName()连接到的槽函数就是“on_pressButton1_pressed”。

2、注意信号的重载隐患

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout
from PyQt5 import QtCore


class MainWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button1 = QPushButton(self)
        self.button1.setObjectName("clickButton1")
        layout = QHBoxLayout()
        layout.addWidget(self.button1)
        self.setLayout(layout)
        self.num = 0

        QtCore.QMetaObject.connectSlotsByName(self)

    # @QtCore.Slot()   # pyside6必需
    def on_clickButton1_clicked(self):
        self.num += 1
        print(f"self.num = {self.num}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

        执行以上代码,会发现,每点击一次按钮,槽函数就会被执行两次(经测试,pyqt5会,pyside6不会执行两次,看来这两个还是有细微的差别的)。在一些场景中,这就会引起歧义,比如该按钮是用于数字加1的,每点击一次却被加了两次1。

原因:clicked 信号的重载特性

QPushButton 的 clicked 信号有两个重载版本

  1. clicked()(无参数)
  2. clicked(bool checked)(带布尔参数,默认 False

    QMetaObject.connectSlotsByName() 在自动匹配时,会同时匹配这两个重载版本,导致同一个槽函数被绑定到两个重载信号上 —— 但由于两个信号本质是「同一操作触发」,最终表现为一次点击触发两次槽函数。

验证如下:

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        self.btn = QPushButton("测试重载信号")
        self.btn.setObjectName("TestButton")
        self.btn.setCheckable(True)  # 启用勾选状态,点击时checked会切换True/False
        layout.addWidget(self.btn)

       
        # 连接无参版本:clicked()
        self.btn.clicked.connect(self.on_clicked_no_param)
        # 等价写法(显式标注无参):self.btn.clicked[].connect(self.on_clicked_no_param)

        # 连接带参版本:clicked(bool)
        self.btn.clicked[bool].connect(self.on_clicked_with_param)

    # 无参槽函数(匹配 clicked())
    def on_clicked_no_param(self):
        print("🔹 无参版 clicked 信号触发")

    # 带参槽函数(匹配 clicked(bool))
    def on_clicked_with_param(self, checked):
        print(f"🔸 带参版 clicked 信号触发 → checked: {checked}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

运行结果:

可以看到,每点击一次按钮,都触发了两个槽函数。

        那么,如果使用connectSlotsByName() 来自动连接到需要参数的槽函数,由于其中一个click在传递时并没有携带参数,就会运行出错:

import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        self.btn = QPushButton("测试重载信号")
        self.btn.setObjectName("TestButton")
        layout.addWidget(self.btn)

        QtCore.QMetaObject.connectSlotsByName(self)

    def on_TestButton_clicked(self, checked):
        print(f"clicked 信号触发, checked: {checked}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

        解决办法是在给槽函数提供足够的有可能出现的参数,并在槽函数中设定筛选条件,接收需要的信号。比如:

import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QHBoxLayout


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.button1 = QPushButton(self)
        self.button1.setObjectName("TestButton")
        self.button1.setCheckable(True)
        layout = QHBoxLayout()
        layout.addWidget(self.button1)
        self.setLayout(layout)

        QtCore.QMetaObject.connectSlotsByName(self)

    def on_TestButton_clicked(self, checked=None):
        if checked is not None:
            print(f"clicked 信号触发 → checked: {checked}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

运行结果:

        由于不带参数的clicked信号并未携带checked参数给槽函数,就被pass掉了。同样的,将槽函数定义为:

    def on_TestButton_clicked(self, checked=None):
        if checked is None:
            todo some

就可以过滤掉带参数的信号,只接受另一个不带checked参数的clicked,运行结果:

import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QHBoxLayout


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.button1 = QPushButton(self)
        self.button1.setObjectName("TestButton")
        # self.button1.setCheckable(True)
        layout = QHBoxLayout()
        layout.addWidget(self.button1)
        self.setLayout(layout)
        self.num = 0

        QtCore.QMetaObject.connectSlotsByName(self)

    def on_TestButton_clicked(self, checked=None):
        if checked is None:
            self.num += 1
            print(f"clicked 信号触发 → self.num: {self.num}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

每点击一次按钮,只接受一个不带参数的信号。


        带重载特性的部件除了按钮(QPushButton),还有很多,比如:QCheckBox(复选框)、QSlider(滑块)、QSpinBox/QDoubleSpinBox(数字输入框)等等。在使用connectSlotsByName() 来实现槽函数的自动连接时,要注意重载特性带来的歧义隐患和传递参数缺失。以QCheckBox(复选框)为例,当每次鼠标点击时,会发出以下几个信号:stateChanged[int]   # 状态值变化
clicked[]   # 不带参数的被点击
clicked[bool]   # bool是被点击后的checked布尔值变化

其中的两个clicked信号可能就需要有所区分,才能实现预期功能。

import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QHBoxLayout, QCheckBox


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.cb = QCheckBox("三态复选框",self)
        self.cb.setTristate(True)  # 启用半选状态
        self.cb.setObjectName("TestCheckBox")
        self.cb.stateChanged[int].connect(lambda state: print(f"状态值变化:{state}"))
        self.cb.clicked.connect(lambda: print(f"被点击"))
        self.cb.clicked[bool].connect(lambda checked: print(f"选中状态变化:{checked}"))
        layout = QHBoxLayout()
        layout.addWidget(self.cb)
        self.setLayout(layout)
        # QtCore.QMetaObject.connectSlotsByName(self)

    def on_TestCheckBox_stateChanged(self, state):
        print(f"状态:{state}")

    def on_TestCheckBox_clicked(self, checked=None):
        if checked is not None:
            print(f"选中:{checked}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

用connectSlotsByName()实现:

import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QHBoxLayout, QCheckBox


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.cb = QCheckBox("三态复选框",self)
        self.cb.setTristate(True)  # 启用半选状态
        self.cb.setObjectName("TestCheckBox")
        layout = QHBoxLayout()
        layout.addWidget(self.cb)
        self.setLayout(layout)
        QtCore.QMetaObject.connectSlotsByName(self)

    def on_TestCheckBox_stateChanged(self, state):
        print(f"状态:{state}")

    def on_TestCheckBox_clicked(self, checked=None):
        if checked is not None:
            print(f"选中:{checked}")
        else:
            print("被点击")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

进一步的:https://blog.csdn.net/xulibo5828/article/details/155742356

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深蓝海拓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值