PDA

View Full Version : How does QComboBox uses/calls QCompleter ?



boo9
1st July 2016, 14:12
I would like to implemet custom completion (multi world matching e.g,) completer on item list from a qcombobox.

The docs for QComboBox show setcompleter(QCompleter *) member.

I figure I need to subclass QCompleter and pass it to QComboBox::setCOmplter()

I looked at QCompleter docs and I dont see many virtual functions, for which I figure I would have to override with my own code.

I am lost/confused,
How does QComboBox would use/call my own custom completer ?
Which members would QComboBox call ? connect to which slot/signals ?

In theory, I would imagine QComboBox would call my completer; e.g, complete(QString phrase); passing phrase that was typed in the edit box of combo box.

Could somone please explain how is QComboBox interacting with QComplter passed to combo box using setCompleter() member ?

cheers

d_stranz
1st July 2016, 18:25
In general, you do not derive from QCompleter to create a custom completer. You instead derive from QAbstractItemModel and set that on your QCompleter as the model it uses to find completions for a partial phrase. The combo box and completer handle the interaction between them and you don't need to care how that works, just what your model does when asked.

If your completions can be simplified to a list of strings, then you can use the version of QCompleter that accepts a QStringList argument, and internally QCompleter will insert that into a QStringListModel.

If your completions are more complex, then you may need to implement your own model. In this case, you have almost total control over how QCompleter searches your model for completions. For example, if you have a table model, you can tell it which column to use for completions, which model role to use to retrieve candidates, and so forth.

