ドリルダウンの例

ドリルダウンの例では、QSqlRelationalTableModel およびQDataWidgetMapper クラスを使用して、データベースからデータを読み取り、変更を送信する方法を示します。

ドリルダウン例のスクリーンショット

サンプル・アプリケーションを実行すると、ユーザーは対応する画像をクリックして各項目に関する情報を取得できます。アプリケーションはデータを表示する情報ウィンドウをポップアップ表示し、ユーザは画像と同様に説明を変更することができます。メインビューは、ユーザが変更を投稿すると更新されます。

この例は3つのクラスで構成されています:

  • ImageItem は画像を表示するためのカスタムグラフィックアイテムのクラスです。
  • View は、ユーザーが様々なアイテムをブラウズするためのメインアプリケーションウィジェットです。
  • InformationWindow はリクエストされた情報を表示し、ユーザーがそれを変更してデータベースに変更を送信できるようにします。

まず、InformationWindow クラスを見て、データベースからデータを読み込んで変更する方法を確認します。次に、メインのアプリケーション・ウィジェット、つまりView クラスと、関連するImageItem クラスを確認します。

InformationWindow クラスの定義

InformationWindow クラスは、QWidget を継承したカスタムウィジェットです:

class InformationWindow : public QDialog
{
    Q_OBJECT
public:
    InformationWindow(int id, QSqlRelationalTableModel *items,
                      QWidget *parent = nullptr);
    int id() const;

Q_SIGNALS:
    void imageChanged(int id, const QString &fileName);

情報ウィンドウを作成するとき、関連するアイテムID、モデルへのポインタ、親をコンストラクタに渡します。モデルへのポインタを使用してウィンドウにデータを入力し、parent パラメータを基底クラスに渡します。IDは将来の参照のために保存されます。

ウィンドウが作成されると、パブリック関数id() を使用して、指定された場所の情報が要求されたときにその場所を特定します。つまり、ユーザーが関連する画像を変更するたびに、IDとファイル名をパラメータとして持つシグナルを発信します。

private Q_SLOTS:
    void revert();
    void submit();
    void enableButtons(bool enable);

ユーザがデータを変更することを許可しているので、ユーザが変更した内容を元に戻して送信する機能を提供する必要があります。enableButtons() スロットは、必要なときにさまざまなボタンを有効にしたり無効にしたりするための便利な機能です。

private:
    void createButtons();

    int itemId;
    QString displayedImage;

    QComboBox *imageFileEditor = nullptr;
    QLabel *itemText = nullptr;
    QTextEdit *descriptionEditor = nullptr;

    QPushButton *closeButton = nullptr;
    QPushButton *submitButton = nullptr;
    QPushButton *revertButton = nullptr;
    QDialogButtonBox *buttonBox = nullptr;

    QDataWidgetMapper *mapper = nullptr;
};

createButtons() 関数も便利な関数で、コンストラクタを単純化するために用意されています。前述したように、将来の参照のためにアイテムIDを保存します。また、imageChanged() シグナルを出すタイミングを判断できるように、現在表示されている画像ファイルの名前も保存しています。

情報ウィンドウでは、QLabel クラスを使ってアイテムの名前を表示する。関連する画像ファイルはQComboBox インスタンスを使って表示され、説明はQTextEdit を使って表示される。さらに、ウィンドウには3つのボタンがあり、データの流れとウィンドウを表示するかどうかを制御する。

最後に、マッパーを宣言する。QDataWidgetMapper クラスは、データモデルのセクションとウィジェットの間のマッピングを提供します。マッパーを使用して、与えられたデータベースからデータを抽出し、ユーザーがデータを変更するたびにデータベースを更新します。

InformationWindowクラスの実装

コンストラクタは、アイテムID、データベースポインタ、親ウィジェットの3つの引数をとります。データベースポインタは、実際にはQSqlRelationalTableModel オブジェクトへのポインタであり、データベーステーブルの編集可能なデータモデル (外部キーサポート付き) を提供します。

