ショートカット・エディタの例

ショートカット・エディタの例では、Qt の標準ビューとQKeySequenceEdit クラスで使用する、基本的な読み書き可能な階層モデルを作成する方法を示します。モデル/ビュー・プログラミングの説明については、モデル/ビュー・プログラミングの概要を参照してください。

Qt のモデル/ビュー・アーキテクチャは、ビューがデータ・ソースの情報を操作するための標準的な方法を提供します。ショートカットエディタのモデルは、アクションをアイテムのツリーとして表現し、インデックスベースのシステムを介してビューがこのデータにアクセスできるようにします。より一般的には、各アイテムが子アイテムのテーブルの親として動作するようにすることで、ツリー構造の形でデータを表現するためにモデルを使用することができます。

設計と概念

データの構造を表現するために使用するデータ構造は、ShortcutEditorModelItem オブジェクトから構築されるツリーの形を取ります。各 ShortcutEditorModelItem は、ツリー・ビューの 1 つの項目を表し、2 列のデータを含んでいます。

ショートカット・エディタの構造

データは、ポインタ ベースのツリー構造でリンクされた ShortcutEditorModelItem オブジェクトを使用してモデル内部に格納されます。一般的に、各 ShortcutEditorModelItem は親アイテムを持ち、いくつかの子アイテムを持つことができます。しかし、ツリー構造のルート項目は親項目を持たず、モデルの外部から参照されることはありません。

各 ShortcutEditorModelItem には、ツリー構造におけるその位置に関する情報が含まれており、親アイテムとその行番号を返すことができます。この情報をすぐに利用できるようにすることで、モデルの実装が容易になります。

通常、ツリービューの各項目はいくつかの列のデータ(この例では名前とショートカット)を含むので、各項目にこの情報を格納するのは自然なことです。簡単のために、QVariant オブジェクトのリストを使用して、項目の各列のデータを格納します。

ポインタベースのツリー構造を使うということは、モデルのインデックスをビューに渡すときに、対応する項目のアドレスをインデックスに記録しておき(QAbstractItemModel::createIndex ()を参照)、後でQModelIndex::internalPointer ()で取り出すことができるということです。これにより、モデルの記述が簡単になり、同じ項目を参照するすべてのモデルインデックスが同じ内部データポインタを持つことが保証されます。

適切なデータ構造を用意することで、モデル・インデックスとデータを他のコンポーネントに供給するための余分なコードを最小限に抑えて、ツリー・モデルを作成することができます。

ShortcutEditorModelItem クラスの定義

ShortcutEditorModelItem クラスは以下のように定義されています:

このクラスは基本的な C++ クラスです。このクラスは基本的な C++ クラスで、QObject を継承したり、シグナルやスロットを提供したりはしません。このクラスは、列データとツリー構造内の位置に関する情報を含む QVariants のリストを保持するために使用されます。関数は以下の機能を提供します:

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

TODO

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

TODO

モデル内のデータのセットアップ

モデルの初期データを設定するために、setupModelData() 関数を使います。この関数は、登録されたアクションテキストを取得し、データとモデル全体の構造を記録するアイテムオブジェクトを作成します。当然ながら、この関数はこのモデル特有の動作をします。この関数の動作について以下に説明し、詳細についてはサンプルコードを参照してください。

モデルが正しく動作するようにするには、正しいデータと親アイテムを持つShortcutEditorModelItemのインスタンスを作成するだけでよい。

サンプルプロジェクト @ code.qt.io

このドキュメントに含まれるコントリビューションの著作権は、それぞれの所有者に帰属します ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。