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()
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()
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
User contributions licensed under CC BY-SA 3.0