InformationWindow::InformationWindow(int id, QSqlRelationalTableModel *items,
                                     QWidget *parent)
    : QDialog(parent)
{
    QLabel *itemLabel = new QLabel(tr("Item:"));
    QLabel *descriptionLabel = new QLabel(tr("Description:"));
    QLabel *imageFileLabel = new QLabel(tr("Image file:"));

    createButtons();

    itemText = new QLabel;
    descriptionEditor = new QTextEdit;

最初に、データベースに含まれるデータを表示するために必要なさまざまなウィジェットを作成します。ほとんどのウィジェットは簡単に作成できます。しかし、画像ファイルの名前を表示するコンボボックスに注目してください:

    imageFileEditor = new QComboBox;
    imageFileEditor->setModel(items->relationModel(1));
    imageFileEditor->setModelColumn(items->relationModel(1)->fieldIndex("file"));

この例では、アイテムに関する情報は、"items" というデータベーステーブルに格納されています。この例では、アイテムに関する情報は "items "というデータベーステーブルに格納されています。モデルを作成する際には、外部キーを使ってこのテーブルと、利用可能な画像ファイル名を格納した2つ目のデータベーステーブル "images "とのリレーションを確立します。これがどのように行われるかは、View クラスをレビューするときに説明します。しかし、このようなリレーションを作成する根拠は、ユーザーがあらかじめ定義された画像ファイルの中からしか選択できないようにしたいからです。

images "データベーステーブルに対応するモデルは、QSqlRelationalTableModel'のrelationModel()関数で利用可能で、外部キー(この場合は "imagefile "カラム番号)を引数として必要とします。コンボボックスに "images "モデルを使用させるために、QComboBox'のsetModel()関数を使用します。また、このモデルには2つのカラム("itemid "と "file")があるので、QComboBox::setModelColumn ()関数を使って、どちらのカラムを表示させたいかを指定します。

    mapper = new QDataWidgetMapper(this);
    mapper->setModel(items);
    mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit);
    mapper->setItemDelegate(new QSqlRelationalDelegate(mapper));
    mapper->addMapping(imageFileEditor, 1);
    mapper->addMapping(itemText, 2, "text");
    mapper->addMapping(descriptionEditor, 3);
    mapper->setCurrentIndex(id);

次にマッパーを作成します。QDataWidgetMapper クラスは、アイテムモデルのセクションにマッピングすることで、データウェアウィジェットを作成することができます。

addMapping() 関数は、指定されたウィジェットとモデルの指定されたセクション間のマッピングを追加します。マッパーの向きが水平(デフォルト)の場合、セクションはモデルの列となり、そうでない場合は行となります。setCurrentIndex() 関数を呼び出して、指定されたアイテム ID に関連付けられたデータでウィジェットを初期化します。現在のインデックスが変更されるたびに、すべてのウィジェットがモデルからの内容で更新されます。

また、マッパのサブミットポリシーをQDataWidgetMapper::ManualSubmit に設定します。これは、ユーザが明確にサブミットを要求するまで、データがデータベースにサブミットされないことを意味します(代替はQDataWidgetMapper::AutoSubmit で、対応するウィジェットがフォーカスを失ったときに自動的に変更をサブミットします)。最後に、マッパービューがアイテムに使用するアイテムデリゲートを指定します。QSqlRelationalDelegate クラスは、デフォルトのデリゲートとは異なり、他のテーブルの外部キーであるフィールド("items "テーブルの "imagefile "のような)に対してコンボボックス機能を有効にするデリゲートを表します。

    connect(descriptionEditor, &QTextEdit::textChanged, this, [this]() { enableButtons(true); });
    connect(imageFileEditor, &QComboBox::currentIndexChanged, this, [this]() { enableButtons(true); });

    QFormLayout *formLayout = new QFormLayout;
    formLayout->addRow(itemLabel, itemText);
    formLayout->addRow(imageFileLabel, imageFileEditor);
    formLayout->addRow(descriptionLabel, descriptionEditor);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addLayout(formLayout);
    layout->addWidget(buttonBox);
    setLayout(layout);

    itemId = id;
    displayedImage = imageFileEditor->currentText();

    setWindowFlags(Qt::Window);
    enableButtons(false);
    setWindowTitle(itemText->text());
}

最後に、エディターの "something's changed "シグナルをカスタムenableButtons スロットに接続し、ユーザーが変更内容を送信したり戻したりできるようにします。enableButtons スロットのシグネチャがQTextEdit::textChangedQComboBox::currentIndexChanged と一致しないため、接続にラムダを使用する必要があります。

