QPushButton.clicked() fires twice when autowired u

2019-08-11 01:50发布

问题:

Consider this setup:

Main script, main.py:

import sys
from PyQt5 import uic
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.ui = uic.loadUi("mw.ui", self)

    def on_btnFunc_clicked(self):
        print('naked function call')

    @pyqtSlot()
    def on_btnSlot_clicked(self, bool):
        print('slotted function call')

app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec_())

Qt Designer .ui form, mw.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>153</width>
    <height>83</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QPushButton" name="btnFunc">
      <property name="text">
       <string>naked func</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="btnSlot">
      <property name="text">
       <string>slotted func</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

This setup uses Qt's signal-slot autowire mechanic to bind button clicks to respective callbacks. Why does the naked callback get called twice while the slotted one only once as intended?

I found this and this but those setups are a bit different from mine, since I don't bind signals manually nor do I install an event filter.

I thought this behavior might occur due to signals with different signatures get bound to the same slot, but (if I understand correctly) QPushButton has only one clicked() signal.

Can someone, please, explain?

回答1:

First of all, if the Qt's signal-slot autowire mechanic is used, the method is used QMetaObject::connectSlotsByName(), so this behavior is due to the translation of that function from c++ to Python, in the case of C++ the QMetaObject::connectSlotsByName() function only connect to slots, but in Python it extended to invoke functions that are not slots.

The problem is that when you click is an overloaded signal, which in the case of C ++ allows you to implement using a default parameter:

void QAbstractButton::clicked(bool checked = false)

but in python 2 signatures must be used:

clicked = QtCore.pyqtSignal([], [bool])

Therefore, in the connection made by PyQt to a slot it is used to QMetaObject::connectSlotsByName() that uses the QMetaObject of the object that obtains the signatures using the QMetaMethod, however if it is not a slot you can not get that information so the connection is equivalent to an invocation.


In the case of @pyqtSlot() have the following signature:

@pyqtSlot()
def on_btnSlot_clicked(self):
    print('slotted function call')

The connection made by PyQt the following:

self.btnSlot.clicked.connect(self.on_btnSlot_clicked)

but if the signature of the @pyqtSlot(bool) is:

@pyqtSlot(bool)
def on_btnSlot_clicked(self, checked):
    print('slotted function call', checked)

The connection made by PyQt the following:

self.btnSlot.clicked[bool].connect(self.on_btnSlot_clicked)

But in the case that it is connected to a function that is not a slot, it does not take into account those elements, since it uses the QMetaObject, so it will make the connections with all the possible signatures.

self.btnSlot.clicked[bool].connect(self.on_btnFunc_clicked)
self.btnSlot.clicked.connect(self.on_btnFunc_clicked)

In conclusion:

  • When QMetaObject::connectSlotsByName(...) is used, if it is connected to a @pyqtSlot(...), the signatures are verified. If a signal is connected to a function that is not an @pyqtSlot(...) they will connect with all possible signatures, so if the signal is overloaded with n signatures it will be called n-times.

  • You must use @pyqtSlot() to avoid the previous problem, since apart it has advantages for the rapidity and the saving of resources.