I found myself wanting to do something similar recently and was surprised that there's no simple way to do it; there are ways to do it but not really a dedicated API for it, not even for widgets.
I've tried both of the answers mentioned here and would like to summarise them, as well provide complete examples for each approach. My requirement was to have a "None" entry, so my answer is in that context, but you can easily replace that with "Select All".
Using QSortFilterProxyModel
The C++ code for this is based on this answer by @SvenA (thank you for sharing working code!).
Pros:
- To avoid repeating myself too much: the pros for this are the absence of the cons in the other approach. For example: key navigation works, no need to touch any styling stuff, etc. These two alone are pretty big reasons why you would want to choose this approach, even if it does mean extra work writing (or copy-pasting :)) the model code (which is something you will only have to do once).
Cons:
- Since you are using the
0
index for the "None" entry, you have to treat it as a special entry, unlike the -1
index, which is already established as meaning no item is selected. This means a little extra JavaScript code to handle that index being selected, but the header approach also requires this when it's clicked.
- It is a lot of code for one extra entry, but again; you should only have to do it once, and then you can reuse it.
- It is an extra level of indirection in terms of model operations. Assuming most ComboBox models are relatively small, this is not a problem. In practice I doubt this would be a bottleneck.
- Conceptually a "None" entry could be considered a kind of metadata; i.e. it doesn't belong in the model itself, so this solution could be seen as less conceptually correct.
main.qml:
import QtQuick 2.15
import QtQuick.Controls 2.15
import App 1.0
ApplicationWindow {
width: 640
height: 480
visible: true
title: "\"None\" entry (proxy) currentIndex=" + comboBox.currentIndex + " highlightedIndex=" + comboBox.highlightedIndex
ComboBox {
id: comboBox
textRole: "display"
model: ProxyModelNoneEntry {
sourceModel: MyModel {}
}
}
}
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QSortFilterProxyModel>
#include <QDebug>
class MyModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit MyModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
private:
QVector<QString> mData;
};
MyModel::MyModel(QObject *parent) :
QAbstractListModel(parent)
{
for (int i = 0; i < 10; ++i)
mData.append(QString::fromLatin1("Item %1").arg(i + 1));
}
int MyModel::rowCount(const QModelIndex &) const
{
return mData.size();
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
switch (role) {
case Qt::DisplayRole:
return mData.at(index.row());
}
return QVariant();
}
class ProxyModelNoneEntry : public QSortFilterProxyModel
{
Q_OBJECT
public:
ProxyModelNoneEntry(QString entryText = tr("(None)"), QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
private:
QString mEntryText;
};
ProxyModelNoneEntry::ProxyModelNoneEntry(QString entryText, QObject *parent) :
QSortFilterProxyModel(parent)
{
mEntryText = entryText;
}
int ProxyModelNoneEntry::rowCount(const QModelIndex &/*parent*/) const
{
return QSortFilterProxyModel::rowCount() + 1;
}
QModelIndex ProxyModelNoneEntry::mapFromSource(const QModelIndex &sourceIndex) const
{
if (!sourceIndex.isValid())
return QModelIndex();
else if (sourceIndex.parent().isValid())
return QModelIndex();
return createIndex(sourceIndex.row()+1, sourceIndex.column());
}
QModelIndex ProxyModelNoneEntry::mapToSource(const QModelIndex &proxyIndex) const
{
if (!proxyIndex.isValid())
return QModelIndex();
else if (proxyIndex.row() == 0)
return QModelIndex();
return sourceModel()->index(proxyIndex.row() - 1, proxyIndex.column());
}
QVariant ProxyModelNoneEntry::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
if (index.row() == 0) {
if (role == Qt::DisplayRole)
return mEntryText;
else
return QVariant();
}
return QSortFilterProxyModel::data(createIndex(index.row(),index.column()), role);
}
Qt::ItemFlags ProxyModelNoneEntry::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
if (index.row() == 0)
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
return QSortFilterProxyModel::flags(createIndex(index.row(),index.column()));
}
QModelIndex ProxyModelNoneEntry::index(int row, int column, const QModelIndex &/*parent*/) const
{
if (row > rowCount())
return QModelIndex();
return createIndex(row, column);
}
QModelIndex ProxyModelNoneEntry::parent(const QModelIndex &/*child*/) const
{
return QModelIndex();
}
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<ProxyModelNoneEntry>("App", 1, 0, "ProxyModelNoneEntry");
qmlRegisterType<MyModel>("App", 1, 0, "MyModel");
qmlRegisterAnonymousType<QAbstractItemModel>("App", 1);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
#include "main.moc"
Using ListView's header
Pros:
- The
-1
index -- which is already established as meaning no item is selected -- can be used to refer to the "None" entry.
- No need to set up a QSortFilterProxyModel-subclass in C++ and expose it to QML.
- Conceptually a "None" entry could be considered a kind of metadata; i.e. it doesn't belong in the model itself, so this solution could be seen as more conceptually correct.
Cons:
- Not possible to select the "None" entry with arrow key navigation. I briefly tried working around this (see commented-out code), but had no success.
- Have to mimic the "current item" styling that the delegate component has. What that entails depends on the style; if you wrote the style yourself, then you could move the
delegate
component into its own file and reuse it for the header. However, if you you're using someone else's style, you can't do that, and will have to write it from scratch (though, you will usually only need to do this once). For example, for the Default ("Basic", in Qt 6) style it means:
- Setting an appropriate
font.weight
.
- Setting
highlighted
.
- Setting
hoverEnabled
.
- Have to set
displayText
yourself.
- Since the header item isn't considered a ComboBox item, the
highlightedIndex
property (which is read-only) will not account for it. Can be worked around by setting highlighted
to hovered
in the delegate.
- Have to do the following when the header is clicked:
- Set
currentIndex
(i.e. to -1
on click).
- Close the ComboBox's popup.
- Emit
activated()
manually.
main.qml:
import QtQuick 2.0
import QtQuick.Controls 2.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: "\"None\" entry (header) currentIndex=" + comboBox.currentIndex + " highlightedIndex=" + comboBox.highlightedIndex
Binding {
target: comboBox.popup.contentItem
property: "header"
value: Component {
ItemDelegate {
text: qsTr("None")
font.weight: comboBox.currentIndex === -1 ? Font.DemiBold : Font.Normal
palette.text: comboBox.palette.text
palette.highlightedText: comboBox.palette.highlightedText
highlighted: hovered
hoverEnabled: comboBox.hoverEnabled
width: ListView.view.width
onClicked: {
comboBox.currentIndex = -1
comboBox.popup.close()
comboBox.activated(-1)
}
}
}
}
ComboBox {
id: comboBox
model: 10
displayText: currentIndex === -1 ? qsTr("None") : currentText
onActivated: print("activated", index)
// Connections {
// target: comboBox.popup.contentItem.Keys
// function onUpPressed(event) { comboBox.currentIndex = comboBox.currentIndex === 0 ? -1 : comboBox.currentIndex - 1 }
// }
}
}
Conclusion
I agree with the idea that "None" and "Select All" are more metadata than model data. In that sense I prefer the header
approach. In my particular use case that made me look into this, I don't allow key navigation and I have already overridden the delegate
property of ComboBox, so I can reuse that code for the header
.
However, if you need key navigation, or you don't want to have to reimplement the delegate
for the header
, the QSortFilterProxyModel approach would be more practical.