すべてのウィジェットをレイアウトに追加し、後で参照できるようにアイテムIDと表示される画像ファイルの名前を保存し、ウィンドウのタイトルと初期サイズを設定します。

ウィジェットがウィンドウ・システム・フレームとタイトル・バーを持つウィンドウであることを示すために、Qt::Window ウィンドウ・フラグも設定します。

int InformationWindow::id() const
{
    return itemId;
}

ウィンドウが作成されると、メイン・アプリケーションが終了するまで削除されません(つまり、ユーザーが情報ウィンドウを閉じても、非表示になるだけです)。このため、各項目ごとにInformationWindow オブジェクトを複数作成することは避け、ユーザーがその場所に関する情報を要求したときに、指定された場所のウィンドウがすでに存在するかどうかを判断できるように、パブリック関数id() を提供しています。

void InformationWindow::revert()
{
    mapper->revert();
    enableButtons(false);
}

revert() スロットは、ユーザーがRevert ボタンを押すたびにトリガーされます。

QDataWidgetMapper::ManualSubmit submit policyを設定したので、ユーザーが明示的にすべてをsubmitすることを選択しない限り、ユーザーの変更はモデルに書き戻されません。それでも、QDataWidgetMapper'のrevert() スロットを使ってエディタウィジェットをリセットし、すべてのウィジェットにモデルの現在のデータを再投入することができます。

void InformationWindow::submit()
{
    QString newImage(imageFileEditor->currentText());

    if (displayedImage != newImage) {
        displayedImage = newImage;
        emit imageChanged(itemId, newImage);
    }

    mapper->submit();
    mapper->setCurrentIndex(itemId);

    enableButtons(false);
}

同様に、submit() スロットは、ユーザーがSubmit ボタンを押して変更を投稿することを決定するたびにトリガーされます。

QDataWidgetMappersubmit() スロットを使って、マップされたウィジェットからモデル、つまりデータベースにすべての変更を投入します。マッピングされたすべてのセクションに対して、アイテムデリゲートは、ウィジェットから現在の値を読み取り、モデルに設定します。最後に、モデルの submit() 関数が呼び出され、モデルにキャッシュされたデータを永続ストレージに送信することを知らせます。

データが送信される前に、以前に格納されたdisplayedImage 変数を参照して、ユーザが別の画像ファイルを選択したかどうかをチェックすることに注意してください。現在のファイル名と保存されているファイル名が異なる場合、新しいファイル名を保存し、imageChanged() シグナルを発する。

void InformationWindow::createButtons()
{
    closeButton = new QPushButton(tr("&Close"));
    revertButton = new QPushButton(tr("&Revert"));
    submitButton = new QPushButton(tr("&Submit"));

    closeButton->setDefault(true);

    connect(closeButton, &QPushButton::clicked, this, &InformationWindow::close);
    connect(revertButton, &QPushButton::clicked, this, &InformationWindow::revert);
    connect(submitButton, &QPushButton::clicked, this, &InformationWindow::submit);

createButtons() 関数は便宜上、つまりコンストラクタを簡略化するために用意されている。

Close ボタンをデフォルト・ボタン、つまりユーザーがEnter を押したときに押されるボタンにし、そのclicked() シグナルをウィジェットのclose() スロットに接続します。前述したように、ウィンドウを閉じても、ウィジェットは隠れるだけで、削除されません。また、SubmitRevert ボタンを、対応するsubmit()revert() スロットに接続します。

    buttonBox = new QDialogButtonBox(this);
    buttonBox->addButton(submitButton, QDialogButtonBox::AcceptRole);
    buttonBox->addButton(revertButton, QDialogButtonBox::ResetRole);
    buttonBox->addButton(closeButton, QDialogButtonBox::RejectRole);
}

QDialogButtonBox クラスは、現在のウィジェット・スタイルに適したレイアウトでボタンを表示するウィジェットです。情報ウィンドウのようなダイアログは、通常、そのプラットフォームのインターフェース・ガイドラインに準拠したレイアウトでボタンを表示します。QDialogButtonBox 、ユーザーのデスクトップ環境に適したレイアウトで自動的にボタンを追加することができます。

