本页内容

快捷方式编辑器示例

“快捷方式编辑器”示例演示了如何创建一个基本的、可读写的层次结构模型,以便与 Qt 的标准视图类和QKeySequenceEdit 类配合使用。有关模型/视图编程的说明,请参阅模型/视图编程概述。

操作列表及其对应的键盘快捷键

Qt的模型/视图架构为视图提供了一种标准方式,用于操作数据源中的信息,它利用数据的抽象模型来简化和标准化数据访问方式。快捷方式编辑器模型将操作表示为一个项目树,并允许视图通过基于索引的系统访问这些数据。 更普遍地说,模型可通过让每个项目作为子项目表的父项,将数据以树结构的形式表示出来。

设计与概念

我们用来表示数据结构的数据结构采用由 ShortcutEditorModelItem 对象构建的树形式。每个 ShortcutEditorModelItem 代表树视图中的一个项目,并包含两列数据。

按行排列的快捷方式模型结构快捷方式编辑器结构

数据在模型内部通过 ShortcutEditorModelItem 对象进行存储,这些对象以基于指针的树结构相互链接。通常,每个 ShortcutEditorModelItem 都有一个父项,并可拥有多个子项。然而,树结构中的根项没有父项,且在模型外部永远不会被引用。

每个 ShortcutEditorModelItem 都包含其在树结构中的位置信息;它可以返回其父项及其行号。随时可获取这些信息,使得模型的实现更加容易。

由于树视图中的每个项目通常包含多列数据(本例中为名称和快捷键),因此将这些信息存储在每个项目中是自然而然的。为简化起见,我们将使用一个QVariant 对象列表来存储项目中各列的数据。

使用基于指针的树结构意味着,在将模型索引传递给视图时,我们可以记录索引中对应项的地址(参见QAbstractItemModel::createIndex()),并稍后通过QModelIndex::internalPointer() 检索该项。这简化了模型的编写,并确保所有指向同一项的模型索引都拥有相同的内部数据指针。

在建立了适当的数据结构后,我们只需编写极少的额外代码,即可创建一个树形模型,从而向其他组件提供模型索引和数据。

ShortcutEditorModelItem 类定义

ShortcutEditorModelItem 类的定义如下:

该类是一个基本的 C++ 类。它既不继承自 `QObject `,也不提供信号和插槽。它用于存储一个包含列数据的 `QVariant` 列表,以及该项在树结构中的位置信息。其函数提供以下功能:

  • 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。模型索引的创建方式与index() 函数中的createIndex() 相同。

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);
            }
        }
    }
}

待办事项

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 的实例即可。

示例项目 @ code.qt.io

© 2026 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.