Пользовательские правила завершения 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
в chicken soup
произвести другое совпадение, которое не только на содержание текста. Я могу справиться с алгоритмом, но,
Какие из функций QCompleter мне нужно переопределить?
Я не совсем уверен, где я должен искать...
8 ответов
Основываясь на предложении @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://stackru.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)
Обновить:
Я полагал, что мое предыдущее решение работало, пока строка в выпадающем списке не соответствовала ни одному из элементов списка. Тогда QFilterProxyModel
был пуст, и это, в свою очередь, сбросило text
из выпадающего списка. Я пытался найти элегантное решение этой проблемы, но я сталкивался с проблемами (ссылаясь на ошибки удаленных объектов) всякий раз, когда пытался что-то изменить в self.filterProxyModel
, Так что теперь взломать, чтобы установить модель self.filterProxyModel
каждый раз, когда новый шаблон обновляется. И всякий раз, когда шаблон больше ничего не соответствует в модели, чтобы дать ему новую модель, которая просто содержит текущий текст (иначе path
в splitPath
). Это может привести к проблемам с производительностью, если вы имеете дело с очень большими моделями, но для меня взлом работает довольно хорошо.
Обновление 2:
Я понял, что это все еще не идеальный путь, потому что, если новая строка введена в поле со списком и пользователь нажимает клавишу ввода, поле со списком снова очищается. Единственный способ ввести новую строку - это выбрать ее из выпадающего меню после ввода.
Обновление 3:
Теперь введите работает также. Я работал над сбросом текста в выпадающем списке, просто снимая его, когда пользователь нажимает ввод. Но я вернул его обратно, чтобы функциональность завершения осталась на месте. Если пользователь решает сделать дальнейшие изменения.
Использование 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)
Спасибо Thorbjørn, я действительно решил проблему, унаследовав от QSortFilterProxyModel
,
filterAcceptsRow
Метод должен быть перезаписан, а затем вы просто возвращаете true или false в зависимости от того, хотите ли вы, чтобы этот элемент отображался.
Проблема с этим решением состоит в том, что оно скрывает только элементы в списке, и поэтому вы никогда не сможете изменить их порядок (что я и хотел сделать, чтобы дать приоритет некоторым элементам).
[РЕДАКТИРОВАТЬ]
Я думал, что добавлю это в решение, поскольку это [в основном] то, что я в итоге сделал (потому что вышеупомянутое решение не было адекватным). Я использовал 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
для магии, если вы заинтересованы).
В то же время вы можете проголосовать за QTBUG-7830, который позволяет настроить способ сопоставления элементов завершения так, как вы хотите. Но не задерживай дыхание на этом.
Эта страница была просмотрена более 14 тысяч раз, и на нее ссылаются многие другие сообщения на SO. Такое впечатление, что люди каждый раз при вызове создают и настраивают новую модель прокси, что совершенно не нужно (и дорого для больших моделей). Нам просто нужно установить прокси-модель один раз в файле .
Как упомянул @bruno:
Похоже, что модель нужно устанавливать каждый раз
splitPath
вызывается (установка прокси один раз вsetModel
не работает).
Это потому, что если мы не отменим текущую фильтрацию, прокси-модель не будет обновляться внутри. Просто убедитесь, что все текущие фильтры или сортировки в прокси-модели недействительны, и тогда вы сможете увидеть обновления:
def splitPath(self, path):
self.local_completion_prefix = path
self.proxyModel.invalidateFilter() # invalidate the current filtering
self.proxyModel.invalidate() # or invalidate both filtering and sorting
return ""
Это доступно, начиная с Qt 4.3, см. https://doc.qt.io/qt-5/qsortfilterproxymodel.html#invalidateFilter .
Самое простое решение с PyQt5:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QCompleter
completer = QCompleter()
completer.setFilterMode(Qt.MatchContains)
Вы можете обойти QTBUG-7830, как упомянуто выше, предоставив пользовательскую роль и выполнив эту роль. В обработчике этой роли вы можете сделать так, чтобы QCompleter знал, что этот элемент есть. Это будет работать, если вы также переопределите filterAcceptsRow в своей модели SortFilterProxy.