ダイアログのほとんどのボタンはある役割に従います。Submit Revert reset すなわち、ボタンを押すとフィールドがデフォルト値(この場合はデータベースに含まれる情報)にリセットされることを示します。reject の役割は、ボタンをクリックするとダイアログが拒否されることを示します。一方、情報ウィンドウを隠すだけなので、ユーザーが明示的に元に戻すか送信するまで、ユーザーが行った変更は保存されます。

void InformationWindow::enableButtons(bool enable)
{
    revertButton->setEnabled(enable);
    submitButton->setEnabled(enable);
}

enableButtons() スロットは、ユーザーが提示されたデータを変更するたびに、ボタンを有効にするために呼び出されます。同様に、ユーザが変更を送信することを選択すると、ボタンは無効になり、現在のデータがデータベースに保存されていることを示します。

これでInformationWindow 。それでは、サンプル・アプリケーションでどのように使用したかを見てみましょう。

ビュー・クラスの定義

View クラスはメイン・アプリケーション・ウィンドウを表し、QGraphicsView を継承しています:

class View : public QGraphicsView
{
    Q_OBJECT
public:
    View(const QString &items, const QString &images, QWidget *parent = nullptr);

protected:
    void mouseReleaseEvent(QMouseEvent *event) override;

private Q_SLOTS:
    void updateImage(int id, const QString &fileName);

QGraphicsView クラスは、画像を表示するために使用するGraphics View Frameworkの一部です。画像がクリックされたときに適切な情報ウィンドウを表示してユーザーとのインタラクションに応答できるように、QGraphicsViewmouseReleaseEvent() 関数を再実装します。

コンストラクタは2つのデータベース・テーブルの名前を受け取ることに注意してください:1つはアイテムの詳細情報、もう1つは利用可能な画像ファイル名です。また、InformationWindow'のimageChanged() シグナルをキャッチするために、プライベートなupdateImage() スロットを用意しています。

private:
    void addItems();
    InformationWindow *findWindow(int id) const;
    void showInformation(ImageItem *image);

    QGraphicsScene *scene;
    QList<InformationWindow *> informationWindows;

addItems() 関数は、コンストラクタを単純化するために用意された便利な関数です。この関数は一度だけ呼び出され、様々なアイテムを作成し、ビューに追加します。

一方、findWindow() 関数は頻繁に使用されます。これは、showInformation() 関数から呼び出され、指定された項目のウィンドウがすでに作成されているかどうかを判断します(InformationWindow オブジェクトを作成するたびに、informationWindows リストにその参照を格納します)。後者の関数は、カスタムのmouseReleaseEvent() 実装から呼び出されます。

    QSqlRelationalTableModel *itemTable;
};

最後に、QSqlRelationalTableModel ポインターを宣言します。前述したように、QSqlRelationalTableModel クラスは外部キーをサポートした編集可能なデータモデルを提供します。QSqlRelationalTableModel クラスを使用する際には、いくつか注意すべき点があります:テーブルには主キーが宣言されていなければならず、このキーは他のテーブルとのリレーションを含むことはできません。また、リレーショナルテーブルに、参照先のテーブルに存在しない行を参照するキーが含まれている場合、その無効なキーを含む行はモデルを通して公開されないことに注意してください。参照整合性を維持するのは、ユーザーまたはデータベースの責任です。

ビュー・クラスの実装

コンストラクタは、オフィスの詳細情報を含むテーブルと、利用可能な画像ファイル名を含むテーブル の両方の名前を要求しますが、"items" テーブルのQSqlRelationalTableModel オブジェクトを作成するだけです:

View::View(const QString &items, const QString &images, QWidget *parent)
    : QGraphicsView(parent)
{
    itemTable = new QSqlRelationalTableModel(this);
    itemTable->setTable(items);
    itemTable->setRelation(1, QSqlRelation(images, "itemid", "file"));
    itemTable->select();

その理由は、アイテムの詳細を含むモデルができれば、QSqlRelationalTableModelsetRelation() 関数を使用して、利用可能な画像ファイルへのリレーションを作成できるからです。この関数は、与えられたモデルのカラムに対して外部キーを作成します。キーは、キーが参照するテーブルの名前、キーがマッピングされるフィールド、そしてユーザーに表示されるフィールドによって構成されるQSqlRelation オブジェクトによって指定されます。

テーブルを設定することは、モデルが操作するテーブルを指定するだけであることに注意してください。つまり、モデルにデータを入力するためには、モデルのselect() 関数を明示的に呼び出す必要があります。

    scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, 465, 365);
    setScene(scene);

    addItems();

    setMinimumSize(470, 370);
    setMaximumSize(470, 370);

    QLinearGradient gradient(QPointF(0, 0), QPointF(0, 370));
    gradient.setColorAt(0, QColor("#868482"));
    gradient.setColorAt(1, QColor("#5d5b59"));
    setBackgroundBrush(gradient);
}

