Search code examples
pythonpyqt5qspinbox

Call a function if cell value is changed - for dynamic number of spinBoxes in a table


I'm writing a software tool that has a table filled with two columns: parameter and precision. Some parameters in the "Precision" column have spinBox widget and some parameters have just text "N/A". Number of spinBoxes is dynamic. How to call a function if the value of the cells with spinBoxes changed?

I tried to do it with self.table.currentItemChanged.connect(self._print) but it doesn't do exactly what I need - it calls function only when I change the spinBox value and after that change row selection, while I need to call function each time the spinBox value changes.

import sys
from PyQt5.QtWidgets import *
from PyQt5 import QtCore
import pandas as pd

class Window(QWidget):
    singleton: 'Window' = None

    def __init__(self):
        super(Window, self).__init__()
        self.setWindowTitle("Software tool")
        self.setGeometry(50, 50, 1800, 900)
        self.mainLayout=QHBoxLayout()
        self.setLayout(self.mainLayout)
        self.UI()

    def UI(self):
        self.sublayouts = {}
        self.buttons = {}
        self._view()
        self._fillListWidget()
        self.table.currentItemChanged.connect(self._print)
        self.show()

    def _view(self):
        self.table = QTableWidget(0, 2)
        self.table.setHorizontalHeaderLabels(['Parameter', 'Precision'])
        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)

        self.sublayouts['table'] = QGridLayout()
        self.sublayouts['table'].addWidget(self.table, 1, 0, 4, 4)
        self.sublayouts['table'].setRowStretch(4, 1)

        self.mainLayout.addLayout(self.sublayouts['table'])

    def _fillListWidget(self):
        listCol = {
            'Parameters': ['a', 'b', 'c', 'd', 'e'],
            'Precision': [-1, -1, 2, 3, 1]}
        self.df = pd.DataFrame(listCol)
        for row in zip(*self.df.to_dict('list').values()):
            itemColumn = QTableWidgetItem(
                row[self.df.columns.get_loc("Parameters")])
            rowPosition = self.table.rowCount()
            self.table.insertRow(rowPosition)
            itemTable = self._tableCell(row[self.df.columns.get_loc("Parameters")])
            self.table.setItem(rowPosition, 0, itemTable)
            df_tmp = self.df[self.df['Parameters'] == row[self.df.columns.get_loc("Parameters")]]
            if df_tmp['Precision'].values[0] >= 0:
                self.spinBox=QSpinBox()
                self.spinBox.setValue(df_tmp['Precision'].values[0])
                self.table.setCellWidget(rowPosition, 1, self.spinBox)
            else:
                itemTable = self._tableCell('N/A')
                self.table.setItem(rowPosition, 1, itemTable)

    def _tableCell(self, text):
        item = QTableWidgetItem()
        item.setText(text)
        return item

    def _readTable(self):
        list_Parameters = []
        list_Precision = []
        print('1')
        for i in range(self.table.rowCount()):
            print(self.table.item(i, 0).text())
            list_Parameters.append(self.table.item(i, 0).text())
            list_Precision.append(self.table.item(i, 1).text())

    def _print(self):
        print('Precision changed')

def main():
    App=QApplication(sys.argv)
    window =Window()
    sys.exit(App.exec_())

if __name__ == '__main__':
    main()

Solution

  • Instead of setting a widget item, you should set the value of the index with the Qt.DisplayRole and open a persistent editor, which by default is already a spinbox for numeric values.

    Since you want a specialized behavior, you can use a custom delegate that implements that by returning a customized QSpinBox with createEditor(), and also connects the spin box valueChanged signal to the delegate's commitData() that will actually set the data on the model.

    In this way, not only you can then connect to the table's itemChanged to be notified about data changes, but you can also directly access the table data (which wouldn't have worked in your case, because the data wasn't set and was only available in the spin box).

    class PrecisionDelegate(QStyledItemDelegate):
        def createEditor(self, parent, opt, index):
            editor = QSpinBox(parent)
            editor.setMinimum(-1)
            editor.setSpecialValueText('N/A')
            editor.valueChanged.connect(lambda: self.commitData.emit(editor))
            return editor
    
    
    class Window(QWidget):
        def __init__(self):
            super(Window, self).__init__()
            self.setWindowTitle("Software tool")
            self.mainLayout = QHBoxLayout(self)
            self.UI()
            self.table.itemChanged.connect(self._print)
    
        def _view(self):
            # ...
            self.table.setItemDelegateForColumn(1, PrecisionDelegate(self.table))
    
        def _fillListWidget(self):
            listCol = {
                'Parameters': ['a', 'b', 'c', 'd', 'e'],
                'Precision': [-1, -1, 2, 3, 1]}
            self.df = pd.DataFrame(listCol)
            for parameter, precision in zip(self.df['Parameters'], self.df['Precision']):
                rowPosition = self.table.rowCount()
                self.table.insertRow(rowPosition)
                self.table.setItem(rowPosition, 0, QTableWidgetItem(parameter))
                precisionItem = QTableWidgetItem()
                precisionItem.setData(QtCore.Qt.DisplayRole, precision)
                self.table.setItem(rowPosition, 1, precisionItem)
                self.table.openPersistentEditor(self.table.item(rowPosition, 1))