如何向不在模型中的 QML ComboBox 添加额外的项目?
How to add an extra item to a QML ComboBox which is not in the model?
我有一个 QML ComboBox
,它附加了一个 QAbstractListModel
。像这样:
ComboBox {
model: customListModel
}
我希望它在下拉列表中显示模型中没有的额外项目。
例如,假设 customListModel
中有两个项目:Apple 和 Orange。
在下拉列表中,它应该显示以下选项:
- Select全部
- 苹果
- 橙色
我无法将它添加到模型中,因为它包含自定义对象,而且我在程序的其他几个地方使用了这个模型,它会搞砸一切。
如何将这个“Select 全部”选项添加到 ComboBox
???
一种方法是创建某种代理模型。这里有一些想法:
您可以派生自己的 QAbstractProxyModel,将“Select 全部”项添加到数据中。这可能是更复杂的选择,但也更有效。可以找到以这种方式创建代理的示例 here。
您也可以在 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);
}
}
}
}
一种解决方案是自定义弹出窗口以添加 header。
您可以实现整个弹出组件,或者利用它的 contentItem
是一个 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()
}
}
}
}
我发现自己最近想做一些类似的事情,但很惊讶没有简单的方法可以做到;有很多方法可以做到这一点,但并不是真正的专用 API,not even for widgets。
我已经尝试了这里提到的两个答案,并想对它们进行总结,并为每种方法提供完整的示例。我的要求是有一个“None”条目,所以我的回答是在那个上下文中,但您可以轻松地将其替换为“Select All”。
使用 QSortFilterProxyModel
此 C++ 代码基于@SvenA 的this answer(感谢您分享工作代码!)。
优点:
- 为了避免重复太多:这样做的优点是没有另一种方法的缺点。例如:按键导航有效,无需触摸任何样式等。仅这两个就是您选择这种方法的重要原因,即使这确实意味着额外的写作工作(或 copy-pasting :) ) 型号代码(您只需要做一次)。
缺点:
- 由于您正在为“None”条目使用
0
索引,因此您必须将其视为一个特殊条目,不像 -1
索引,后者已经建立这意味着没有项目是 selected。这意味着需要一些额外的 JavaScript 代码来处理 selected 的索引,但是 header 方法在单击时也需要这样做。
- 一个额外的条目需要很多代码,但是又一次;你应该只需要做一次,然后你就可以重复使用它。
- 就模型操作而言,这是一个额外的间接级别。假设大多数 ComboBox 模型都比较小,这不是问题。实际上,我怀疑这会是一个瓶颈。
- 从概念上讲,“None”条目可以被视为一种元数据;即它不属于模型本身,因此该解决方案在概念上可能被视为不太正确。
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"
使用 ListView 的 header
优点:
-1
索引 -- 已经确定没有项目被 selected -- 可以用来指代“None”条目。
- 无需在 C++ 中设置 QSortFilterProxyModel-subclass 并将其公开给 QML。
- 从概念上讲,“None”条目可以被视为一种元数据;即它不属于模型本身,所以这个解决方案在概念上可以被视为更正确。
缺点:
- 无法 select 使用箭头键导航的“None”条目。我曾短暂地尝试解决这个问题(参见 commented-out 代码),但没有成功。
- 必须模仿委托组件具有的“当前项目”样式。这需要什么取决于风格;如果您自己编写样式,那么您可以将
delegate
组件移动到它自己的文件中,然后将其重新用于 header。但是,如果您使用的是其他人的样式,则不能这样做,并且必须从头开始编写(尽管通常只需要这样做一次)。例如,对于默认(“基本”,在 Qt 6 中)样式,它意味着:
- 设置合适的
font.weight
.
- 设置
highlighted
.
- 设置
hoverEnabled
.
- 必须自己设置
displayText
。
- 由于 header 项不被视为 ComboBox 项,因此
highlightedIndex
属性(即 read-only)不会考虑它。可以通过在委托中将 highlighted
设置为 hovered
来解决。
- 单击 header 时必须执行以下操作:
- 设置
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 }
// }
}
}
结论
我同意“None”和“Select 全部”是元数据多于模型数据的观点。从这个意义上说,我更喜欢 header
方法。在让我对此进行调查的特定用例中,我不允许按键导航,并且我已经覆盖了 ComboBox 的 delegate
属性,因此我可以将该代码重新用于 header
.
但是,如果您需要按键导航,或者您不想为 header
重新实现 delegate
,QSortFilterProxyModel 方法会更实用。
我有一个 QML ComboBox
,它附加了一个 QAbstractListModel
。像这样:
ComboBox {
model: customListModel
}
我希望它在下拉列表中显示模型中没有的额外项目。
例如,假设 customListModel
中有两个项目:Apple 和 Orange。
在下拉列表中,它应该显示以下选项:
- Select全部
- 苹果
- 橙色
我无法将它添加到模型中,因为它包含自定义对象,而且我在程序的其他几个地方使用了这个模型,它会搞砸一切。
如何将这个“Select 全部”选项添加到 ComboBox
???
一种方法是创建某种代理模型。这里有一些想法:
您可以派生自己的 QAbstractProxyModel,将“Select 全部”项添加到数据中。这可能是更复杂的选择,但也更有效。可以找到以这种方式创建代理的示例 here。
您也可以在 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);
}
}
}
}
一种解决方案是自定义弹出窗口以添加 header。
您可以实现整个弹出组件,或者利用它的 contentItem
是一个 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()
}
}
}
}
我发现自己最近想做一些类似的事情,但很惊讶没有简单的方法可以做到;有很多方法可以做到这一点,但并不是真正的专用 API,not even for widgets。
我已经尝试了这里提到的两个答案,并想对它们进行总结,并为每种方法提供完整的示例。我的要求是有一个“None”条目,所以我的回答是在那个上下文中,但您可以轻松地将其替换为“Select All”。
使用 QSortFilterProxyModel
此 C++ 代码基于@SvenA 的this answer(感谢您分享工作代码!)。
优点:
- 为了避免重复太多:这样做的优点是没有另一种方法的缺点。例如:按键导航有效,无需触摸任何样式等。仅这两个就是您选择这种方法的重要原因,即使这确实意味着额外的写作工作(或 copy-pasting :) ) 型号代码(您只需要做一次)。
缺点:
- 由于您正在为“None”条目使用
0
索引,因此您必须将其视为一个特殊条目,不像-1
索引,后者已经建立这意味着没有项目是 selected。这意味着需要一些额外的 JavaScript 代码来处理 selected 的索引,但是 header 方法在单击时也需要这样做。 - 一个额外的条目需要很多代码,但是又一次;你应该只需要做一次,然后你就可以重复使用它。
- 就模型操作而言,这是一个额外的间接级别。假设大多数 ComboBox 模型都比较小,这不是问题。实际上,我怀疑这会是一个瓶颈。
- 从概念上讲,“None”条目可以被视为一种元数据;即它不属于模型本身,因此该解决方案在概念上可能被视为不太正确。
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"
使用 ListView 的 header
优点:
-1
索引 -- 已经确定没有项目被 selected -- 可以用来指代“None”条目。- 无需在 C++ 中设置 QSortFilterProxyModel-subclass 并将其公开给 QML。
- 从概念上讲,“None”条目可以被视为一种元数据;即它不属于模型本身,所以这个解决方案在概念上可以被视为更正确。
缺点:
- 无法 select 使用箭头键导航的“None”条目。我曾短暂地尝试解决这个问题(参见 commented-out 代码),但没有成功。
- 必须模仿委托组件具有的“当前项目”样式。这需要什么取决于风格;如果您自己编写样式,那么您可以将
delegate
组件移动到它自己的文件中,然后将其重新用于 header。但是,如果您使用的是其他人的样式,则不能这样做,并且必须从头开始编写(尽管通常只需要这样做一次)。例如,对于默认(“基本”,在 Qt 6 中)样式,它意味着:- 设置合适的
font.weight
. - 设置
highlighted
. - 设置
hoverEnabled
.
- 设置合适的
- 必须自己设置
displayText
。 - 由于 header 项不被视为 ComboBox 项,因此
highlightedIndex
属性(即 read-only)不会考虑它。可以通过在委托中将highlighted
设置为hovered
来解决。 - 单击 header 时必须执行以下操作:
- 设置
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 }
// }
}
}
结论
我同意“None”和“Select 全部”是元数据多于模型数据的观点。从这个意义上说,我更喜欢 header
方法。在让我对此进行调查的特定用例中,我不允许按键导航,并且我已经覆盖了 ComboBox 的 delegate
属性,因此我可以将该代码重新用于 header
.
但是,如果您需要按键导航,或者您不想为 header
重新实现 delegate
,QSortFilterProxyModel 方法会更实用。