次に、ビューのコンテンツ、つまりシーンとそのアイテムを作成します。ラベルは通常のQGraphicsTextItem オブジェクトで、画像はQGraphicsPixmapItem から派生したImageItem クラスのインスタンスです。addItems() 関数については、また後ほど説明します。

最後に、メイン・アプリケーション・ウィジェットのサイズ制約とウィンドウ・タイトルを設定します。

void View::addItems()
{
    int itemCount = itemTable->rowCount();

    int imageOffset = 150;
    int leftMargin = 70;
    int topMargin = 40;

    for (int i = 0; i < itemCount; i++) {
        QSqlRecord record = itemTable->record(i);

        int id = record.value("id").toInt();
        QString file = record.value("file").toString();
        QString item = record.value("itemtype").toString();

        int columnOffset = ((i % 2) * 37);
        int x = ((i % 2) * imageOffset) + leftMargin + columnOffset;
        int y = ((i / 2) * imageOffset) + topMargin;

        ImageItem *image = new ImageItem(id, QPixmap(":/" + file));
        image->setData(0, i);
        image->setPos(x, y);
        scene->addItem(image);

        QGraphicsTextItem *label = scene->addText(item);
        label->setDefaultTextColor(QColor("#d7d6d5"));
        QPointF labelOffset((120 - label->boundingRect().width()) / 2, 120.0);
        label->setPos(QPointF(x, y) + labelOffset);
    }
}

addItems() 関数は、メイン・アプリケーション・ウィンドウを作成するときに一度だけ呼び出されます。データベース・テーブルの各行について、まずモデルのrecord() 関数を使用して対応するレコードを抽出します。QSqlRecord クラスは、データベースレコードの機能と特性の両方をカプセル化し、フィールドの追加と削除、およびフィールド値の設定と取得をサポートします。QSqlRecord::value() 関数は、指定された名前またはインデックスを持つフィールドの値をQVariant オブジェクトとして返します。

各レコードについて、ラベル・アイテムと画像アイテムを作成し、それらの位置を計算してシーンに追加する。画像アイテムは、ImageItem クラスのインスタンスで表されます。カスタム・アイテム・クラスを作成しなければならない理由は、アイテムのホバー・イベントをキャッチし、マウス・カーソルが画像の上にあるときにアイテムをアニメーションさせたいからです(デフォルトでは、アイテムはホバー・イベントを受け付けません)。詳しくはGraphics View FrameworkのドキュメントとGraphics View Examplesを参照してください。

void View::mouseReleaseEvent(QMouseEvent *event)
{
    if (QGraphicsItem *item = itemAt(event->position().toPoint())) {
        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item))
            showInformation(image);
    }
    QGraphicsView::mouseReleaseEvent(event);
}

QGraphicsViewmouseReleaseEvent() イベントハンドラを再実装して、ユーザーとのインタラクションに対応します。ユーザーが画像アイテムのいずれかをクリックすると、この関数は、関連する情報ウィンドウをポップアップするためにプライベートshowInformation() 関数を呼び出します。

Graphics View Frameworkはqgraphicsitem_cast()関数を提供し、与えられたQGraphicsItem インスタンスが与えられたタイプであるかどうかを判断します。イベントが画像アイテムのいずれにも関連していない場合は、基底クラスの実装に渡すことに注意してください。

void View::showInformation(ImageItem *image)
{
    int id = image->id();
    if (id < 0 || id >= itemTable->rowCount())
        return;

    InformationWindow *window = findWindow(id);
    if (!window) {
        window = new InformationWindow(id, itemTable, this);

        connect(window, &InformationWindow::imageChanged,
                this, &View::updateImage);

        window->move(pos() + QPoint(20, 40));
        window->show();
        informationWindows.append(window);
    }

    if (window->isVisible()) {
        window->raise();
        window->activateWindow();
    } else
        window->show();
}

