Can QCompleter's list of values be modified/updated later again in pyqt5?

0

I am making a GUI using Pyqt5 where there are 5 Qlineedit fields.

The range of values for each can be say [1 -5]. If the user tried to select third Qlineedit field and selected the value '2'. The other fields range should now not include '2' in it.

Similarly if he selected another field with value '4', the remaining fields should have only [1, 3, 5] as available options.

(Please bear in mind that user can delete the value in any field too and available values should be updated accordingly.)

I tried to use list and update QCompleter for the fields as soon as I see any textChanged signal.

The below code works perfectly except when the user lets say had given the input '14' in some field before and now tries to delete it using backspace. '4' will get deleted but on deleting '1' it will crash without any backtrace.

"Process finished with exit code -1073741819 (0xC0000005)"

If I click on the lineedit field before deleting '1', it works fine.

Minimal code -

#!/usr/bin/env python

import logging
import sys
import traceback
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QApplication, QCompleter

class MyWidget(QWidget):
    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)

        vbox = QVBoxLayout(self)
        self.setLayout(vbox)
        self.names = ['11', '12', '13', '14', '15']
        self.names.sort()
        self.line_edits_list = [0]*5

        for i in range(len(self.names)):
            self.line_edits_list[i] = QLineEdit(self)
            completer = QCompleter(self.names, self.line_edits_list[i])
            self.line_edits_list[i].setCompleter(completer)
            vbox.addWidget(self.line_edits_list[i])
            self.line_edits_list[i].textEdited.connect(self.text_changed)

    def text_changed(self, text):
        names_sel = []
        # Check if current text matches anything in our list, if it does add it to a new list
        for i in range(len(self.line_edits_list)):
            if self.line_edits_list[i].text() in self.names and self.line_edits_list[i].text() not in names_sel:
                names_sel.append(self.line_edits_list[i].text())

        # The remaining textfields should get their qcompleter ranges updated with unique values of the two lists
        for i in range(len(self.line_edits_list)):
            if self.line_edits_list[i].text() not in self.names:
                try:
                    new_range = list((set(self.names) - set(names_sel)))
                    completer = QCompleter(new_range, self.line_edits_list[i])
                    self.line_edits_list[i].setCompleter(completer)
                except:
                    print(traceback.format_exc())


def test():
    app = QApplication(sys.argv)
    w = MyWidget()
    w.show()
    app.exec_()
    print("END")


if __name__ == '__main__':
    test()

python
pyqt
pyqt5
qcompleter
asked on Stack Overflow Feb 17, 2021 by Abhinav Saini • edited Feb 17, 2021 by eyllanesc

2 Answers

1

As I pointed out in the comments, using QLineEdit is not a suitable widget if you want to restrict the values that the user can select. In this case, as the user has multiple options, then a suitable widget is the QComboBox. To do the filtering you can use QSortFilterProxyModel overriding the filterAcceptsRow method to implement custom logic.

#!/usr/bin/env python

import sys

from PyQt5.QtCore import QSortFilterProxyModel
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QApplication, QComboBox, QVBoxLayout, QWidget


class ProxyModel(QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._hide_items = []

    @property
    def hide_items(self):
        return self._hide_items

    @hide_items.setter
    def hide_items(self, items):
        self._hide_items = items
        self.invalidateFilter()

    def filterAcceptsRow(self, sourceRow, sourceParent):
        index = self.sourceModel().index(sourceRow, 0, sourceParent)
        return index.data() not in self.hide_items


class MyWidget(QWidget):
    def __init__(self, names, parent=None):
        super(MyWidget, self).__init__(parent)

        self._comboboxes = []

        model = QStandardItemModel()

        vbox = QVBoxLayout(self)

        for name in names:
            model.appendRow(QStandardItem(name))
            proxy_model = ProxyModel()
            proxy_model.setSourceModel(model)
            combobox = QComboBox()
            combobox.setModel(proxy_model)
            vbox.addWidget(combobox)
            combobox.setCurrentIndex(-1)
            combobox.currentIndexChanged.connect(self.handle_currentIndexChanged)
            self.comboboxes.append(combobox)

        self.resize(640, 480)

    @property
    def comboboxes(self):
        return self._comboboxes

    def handle_currentIndexChanged(self):
        rows = []
        for combobox in self.comboboxes:
            if combobox.currentIndex() != -1:
                rows.append(combobox.currentText())

        for i, combobox in enumerate(self.comboboxes):
            index = combobox.currentIndex()
            proxy_model = combobox.model()
            r = rows[:]
            if index != -1:
                text = combobox.currentText()
                if text in r:
                    r.remove(text)
            combobox.blockSignals(True)
            proxy_model.hide_items = r
            combobox.blockSignals(False)


def test():
    app = QApplication(sys.argv)

    names = ["11", "12", "13", "14", "15"]

    w = MyWidget(names)
    w.show()

    app.exec_()


if __name__ == "__main__":
    test()
answered on Stack Overflow Feb 17, 2021 by eyllanesc • edited Feb 17, 2021 by eyllanesc
0

The correct way to implement this is by using a model with the QCompleter. The model can change its content over time and the completer will react to it accordingly.

In your case, you could create a model that contains all possible values first. Then, you could use a QSortFilterProxyModel, which, given you current UI state, could reduce the set. The proxy model is the one that you use with QCompleter.

This (otherwise unrelated) question is example Python code for implementing such a proxy model: QSortFilterProxyModel does not apply Caseinsensitive

answered on Stack Overflow Feb 17, 2021 by ypnos

User contributions licensed under CC BY-SA 3.0