问题描述
我有一个附加了ComboBox
的QML QAbstractListModel
。像这样:
ComboBox {
model: customListModel
}
例如,假设customListModel
中有两项:Apple和Orange。
并在下拉列表中显示以下选项:
- 全选
- 苹果
- 橙色
我无法将其添加到模型中,因为它包含自定义对象,并且我在程序中的其他两个地方使用了该模型,这会破坏所有内容。
解决方法
一种方法是创建某种代理模型。这里有一些想法:
-
您可以派生自己的QAbstractProxyModel,将“全选”项添加到数据中。这可能是更复杂的选择,但也更有效率。在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);
}
}
}
}
,
一种解决方案是自定义弹出窗口以添加标题。
您可以实现整个弹出组件,也可以利用其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()
}
}
}
}
,
我发现自己最近想做类似的事情,但感到惊讶的是没有简单的方法可以做到。有很多方法可以做到,但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方法将更加实用。