Khalil Al Hooti
Khalil Al Hooti

Reputation: 4506

Remove QComboBox when QCheckBox is toggled off or on using QItemDelegate

I would like to update the cell content of a QTableView with a ComboBox whenever the toggle of a Checkbox above changes. I am using QTableView and a custom delegate to draw the ComboBoxes. The Checkboxes are controlled within the QTableView itself. Currently, when I toggle the checkboxes, the Comboboxes below will appear, but I could not manage to remove the ComboBoxes when toggling off the CheckBoxes.

My code sample is below.

import sys
import pandas as pd
from pandas.api.types import is_numeric_dtype
import numpy as np

from PyQt5.QtCore import (QAbstractTableModel, Qt, pyqtProperty, pyqtSlot,
                          QVariant, QModelIndex, pyqtSignal)
from PyQt5.QtWidgets import (QComboBox, QApplication, QAbstractItemView,
                             QItemDelegate, QCheckBox, QMainWindow, QTableView)


class DataFrameModel(QAbstractTableModel):
    DtypeRole = Qt.UserRole + 1000
    ValueRole = Qt.UserRole + 1001

    def __init__(self, df=pd.DataFrame(), parent=None):
        super(DataFrameModel, self).__init__(parent)
        self._dataframe = df
        self.df2 = pd.DataFrame(self._dataframe.iloc[2:3, :].to_dict())

    def setDataFrame(self, dataframe):
        self.beginResetModel()
        self._dataframe = dataframe.copy()
        self.endResetModel()

    def dataFrame(self):
        return self._dataframe

    dataFrame = pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)

    @pyqtSlot(int, Qt.Orientation, result=str)
    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return QVariant()

        if orientation == Qt.Horizontal:
            try:
                return self._dataframe.columns.tolist()[section]
            except (IndexError,):
                return QVariant()
        elif orientation == Qt.Vertical:
            try:
                if section in [0, 1]:
                    pass
                else:
                    return self._dataframe.index.tolist()[section - 2]
            except (IndexError,):
                return QVariant()

    def rowCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0
        return len(self._dataframe.index)

    def columnCount(self, parent=QModelIndex()):

        if parent.isValid():
            return 0
        return self._dataframe.columns.size

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        col = index.column()
        row = index.row()
        dt = self.df2[self.df2.columns[col]].dtype
        is_numeric = is_numeric_dtype(self.df2[self.df2.columns[col]])
        if row == 0 and is_numeric:
            value = self._dataframe.iloc[row, col].text()
        else:
            value = self._dataframe.iloc[row, col]
        if role == Qt.DisplayRole:
            return value
        elif role == Qt.CheckStateRole:
            if row == 0 and is_numeric:
                if self._dataframe.iloc[row, col].isChecked():
                    return Qt.Checked
                else:
                    return Qt.Unchecked

        elif role == DataFrameModel.ValueRole:
            return value
        elif role == DataFrameModel.ValueRole:
            return value
        if role == DataFrameModel.DtypeRole:
            return dt
        return QVariant()

    def roleNames(self):
        roles = {
            Qt.DisplayRole: b'display',
            DataFrameModel.DtypeRole: b'dtype',
            DataFrameModel.ValueRole: b'value'
        }
        return roles

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        col = index.column()
        row = index.row()
        is_numeric = is_numeric_dtype(self.df2[self.df2.columns[col]])
        if role == Qt.CheckStateRole and index.row() == 0:
            if is_numeric:
                if value == Qt.Checked:
                    self._dataframe.iloc[row, col].setChecked(True)
                    self._dataframe.iloc[row, col].setText("Grade Item")
                else:
                    self._dataframe.iloc[row, col].setChecked(False)
                    self._dataframe.iloc[row, col].setText("Not a Grade")
        elif row == 1 and role == Qt.EditRole:
            if isinstance(value, QVariant):
                value = value.value()
            if hasattr(value, 'toPyObject'):
                value = value.toPyObject()
            self._dataframe.iloc[row, col] = value
        elif row >= 2 and role == Qt.EditRole:
            try:
                value = eval(value)
                if not isinstance(
                        value,
                        self._dataframe.applymap(type).iloc[row, col]):
                    value = self._dataframe.iloc[row, col]
            except:
                value = self._dataframe.iloc[row, col]
            self._dataframe.iloc[row, col] = value
        self.dataChanged.emit(index, index, (Qt.DisplayRole,))
        return True

    def flags(self, index):
        if not index.isValid():
            return None

        if index.row() == 0:
            return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
                    Qt.ItemIsUserCheckable)
        else:
            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable

    def sort(self, column, order):
        self.layoutAboutToBeChanged.emit()
        col_name = self._dataframe.columns.tolist()[column]
        sheet1 = self._dataframe.iloc[:2, :]
        sheet2 = self._dataframe.iloc[2:, :].sort_values(
            col_name, ascending=order == Qt.AscendingOrder, inplace=False)

        sheet2.reset_index(drop=True, inplace=True)
        sheet3 = pd.concat([sheet1, sheet2], ignore_index=True)
        self.setDataFrame(sheet3)
        self.layoutChanged.emit()


