When a QPushButton is clicked, it fires twice
Asked Answered
H

3

3

I used PyQt5 for a project and have the following snippet (button is a QPushButton)

def on_receive(self, query):
    print("receiving", query)
    datapackages = json.loads(query)

    for button, datapackage in zip(self.buttonArray, datapackages):
        self.wire_up_button(datapackage, button) 

def wire_up_button(self, datapackage, button):
    title, songid = datapackage["title"], datapackage["songid"]
    button.setText(title + " (" + str(datapackage["votes"]) + ")")
    button.clicked.connect(lambda: self.upvote(songid))

def upvote(self, sid):
    text = '{"action":"upvote", "value":"' + sid + '"}\n'
    print(text)
    self.send(text)

def send(self, text):
    print("Sending")

The on_receive function is connected to a soccet client and will be called wheneever a data package is received. The layout is a bit complicated because my UI has so many buttons it's handier to iterate over them than to hard-code every single one.

Whenever I click the button, the wire-up function wires the button to the upvote function, which creates a json protocl and sends it to the socket server. However, the wireup-function is called twice per click. (I am certain of this because of the debug print commands). There is no other call in the send function in my program.

I speculate that this might be due to how clicked.connect works (maybe it fires upon click and release).

I used the QtDesigner to create the UI and loaded the .uic in my main.py

Hardboiled answered 14/10, 2017 at 17:21 Comment(8)
please, could you add more context, like where is located the connect signal line ? by seeing songid it seems to be inside a functionDemure
@Demure I've done so, it might obfuscate what's going on there a bit. I tried my best to make it as readable as possible...Hardboiled
does upvote() called with the same sid? Are you sure that you don't call wire_up_button for same button twice?Prey
oh, wire_up is called twice. does self.buttonArray contain duplicate buttons? how do you create it?Prey
@Prey It doesn't. I created it by hard-coding (just stuffing all the objects in one array manually). Link to the full sourceHardboiled
will it work with try: button.clicked.disconnect() except Exception: pass inside wire_up_button (before .clicked.connect)?Prey
@Prey sorry, wireup isn't called twice per button. I'm bad at maths ;). It is only the upvote and send function that are being called twice.Hardboiled
Let us continue this discussion in chat.Prey
P
5

Every time you receive anything from socket you do

for button, datapackage in zip(self.buttonArray, datapackages):
    self.wire_up_button(datapackage, button)

and in self.wire_up_button you connect to button clicked event. Note, that self.buttonArray is always the same list of buttons, so every time on_receive is called you add 1 new subscription to each button click. But previous subscription to button click still exists, so on button click upvote will be called multiple times with different sid. You need to disconnect from button click event before adding new one:

def wire_up_button(self, datapackage, button):
    try:
        button.clicked.disconnect()
    except:
        pass
    title, songid = datapackage["title"], datapackage["songid"]
    button.setText(title + " (" + str(datapackage["votes"]) + ")")
    button.clicked.connect(lambda: self.upvote(songid))

try ... except block is required, because button.clicked.disconnect() raises exception if no functions were connected to click event.

Prey answered 14/10, 2017 at 19:7 Comment(0)
S
0

In my case, I had buttons with the clicked event firing twice, and this was fixed by the very simple step of remembering to add the @pyqtSlot decorator!

QMetaObject.connectSlotsByName was being called only once, in the Ui script created by pyuic, but with this code:

def on_testBtn_clicked(self):
    print('on_testBtn_clicked')

the clicked event on QPushButton named testBtn was called twice. With this code:

from PyQt5.QtCore import pyqtSlot

@pyqtSlot()
def on_testBtn_clicked(self):
    log.debug('on_testBtn_clicked')

it correctly was only called once.

self.testBtn.dumpObjectInfo() may be useful to check how many slots are connected. Without the decorator I got:

OBJECT QPushButton::testBtn
  SIGNALS OUT
        signal: destroyed(QObject*)
          <functor or function pointer>
          --> PyQtSlotProxy::unnamed disable()
          --> PyQtSlotProxy::unnamed disable()
        signal: clicked(bool)
          --> PyQtSlotProxy::unnamed unislot()
          --> PyQtSlotProxy::unnamed unislot()
  SIGNALS IN
        <None>

With the decorator:

OBJECT QPushButton::testBtn
  SIGNALS OUT
        signal: destroyed(QObject*)
          <functor or function pointer>
        signal: clicked(bool)
          --> MainWindow::MainWindow on_testBtn_clicked()
  SIGNALS IN
        <None>
Stickweed answered 1/1 at 12:53 Comment(0)
E
0

I have a similar problem and looked up the above solution. However, I find the following implementation easier. In my case, a button is defined in a class as:

class ProjectClass:
...
def initialize_button(self):
    self.importButton = QtWidgets.QPushButton(self.frame)

def create_project_button_bindings(self):
    self.your_button.clicked.connect(self.trigger_function)

def trigger_function(self):
    enabled = self.your_button.isEnabled()
    self.your_button.setEnabled(not enabled) # toggle the enable and disable
    if enabled:
        # your statement here...
...

Now, the enable status will toggle and set to disable for the first and enable in second. For another trigger, the button will be enabled.

Escheat answered 16/4 at 8:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.