QCompleter自定义完成规则

25
我正在使用Qt4.6,并且我有一个带有QCompleter的QComboBox。
通常的功能是基于前缀提供完成提示(这些提示可以在下拉列表中而不是内联显示-这是我的用法)。例如,给定以下内容:
chicken soup
chilli peppers
grilled chicken

输入 ch 可以匹配 chicken soupchilli peppers,但不能匹配 grilled chicken

我想要的是能够输入 ch 并匹配全部,更具体地说,可以输入 chicken 并匹配 chicken soupgrilled chicken
我还想为 chicken soup 分配一个标签 chs,以便产生另一个匹配,不仅基于文本内容。我可以处理算法,但是,

我需要重写哪个 QCompleter 函数呢?
我真的不确定应该在哪里查找...

8个回答

12

根据@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)

好东西。如果想要使用它,请确保将组合框和自动完成器中的所有选项设置为上面显示的代码中的选项。 - P.R.
2
我在另一个答案中扩展了您的解决方案,因为我在您的解决方案中遇到了一个错误。当我输入时,组合框会建议正确的项目。但是,一旦我按下退格键来更正我正在查找的字符串,组合框就不再建议任何项目了。 - P.R.

10

基于@Bruno的回答,我正在使用标准的QSortFilterProxyModel函数setFilterRegExp来更改搜索字符串。这样就不需要子类化了。

它还修复了@Bruno答案中的一个错误,一旦输入字符串通过退格键进行校正,建议就会消失。

class CustomQCompleter(QtGui.QCompleter):
    """
    adapted from: https://dev59.com/32435IYBdhLWcg3w7kyb#7767999
    """
    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的模型为新模型。并且每当模式不再与模型中的任何内容匹配时,将其替换为只包含当前文本(即splitPath中的path)的新模型。如果您处理非常大的模型,则可能会导致性能问题,但对于我来说,这个方法可以很好地工作。

更新 2:

我意识到这仍然不是最完美的方式,因为如果在组合框中键入新字符串并按enter键,组合框将再次被清除。输入新字符串的唯一方法是在键入后从下拉菜单中选择它。

更新 3:

现在按Enter键也可以了。我通过在用户按下Enter键时简单地将其取消来解决了组合框文本重置的问题。但我重新加回它,以保持完成功能的存在。如果用户决定进行进一步编辑。


关于常量和注释“QtCore.Qt.Key_Enter)不知何故无法工作”,您可以使用QtCore.Qt.Key_Return代替。 - shao.lo

9
请使用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)

我其实也想控制匹配结果的顺序,所以据我理解这仍然不够。 - jcuenod
在当前的实现中,您无法查看Qt :: MatchFlags可能的值。此外,您可以预先对模型进行排序以获得所需的顺序。 - Aleksey Kontsevich

2
感谢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;
}

很高兴知道QSortFilterProxyModel至少允许您自定义项目过滤的方式!顺便问一下,您确定在排序过滤器过滤结果后,QCompleter不会仍然“也”应用其内置过滤吗? - Thorbjørn Lindeijer
@Thorbjørn Lindeijer,为了避免QCompleter过滤器,可以将""作为路径并在过滤模型中显示所有内容(我刚刚发布了一个示例)。可能不是最有效的方法,但似乎可以工作。 - Bruno

1

使用PyQt5的最简单解决方案:

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QCompleter

completer = QCompleter()
completer.setFilterMode(Qt.MatchContains)

1

很不幸,目前的答案是不可能的。要做到这一点,您需要在自己的应用程序中复制QCompleter的大部分功能(如果您感兴趣,Qt Creator为其定位器执行此操作,请参见src/plugins/locator/locatorwidget.cpp以获取更多信息)。

同时,您可以投票QTBUG-7830,这是关于使自定义匹配完成项的方式成为可能的问题,就像您想要的那样。但是不要抱太大希望。


1

此页面已被浏览超过14k次,并被许多其他SO帖子引用。似乎每次调用splitPath时,人们都会创建和设置一个新的代理模型,这是完全不必要的(对于大型模型来说也很昂贵)。我们只需要在setModel中设置一次代理模型即可。

正如@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


0
你可以通过提供自定义角色并在该角色的处理程序中执行特殊操作,从而解决如上所述的QTBUG-7830问题。在该角色的处理程序中,您可以进行一些技巧来让QCompleter知道该项已存在。如果您还覆盖了SortFilterProxy模型中的filterAcceptsRow方法,则此方法也将起作用。

1
嘿@psp。答案看起来不错,但如果您认为可以添加一个代码示例,它可能会更清晰明了。 - StackExchange What The Heck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接