Пользовательские Правила Завершения QCompleter
Я использую Qt4. 6, и у меня есть QComboBox с QCompleter в нем.
Обычная функциональность заключается в предоставлении подсказок завершения (они могут быть в выпадающем списке, а не встроены-что является моим использованием) на основе префикса. Например, дано
chicken soup
chilli peppers
grilled chicken
Ввод ch
будет соответствовать chicken soup
и chilli peppers
, но не grilled chicken
.
ch
и сопоставить их все или, более конкретно, chicken
и сопоставить chicken soup
и grilled chicken
.Я также хочу иметь возможность назначить тег типа
chs
to chicken soup
для получения другого соответствия, которое не только на содержании текста. Я могу справиться с алгоритмом, но,
Какие функции QCompleter мне нужно переопределить?
Я не совсем уверен, куда мне следует смотреть...
6 ответов:
Основываясь на предложении @j3frea, вот рабочий пример (используя
PySide
). Оказывается, что модель нужно устанавливать каждый раз, когда вызываетсяsplitPath
(Установка прокси один раз вsetModel
не работает).combobox.setEditable(True) combobox.setInsertPolicy(QComboBox.NoInsert) class CustomQCompleter(QCompleter): def __init__(self, parent=None): super(CustomQCompleter, self).__init__(parent) self.local_completion_prefix = "" self.source_model = None def setModel(self, model): self.source_model = model super(CustomQCompleter, self).setModel(self.source_model) def updateModel(self): local_completion_prefix = self.local_completion_prefix class InnerProxyModel(QSortFilterProxyModel): def filterAcceptsRow(self, sourceRow, sourceParent): index0 = self.sourceModel().index(sourceRow, 0, sourceParent) return local_completion_prefix.lower() in self.sourceModel().data(index0).lower() proxy_model = InnerProxyModel() proxy_model.setSourceModel(self.source_model) super(CustomQCompleter, self).setModel(proxy_model) def splitPath(self, path): self.local_completion_prefix = path self.updateModel() return "" completer = CustomQCompleter(combobox) completer.setCompletionMode(QCompleter.PopupCompletion) completer.setModel(combobox.model()) combobox.setCompleter(completer)
Основываясь на ответе @Bruno, я использую стандартную функцию
QSortFilterProxyModel
setFilterRegExp
для изменения строки поиска. Таким образом, нет необходимости в подклассах.Он также исправляет ошибку в ответе @Bruno, из-за которой предложения исчезали по некоторым причинам, когда входная строка исправлялась с помощью backspace во время ввода.
class CustomQCompleter(QtGui.QCompleter): """ adapted from: http://stackoverflow.com/a/7767999/2156909 """ def __init__(self, *args):#parent=None): super(CustomQCompleter, self).__init__(*args) self.local_completion_prefix = "" self.source_model = None self.filterProxyModel = QtGui.QSortFilterProxyModel(self) self.usingOriginalModel = False def setModel(self, model): self.source_model = model self.filterProxyModel = QtGui.QSortFilterProxyModel(self) self.filterProxyModel.setSourceModel(self.source_model) super(CustomQCompleter, self).setModel(self.filterProxyModel) self.usingOriginalModel = True def updateModel(self): if not self.usingOriginalModel: self.filterProxyModel.setSourceModel(self.source_model) pattern = QtCore.QRegExp(self.local_completion_prefix, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString) self.filterProxyModel.setFilterRegExp(pattern) def splitPath(self, path): self.local_completion_prefix = path self.updateModel() if self.filterProxyModel.rowCount() == 0: self.usingOriginalModel = False self.filterProxyModel.setSourceModel(QtGui.QStringListModel([path])) return [path] return [] class AutoCompleteComboBox(QtGui.QComboBox): def __init__(self, *args, **kwargs): super(AutoCompleteComboBox, self).__init__(*args, **kwargs) self.setEditable(True) self.setInsertPolicy(self.NoInsert) self.comp = CustomQCompleter(self) self.comp.setCompletionMode(QtGui.QCompleter.PopupCompletion) self.setCompleter(self.comp)# self.setModel(["Lola", "Lila", "Cola", 'Lothian']) def setModel(self, strList): self.clear() self.insertItems(0, strList) self.comp.setModel(self.model()) def focusInEvent(self, event): self.clearEditText() super(AutoCompleteComboBox, self).focusInEvent(event) def keyPressEvent(self, event): key = event.key() if key == 16777220: # Enter (if event.key() == QtCore.Qt.Key_Enter) does not work # for some reason # make sure that the completer does not set the # currentText of the combobox to "" when pressing enter text = self.currentText() self.setCompleter(None) self.setEditText(text) self.setCompleter(self.comp) return super(AutoCompleteComboBox, self).keyPressEvent(event)
Обновление:
Я полагал, что мое предыдущее решение работало до тех пор, пока строка в combobox не совпадала ни с одним из элементов списка. ТогдаQFilterProxyModel
был пуст. и это, в свою очередь, сбросилоtext
из combobox. Я попытался найти элегантное решение этой проблемы, но столкнулся с проблемами (ссылками на ошибки удаленных объектов) всякий раз, когда пытался что-то изменить вself.filterProxyModel
. Так что теперь Хак должен установить модельself.filterProxyModel
каждый раз, когда ее шаблон обновляется. И всякий раз, когда шаблон больше не соответствует ничему в модели, чтобы дать ему новую модель, которая просто содержит текущий текст (он жеpath
вsplitPath
). Это может привести к проблемам с производительностью, если вы мы имеем дело с очень большими моделями, но для меня хак работает довольно хорошо.Обновление 2:
Я понял, что это все еще не идеальный способ, потому что, если новая строка вводится в combobox и пользователь нажимает enter, combobox снова очищается. Единственный способ ввести новую строку-это выбрать ее из выпадающего меню после ввода.
Обновление 3:
Теперь enter работает также. Я работал вокруг перезагрузки текстовое поле со списком, просто снимая ее заряжайте, когда пользователь нажимает enter. Но я вставил его обратно, так что функциональность завершения осталась на месте. Если пользователь решит сделать дальнейшие правки.
Используйте свойство
filterMode : Qt::MatchFlags
. Это свойство определяет, как выполняется фильтрация. Если filterMode имеет значениеQt::MatchStartsWith
, то будут отображаться только те записи, которые начинаются с введенных символов.Qt::MatchContains
отобразит записи, содержащие типизированные символы, иQt::MatchEndsWith
те, которые заканчиваются типизированными символами. в настоящее время реализованы только эти три режима . Установка filterMode на любой другойQt::MatchFlag
выдаст предупреждение, и никакие действия не будут выполнены. Режим по умолчанию:Qt::MatchStartsWith
.Это свойство было введено в Qt 5.2.
Функции доступа:
Qt::MatchFlags filterMode() const void setFilterMode(Qt::MatchFlags filterMode)
Спасибо, Торбьерн, Я действительно решил эту проблему, унаследовав от
QSortFilterProxyModel
.Метод
Проблема с этим решением заключается в том, что оно только скрывает элементы в списке, и поэтому вы никогда не сможете их переставить (что я и хотел сделать, чтобы придать определенным элементам приоритет).filterAcceptsRow
должен быть перезаписан, а затем вы просто возвращаете true или false в зависимости от того, хотите ли вы, чтобы этот элемент отображался.[EDIT]
Я думал, что брошу это в решение, так как это [в основном] то, что я в итоге сделал (потому что вышеописанное решение не было адекватным). Я использовал http://www.cppblog.com/biao/archive/2009/10/31/99873.html :#include "locationlineedit.h" #include <QKeyEvent> #include <QtGui/QListView> #include <QtGui/QStringListModel> #include <QDebug> LocationLineEdit::LocationLineEdit(QStringList *words, QHash<QString, int> *hash, QVector<int> *bookChapterRange, int maxVisibleRows, QWidget *parent) : QLineEdit(parent), words(**&words), hash(**&hash) { listView = new QListView(this); model = new QStringListModel(this); listView->setWindowFlags(Qt::ToolTip); connect(this, SIGNAL(textChanged(const QString &)), this, SLOT(setCompleter(const QString &))); connect(listView, SIGNAL(clicked(const QModelIndex &)), this, SLOT(completeText(const QModelIndex &))); this->bookChapterRange = new QVector<int>; this->bookChapterRange = bookChapterRange; this->maxVisibleRows = &maxVisibleRows; listView->setModel(model); } void LocationLineEdit::focusOutEvent(QFocusEvent *e) { listView->hide(); QLineEdit::focusOutEvent(e); } void LocationLineEdit::keyPressEvent(QKeyEvent *e) { int key = e->key(); if (!listView->isHidden()) { int count = listView->model()->rowCount(); QModelIndex currentIndex = listView->currentIndex(); if (key == Qt::Key_Down || key == Qt::Key_Up) { int row = currentIndex.row(); switch(key) { case Qt::Key_Down: if (++row >= count) row = 0; break; case Qt::Key_Up: if (--row < 0) row = count - 1; break; } if (listView->isEnabled()) { QModelIndex index = listView->model()->index(row, 0); listView->setCurrentIndex(index); } } else if ((Qt::Key_Enter == key || Qt::Key_Return == key || Qt::Key_Space == key) && listView->isEnabled()) { if (currentIndex.isValid()) { QString text = currentIndex.data().toString(); setText(text + " "); listView->hide(); setCompleter(this->text()); } else if (this->text().length() > 1) { QString text = model->stringList().at(0); setText(text + " "); listView->hide(); setCompleter(this->text()); } else { QLineEdit::keyPressEvent(e); } } else if (Qt::Key_Escape == key) { listView->hide(); } else { listView->hide(); QLineEdit::keyPressEvent(e); } } else { if (key == Qt::Key_Down || key == Qt::Key_Up) { setCompleter(this->text()); if (!listView->isHidden()) { int row; switch(key) { case Qt::Key_Down: row = 0; break; case Qt::Key_Up: row = listView->model()->rowCount() - 1; break; } if (listView->isEnabled()) { QModelIndex index = listView->model()->index(row, 0); listView->setCurrentIndex(index); } } } else { QLineEdit::keyPressEvent(e); } } } void LocationLineEdit::setCompleter(const QString &text) { if (text.isEmpty()) { listView->hide(); return; } /* This is there in the original but it seems to be bad for performance (keeping listview hidden unnecessarily - havn't thought about it properly though) */ // if ((text.length() > 1) && (!listView->isHidden())) // { // return; // } model->setStringList(filteredModelFromText(text)); if (model->rowCount() == 0) { return; } int maxVisibleRows = 10; // Position the text edit QPoint p(0, height()); int x = mapToGlobal(p).x(); int y = mapToGlobal(p).y() + 1; listView->move(x, y); listView->setMinimumWidth(width()); listView->setMaximumWidth(width()); if (model->rowCount() > maxVisibleRows) { listView->setFixedHeight(maxVisibleRows * (listView->fontMetrics().height() + 2) + 2); } else { listView->setFixedHeight(model->rowCount() * (listView->fontMetrics().height() + 2) + 2); } listView->show(); } //Basically just a slot to connect to the listView's click event void LocationLineEdit::completeText(const QModelIndex &index) { QString text = index.data().toString(); setText(text); listView->hide(); } QStringList LocationLineEdit::filteredModelFromText(const QString &text) { QStringList newFilteredModel; //do whatever you like and fill the filteredModel return newFilteredModel; }
К сожалению, в настоящее время ответ таков, что это невозможно. Для этого вам нужно будет дублировать большую часть функциональности QCompleter в вашем собственном приложении (Qt Creator делает это для своего локатора, смотрите
src/plugins/locator/locatorwidget.cpp
для magic, если вам интересно).Тем временем вы можете проголосовать заQTBUG-7830 , который позволяет настроить способ сопоставления элементов завершения, как вы хотите. Но не задерживайте дыхание на этом.
Вы можете обойти QTBUG-7830, как упоминалось выше, предоставив пользовательскую роль и выполнив завершение для этой роли. В обработчике этой роли вы можете сделать трюк, чтобы QCompleter знал, что элемент есть. Это будет работать, если вы также переопределите filterAcceptsRow в вашей модели SortFilterProxy.