4

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

ComboBox {
    model: customListModel
}

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

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

  • 全选
  • 苹果
  • 橙子

我无法将它添加到模型中,因为它包含自定义对象,并且我在程序中的其他几个地方使用了这个模型,它会搞砸一切。

如何将此“全选”选项添加到ComboBox???

4

3 回答 3

4

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

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

  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);
            }
        }
    }
}
于 2020-10-07T02:59:39.877 回答
3

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

contentItem您可以实现整个弹出组件,或者利用它是 a的事实ListView并使用该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()
            }
        }
    }
}
于 2020-10-07T08:38:15.043 回答
1

我最近发现自己想做类似的事情,并且很惊讶没有简单的方法可以做到。有办法做到这一点,但不是真正的专用 API,甚至对于 widgets也没有。

我已经尝试了这里提到的两个答案,并想总结它们,并为每种方法提供完整的示例。我的要求是有一个“无”条目,所以我的答案是在这种情况下,但您可以轻松地将其替换为“全选”。

使用 QSortFilterProxyModel

对此的 C++ 代码基于@SvenA 的这个答案(感谢您分享工作代码!)。

优点:

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

缺点:

  • 由于您将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 {}
        }
    }
}

主.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"

使用 ListView 的标题

优点:

  • 索引——-1已经确定为没有选择任何项目——可用于引用“无”条目。
  • 无需在 C++ 中设置 QSortFilterProxyModel 子类并将其公开给 QML。
  • 从概念上讲,“无”条目可以被认为是一种元数据;即它不属于模型本身,所以这个解决方案在概念上可以被视为更正确。

缺点:

  • 无法使用箭头键导航选择“无”条目。我曾短暂尝试过解决此问题(请参阅注释掉的代码),但没有成功。
  • 必须模仿委托组件具有的“当前项目”样式。这需要什么取决于风格;如果您自己编写样式,那么您可以将delegate组件移动到自己的文件中并将其重用于标题。但是,如果您使用的是其他人的样式,则不能这样做,并且必须从头开始编写(不过,您通常只需要这样做一次)。例如,对于 Default(Qt 6 中的“Basic”)样式,它意味着:
    • 设置适当的font.weight.
    • 设置highlighted
    • 设置hoverEnabled
  • displayText自己定。
  • 由于标题项不被视为 ComboBox 项,因此highlightedIndex属性(只读)将不考虑它。可以通过在委托中设置highlighted为来解决。hovered
  • 单击标题时必须执行以下操作:
    • 设置currentIndex(即-1点击)。
    • 关闭组合框的弹出窗口。
    • 手动发射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方法。在让我研究这个的特定用例中,我不允许键导航,并且我已经覆盖了delegateComboBox 的属性,因此我可以将该代码重用于header.

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

于 2020-10-24T12:50:29.090 回答