class ComboBoxDelegate(QItemDelegate):
    def __init__(self, parent, choices=None):
        super().__init__(parent)
        self.items = choices

    def createEditor(self, parent, option, index):
        self.parent().model().dataChanged.emit(index, index, (Qt.DisplayRole,))
        if is_numeric_dtype(
                self.parent().model().df2[
                    self.parent().model().df2.columns[index.column()]]):
            checked = self.parent().model().dataFrame.iloc[
                0, index.column()].isChecked()
            if checked:
                editor = QComboBox(parent)
                editor.addItems(self.items)
                editor.currentIndexChanged.connect(self.currentIndexChanged)
                return editor

    def paint(self, painter, option, index):
        if isinstance(self.parent(), QAbstractItemView):
            self.parent().openPersistentEditor(index)

    def setModelData(self, editor, model, index):
        value = editor.currentText()
        model.setData(index, value, Qt.DisplayRole)

    def setEditorData(self, editor, index):
        text = index.data(Qt.DisplayRole) or ""
        editor.setCurrentText(text)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    @pyqtSlot()
    def currentIndexChanged(self):
        editor = self.sender()
        self.commitData.emit(editor)


class MainWindow(QMainWindow):
    def __init__(self, pandas_sheet):
        super().__init__()
        self.pandas_sheet = pandas_sheet

        self.table = QTableView()
        self.setCentralWidget(self.table)

        check_bx_lst = []
        is_number = [is_numeric_dtype(self.pandas_sheet[col]) for col
                     in self.pandas_sheet.columns]
        for is_numb in is_number:
            if is_numb:
                checkbox = QCheckBox('Not a Grade')
                checkbox.setChecked(False)
                check_bx_lst.append(checkbox)
            else:
                check_bx_lst.append(None)

        for i in range(2):
            self.pandas_sheet.loc[-1] = [' '] * \
                                        self.pandas_sheet.columns.size
            self.pandas_sheet.index = self.pandas_sheet.index + 1
            self.pandas_sheet = self.pandas_sheet.sort_index()

        self.pandas_sheet.loc[0] = check_bx_lst

        model = DataFrameModel(self.pandas_sheet)
        self.table.setModel(model)
        self.table.setSortingEnabled(True)

        delegate = ComboBoxDelegate(self.table,
                                    [None, 'Test', 'Quiz'])
        self.table.setItemDelegateForRow(1, delegate)
        self.table.resizeColumnsToContents()
        self.table.resizeRowsToContents()


if __name__ == '__main__':
    df = pd.DataFrame({'a': ['student ' + str(i) for i in range(5)],
                       'b': np.arange(5),
                       'c': np.random.rand(5)})
    app = QApplication(sys.argv)
    window = MainWindow(df)
    window.table.model().sort(df.columns.get_loc("a"), Qt.AscendingOrder)
    window.setFixedSize(280, 200)
    window.show()

    sys.exit(app.exec_())

I would like to remove the Combobox when the checkbox above is toggled off.

Any help is really appreciated.

enter image description here

Upvotes: 0

Views: 296

Answers (1)

musicamante
musicamante

Reputation: 48231

You are using openPersistentEditor within the paint function, which is simply wrong: painting happens very often for every index the delegate is used, and you're practically calling createEditor each time each cell in that row is painted, something that happens for all the cells when the view is scrolled, or for any cell hovered by the mouse.

Following your logic, the creator is finally created probably due to painting requested by data change, because at that point the if conditions in createEditor are True. From that point on, you create a persistent editor, and if you don't remove it it will just stay there.
Obviously, all this is not a good approach, mostly because you virtually check if it's ok to create the editor from a paint function, which doesn't make much sense.

You should connect to the dataChanged signal for the model to verify the checked status and then open or close the editor accordingly.

class MainWindow(QMainWindow):
    def __init__(self, pandas_sheet):
        # ...
        model.dataChanged.connect(self.dataChanged)

    def dataChanged(self, topLeft, bottomRight, roles):
        if topLeft.row() == 0:
            if topLeft.data(QtCore.Qt.CheckStateRole):
                self.table.openPersistentEditor(topLeft.sibling(1, topLeft.column()))
            else:
                self.table.closePersistentEditor(topLeft.sibling(1, topLeft.column()))

I've over-simplified the if condition, but I assume that the concept is clear enough.

Upvotes: 2

Related Questions