快捷键编辑器示例
快捷键编辑器示例展示了如何创建一个基本的读写分层模型,并与 Qt XML 的标准视图和QKeySequenceEdit 类一起使用。有关模型/视图编程的描述,请参阅模型/视图编程概述。
Qt 的模型/视图架构为视图处理数据源中的信息提供了一种标准方式,它使用数据的抽象模型来简化和规范访问数据的方式。快捷键编辑器模型以项目树的形式表示操作,并允许视图通过基于索引的系统访问这些数据。更广泛地说,模型可用于以树形结构的形式表示数据,允许每个项目作为子项目表的父项。
设计和概念
我们用来表示数据结构的数据结构是由快捷方式编辑器模型项目对象构建的树形结构。每个 ShortcutEditorModelItem 代表树形视图中的一个项目,并包含两列数据。
![]() | 快捷方式编辑器结构 数据在模型内部使用 ShortcutEditorModelItem 对象存储,这些对象以基于指针的树形结构链接在一起。一般来说,每个 ShortcutEditorModelItem 都有一个父项,并且可以有多个子项。但是,树形结构中的根项目没有父项目,也不会被模型外部引用。 每个 ShortcutEditorModelItem 都包含有关其在树结构中位置的信息;它可以返回其父项及其行号。有了这些随时可用的信息,执行模型就变得更容易了。 由于树形视图中的每个项目通常都包含几列数据(本例中是名称和快捷方式),因此在每个项目中存储这些信息是很自然的。为简单起见,我们将使用QVariant 对象列表来存储项中每一列的数据。 |
使用基于指针的树形结构意味着,在将模型索引传递给视图时,我们可以在索引中记录相应项的地址(参见QAbstractItemModel::createIndex()) 并在以后使用QModelIndex::internalPointer() 进行检索。这使得编写模型变得更容易,并确保所有引用同一项目的模型索引具有相同的内部数据指针。
有了适当的数据结构,我们就可以创建一个树形模型,只需少量额外代码就能为其他组件提供模型索引和数据。
ShortcutEditorModelItem 类定义
ShortcutEditorModelItem 类定义如下:
该类是一个基本的 C++ 类。它不继承于QObject ,也不提供信号和插槽。该类用于保存 QVariants 列表,其中包含列数据及其在树结构中的位置信息。函数提供以下功能:
appendChildItem()
用于在首次构建模型时添加数据,在正常使用时不使用。child()
和childCount()
函数允许模型获取任何子项的信息。- 与项目相关的列数信息由
columnCount()
提供,每一列的数据可通过 data() 函数获取。 row()
和parent()
函数用于获取项的行号和父项。
父项和列数据存储在parentItem
和itemData
私有成员变量中。childItems
变量包含指向项目自身子项目的指针列表。
ShortcutEditorModel 类定义
ShortcutEditorModel
类定义如下:
class ShortcutEditorModel : public QAbstractItemModel { Q_OBJECT class ShortcutEditorModelItem { public: explicit ShortcutEditorModelItem(const QList<QVariant> &data, ShortcutEditorModelItem *parentItem = nullptr); ~ShortcutEditorModelItem(); void appendChild(ShortcutEditorModelItem *child); ShortcutEditorModelItem *child(int row) const; int childCount() const; int columnCount() const; QVariant data(int column) const; int row() const; ShortcutEditorModelItem *parentItem() const; QAction *action() const; private: QList<ShortcutEditorModelItem *> m_childItems; QList<QVariant> m_itemData; ShortcutEditorModelItem *m_parentItem; }; public: explicit ShortcutEditorModel(QObject *parent = nullptr); ~ShortcutEditorModel() override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &index = QModelIndex()) const override; int columnCount(const QModelIndex &index = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; void setActions(); private: void setupModelData(ShortcutEditorModelItem *parent); ShortcutEditorModelItem *m_rootItem; };
该类与QAbstractItemModel 的大多数其他提供读写模型的子类类似。只有构造函数和setupModelData()
函数的形式是该模型所特有的。此外,我们还提供了一个析构函数,以便在模型被销毁时进行清理。
ShortcutEditorModel 类的实现
构造函数接收一个参数,该参数包含模型将与视图和委托共享的数据:
ShortcutEditorModel::ShortcutEditorModel(QObject *parent) : QAbstractItemModel(parent) { m_rootItem = new ShortcutEditorModelItem({tr("Name"), tr("Shortcut")}); }
构造函数为模型创建一个根项。为方便起见,该项只包含垂直标题数据。我们还用它来引用包含模型数据的内部数据结构,并用它来表示模型中顶层项的假想父项。
模型的内部数据结构由setupModelData()
函数填充项。我们将在本文档末尾单独讨论该函数。
析构函数确保在销毁模型时删除根项及其所有子项:
ShortcutEditorModel::~ShortcutEditorModel() { delete m_rootItem; }
由于我们不能在模型构建和设置后向其添加数据,因此这简化了管理内部项树的方式。
模型必须实现index()
函数,以便在访问数据时为视图和委托提供索引。当其他组件通过其行和列编号及其父模型索引被引用时,索引就会被创建。如果指定了一个无效的模型索引作为父索引,那么将由模型来返回一个与模型中顶层项相对应的索引。
当我们得到一个模型索引时,首先会检查它是否有效。如果无效,我们就假定正在引用的是顶层项;否则,我们就用internalPointer() 函数从模型索引中获取数据指针,并用它来引用TreeItem
对象。请注意,我们构建的所有模型索引都将包含一个指向现有TreeItem
的指针,因此我们可以保证,我们收到的任何有效模型索引都将包含一个有效的数据指针。
void ShortcutEditorModel::setActions() { beginResetModel(); setupModelData(m_rootItem); endResetModel(); }
由于该函数的行和列参数指向相应父项的子项,因此我们使用TreeItem::child()
函数获取该项目。createIndex() 函数用于创建要返回的模型索引。我们指定行和列的编号,以及指向项目本身的指针。模型索引可用于以后获取项目数据。
TreeItem
对象的定义方式使parent()
函数的编写变得简单:
QModelIndex ShortcutEditorModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); ShortcutEditorModelItem *parentItem; if (!parent.isValid()) parentItem = m_rootItem; else parentItem = static_cast<ShortcutEditorModelItem*>(parent.internalPointer()); ShortcutEditorModelItem *childItem = parentItem->child(row); if (childItem) return createIndex(row, column, childItem); return QModelIndex(); }
我们只需确保永远不返回与根项目相对应的模型索引。为了与index()
函数的实现方式保持一致,我们将为模型中任何顶层项的父项返回一个无效的模型索引。
在创建要返回的模型索引时,我们必须指定父项在其父项中的行号和列号。通过TreeItem::row()
函数,我们可以很容易地找到行号,但我们按照惯例指定 0 作为父项的列号。使用createIndex() 创建模型索引的方法与index()
函数相同。
rowCount()
函数仅返回与给定模型索引相对应的TreeItem
的子项数,如果指定的索引无效,则返回顶层项数:
QModelIndex ShortcutEditorModel::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); ShortcutEditorModelItem *childItem = static_cast<ShortcutEditorModelItem*>(index.internalPointer()); ShortcutEditorModelItem *parentItem = childItem->parentItem(); if (parentItem == m_rootItem) return QModelIndex(); return createIndex(parentItem->row(), 0, parentItem); }
由于每个项都管理自己的列数据,因此columnCount()
函数必须调用项自身的columnCount()
函数,以确定给定模型索引有多少列。与rowCount()
函数一样,如果指定了无效的模型索引,返回的列数将由根项决定:
int ShortcutEditorModel::rowCount(const QModelIndex &parent) const { ShortcutEditorModelItem *parentItem; if (parent.column() > 0) return 0; if (!parent.isValid()) parentItem = m_rootItem; else parentItem = static_cast<ShortcutEditorModelItem*>(parent.internalPointer()); return parentItem->childCount(); }
数据通过data()
从模型中获取。由于项管理自己的列,因此我们需要使用列号来通过TreeItem::data()
函数检索数据:
int ShortcutEditorModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return static_cast<ShortcutEditorModelItem*>(parent.internalPointer())->columnCount(); return m_rootItem->columnCount(); }
请注意,我们在此实现中只支持DisplayRole ,对于无效的模型索引,我们也会返回无效的QVariant 对象。
我们使用flags()
函数来确保视图知道模型是只读的:
QVariant ShortcutEditorModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role != Qt::DisplayRole && role != Qt::EditRole) return QVariant(); ShortcutEditorModelItem *item = static_cast<ShortcutEditorModelItem*>(index.internalPointer()); return item->data(index.column()); }
headerData()
函数会返回我们方便地存储在根项目中的数据:
Qt::ItemFlags ShortcutEditorModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; Qt::ItemFlags modelFlags = QAbstractItemModel::flags(index); if (index.column() == static_cast<int>(Column::Shortcut)) modelFlags |= Qt::ItemIsEditable; return modelFlags; }
这些信息可以通过不同的方式提供:在构造函数中指定,或者硬编码到headerData()
函数中。
QVariant ShortcutEditorModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { return m_rootItem->data(section); } return QVariant(); }
待办事项
void ShortcutEditorModel::setupModelData(ShortcutEditorModelItem *parent) { ActionsMap actionsMap; Application *application = static_cast<Application *>(QCoreApplication::instance()); ActionManager *actionManager = application->actionManager(); const QList<QAction *> registeredActions = actionManager->registeredActions(); for (QAction *action : registeredActions) { QString context = actionManager->contextForAction(action); QString category = actionManager->categoryForAction(action); actionsMap[context][category].append(action); } QAction *nullAction = nullptr; const QString contextIdPrefix = "root"; // Go through each context, one context - many categories each iteration for (const auto &contextLevel : actionsMap.keys()) { ShortcutEditorModelItem *contextLevelItem = new ShortcutEditorModelItem({contextLevel, QVariant::fromValue(nullAction)}, parent); parent->appendChild(contextLevelItem); // Go through each category, one category - many actions each iteration for (const auto &categoryLevel : actionsMap[contextLevel].keys()) { ShortcutEditorModelItem *categoryLevelItem = new ShortcutEditorModelItem({categoryLevel, QVariant::fromValue(nullAction)}, contextLevelItem); contextLevelItem->appendChild(categoryLevelItem); for (QAction *action : actionsMap[contextLevel][categoryLevel]) { QString name = action->text(); if (name.isEmpty() || !action) continue; ShortcutEditorModelItem *actionLevelItem = new ShortcutEditorModelItem({name, QVariant::fromValue(reinterpret_cast<void *>(action))}, categoryLevelItem); categoryLevelItem->appendChild(actionLevelItem); } } } }
TODO
bool ShortcutEditorModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role == Qt::EditRole && index.column() == static_cast<int>(Column::Shortcut)) { QString keySequenceString = value.toString(); ShortcutEditorModelItem *item = static_cast<ShortcutEditorModelItem *>(index.internalPointer()); QAction *itemAction = item->action(); if (itemAction) { if (keySequenceString == itemAction->shortcut().toString(QKeySequence::NativeText)) return true; itemAction->setShortcut(keySequenceString); } Q_EMIT dataChanged(index, index); if (keySequenceString.isEmpty()) return true; } return QAbstractItemModel::setData(index, value, role); }
待办事项
设置模型中的数据
我们使用setupModelData()
函数来设置模型中的初始数据。该函数检索已注册的操作文本,并创建记录数据和整体模型结构的项目对象。当然,这个函数的工作方式对这个模型来说是非常特殊的。我们将对其行为作如下描述,更多信息请读者参阅示例代码本身。
为确保模型正常工作,只需创建具有正确数据和父项的 ShortcutEditorModelItem 实例即可。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.