The QCompleter example (http://doc.qt.io/qt-5/qtwidgets-tools-completer-example.html) in the docs will help. There is a more advanced example (http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html) as well. In both cases, note that they use a plain old QCompleter and customize it using models.

boo9
1st July 2016, 22:34
I examined above examples but I dont think they answer my question,

Let me be more specific
I need a completer that will complete on many words, e.g, user types in "aa bb" into QEditBox edit, and completer should complete a list where each item on the complete list matches (not just begins with) "aa" and "bb".
that is, the completion for "aa bb" filter from a list {aaa_a, aaa_bbb, aabb, bbb_a}, should be {aaa_bbb, aabb}.

I managed to do that but I needed to sublcass both qeditbox and qcompleter.
Is there any way to accomplish that w/o subclassing QEditBox ?

boo9
2nd July 2016, 17:20
I subclassed QCompleter



class mySearchFilterComplter : public QCompleter
{
Q_OBJECT
public:
mySearchFilterComplter(QObject * parent) : QCompleter(parent), m_model()
{
setModel(&m_model);
}
public slots:
void update(const QString & text)
{
QDebug() << "update";
m_model.setStringList(QStringList() << "one" << "two" << "three");
complete();
popup()->setCurrentIndex(completionModel()->index(0, 0));
}
private:
QStringListModel m_model;
};


then connected combo box editTextChanged() signal to my completer update() slot.



mySearchFilterComplter * cc = new mySearchFilterComplter(this);
ui->comboBox->setComplter(cc)
connect(ui->comboBox, SIGNAL(editTextChanged(QString)), cc, SLOT(update(QString)));


The update() gets called with each keystroke in combo box line edit but there is no popup :( ?
What am I doing wrong ?
I have similar code with subsclassed QLineEdit and subclassed QCompleter and the completer popup works. (but the completer::update() is called from keyPressedEvent() though)



class MyCompleter : public QCompleter
{
Q_OBJECT;
public:
inline MyCompleter(const QStringList& words, QObject * parent) :
QCompleter(parent), m_model()
{
setModel(&m_model);
}
inline void update(QString word)
{
m_model.setStringList(QStringList() << "one" << "two" << "three");
complete();
}
private:
QStringList m_list;
QStringListModel m_model;
};

class MyLineEdit : public QLineEdit
{
Q_OBJECT;
public:
MyLineEdit(QWidget *parent = 0);
~MyLineEdit();
void setCompleter(MyCompleter *c);
MyCompleter *completer() const;
protected:
void keyPressEvent(QKeyEvent *e);
private slots:
void insertCompletion(const QString &completion);
private:
MyCompleter *c;
};

MyLineEdit::MyLineEdit(QWidget *parent)
: QLineEdit(parent), c(0)
{
}

MyLineEdit::~MyLineEdit()
{
}

void MyLineEdit::setCompleter(MyCompleter *completer)
{
if (c)
QObject::disconnect(c, 0, this, 0);

c = completer;

if (!c)
return;

c->setWidget(this);
connect(completer, SIGNAL(activated(const QString&)), this, SLOT(insertCompletion(const QString&)));
}

MyCompleter *MyLineEdit::completer() const
{
return c;
}

void MyLineEdit::insertCompletion(const QString& completion)
{
setText(completion);
selectAll();
}
void MyLineEdit::keyPressEvent(QKeyEvent *e)
{
if (c && c->popup()->isVisible())
{
// The following keys are forwarded by the completer to the widget
switch (e->key())
{
case Qt::Key_Enter:
case Qt::Key_Return:
case Qt::Key_Escape:
case Qt::Key_Tab:
case Qt::Key_Backtab:
e->ignore();
return; // Let the completer do default behavior
}
}

bool isShortcut = (e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_E;
if (!isShortcut)
QLineEdit::keyPressEvent(e); // Don't send the shortcut (CTRL-E) to the text edit.

if (!c)
return;

bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
if (!isShortcut && !ctrlOrShift && e->modifiers() != Qt::NoModifier)
{
c->popup()->hide();
return;
}

c->update(text());
c->popup()->setCurrentIndex(c->completionModel()->index(0, 0));
}

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
MyLineEdit * te = new MyLineEdit();


MyCompleter * completer = new MyCompleter(QStringList() << "112233" << "223344" << "334455", this);
completer->setCaseSensitivity(Qt::CaseInsensitive);
te->setCompleter(completer);

QVBoxLayout * layout = new QVBoxLayout;
layout->addWidget(te);
QWidget *centralWidget = new QWidget;
centralWidget->setLayout(layout);
setCentralWidget(centralWidget);
}

MainWindow::~MainWindow()
{

}

d_stranz
2nd July 2016, 17:46
If you are fixated on subclassing QCompleter, I can't help you.

You could probably accomplish what you want without subclassing by using a combination of a QSortFilterProxyModel, QStringListModel, and QRegExp to set a filter on the completion strings returned by the string list model. Then all of these behavioral problems you are battling with should magically disappear because you are using a wheel that already works rather than trying to invent your own.

boo9
2nd July 2016, 23:12
> You could probably accomplish what you want without subclassing by using a combination of a QSortFilterProxyModel, QStringListModel, and QRegExp

You mean to disable default completer on qcombobox and filter items in the combobox directly ?
e.g, on each combobox::editTextChanged() notification, programatically rebuild new combo box item list ?
when user enters "aa bb cc" into combobox edit, then leave only items in the combobox list that contain "aa" and "bb" and "cc" ?

d_stranz
3rd July 2016, 16:38
ou mean to disable default completer on qcombobox and filter items in the combobox directly ?

No. The "chain of command" is like this: combo box has a completer, completer has a proxy model, proxy model has the string list model with *all* strings and a regular expression that matches a subset (or none) of the strings.

You create your complete string model instance and populate it with strings. You also create the proxy model instance and give it the pointer to the string model as its source model. When you call setModel() on the completer, you would give it the pointer to the proxy model. The completer has no idea it is looking at a proxy instead of the full model; the interfaces and interactions are identical, it's just that the proxy is doing some magic to return only a part of the full model to the completer.

The proxy model does what your manual code does now - it takes the full list of possibilities, applies some logic to select out the matching subset, and passes that to the completer. The "logic" in this case is defined by a QRegExp regular expression.

You will probably have to create your regular expressions on the fly to match what the user is typing, and it will change with every keystroke. So when the user types "a" and you want to match every string that starts with an "a", then your regular expression is "^a[a-z]*[ _]{0,1}[a-z]*" In English this says, "match any string that starts with lowercase 'a', followed by zero or more lowercase letters of any type, followed by 0 or 1 spaces or underscores, followed by zero or more lowercase letters".

When the user types "aa bb", then you need "^aa[a]*[ _]{0,1}bb[b]*" which says, match any string starting with two or more lowercase 'a', followed by zero or one underscore or blank space, followed by two or more 'b' characters."

In essence, you are letting the QRegExp do the filtering for you by changing the rules it uses for filtering. These "rules" would probably be very similar to the "rules" you would be coding by hand, except these are expressed in regular expression syntax rather than a bunch of if / else / endif logic.

boo9
4th July 2016, 00:07
Thank you for detailed explanation, however RE will not make it in general case.
For simple searches with two tokens, "aa bb" query means aa followed by bb OR bb followed by aa. One can express that in simple RE
aa.*bb|bb.*aa

However when there is more tokens e.g, "aa bb cc dd" permuting all sequences of aa,bb,cc is not feasable.

3 tokens => 3! = 6 permutations
4 tokens => 4! = 24 permutations

cheers

d_stranz
5th July 2016, 23:54
I guess I have no idea what you are trying to do with your completer or what the use case might be.

boo9
6th July 2016, 13:23
> I guess I have no idea what you are trying to do with your completer

I have a list with 100 items.
Each list item is a text of 10-50 characters, 2-10 words
When I type in "aa" I would like only items that contain "aa" filtered
When I type in "aa bb" I would like only items that contain "aa" AND "bb" filtered, "aa bb" does not mean that bb follows aa, it only means that list item must contain "aa" and "bb" anywhere in the item text.
When I type in "aa bb cc" I would like only items that contain "aa" AND "bb" AND "cc" filtered, aa,bb,cc can appear at any position in the item text.

d_stranz
7th July 2016, 04:56
I have a list with 100 items.
Each list item is a text of 10-50 characters, 2-10 words

OK... can you please give an example with some of the real strings that you have in your list, and what the user might type? "aa bb cc" is just too abstract for me to understand how you might be using this in a real application.

boo9
7th July 2016, 12:05
item1: * (Search resistor range)(res-range) eltype=resistor rlcvalue >= %1 rlcvalue <= %2
item2: * (Search electrolite capacitor range)(cap-range) eltype=capacitor electrolite rlcvalue >= %1 and rlcvalue <= %2
item3: * (Search electrolite capacitor range metallic)(metcap-range) ceramic eltype=capacitor rlcvalue >= %1 and rlcvalue <= %2
....
item100: ....

how I want to quicky find correct itemX without scrolling through a list
- entering "cap electro" exepect to filter 2
- entering "cap range" expect to filter 2,3


I dont understand why such requirements appear strange and uncommon.
When one searches an email archive and have thousands of item in the mail box, one would expect to search for email message with words/tokens, and would expect the tokens in the mail message would appear in any order, not necessarilly the order they were entered in the search spec.

d_stranz
7th July 2016, 18:28
OK, now it makes more sense. Take a look at this example of a custom sort/filter proxy model (http://doc.qt.io/qt-5/qtwidgets-itemviews-customsortfiltermodel-example.html).

You would re-implement the QSortFilterProxyModel::filterAcceptsRow(), and add your own method to the custom proxy that takes the string typed by the user in the combobox. In this method, split the string into tokens (QString::split()) and save the stringlist as a member variable in your proxy.

In the filterAcceptsRow() method, you retrieve the full string from your model into a QString (Use QStringListModel::data() and the Qt::DisplayRole ItemDataRole). For each token in the user's input, you call QString::contains() to see if the model string contains the token. If the answer is yes for all of the tokens in the user's string, you return true from filterAcceptsRow, otherwise you return false. Only those entries for which the result is true will be shown as selections in the combobox drop-down list.

This will accomplish your goals of returning only those strings that contain all of the tokens the user has typed and allowing those tokens to appear in any order.

boo9
7th July 2016, 19:05
> You would re-implement the QSortFilterProxyModel::filterAcceptsRow()

finally, that's what I was looking for, a custom code that does filtering to my own logic

many thanks

cheers,

boo9
9th July 2016, 00:26
I cannot get basic proof of concept proxy working with Qcombobox. The drop down list does not popup when I hit down arrow on combo box.
If I dont use proxy model, and load combo box with QStringList, the popup works.



MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QComboBox * cb = ui->comboBox;
QStringList items;
items << "aa bbb ccc ddd" << "bb ccc dd eee" << "cc ddd ee fff" << "dd eee ff ggg";
QStringListModel model(items, this);

myProxy proxy(this);
proxy.setDynamicSortFilter(true);
proxy.setSourceModel(&model);

cb->setModel(&proxy);
cb->setEditable(true);

qDebug() << "cb count = " << cb->count();

}
bool myProxy::filterAcceptsRow(int source_row, const QModelIndex &) const
{
qDebug() << "acceptRow " << source_row;
return true;
}


EDIT: resolved, I keep forgetting still not used to "new xxx" paradigm, must create Proxy/Model on heap, not on stack.

boo9
9th July 2016, 15:01
I have been playing with proxy model do do the completion, there are some quirks and the behaviour does not match the completer only (no proxy) behaviour.

Completing on QComboBox editable.

Without proxy model, the default completer works as follows
1) when I enter "aaa", the completer pops up a drop down with items matching "aaa"
2) when I want to see entire list (all items) I simple click on expand items arrow (down arrow on right side) of combo box and all items appear.
This does not work same way when proxy model is used, with proxy model the list contains only items matching "aaa", so when I type "zzz" that dont match any item, clicking on combo expand button, shows empty list. SO when one entered "zzz" dont dont match any items, there is no way to select an item in the list from the list of all items, one must erase line edit to see the complete list.


