Dirich
Dirich

Reputation: 442

QLineEdit displays no data

I created a gui that supposedly should allow to write some notes in a QLineEdit. If I use no custom approach and just display the data via the basic QTableView I can see everything normally, but when I start using a custom approach (because of dates, combo boxes etc), every widget works correctly with the exception of QLineEdit, which does not display the model data and refuses to display anything I write into it (albeit during editing I can see what I'm typing).

I've fought with this for over a week, trying many different approaches (and creating more bugs). My current suspicion is that for some reason I'm not interacting with the right QLineEdit widget, and that somehow my code creates an additional one right on top of the one that is connected to the model. Still, I created this by following the basic examples, and I'm not able to see where the issue is, nor I have been able to debug since python debugger skips over all the default implementations (because they are C++, I guess).

The minimal example is still a bit fat, so I've put it into a file: download

All that I expect is for the second column widgets to display text:

  1. At start, since they are initialised (or should be) with the model data.
  2. After an edit.

In short, they should always display the text.

Just run and observe the 2nd column (try to edit too). The TableView sets the Delegate responsible for the widget interaction and manages the model. I would expect the issue to be between TableView and Delegate.

EDIT: someone requested I paste the minimalistic example instead of the file, so here it is.

#!/usr/bin/env python3
from collections import OrderedDict
from PyQt5 import QtCore, QtWidgets
import pandas as pd


def validate(value, expected_type):
    """ Returns an object of expected_type, with the passed value if possible, the default value otherwise. """
    if type(value) == QtCore.QVariant:
        value = None if value.isNull() or not value.isValid() else value.value()
    value = expected_type() if value is None else expected_type(str(value))
    return value


class ComboBox(QtWidgets.QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)

    def data(self):
        return self.currentIndex()

    def set_data(self, value):
        print("ComboBox received {}".format(type(value)))
        print("value {}".format(value))
        value = int(validate(value, float))
        print("valid value {}".format(value))
        self.setCurrentIndex(value)
        self.update()


class InfoTypeSelector(ComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.addItems(["A1", "A2", "A3", "A4"])


class DateEdit(QtWidgets.QDateEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setCalendarPopup(True)

    def data(self):
        return self.date().toString()

    def set_data(self, value):
        print("DateEdit received {}".format(type(value)))
        print("value {}".format(value))
        value = validate(value, str)
        print("valid value {}".format(value))
        self.setDate(QtCore.QDate.fromString(value))
        self.update()


class TimeEdit(QtWidgets.QTimeEdit):
    def data(self):
        return self.time().toString()

    def set_data(self, value):
        print("TimeEdit received {}".format(type(value)))
        print("value {}".format(value.value()))
        value = validate(value, str)
        print("valid value {}".format(value))
        self.setTime(QtCore.QTime.fromString(value))
        self.update()


class LineEdit(QtWidgets.QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAlignment(QtCore.Qt.AlignLeft)

    def data(self):
        return str(self.text())

    def set_data(self, value):
        print("LineEdit received {}".format(type(value)))
        print("value {}".format(value))
        value = validate(value, str)
        print("valid value {}".format(value))
        self.setText(value)
        self.update()

    def displayText(self):
        print("dysplaying: {}".format(super().displayText()))
        return super().displayText()

    def setText(self, value):
        print("updating: {}".format(value))
        super().setText(value)


class CheckBox(QtWidgets.QCheckBox):
    def data(self):
        return self.checkState()

    def set_data(self, value):
        print("CheckBox received {}".format(type(value)))
        print("value {}".format(value))
        value = validate(value, bool)
        print("valid value {}".format(value))
        self.setCheckState(value)
        self.update()


class Delegate(QtWidgets.QStyledItemDelegate):
    """ This handles widget creation, allowing to pass a dataframe and a 2d index on construction. """
    def __init__(self, factories, model):
        """ data is the dataframe used for initialisation of the widgets, as well as to store the widget values
            so that they can be serialized.
            factories is a list of widget generators (their type), indexed by the column index. """
        super().__init__()
        self.factories = factories
        self.model = model

    def createEditor(self, parent, option, index):
        if self.factories[index.column()] == str:
            return super().createEditor(parent, option, index)
        value = self.model.data(index)
        widget = self.factories[index.column()](parent)
        widget.set_data(value)
        return widget

    def setModelData(self, widget, model, index):
        if self.factories[index.column()] == str:
            super().setModelData(widget, model, index)
            return
        self.model.setData(index, widget.data())


class TableView(QtWidgets.QTableView):
    def __init__(self, name, dataframe, gui_column_types, default_column_values, parent=None, editable=False):
        """ dataframe holds the serializable data that store the internal state of the widgets in guiframe.
            gui_column_types is a list of types, ordered as the columns of guiframe should be. """
        super().__init__(parent)
        self.setSortingEnabled(True)
        self.setObjectName(name)

        self.editable = editable
        self.gui_column_types = gui_column_types
        self.default_column_values = default_column_values

        self.model = SerializableModel(dataframe, default_column_values, self, editable=self.editable)

        # Delegate Setup
        delegate = Delegate(gui_column_types, self.model)
        self.setItemDelegate(delegate)
        # Show the edit widget as soon as the user clicks in the cell (needed for item delegate)
        self.setEditTriggers(self.CurrentChanged)
        self.setModel(self.model)

    def init(self):
        # self.model.init()
        for row in range(self.model.rowCount()):
            for column in range(self.model.columnCount()):
                self.enable_cell(row, column)

    def row_add(self):
        # This cleans filters. If it didn't the added row might be filtered out and thus be invisible to the user.
        #0. clean filters
        #1. add to data
        self.model.row_append()
        return

    def row_delete(self):
        # This can be done while filtering.
        #0. remove from data
        #1. remove from filtered
        return

    def enable_cell(self, row, column):
        if self.gui_column_types[column] == str:
            return
        index = self.model.index(row, column)
        self.openPersistentEditor(index)

    def model_data(self):
        return self.model.dataframe


class SerializableModel(QtCore.QAbstractTableModel):
    def __init__(self, dataframe, default_column_values, table_view, parent=None, editable=False):
        "empty_row is a list of types ordered according to the columns in the dataframe, which represents an empty row"
        QtCore.QAbstractTableModel.__init__(self, parent=parent)
        self.empty_row = pd.DataFrame(data=default_column_values, columns=list(dataframe.columns))
        self.dataframe = dataframe
        self.filtered = self.dataframe
        self.table_view = table_view
        self.editable = editable

    def flags(self, index):
        defaults = super().flags(index)
        if self.editable:
            return defaults | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable
        return defaults | QtCore.Qt.ItemIsEnabled

    def data(self, index, role=QtCore.Qt.DisplayRole):
        # print("reading data {} {}".format(index.row(), index.column()))
        if role == QtCore.Qt.TextAlignmentRole:
            return QtCore.Qt.AlignCenter
        if not index.isValid() or role != QtCore.Qt.DisplayRole or index.row() >= self.filtered.shape[0]:
            return QtCore.QVariant()
        value = self.filtered.iat[index.row(), index.column()]
        return QtCore.QVariant(str(value))

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        column = self.filtered.columns[index.column()]
        column_dtype = self.filtered[column].dtype
        if column_dtype != object:
            value = None if value == '' else column_dtype.type(value)
        row = self.filtered.index.values[index.row()]
        self.dataframe.iat[row, index.column()] = value
        self.dataChanged.emit(index, index)
        return True

    def rowCount(self, parent=QtCore.QModelIndex()):
        return self.filtered.shape[0] if not parent.isValid() else 0

    def columnCount(self, parent=QtCore.QModelIndex()):
        return self.filtered.shape[1] if not parent.isValid() else 0

    def insertRows(self, row, count, parent=QtCore.QModelIndex()):
        """ Since we only need append, we assume row == rowCount. """
        for _ in range(count):
            self.dataframe = self.dataframe.append(self.empty_row, ignore_index=True)
        return True

    def row_append(self, count=1, parent=QtCore.QModelIndex()):
        """ Reset filters, to ensure the new rows are not hidden by them, and appends rows with default values. """
        self.filtered = self.dataframe
        self.beginInsertRows()
        self.insertRows(self.rowCount(), count, parent)
        self.endInsertRows()
        self.layoutChanged.emit()


DF_PROTOTYPE_GLOBAL = pd.DataFrame(data=OrderedDict((
    ("Type", [int()]),
    ("Details", [str()]),
)))

DF_PROTOTYPE_GLOBAL_SAVED = pd.DataFrame(data=OrderedDict((
    ("Type", [2, 0]),
    ("Details", ["item 1", "item 2"]),
)))

DF_PROTOTYPE_GLOBAL_GUIFRAME_TYPES = [InfoTypeSelector, LineEdit]

class InfoManager:
    def __init__(self):
        self.window = QtWidgets.QMainWindow()
        self.ui = Ui_InfoWindow()
        self.ui.setupUi(self.window)

        # Overwrites
        self.ui.view_global.setParent(None)
        self.ui.view_global = TableView("view_global", DF_PROTOTYPE_GLOBAL_SAVED,
                                              DF_PROTOTYPE_GLOBAL_GUIFRAME_TYPES,
                                              DF_PROTOTYPE_GLOBAL,
                                              parent=self.ui.tab_global, editable=True)
        self.ui.verticalLayout_4.addWidget(self.ui.view_global)
        self.ui.view_global.init()

        # self.ui.view_notes.setParent(None)
        # self.ui.view_notes = iwl.DataFrameWidget(name="view_notes", parent=self.ui.tab_notes, df=data[1], editable=True)
        # self.ui.verticalLayout_2.addWidget(self.ui.view_notes)
        #
        # self.ui.view_events.setParent(None)
        # self.ui.view_events = iwl.DataFrameWidget(name="view_events", parent=self.ui.tab_events, df=data[2], editable=True)
        # self.ui.verticalLayout.addWidget(self.ui.view_events)

    def show(self):
        self.window.show()


class Ui_InfoWindow(object):
    def setupUi(self, InfoWindow):
        InfoWindow.setObjectName("InfoWindow")
        InfoWindow.resize(1000, 748)
        self.centralwidget = QtWidgets.QWidget(InfoWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_5.setObjectName("verticalLayout_5")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.verticalLayout_5.addLayout(self.horizontalLayout)
        self.info_tabs = QtWidgets.QTabWidget(self.centralwidget)
        self.info_tabs.setObjectName("info_tabs")
        self.tab_global = QtWidgets.QWidget()
        self.tab_global.setObjectName("tab_global")
        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tab_global)
        self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_4.setObjectName("verticalLayout_4")
        self.view_global = QtWidgets.QTableView(self.tab_global)
        self.view_global.setSortingEnabled(True)
        self.view_global.setObjectName("view_global")
        self.verticalLayout_4.addWidget(self.view_global)
        self.info_tabs.addTab(self.tab_global, "")
        self.verticalLayout_5.addWidget(self.info_tabs)
        InfoWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(InfoWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 1000, 20))
        self.menubar.setObjectName("menubar")
        InfoWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(InfoWindow)
        self.statusbar.setObjectName("statusbar")
        InfoWindow.setStatusBar(self.statusbar)
        self.action_email_tab = QtWidgets.QAction(InfoWindow)
        self.action_email_tab.setObjectName("action_email_tab")
        self.action_exit = QtWidgets.QAction(InfoWindow)
        self.action_exit.setObjectName("action_exit")

        self.retranslateUi(InfoWindow)
        self.info_tabs.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(InfoWindow)

    def retranslateUi(self, InfoWindow):
        _translate = QtCore.QCoreApplication.translate
        InfoWindow.setWindowTitle(_translate("InfoWindow", "Info Window"))
        self.info_tabs.setTabText(self.info_tabs.indexOf(self.tab_global), _translate("InfoWindow", "Global"))
        self.action_email_tab.setText(_translate("InfoWindow", "Email Current Tab"))
        self.action_exit.setText(_translate("InfoWindow", "Exit"))


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    info_manager = InfoManager()
    info_manager.show()
    sys.exit(app.exec_())

Upvotes: 0

Views: 257

Answers (1)

musicamante
musicamante

Reputation: 48260

There are some issues with your code, with the most important being a too convoluted and far to be a Minimal, Reproducible Example. More about this at the bottom of this answer.

First of all, you're checking the data type against factories[column], which returns the field editor class, which doesn't make much sense. You should probably set an attribute for the widget class data type, and probably use a try/except statement to find if the data type is actually the one you are looking for (str, in your case), otherwise let Qt return a standard editor for the data type.

Then, since you're using a persistent editor, you've to remember that whenever you submit its data you will get the following, respectively:

  • QAbstractItemDelegate.commitData: the delegate says that it has some data to set
  • QAbstractItemDelegate.setModelData: the delegate tries to set the data to its model
  • QAbstractItemModel.setData: the model sets the data according to what the delegate's setModelData tells
  • [if the model is supports it] QAbstractItemModel.commitData: the model "saves" the data, for example commits it to the current SQL database for QSql[Table|Query]Models
  • QAbstractItemModel.dataChanged: the model signals that some data has changed (if the previous setData returns True) to every part interested
  • QAbstractItemDelegate.setEditorData: [important for this scenario] the delegate sets back the data to the editor, according to the model data, since it is a persistent one, but only if its data is a user property (that's a bit convoluted, but after some thinking I can understand why)

Your problem is exactly in the last part: whenever you set the data, the data is changed to the model, but then the editor data is not updated accordingly, then it's updated again using the "user property" data.

I believe that adding this method to the Delegate might suffice:

    def setEditorData(self, widget, index):
        # check if the data type is compatible
        try:
            if self.factories[index.columnn()].dataType:
                widget.set_data(index.data())
        except:
            # whatever else


A final, unrelated note.
I know it's hard to get a good MRE (expecially the minimal part), but remember that, while it could take you a lot of time (I know that, believe me), 99% of the times is really worth your efforts, and that's for 2 reasons.
First, it will make people (somebody willing to help you no matter what) much more inclined to actually give you a hand: if somebody finds him/herself too much involved in understanding your code than actually trying to find a solution, it's likely that they'll give up in the first place; it actually took me more than 10 minutes to have a glimpse of how your code actually works; that's not good.
I can assume that there's probably at least more than 20 users here (over way more than a million) able to answer your question; imagine if everyone of them decide to take more than 5 minutes just to understand your question (making almost two hour in total), and probably much - if not all - of them would give up after the first minute, leaving you without an useful answer.
Secondly, you might even find where the issue was, all by yourself. Which will not only solve your issue, but allow you to learn something more in the meantime.
Long story short: it's hard, but providing an minimal reproducible example is always worth the effort.

Upvotes: 2

Related Questions