Orchainu
Orchainu

Reputation: 59

PyQt - How to connect multiple signals to the same widget

[ ]All1           [ ]All2        

[ ]checkbox1A     [ ]checkbox1B

[ ]checkbox2A     [ ]checkbox2B

Based on the chart above, a few things need to happen:

  1. The All checkboxes only affect the on/off of the column it resides in, and checks on/off all the checkboxes in that column.
  2. All checkboxes work in pairs, so if checkbox1A is on/off, checkbox1B needs to be on/off
  3. If an All checkbox is checked on, and then the user proceeds to check off one or more checkbox in the column, the All checkbox should be unchecked, but all the checkboxes that are already checked should remain checked.

So really this is more like a chain reaction setup. If checkbox All1 is on, then chieckbox1A and 2A will be on, and because they are on, checkbox1B and 2B are also on, but checkbox All2 remains off. I tried hooking up the signals based on this logic, but only the paired logic works 100%. The All checkbox logic only works 50% of the time, and not accurately, and there's no way for me to turn off the All checkbox without turning all already checked checkboxes off.

Really really need help ... T-T

Sample code:

cbPairKeys = cbPairs.keys()
    for key in cbPairKeys:
        cbOne = cbPairs[key][0][0]
        cbTwo = cbPairs[key][1][0]
        cbOne.stateChanged.connect(self.syncCB)
        cbTwo.stateChanged.connect(self.syncCB)

def syncCB(self):               
    pairKeys = cbPairs.keys()
    for keys in pairKeys:
        cbOne = cbPairs[keys][0][0]
        cbOneAllCB = cbPairs[keys][0][4]
        cbTwo = cbPairs[keys][1][0]
        cbTwoAllCB = cbPairs[keys][1][4]

        if self.sender() == cbOne:
            if cbOne.isChecked() or cbTwoAllCB.isChecked():
                cbTwo.setChecked(True)
            else:
                cbTwo.setChecked(False)
        else:
            if cbTwo.isChecked() or cbOneAllCB.isChecked():
                cbOne.setChecked(True)
            else:
                cbOne.setChecked(False) 

EDIT

Thanks to user Avaris's help and patience, I was able to reduce the code down to something much cleaner and works 100% of the time on the 1st and 2nd desired behavior:

#Connect checkbox pairs     
cbPairKeys = cbPairs.keys()
    for key in cbPairKeys:
        cbOne = cbPairs[key][0][0]
        cbTwo = cbPairs[key][1][0]
        cbOne.toggled.connect(cbTwo.setChecked)
        cbTwo.toggled.connect(cbOne.setChecked) 

#Connect allCB and allRO signals    
cbsKeys = allCBList.keys()
    for keys in cbsKeys:
        for checkbox in allCBList[keys]:
            keys.toggled.connect(checkbox.setChecked)

Only need help on turning off the All checkbox when the user selectively turns off the modular checkboxes now

Upvotes: 1

Views: 3787

Answers (2)

Avaris
Avaris

Reputation: 36715

If I'm understanding your data structure, I have a solution. Correct me if I'm wrong: allCBList is a dict (confusing name! :) ). Its keys are the all* checkboxes. And a value allCBList[key] is a list with checkboxes associated with that all checkbox. For your example structure it'll be something like this:

{ All1 : [checkbox1A, checkbox1B],
  All2 : [checkbox2A, checkbox2B]}

Then what you need to is this: when a checkbox is toggled and it is in checked state, then you need to check the All* checkbox if all the other checkboxes are in checked state. Otherwise it will be unchecked.

for key, checkboxes in allCBList.iteritems():
    for checkbox in checkboxes:
        checkbox.toggled.connect(lambda checked, checkboxes=checkboxes, key=key: key.setChecked(checked and all(checkbox.isChecked() for checkbox in checkboxes))

I guess, this statement requires a bit of explanation:

lambda checked, checkboxes=checkboxes, key=key:

lambda creates the callable that is connected to the signal. toggled passes checkbox status, and it will be passed to checked variable. checkboxes=checkboxes and key=key parts pass the current values to checkboxes and key parameters of the lambda. (You need this because of the closure in lambdas)

Next comes:

key.setChecked(...)

We are setting the checked state of key which is the appropriate All* checkbox. And inside this:

checked and all(checkbox.isChecked() for checkbox in checkboxes)

all is True if everything inside is True, where we check every checkboxs state. And this will return True if all are checked (i.e. isChecked() returns True).

checked and ... part is there to short-circuit the all. If the current checkbox turns unchecked, then we don't need to check others. All* would be unchecked.

(PS: By the way, you don't need to get .keys() of a dict to iterate over keys. You can just iterate over the dict and it will iterate over its keys.)

Edit: Just to avoid chain reaction with All* checkboxes toggled by clicking any sub-checkboxes, it's necessary to change the signal for All* checkboxes to clicked, instead of toggled. So, the All* check boxes will affect other below them only in the case of user interaction.

In the end, your modified code will be:

# Connect checkbox pairs
# you just use the values
# change 'itervalues' to 'values' if you are on Python 3.x
for cbPair in cbPairs.itervalues():
    cbOne = cbPair[0][0]
    cbTwo = cbPair[1][0]
    cbOne.toggled.connect(cbTwo.setChecked)
    cbTwo.toggled.connect(cbOne.setChecked) 

# Connect allCB and allRO signals
# change 'iteritems' to 'items' if you are on Python 3.x
for key, checkboxes in allCBList.iteritems():
    for checkbox in checkboxes:
        key.clicked.connect(checkbox.setChecked)
        checkbox.toggled.connect(lambda checked, checkboxes=checkboxes, key=key: key.setChecked(checked and all(checkbox.isChecked() for checkbox in checkboxes))

Upvotes: 3

Vicent
Vicent

Reputation: 5452

Your problem is that your checkboxes are connecting the toggled signal and toggling their state in your connected slots so the signal is emitted again (so the slots are executed again...) and you get unpredictable results. Obviously that is not your wanted behavior. You can fix it in several ways:

  • by disconnecting the signals at the beginning of the slots and connecting them again at the end
  • by using some clever code that controls the re-emission of signals (I think this is what Avari's code does in a very compact way, but I'm not completely sure)
  • by using a clicked signal because it is not re-emitted when the checkbox state changes

Which approach you follow is up to you. The following code uses the third approach:

    self.cbPair = {}
    self.cbPair['0'] = (QtGui.QCheckBox('all1', parent), 
        QtGui.QCheckBox('all2', parent))
    self.cbPair['1'] = (QtGui.QCheckBox('1a', parent), 
        QtGui.QCheckBox('1b', parent))
    self.cbPair['2'] = (QtGui.QCheckBox('2a', parent), 
        QtGui.QCheckBox('2b', parent))

    for v in self.cbPair.values():
        for cb in v:
            cb.clicked.connect(self.updateCB)

def updateCB(self):
    cb = self.sender()
    is_checked = cb.isChecked()
    id = str(cb.text())
    try:
        # Update a whole column
        column = int(id[-1]) - 1
        rows = ('1', '2')
    except ValueError:
        # Update a row and the headers row
        rows = (id[0], )
        column = {'a': 1, 'b': 0}.get(id[-1])
        if not is_checked:
            for c in (0, 1):
                self.cbPair['0'][c].setChecked(is_checked)
    for r in rows:
        self.cbPair[r][column].setChecked(is_checked)

Note that I'm using the checkboxes text as a UID from wich row and colum values are calculated. If you want to use different text labels for your checkboxes you may need to set the UIDs as attributes to every checkbox.

Upvotes: 0

Related Questions