qt - Reglas de finalización personalizadas de QCompleter
qt4 qt4.6 (6)
Estoy usando Qt4.6 y tengo un QComboBox con un QCompleter.
La funcionalidad habitual es proporcionar pistas de finalización (pueden ser en un menú desplegable en lugar de en línea, que es mi uso) en función de un prefijo. Por ejemplo, dado
chicken soup
chilli peppers
grilled chicken
Al entrar en ch
coincidiría con chicken soup
y los chilli peppers
pero no grilled chicken
.
Lo que quiero es poder ingresar y combinar todas ellas o, más específicamente, chicken
y chicken soup
y grilled chicken
.
También deseo poder asignar una etiqueta como chs
a chicken soup
para producir otra coincidencia que no esté solo en el contenido del texto. Puedo manejar el algoritmo pero,
¿Cuál de las funciones de QCompleter necesito anular?
No estoy seguro de dónde debería estar mirando ...
Basado en la sugerencia de @ j3frea, aquí hay un ejemplo de trabajo (usando PySide
). Parece que el modelo debe establecerse cada vez que se splitPath
(configurar el proxy una vez en setModel
no funciona).
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)
Gracias Thorbjørn, en realidad QSortFilterProxyModel
el problema heredando de QSortFilterProxyModel
.
El método filterAcceptsRow
se debe sobrescribir y, a continuación, debe devolver verdadero o falso según si desea que se muestre ese elemento.
El problema con esta solución es que solo oculta elementos en una lista y, por lo tanto, nunca puede reorganizarlos (que es lo que quería hacer para dar prioridad a ciertos elementos).
[EDITAR]
Pensé en incluir esto en la solución, ya que es [básicamente] lo que terminé haciendo (porque la solución anterior no era adecuada). Usé 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;
}
Lamentablemente, actualmente la respuesta es que no es posible. Para hacer eso, necesitarías duplicar gran parte de la funcionalidad de QCompleter en tu propia aplicación (Qt Creator hace eso para su Locator, mira src/plugins/locator/locatorwidget.cpp
para la magia si estás interesado).
Mientras tanto, podría votar en QTBUG-7830 , que trata sobre la posibilidad de personalizar la manera en que se complementan los elementos de finalización, como lo desea. Pero no contengas la respiración en eso.
Puede obtener alrededor de QTBUG-7830 como se menciona anteriormente al proporcionar un rol personalizado y realizar la finalización de ese rol. En el controlador de ese rol, puede hacer el truco para que QCompleter sepa que ese elemento está allí. Esto funcionará si también reemplaza filterAcceptsRow en su modelo SortFilterProxy.
Sobre la base de la respuesta de @Bruno, estoy usando la función estándar QSortFilterProxyModel
setFilterRegExp
para cambiar la cadena de búsqueda. De esta forma, no es necesario subclasificar.
También corrige un error en la respuesta de @ Bruno, que hacía desaparecer las sugerencias por alguna razón una vez que la cadena de entrada se corrigió con el retroceso mientras se escribía.
class CustomQCompleter(QtGui.QCompleter):
"""
adapted from: http://.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)
Actualizar:
Pensé que mi solución anterior funcionaba hasta que la cadena en el cuadro combinado no coincidía con ninguno de los elementos de la lista. Entonces el QFilterProxyModel
estaba vacío y esto a su vez reseteó el text
del cuadro combinado. Traté de encontrar una solución elegante a este problema, pero me encontré con problemas (haciendo referencia a los errores de objetos eliminados) cada vez que intenté cambiar algo en self.filterProxyModel
. Así que ahora el truco consiste en establecer el modelo de self.filterProxyModel
cada vez que sea nuevo cuando se actualice su patrón. Y siempre que el patrón ya no coincida con nada en el modelo, para darle un nuevo modelo que solo contenga el texto actual (también conocido como path
en splitPath
). Esto puede ocasionar problemas de rendimiento si se trata de modelos muy grandes, pero para mí el truco funciona bastante bien.
Actualización 2:
Me di cuenta de que este no es el camino perfecto, porque si se escribe una nueva cadena en el cuadro combinado y el usuario presiona enter, el cuadro combinado se borra nuevamente. La única forma de ingresar una nueva cadena es seleccionarla del menú desplegable después de escribir.
Actualización 3:
Ahora ingrese también a las obras. Trabajé alrededor del reinicio del texto del combobox simplemente quitándolo de la carga cuando el usuario presiona enter. Pero lo vuelvo a poner, para que la funcionalidad de finalización permanezca en su lugar. Si el usuario decide hacer más ediciones.
Utilice la propiedad filterMode : Qt::MatchFlags
. Esta propiedad contiene cómo se realiza el filtrado. Si filterMode se establece en Qt::MatchStartsWith
, solo se Qt::MatchStartsWith
las entradas que comiencen con los caracteres escritos. Qt::MatchContains
mostrará las entradas que contienen los caracteres escritos, y Qt::MatchEndsWith
los que terminan con los caracteres escritos. Actualmente, solo estos tres modos están implementados . Establecer filterMode en cualquier otro Qt::MatchFlag
emitirá una advertencia y no se realizará ninguna acción. El modo predeterminado es Qt::MatchStartsWith
.
Esta propiedad fue introducida en Qt 5.2.
Funciones de acceso:
Qt::MatchFlags filterMode() const
void setFilterMode(Qt::MatchFlags filterMode)