我最近发现自己想做类似的事情,并且很惊讶没有简单的方法可以做到。有办法做到这一点,但不是真正的专用 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
方法。在让我研究这个的特定用例中,我不允许键导航,并且我已经覆盖了delegate
ComboBox 的属性,因此我可以将该代码重用于header
.
但是,如果您需要按键导航,或者您不想重新实现delegate
for header
,则 QSortFilterProxyModel 方法会更实用。