showInformation() 関数は、引数としてImageItem オブジェクトを受け取り、まずアイテムのアイテムIDを抽出します。

そして、この場所の情報ウィンドウがすでに作成されているかどうかを判断する。指定された場所のウィンドウが存在しない場合、アイテムID、モデルへのポインタ、親としてのビューをInformationWindow コンストラクタに渡してウィンドウを作成します。情報ウィンドウのimageChanged() シグナルをこのウィジェットのupdateImage() スロットに接続してから、適切な位置を与え、既存のウィンドウのリストに追加することに注意してください。指定された位置にウィンドウがあり、そのウィンドウが表示されている場合、ウィンドウをウィジェットスタックの一番上に上げてアクティブにします。非表示の場合は、show ()スロットを呼び出すと、同じ結果が得られます。

void View::updateImage(int id, const QString &fileName)
{
    QList<QGraphicsItem *> items = scene->items();

    while(!items.empty()) {
        QGraphicsItem *item = items.takeFirst();

        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item)) {
            if (image->id() == id){
                image->setPixmap(QPixmap(":/" +fileName));
                image->adjust();
                break;
            }
        }
    }
}

updateImage() スロットは、引数としてアイテムIDと画像ファイル名を取る。画像アイテムをフィルタリングし、指定されたアイテムIDに対応するアイテムを、指定された画像ファイルで更新します。

InformationWindow *View::findWindow(int id) const
{
    for (auto window : informationWindows) {
        if (window && (window->id() == id))
            return window;
    }
    return nullptr;
}

findWindow() 関数は単に既存のウィンドウのリストを検索し、与えられたアイテムIDに一致するウィンドウへのポインタを返します。ウィンドウが存在しない場合はnullptr を返します。

最後に、カスタム・クラスImageItem を見てみましょう:

ImageItemクラスの定義

ImageItem クラスは、画像アイテムのアニメーションを容易にするために用意されています。QGraphicsPixmapItem を継承し、ホバー・イベント・ハンドラを再実装しています:

class ImageItem : public QObject, public QGraphicsPixmapItem
{
    Q_OBJECT
public:
    enum { Type = UserType + 1 };

    ImageItem(int id, const QPixmap &pixmap, QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }
    void adjust();
    int id() const;

protected:
    void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
    void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;

private Q_SLOTS:
    void setFrame(int frame);
    void updateItemPosition();

private:
    QTimeLine timeLine;
    int recordId;
    double z;
};

カスタム・アイテムのためにType enum値を宣言し、type ()を再実装します。これは、qgraphicsitem_cast() を安全に使用できるようにするためです。さらに、関連する場所を特定できるパブリックなid() 関数と、元の画像ファイルに関係なく、画像アイテムに希望するサイズが与えられるように呼び出せるパブリックなadjust() 関数を実装します。

アニメーションは、QTimeLine クラスとイベントハンドラ、privatesetFrame() スロットを使って実装します:画像アイテムは、マウスカーソルがその上に重なると拡大し、カーソルがその境界を離れると元のサイズに戻ります。

最後に、この特定のレコードが関連付けられているアイテムIDとZ値を格納します。Graphics View Frameworkでは、アイテムのZ値はアイテムスタック内の位置を決定します。Z値の高いアイテムは、同じ親アイテムを共有している場合、Z値の低いアイテムの上に描画されます。また、必要に応じてビューをリフレッシュするために、updateItemPosition() 関数も提供しています。

ImageItemクラスの実装

ImageItem QGraphicsPixmapItem つまり、コンストラクタの引数のほとんど(pixmap、parent、scene)を基底クラスのコンストラクタに渡すことができます:

ImageItem::ImageItem(int id, const QPixmap &pixmap, QGraphicsItem *parent)
    : QGraphicsPixmapItem(pixmap, parent)
{
    recordId = id;
    setAcceptHoverEvents(true);

    timeLine.setDuration(150);
    timeLine.setFrameRange(0, 150);

    connect(&timeLine, &QTimeLine::frameChanged, this, &ImageItem::setFrame);
    connect(&timeLine, &QTimeLine::finished, this, &ImageItem::updateItemPosition);

    adjust();
}