There is some mysterious behaviour of my sample app as well, the app uses proxy and disables default completer (otherwise the completer will do second level of filtering on top of proxy filtering)

Case 1: entering non matching word, 1st char lost ?
1) app starts and line edit is empty (per code clear() member)
2) 1st key press "x" into line edit, the list is filtered correctly, but there is _NO_ "x" visible in the line edit.
3) 2nd key press "x" (there should be "xx") but only one "x" appears in the line edit, 3rd key press "x" and there are only "xx" in line edit.

Case 2: entering matching word, 1st char lost ?
1) app starts and line edit is empty
3) 1st key press "d", the list if filtered correctly, but the line edit replaced with 1st list matching item "bb ccc dd", I would like to keep typing more chars after "d" to complete my word and not fight with combo box replacing line edit with 1st item from matching list.
The focus moves from line edit to drop down list as well - it should stay on line edit as I keep typing (
I would like the same behaviour as if completer was active, that is, I keep typing my own words and drop down list changes according to my code in filterAcceptsRow() member.

complete qt project 12026




class MainWindow;

class myProxy : public QSortFilterProxyModel
{
Q_OBJECT
public:
myProxy(QObject* parent, MainWindow * aa) : QSortFilterProxyModel(parent), app(aa) {}

void setFilter(const QString & filter);
protected:
virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;
private:
QString m_filter;
MainWindow * app;
};

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void showPopup();

public slots:
void textChanged(const QString & filter);

private:
Ui::MainWindow *ui;
myProxy * proxy;
};



MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QComboBox * cb = ui->comboBox;
cb->setEditable(true);
qDebug() << "completer=" << cb;
cb->setCompleter(0);
#if 0 // wierd behaviour with default completer as well
cb->setInsertPolicy(QComboBox::NoInsert);
cb->completer()->setFilterMode(Qt::MatchContains);
cb->completer()->setCaseSensitivity(Qt::CaseInsensitive);
cb->completer()->setCompletionMode(QCompleter::PopupCompletion);
#endif

connect(cb->lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(textChanged(QString)));

QStringList items;
items << "aa bbb ccc" << "bb ccc dd" << "cc ddd ee" << "dd eee ff" << "ee fff gg";

QStringListModel * model = new QStringListModel(items, this);

proxy = new myProxy(this, this);
proxy->setSourceModel(model);

cb->setModel(proxy);
cb->lineEdit()->clear();
}

void MainWindow::textChanged(const QString & text)
{
qDebug() << "edit text changed " << text;
proxy->setFilter(text);
}

void MainWindow::showPopup()
{
ui->comboBox->showPopup();
}

bool myProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
QString vv = sourceModel()->index(source_row, 0, source_parent).data().toString();
QStringList tokens = m_filter.split(" ", QString::SkipEmptyParts);
int rc = true;
foreach (QString tok, tokens)
{
if(tok.isEmpty()) continue;
if(! vv.contains(tok, Qt::CaseInsensitive))
{
rc = false;
break;
}
}
qDebug() << "acceptRows row=" << source_row << "filter=" << m_filter << "data=" << vv << " =>" << rc;
return rc;
}

void myProxy::setFilter(const QString & filter)
{
m_filter = filter ;
QSortFilterProxyModel::setFilterFixedString(filter );
app->showPopup();
}