如何向不在模型中的QML ComboBox添加额外的项目? 使用QSortFilterProxyModel 使用ListView的标题结论

问题描述

我有一个附加了ComboBox的QML QAbstractListModel。像这样:

ComboBox {
    model: customListModel
}

我希望它在下拉列表中显示一个不在模型中的额外项目。

例如,假设customListModel中有两项:Apple和Orange。 并在下拉列表中显示以下选项:

  • 全选
  • 苹果
  • 橙色

我无法将其添加到模型中,因为它包含自定义对象,并且我在程序中的其他两个地方使用了该模型,这会破坏所有内容

如何将这个“全选”选项添加ComboBox ???

解决方法

一种方法是创建某种代理模型。这里有一些想法:

  1. 您可以派生自己的QAbstractProxyModel,将“全选”项添加到数据中。这可能是更复杂的选择,但也更有效率。在here中可以找到使用这种方式创建代理的示例。

  2. 您还可以在QML中创建代理。看起来像这样:

Combobox {
    model: ListModel {
        id: proxyModel
        ListElement { modelData: "Select All" }

        Component.onCompleted: {
            for (var i = 0; i < customListModel.count; i++) {
                proxyModel.append(customModel.get(i);
            }
        }
    }
}
,

一种解决方案是自定义弹出窗口以添加标题。

您可以实现整个弹出组件,也可以利用其contentItemListView的事实并使用header属性:

ListModel {
    id: fruitModel
    ListElement {
        name: "Apple"
    }
    ListElement {
        name: "Orange"
    }
}

ComboBox {
    id: comboBox
    model: fruitModel
    textRole: "name"
    Binding {
        target: comboBox.popup.contentItem
        property: "header"
        value: Component {
            ItemDelegate {
                text: "SELECT ALL"
                width: ListView.view.width
                onClicked: doSomething()
            }
        }
    }
}
,

我发现自己最近想做类似的事情,但感到惊讶的是没有简单的方法可以做到。有很多方法可以做到,但not even for widgets并不是专门针对它的专用API。

我已经尝试了这里提到的两个答案,并希望对它们进行总结,并为每种方法提供完整的示例。我的要求是输入“无”,因此我的回答是在这种情况下,但您可以轻松地将其替换为“全选”。

使用QSortFilterProxyModel

用于此目的的C ++代码基于@SvenA的this answer(感谢您共享工作代码!)。

优点:

  • 为避免重复太多自己:这样做的优点是另一种方法没有缺点。例如:按键导航有效,无需触摸任何样式内容,等等。仅这两个是您要选择这种方法的很大原因,即使这确实意味着额外的工作编写(或复制粘贴:))模型代码(只需执行一次即可。)

缺点:

  • 由于将0索引用于“无”条目,因此必须将其视为特殊条目,与-1索引不同,该索引已经建立,意味着未选择任何项目。这意味着需要一些额外的JavaScript代码来处理被选择的索引,但是标头方法在单击时也需要这样做。
  • 一个额外的条目有很多代码,但是再说一次;您只需要执行一次,然后就可以重复使用它。
  • 就模型操作而言,这是一个额外的间接级别。假设大多数ComboBox型号相对较小,这不是问题。实际上,我怀疑这会成为瓶颈。
  • 从概念上讲,“无”条目可以被视为一种元数据;也就是说,它不属于模型本身,因此该解决方案在概念上不那么正确。

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,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,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,"ProxyModelNoneEntry");
    qmlRegisterType<MyModel>("App","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"

使用ListView的标题

优点:

  • -1索引(已经建立,意味着没有选择任何项)可以用来引用“无”条目。
  • 无需在C ++中设置QSortFilterProxyModel子类并将其公开给QML。
  • 从概念上讲,“无”条目可以被视为一种元数据;也就是说,它不属于模型本身,因此该解决方案在概念上可能更正确。

缺点:

  • 无法通过箭头键选择“无”条目。我暂时尝试解决此问题(请参阅注释掉的代码),但没有成功。
  • 必须模仿委托组件具有的“当前项目”样式。所需内容取决于样式。如果您自己编写样式,则可以将delegate组件移到其自己的文件中,并将其重新用作标题。但是,如果您使用的是其他人的样式,则不能这样做,而必须从头开始编写(尽管您通常只需要执行一次)。例如,对于Default样式(在Qt 6中为“ Basic”),其含义是:
    • 设置适当的font.weight
    • 设置highlighted
    • 设置hoverEnabled
  • 必须自行设置displayText
  • 由于标题项不被视为ComboBox项,因此highlightedIndex属性(只读)将不予考虑。可以通过在委托中将highlighted设置为hovered来解决。
  • 单击标题时必须执行以下操作:
    • 设置currentIndex(即点击时设置为-1)。
    • 关闭ComboBox的弹出窗口。
    • 手动发送activated()

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 }
//        }
    }
}

结论

我同意“无”和“全选”比模型数据更多的元数据的想法。从这个意义上讲,我更喜欢header方法。在我的特殊用例中,我不允许进行键导航,并且我已经覆盖了ComboBox的delegate属性,因此可以将该代码重用于header。>

但是,如果您需要按键导航,或者不想为delegate重新实现header,则QSortFilterProxyModel方法将更加实用。