次に、将来の参照用にIDを保存し、イメージアイテムがホバーイベントを受け取れるようにします。ホバーイベントは、現在のマウスグラバーアイテムがないときに送られます。マウスカーソルがアイテムに入ったとき、アイテムの中を移動したとき、アイテムから離れたときに送られます。先に述べたように、Graphics View Frameworkのどのアイテムも、デフォルトではホバーイベントを受け付けません。

QTimeLine クラスは、アニメーションを制御するためのタイムラインを提供します。そのduration プロパティは、ミリ秒単位でタイムラインの合計時間を保持します。デフォルトでは、タイムラインは最初から最後に向かって1回ずつ実行されます。QTimeLine::setFrameRange() 関数は、タイムラインのフレームカウンターを設定します。タイムラインが実行されているときは、フレームが変わるたびにframeChanged() シグナルが出力されます。アニメーションの継続時間とフレーム範囲を設定し、タイムラインのframeChanged() とfinished() シグナルをプライベートなsetFrame()updateItemPosition() スロットに接続します。

最後に、adjust() を呼び出して、アイテムが好みのサイズになるようにします。

void ImageItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
{
    timeLine.setDirection(QTimeLine::Forward);

    if (z != 1.0) {
        z = 1.0;
        updateItemPosition();
    }

    if (timeLine.state() == QTimeLine::NotRunning)
        timeLine.start();
}

void ImageItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
{
    timeLine.setDirection(QTimeLine::Backward);
    if (z != 0.0)
        z = 0.0;

    if (timeLine.state() == QTimeLine::NotRunning)
        timeLine.start();
}

マウスカーソルが画像アイテムに入ったり離れたりするたびに、対応するイベントハンドラがトリガーされます:まず、時間軸の方向を設定し、それぞれアイテムを拡大または縮小します。次に、アイテムのZ値がまだ期待値に設定されていなければ、それを変更します。

hoverenterイベントの場合、アイテムが拡大し始めるとすぐに他のアイテムの上に表示されるようにするため、アイテムの位置を即座に更新します。一方、hoverleaveイベントの場合は、同じ結果を得るために実際の更新を延期します。しかし、アイテムを作成するときに、タイムラインのfinished() シグナルをupdateItemPosition() スロットに接続したことを思い出してください。こうすることで、アニメーションが完了すると、アイテムはアイテムスタックの正しい位置に置かれます。最後に、タイムラインがまだ実行されていなければ、それを開始します。

void ImageItem::setFrame(int frame)
{
    adjust();
    QPointF center = boundingRect().center();

    setTransform(QTransform::fromTranslate(center.x(), center.y()), true);
    setTransform(QTransform::fromScale(1 + frame / 300.0, 1 + frame / 300.0), true);
    setTransform(QTransform::fromTranslate(-center.x(), -center.y()), true);
}

時間軸が実行されると、アイテムコンストラクタで作成した接続によって現在のフレームが変わるたびに、setFrame() スロットがトリガーされます。このスロットがアニメーションを制御し、画像アイテムを段階的に拡大または縮小します。

まず、adjust() 関数を呼び出して、アイテムの元のサイズから開始するようにします。次に、アニメーションの進行に応じた係数でアイテムを拡大縮小します(frame パラメータを使用)。デフォルトでは、変形はアイテムの左上隅を基準に行われることに注意してください。アイテムの中心に対して相対的に変形させたいので、アイテムを拡大縮小する前に座標系を平行移動する必要があります。

最後に、以下の便利な関数だけが残ります:

void ImageItem::adjust()
{
    setTransform(QTransform::fromScale(120.0 / boundingRect().width(),
                                       120.0 / boundingRect().height()));
}

int ImageItem::id() const
{
    return recordId;
}

void ImageItem::updateItemPosition()
{
    setZValue(z);
}

adjust() 関数は変換行列を定義して適用し、元画像のサイズに関係なく、画像アイテムが好みのサイズで表示されるようにします。id() 関数は些細なもので、単にアイテムを識別するために用意されています。updateItemPosition() スロットでは、QGraphicsItem::setZValue() 関数を呼び出し、アイテムの高さを設定します。

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

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