2

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)
New2coding
  • 715
  • 11
  • 23
  • *if the user selects 'Butterfly' within the first combobox, the second combobox will offer 'PP' and 'BR' for selection*. What is the criterion? Why not just PP? or why not all items? – eyllanesc Oct 11 '17 at 14:00
  • There is a list called list1 from where the comboboxes might be sourced. If one selects 'Butterfly' within first combobox it automatically excludes these that does not have 'Butterfly' at first element within given list of lists – New2coding Oct 11 '17 at 14:03
  • but BR does not have Butterfly as a source according to list1 that you show. Is it a bug in list1? – eyllanesc Oct 11 '17 at 14:04
  • I edited it, mistakenly not the last version of my code was posted. Thanks for pointing that out. – New2coding Oct 11 '17 at 14:07
  • The part that you call refining is impossible without fulfilling the first condition, I explain with the following example: let's say that we select in the first QComboBox Butterfly then there will only be PP and BR so we can never get KL. – eyllanesc Oct 11 '17 at 14:39
  • The blank value that is added there should serve as "reset" of the other combobox thought. Thus if user selects BR which would infludence the combobox one in the same row and then changed his mind, it should still be possible to set it blank and get full list of option again within the other combobox. – New2coding Oct 11 '17 at 14:46
  • If the combobox of the first column is blank the second combobox must have all the previous options. I am right – eyllanesc Oct 11 '17 at 14:52
  • Yes, you are right. More informatin: Each line of Qtablewidget is supposed to be independent. Only dependency is between comboxes in the same row. – New2coding Oct 11 '17 at 14:54
  • And what is the blank space of the second combobox? – eyllanesc Oct 11 '17 at 15:00
  • Even after refining the list of options (based upon user selection), it still should be possible to choose a blank option. I now get what you are saying, at the moment there is no such piece of code that would manage this. – New2coding Oct 11 '17 at 15:06
  • Did you work my solution? – eyllanesc Oct 11 '17 at 15:39

1 Answers1

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)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks for this. I tried to be as precise as possible but perhaps there still was a space for uncertainty. 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 – New2coding Oct 11 '17 at 15:57
  • Okay, I understand, I recommend you put this description in your question, it assumes that we are not in your mind and we do not know what you want. Think of all possible cases. – eyllanesc Oct 11 '17 at 16:02
  • For simplicity sake I put there list1 but I intend to source it from database and consequently add a third column of comboboxes for selection of individual products. Still the dictionary is a perfect suggestion. – New2coding Oct 11 '17 at 16:04
  • I have updated my answer and comment if you still lack some detail, if it works, do not forget to mark my answer as correct. – eyllanesc Oct 11 '17 at 17:12
  • If you are going to use a database the task of filtering the data is simpler since the task would do the database. – eyllanesc Oct 11 '17 at 18:35
  • I am about add third column of comboboxes. For that I need to change the structure of dictionary as follows d = {" ": [[" "], [" "]]}, d_inverse = {" ": [[" "], [" "]]}, however, my def createData(key1, key2, key3, data): fails for some reason – New2coding Oct 12 '17 at 08:11
  • I do not recommend you to extend my logic in this way, it is more convenient to use a database for larger structures.He also noticed that you have not understood my solution, – eyllanesc Oct 12 '17 at 08:45
  • Thanks for your suggestion, I am trying to proceed with sqlite3 now (post: https://stackoverflow.com/questions/46710397/dynamic-qcombobox-fill-within-qtablewidget-sourced-from-sqlite3-db) – New2coding Oct 12 '17 at 13:06
  • @ eyllanesc: I replaced your dictionary with dynamic dataframe which appears to do the job just fine, but I fail to make the selections within the comboboxes work, any suggestion (original post is here: https://stackoverflow.com/questions/46710397/dynamic-qcombobox-fill-within-qtablewidget-sourced-from-sqlite3-db?noredirect=1#comment80369118_46710397)? – New2coding Oct 14 '17 at 12:08