我有一个QMLComboBox
,它连接了一个QAbstractListModel
ComboBox {
model: customListModel
}
我希望它在下拉列表中显示一个不在模型中的额外项目。
例如,假设customListModel
中有两个项目:Apple和Orange。在下拉列表中,它应该显示以下选项:
- 全选
- 苹果
- 橙色
我不能将它添加到模型中,因为它包含自定义对象,并且我在程序中的其他几个地方使用了这个模型,它会把一切都搞砸。
我该如何添加此";全选;选项添加到ComboBox
???
一种方法是创建某种代理模型
-
您可以导出自己的QAbstractProxyModel;全选;项添加到数据中。这可能是一个更复杂的选择,但也更有效。可以在这里找到以这种方式创建代理的示例。
-
您也可以在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);
}
}
}
}
解决方案是自定义弹出窗口以添加标题。
您可以实现整个弹出组件,或者利用其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,甚至没有小部件。
我已经尝试了这里提到的两个答案,并想对它们进行总结,同时为每种方法提供完整的示例。我的要求是有一个";无";条目,所以我的答案是在这个上下文中,但你可以很容易地用";全选;。
使用QSortFilterProxyModel
这方面的C++代码是基于@SvenA的这个答案(感谢您分享工作代码!)。
优点:
- 为了避免重复太多:这样做的好处是在另一种方法中没有缺点。例如:键导航可以工作,不需要触摸任何样式的东西,等等。这两个单独的原因就是你想要选择这种方法的很大原因,即使这确实意味着写(或复制粘贴:))模型代码需要额外的工作(这是你只需要做一次的事情)
缺点:
- 由于您使用CCD_;无";条目,您必须将其视为一个特殊条目,而不像
-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, 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的标头
优点:
- CCD_ 10索引——它已经被建立为意味着没有选择任何项目——可以用来指代";无";条目
- 无需在C++中设置QSortFilterProxyModel子类并将其公开给QML
- 从概念上讲;无";条目可以被视为一种元数据;也就是说,它不属于模型本身,所以这个解决方案在概念上更正确
缺点:
- 不可能选择";无";带有箭头键导航的条目。我曾短暂地尝试过解决这个问题(请参阅注释掉的代码),但没有成功
- 必须模仿";当前项目";代理组件具有的样式。这取决于风格;如果您自己编写样式,那么您可以将
delegate
组件移动到它自己的文件中,并将其重新用于标头。然而,如果你使用别人的风格,你就不能这样做,必须从头开始写(不过,你通常只需要这样做一次)。例如,对于Default("Basic",在Qt 6中)样式,它意味着:- 设置适当的
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
方法。在我研究这个问题的特定用例中,我不允许键导航,而且我已经重写了ComboBox的delegate
属性,所以我可以为header
重用该代码。
但是,如果您需要键导航,或者不想为header
重新实现delegate
,那么QSortFilterProxyModel方法将更实用。