【问题标题】:QCompleter Custom Completion RulesQCompleter 自定义完成规则
【发布时间】:2011-07-04 23:49:44
【问题描述】:

我使用的是 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 的哪些函数?
我不确定我应该在哪里寻找......

【问题讨论】:

    标签: qt qt4 qt4.6 qcombobox qcompleter


    【解决方案1】:

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

    【讨论】:

    • 好东西。如果要使用它,请确保在组合框和完成器中​​设置所有选项,如上面显示的代码中所示
    • 我在单独的答案中扩展了您的解决方案,因为我在您的解决方案中遇到了错误。组合框在键入时建议正确的项目。但是,一旦我按退格键更正我要查找的字符串,组合框就不再建议任何项目。
    【解决方案2】:

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

    【讨论】:

    • 我实际上也想控制比赛的顺序,所以据我所知这仍然不够
    • 在当前的实现中,你不能更进一步地查看 Qt::MatchFlags 可能的值。您也可以预先对您的模型进行排序以获得所需的顺序。
    【解决方案3】:

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

    它还修复了@Bruno 的答案中的一个错误,一旦输入字符串在键入时用退格键更正后,由于某些原因,建议就会消失。

    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)
    

    更新:

    我认为我之前的解决方案一直有效,直到组合框中的字符串与列表项都不匹配。然后QFilterProxyModel 为空,这反过来又重置了组合框的text。我试图找到一个优雅的解决方案来解决这个问题,但是每当我尝试在self.filterProxyModel 上更改某些内容时,我都会遇到问题(引用已删除的对象错误)。所以现在的技巧是在每次更新其模式时设置self.filterProxyModel 的模型。并且每当模式不再匹配模型中的任何内容时,给它一个只包含当前文本的新模型(又名path in splitPath)。如果您正在处理非常大的模型,这可能会导致性能问题,但对我来说,hack 效果很好。

    更新 2:

    我意识到这仍然不是一个完美的方法,因为如果在组合框中输入了一个新字符串并且用户按下回车,组合框会再次被清除。输入新字符串的唯一方法是在输入后从下拉菜单中选择它。

    更新 3:

    现在输入也可以了。我通过简单地在用户按下回车键时将其关闭来解决组合框文本的重置问题。但是我把它放回去了,所以完成功能仍然存在。如果用户决定进行进一步的编辑。

    【讨论】:

    • 关于常量和注释“QtCore.Qt.Key_Enter) 由于某种原因不起作用”。您可以改用QtCore.Qt.Key_Return。
    【解决方案4】:

    感谢托尔比约恩, 实际上,我确实通过继承 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,为了避免使用QCompleter 过滤器,可以使用"" 作为路径并显示过滤模型中的所有内容(我刚刚发布了一个示例)。可能不是最有效的,但它似乎有效。
    【解决方案5】:

    不幸的是,目前的答案是不可能。为此,您需要在自己的应用程序中复制QCompleter 的大部分功能(Qt Creator 为其定位器执行此操作,如果您有兴趣,请参阅src/plugins/locator/locatorwidget.cpp 以了解魔法)。

    同时,您可以对QTBUG-7830 进行投票,这样可以自定义完成项的匹配方式,如您所愿。但不要屏住呼吸。

    【讨论】:

      【解决方案6】:

      您可以通过提供自定义角色并完成该角色来绕过上面提到的 QTBUG-7830。在该角色的处理程序中,您可以使用技巧让 QCompleter 知道该项目在那里。如果您还在 SortFilterProxy 模型中覆盖 filterAcceptsRow,这将起作用。

      【讨论】:

      • 嘿@psp。答案看起来不错,但如果您认为可以添加一个代码示例,可能会更清楚?
      猜你喜欢
      • 2013-01-22
      • 2020-09-27
      • 2023-04-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-16
      • 1970-01-01
      相关资源
      最近更新 更多