Dynamic QComboBox fill dependent on user input PyQ

2019-07-19 11:11发布

问题:

I created QTableWidget and in first two columns inserted comboboxes. The first column contains unique records (first elements from list of lists). My aim is to make these combo boxes fully dynamic, i.e. if the user selects 'Butterfly' within the first combobox, the second combobox will offer 'PP' and 'BR' for selection.

Refining the search should work even from other side, i.e. if user selects 'KL' in the second combobox, then the first one will automaticaly fill in 'Toy'.

I tried (using pandas dataframe) to filter out the results based upon user selection with success. However, consequently I wanted put these results into appropriate combobox (with no success).

Then I tried to adopt solution posted on this thread: How can I change the contents of one QComboBox depending on another QComboBox in PyQt5? and incorporate it into my code with no success.

Here below is the code with commented sections that do not work:

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from pandas import DataFrame

class Window(QMainWindow):

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

    def Table_of_widgets(self):

        rowCount = 20
        columnCount = 9

        self.table = QTableWidget()
        self.table.setColumnCount(columnCount)
        self.table.setRowCount(rowCount)
        self.table.setHorizontalHeaderLabels(['Section', 'Label', 'Product description', 'Picture', 'Product ID', "Amount", "Unit price", "Store", "Total price"])
        self.table.verticalHeader().hide()

        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(6, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(7, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(8, QHeaderView.Stretch)

        self.table.showMaximized()

        list1 = [
        ['Butterfly','16/1/001','PP','Pepito Butterfly','350'],
        ['Butterfly','16/1/002','PP','Brown Butterfly','350'],
        ['Butterfly','16/1/003','PP','Blue Butterfly','350'],
        ['Butterfly','bra01','BR','White Butterfly','500'],
        ['Backpack','bra02','BR','Backpack-blue','1500'],
        ['Backpack','bra03','BR','Backpack-black','1250'],
        ['Toy','klv01','KL','Bear','200'],
        ['Toy','klv02','KL','Fish','500'],
        ['Toy','klv03','KL','Rabbit','400'],
        ['Toy','klv04','KL','Owl','450'],
        ]

        dataset = DataFrame(list1)
        fin = list(dataset[0].drop_duplicates())
        fin.insert(0,'')
        fin2 = list(dataset[2].drop_duplicates())
        fin2.insert(0,'')

        for i in range(rowCount):
            comboA = QComboBox()
            comboA.addItems(fin)
##            comboA.currentTextChanged.connect(self.onCurrentTextChanged)
            self.table.setCellWidget(i,0,comboA)

        for i in range(rowCount):
            comboB = QComboBox()
            comboB.addItems(fin2)         
            self.table.setCellWidget(i,1,comboB)

##    def onCurrentTextChanged(self, text):
##        self.comboB.clear()
##        elements = fin1
##        if isinstance(elements, list):
##            self.comboB.addItems(elements)
##        else:
##            self.comboB.addItem(elements)

if __name__ == "__main__":

    app = QApplication(sys.argv)
    app.setApplicationName('MyWindow')
    main = Window()
    sys.exit(app.exec_())

I think I failed to connect the signal in a proper way. Thanks for any suggestion!

Edit:

I tried to be as precise as possible but perhaps there still was a space for uncertainty.

Here is further information what I am after:

All comboxes should be set to blank values by default. If user selects in combo2 blank value, nothing changes for combo1, if user selects PP in combo2, only Butterfly (and blank) will appear in combo1, if user selects BR in combo2, only Butterfly and Backpack (and blank) will appear in combo1. The same should be valid for combo1: if user selects Butterfly in combo1, only BR and PP (and blank) should appear in combo 2, if he chooses Backpack in combo1 only BR (and blank) will appear

Further Edit:

Previously stated goal was reached (thanks to @eyllanesc). Now I plan to add third column of comboboxes offering 4th element (i.e. list1[3]) from list1 (product description column within qtablewidget). For that the dictionary must be changed. After reading some posts (Access an arbitrary element in a dictionary in Python; Dictionaries are ordered in Python 3.6+) I still fail to form the structure that is needed (maybe createData is empty before entries are added within for loop):

d = {" ": [[" "], [" "]]}
d_inverse = {" ": [[" "], [" "]]}

def createData(key1, key2, key3, data):
    if key2 not in data[[" "], [" "]][0]:
        data[[" "], [" "]][0].append(key2)
        if key3 not in data[[" "], [" "]][1]:
            data[[" "], [" "]][1].append(key3)
    if key1 in data.keys():
        if key2 not in data[key1]:
            data[key1].append(key2)
            if key3 not in data[key1]:
                data[key1].append(key3)
    else:
        data[key1] = [" ", key2, key3]
    return data

for item in template:
    item1 = item[0]
    item2 = item[3]
    item3 = item[2]
    d = createData(item1, item2, item3, d)
    d_inverse = createData(item3, item2, item1, d_inverse)

回答1:

The first task that must be done is to create a structure of data that allows to handle the data of simple form, in this case a dictionary is used that contain lists:

self.d = {" ": []}

for item in list1:
    combo1 = item[0]
    combo2 = item[2]
    if combo2 not in self.d[" "]:
        self.d[" "].append(combo2)

    if combo1 in self.d.keys():
        if combo2 not in self.d[combo1]:
            self.d[combo1].append(combo2)
    else:
        self.d[combo1] = []

Output:

{ 
   ' '         : ['PP', 'BR', 'KL'], 
   'Butterfly' : ['PP', 'BR'], 
   'Backpack'  : ['BR'], 
   'Toy'       : ['KL'] 
}

Then connect the currentTextChanged signals of the QComboBox, but you must also pass the other associated QComboBox for that the lambda function is used. with the blockSignals() method we block that a loop is generated between the signals.

class Window(QMainWindow):
    [...]
    def Table_of_widgets(self):
        [...]

        list1 = [...]


        self.d = {" ": [" "]}
        self.d_inverse = {" ": [" "]}

        def createData(key1, key2, data):
            if key2 not in data[" "]:
                data[" "].append(key2)
            if key1 in data.keys():
                if key2 not in data[key1]:
                    data[key1].append(key2)
            else:
                data[key1] = [" ", key2]
            return data

        for item in list1:
            item1 = item[0]
            item2 = item[2]
            self.d = createData(item1, item2, self.d)
            self.d_inverse = createData(item2, item1, self.d_inverse)

        for i in range(rowCount):
            comboA = QComboBox()
            comboB = QComboBox()
            comboA.addItems(self.d.keys())
            comboB.addItems(self.d[comboA.currentText()])
            self.table.setCellWidget(i, 0, comboA)
            self.table.setCellWidget(i, 1, comboB)
            comboA.currentTextChanged.connect(lambda text, row=i: self.onComboACurrentTextChanged(text, row))
            comboB.currentTextChanged.connect(lambda text, row=i: self.onComboBCurrentTextChanged(text, row))

    def updateCombox(self, combo1, combo2, item1, item2):
        text = combo1.currentText()
        combo1.blockSignals(True)
        combo2.blockSignals(True)
        combo1.clear()
        combo2.clear()

        combo2.addItems(item1[text])
        combo2.setCurrentIndex(1 if text != " " else 0)
        combo1.addItems(item2[combo2.currentText()])
        combo1.setCurrentText(text)

        combo1.blockSignals(False)
        combo2.blockSignals(False)

    def onComboACurrentTextChanged(self, text, row):
        comboA = self.table.cellWidget(row, 0)
        comboB = self.table.cellWidget(row, 1)
        self.updateCombox(comboA, comboB, self.d, self.d_inverse)

    def onComboBCurrentTextChanged(self, text, row):
        comboA = self.table.cellWidget(row, 0)
        comboB = self.table.cellWidget(row, 1)
        self.updateCombox(comboB, comboA, self.d_inverse, self.d)