快捷键编辑器示例

快捷键编辑器示例展示了如何创建一个基本的、读写分层模型,用于与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() 函数用于获取项的行号和父项。

父项和列数据存储在私有成员变量 parentItemitemData 中。变量 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

© 2024 The Qt Company Ltd. 有关文档贡献的版权属于其各自的所有者。所提供的文档根据自由软件基金会发布的GNU自由文档许可证1.3版本的条款进行许可。Qt和相应的徽标是芬兰及/或世界其他地区的The Qt Company Ltd.的商标。所有其他商标均为其各自所有者的财产。