Qt QGraphicsViewの描画キャッシュを徹底解説!resetCachedContent()の正しい使い方と注意点

2025-05-27

もう少し詳しく説明します。

QGraphicsView とキャッシュ

QGraphicsViewは、QGraphicsSceneの内容を表示するためのウィジェットです。特に、大規模なシーンや複雑な描画を扱う場合、パフォーマンスを向上させるために描画内容の一部または全体をキャッシュすることがあります。これは、頻繁に再描画する必要があるが、実際の描画内容が変更されていない領域の描画コストを削減するためです。

QGraphicsViewには、キャッシュモードを設定するためのsetCacheMode()という関数があります。例えば、背景の描画をキャッシュするQGraphicsView::CacheBackgroundのようなモードがあります。これにより、ビューのスクロールやアイテムの移動などがあっても、背景の再描画を省略してパフォーマンスを向上させることができます。

resetCachedContent() の役割

resetCachedContent()は、この内部キャッシュを強制的に無効化し、次の描画サイクルで内容が再生成されるようにします。

具体的にどのような場合にこれが必要になるかというと、以下のようなケースが挙げられます。

    • 例えば、ビューの背景をキャッシュしているが、その背景を描画するためのデータ(画像やグラデーションなど)がプログラムの別の部分で変更された場合。QGraphicsViewは、その変更を自動的に検知できないことがあります。
    • カスタムのQGraphicsItemを使用しており、そのアイテムの描画ロジックが変更されたにもかかわらず、ビューのキャッシュが古い描画内容を保持している場合。
  1. 描画がおかしくなった場合

    • 稀に、何らかの理由でビューの描画にアーティファクト(ゴミ)が出たり、一部のアイテムが正しく表示されなくなったりすることがあります。このような場合に、キャッシュをリセットすることで問題が解決することがあります。これは、キャッシュが破損したり、矛盾した状態になったりしている可能性を排除するためです。

resetCachedContent()を呼び出すと、次にビューが描画される際にキャッシュの内容が再生成されるため、その分の描画コストがかかります。したがって、必要のないときに頻繁に呼び出すと、かえってパフォーマンスが低下する可能性があります。

通常、QGraphicsViewは、シーン内のアイテムの変更やビューの変換(拡大縮小、回転など)によって自動的にキャッシュの一部または全体を無効化します。しかし、上記のような特別なケースでは、開発者が明示的にresetCachedContent()を呼び出す必要がある場合があります。



QGraphicsView::resetCachedContent() 関連の一般的なエラーと問題

    • 問題
      シーン内のアイテムを非表示にした、削除した、または移動したにもかかわらず、そのアイテムの「残像」のようなものがビューに残り続ける。特に、カスタムのQGraphicsItemを使用している場合や、シーンの背景をキャッシュしている場合に発生しやすいです。
    • 原因
      QGraphicsViewのキャッシュが古い描画内容を保持しているためです。ビューは、変更された領域のみを再描画しようとしますが、キャッシュされた内容がその変更を反映していない場合があります。
    • resetCachedContent()との関連
      このような場合にresetCachedContent()を呼び出すと、キャッシュがクリアされ、ビュー全体が強制的に再描画されるため、問題が解決することが多いです。
  1. パフォーマンスの低下

    • 問題
      resetCachedContent()を頻繁に呼び出すと、アプリケーションの応答性が悪くなったり、描画がカクついたりする。
    • 原因
      resetCachedContent()はキャッシュをリセットし、ビュー全体を再描画させるため、描画コストが高い操作です。これを必要以上に頻繁に呼び出すと、無駄な再描画が発生し、パフォーマンスが低下します。
    • resetCachedContent()との関連
      これはresetCachedContent()そのもののエラーというよりは、誤った使用方法による問題です。
  2. 予期せぬ描画のちらつき(フリッカー)

    • 問題
      resetCachedContent()を呼び出すたびに、ビューが一度真っ白になったり、不自然にちらついたりする。
    • 原因
      キャッシュがリセットされ、ビューが完全に再描画される際に、描画処理の途中の状態が見えてしまうためです。特に、V-Sync (垂直同期) が適切に設定されていない場合や、複雑な描画処理が走る場合に顕著になります。
    • resetCachedContent()との関連
      resetCachedContent()がトリガーする全面再描画の特性によるものです。
  3. drawBackground() / drawForeground() が呼ばれない

    • 問題
      QGraphicsViewQGraphicsScenedrawBackground()またはdrawForeground()をオーバーライドしてカスタムな背景/前景を描画しているが、内容を変更しても再描画されない。
    • 原因
      QGraphicsViewは、これらの描画内容もキャッシュすることがあります。背景や前景の描画ロジックを変更しても、ビューがそれを「変更」として認識せず、キャッシュを更新しないことがあります。
    • resetCachedContent()との関連
      この場合も、resetCachedContent()を呼び出すことで、キャッシュがクリアされ、drawBackground()drawForeground()が再呼び出しされて描画が更新されます。Qtのドキュメントにも、「背景ブラシやシーンの背景ブラシプロパティが変更されると、この関数は自動的に呼び出されますが、カスタムの背景を描画するためにQGraphicsScene::drawBackground()やQGraphicsView::drawBackground()を再実装し、完全な再描画をトリガーする必要がある場合にのみ、この関数を呼び出す必要があります」と記載されています。

resetCachedContent()に関連する問題が発生した場合のトラブルシューティングは以下の通りです。

  1. 本当にresetCachedContent()が必要か再確認する

    • 多くの場合、QGraphicsScene::update()QGraphicsItem::update()、あるいはQGraphicsView::viewport()->update()など、より限定的な更新関数で事足りる可能性があります。
    • アイテムの形状や位置が変更された場合は、QGraphicsItem::prepareGeometryChange()を呼び出してからupdate()することで、Qtが適切な再描画領域を計算してくれます。
    • シーン全体の更新が必要な場合でも、QGraphicsScene::update()の引数に更新が必要な領域を示すQRectFを指定することで、無駄な再描画を避けることができます。
    • QGraphicsView::setViewportUpdateMode()QGraphicsView::FullViewportUpdateに設定することで、ビュー全体を常に更新するようにできますが、これはパフォーマンスに大きな影響を与えるため、最終手段と考えるべきです。
  2. QGraphicsItem::boundingRect() と QGraphicsItem::shape() の正確性を確認する

    • 描画のアーティファクトが発生する場合、カスタムアイテムのboundingRect()shape()の実装が不正確である可能性が高いです。
    • boundingRect()はアイテムが占める最小の矩形領域を正確に返す必要があります。描画がこの矩形からはみ出している場合、Qtはそのはみ出した部分が変更されたことを認識できません。
    • 特にペン幅(QPen)を考慮に入れる必要があります。ペンで線を描画する場合、線の半分はboundingRect()の外側にはみ出すため、boundingRect()は描画内容全体を包含するように適切に拡大する必要があります。
    • shape()は、アイテムの正確な形状を返します。boundingRect()よりも正確な当たり判定や描画領域の計算に利用されます。
  3. キャッシュモードを確認する

    • QGraphicsView::cacheMode()の設定が意図しない挙動を引き起こしている可能性があります。QGraphicsView::NoCacheに設定することで、キャッシュ自体を無効化し、問題が解決するかどうか確認できます(ただし、パフォーマンスは低下します)。
    • QGraphicsView::CacheBackgroundQGraphicsView::CacheNoneなど、適切なキャッシュモードを選択しているか確認してください。
  4. デバッグツールを活用する

    • Qt Creatorのデバッガを使用して、resetCachedContent()が呼び出されるタイミングや頻度を確認します。
    • 描画イベントや更新領域に関するデバッグ情報を出力するQtの環境変数(例: QT_GRAPHICSVIEW_DEBUG=1)を活用することで、問題の原因を特定できる場合があります。
  5. 最小限の再現コードを作成する

    • 問題が複雑な場合、問題の核心となる部分だけを抜き出した最小限のコードを作成し、そのコードで問題が再現するかどうかを確認します。これにより、不要な要素を排除し、問題の特定を容易にできます。


例1:背景のカスタム描画を更新する場合

QGraphicsViewの背景をカスタムで描画していて、その描画内容を動的に変更したときにresetCachedContent()が必要になる典型的なケースです。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QPainter>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QTimer>

// カスタムのQGraphicsViewクラス
class CustomGraphicsView : public QGraphicsView
{
    Q_OBJECT // シグナル/スロットを使用するために必要

public:
    CustomGraphicsView(QGraphicsScene* scene, QWidget* parent = nullptr)
        : QGraphicsView(scene, parent), m_backgroundCounter(0)
    {
        // 背景のキャッシュを有効にする (パフォーマンス向上のため)
        setCacheMode(QGraphicsView::CacheBackground);
    }

    void updateBackgroundColor()
    {
        m_backgroundCounter = (m_backgroundCounter + 1) % 3; // 色を切り替える
        // 背景が変更されたことをビューに伝えるためにキャッシュをリセット
        // QGraphicsView::drawBackground() がキャッシュされているため、明示的にリセットが必要
        resetCachedContent();
        viewport()->update(); // ビューポートの再描画を要求
    }

protected:
    void drawBackground(QPainter* painter, const QRectF& rect) override
    {
        // 親クラスの背景描画を呼び出す(通常、デフォルトの背景を描画)
        QGraphicsView::drawBackground(painter, rect);

        // カスタムの背景を描画
        switch (m_backgroundCounter) {
            case 0:
                painter->fillRect(rect, Qt::lightGray);
                break;
            case 1:
                painter->fillRect(rect, Qt::darkGray);
                break;
            case 2:
                painter->fillRect(rect, Qt::blue);
                break;
        }

        painter->drawText(rect.center(), QString("Background Update: %1").arg(m_backgroundCounter));
    }

private:
    int m_backgroundCounter;
};

// メインウィンドウクラス
class MainWindow : public QWidget
{
    Q_OBJECT
public:
    MainWindow(QWidget* parent = nullptr) : QWidget(parent)
    {
        QGraphicsScene* scene = new QGraphicsScene(this);
        scene->setSceneRect(-200, -200, 400, 400); // シーンの範囲を設定

        // シーンにアイテムを追加
        QGraphicsRectItem* rectItem = new QGraphicsRectItem(-50, -50, 100, 100);
        rectItem->setBrush(Qt::red);
        scene->addItem(rectItem);

        CustomGraphicsView* view = new CustomGraphicsView(scene, this);

        QPushButton* resetButton = new QPushButton("背景色を更新", this);
        connect(resetButton, &QPushButton::clicked, view, &CustomGraphicsView::updateBackgroundColor);

        QVBoxLayout* layout = new QVBoxLayout(this);
        layout->addWidget(view);
        layout->addWidget(resetButton);

        setLayout(layout);
        setWindowTitle("QGraphicsView::resetCachedContent() 例");
        resize(600, 500);

        // 3秒ごとに背景を自動更新
        QTimer* timer = new QTimer(this);
        connect(timer, &QTimer::timeout, view, &CustomGraphicsView::updateBackgroundColor);
        timer->start(3000);
    }
};

#include "main.moc" // mocファイルをインクルード

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.show();

    return a.exec();
}

解説

  • viewport()->update()を呼び出すことで、ビューポートの再描画を明示的に要求しています。resetCachedContent()はキャッシュをリセットするだけで、すぐに描画が実行されるわけではないため、これは重要です。
  • 背景の色が変わったことをQGraphicsViewに伝えるために、resetCachedContent()を呼び出しています。これにより、キャッシュされた背景が無効化され、次の描画サイクルで新しい色の背景が再描画されます。
  • updateBackgroundColor()関数で背景の色を変更していますが、この変更はdrawBackground()の中で行われます。
  • setCacheMode(QGraphicsView::CacheBackground)を設定することで、背景の描画結果がキャッシュされます。これにより、ビューのスクロールなどがあっても背景が毎回描画されず、パフォーマンスが向上します。
  • CustomGraphicsViewクラスでは、drawBackground()をオーバーライドしてカスタムの背景を描画しています。

例2:複雑なカスタムアイテムが動的に変化する場合(あまり推奨されないが例として)

この例は、QGraphicsItem::prepareGeometryChange()QGraphicsItem::update()を使用することがより適切ですが、resetCachedContent()がどのように作用するかを示すために、あえて使ってみます。通常、アイテム自体の変更はアイテムのupdate()で対処すべきです。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsItem>
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QTimer>
#include <QRandomGenerator>

// ランダムに形状が変わるカスタムアイテム
class DynamicShapeItem : public QGraphicsItem
{
public:
    DynamicShapeItem() : m_shapeType(0) {
        setPos(0, 0); // 中心に配置
    }

    QRectF boundingRect() const override {
        // アイテムの最大描画範囲を返す
        // ペンの幅などを考慮して少し広めにする
        return QRectF(-60, -60, 120, 120);
    }

    void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override {
        Q_UNUSED(option);
        Q_UNUSED(widget);

        painter->setPen(QPen(Qt::black, 2));

        switch (m_shapeType) {
            case 0: // 円
                painter->setBrush(Qt::red);
                painter->drawEllipse(-50, -50, 100, 100);
                break;
            case 1: // 四角
                painter->setBrush(Qt::green);
                painter->drawRect(-50, -50, 100, 100);
                break;
            case 2: // 星型
                painter->setBrush(Qt::blue);
                QPolygonF star;
                for (int i = 0; i < 5; ++i) {
                    qreal angle = 2 * M_PI * i / 5.0 - M_PI / 2.0;
                    star << QPointF(50 * std::cos(angle), 50 * std::sin(angle));
                    angle += M_PI / 5.0; // 内部の点
                    star << QPointF(20 * std::cos(angle), 20 * std::sin(angle));
                }
                painter->drawPolygon(star);
                break;
        }
    }

    void changeShape() {
        m_shapeType = QRandomGenerator::global()->bounded(3); // 0, 1, 2 のいずれか
        // アイテムの形状が大きく変わったことを通知
        // 通常は prepareGeometryChange() を呼び出し、その後に update() を呼び出すべきですが、
        // この例では QGraphicsView::resetCachedContent() の効果を見るため、
        // あえてアイテム自身の更新メカニズムに頼らないケースを想定します。
        // ただし、このやり方はパフォーマンス上非効率です。
    }

private:
    int m_shapeType;
};

// メインウィンドウクラス
class MainWindow : public QWidget
{
    Q_OBJECT
public:
    MainWindow(QWidget* parent = nullptr) : QWidget(parent)
    {
        QGraphicsScene* scene = new QGraphicsScene(this);
        scene->setSceneRect(-200, -200, 400, 400);

        DynamicShapeItem* item = new DynamicShapeItem();
        scene->addItem(item);

        QGraphicsView* view = new QGraphicsView(scene, this);
        // ここでは QGraphicsView のキャッシュモードを特に設定していません。
        // デフォルトでは MinimalViewportUpdate です。

        QPushButton* changeShapeButton = new QPushButton("アイテムの形状を変更", this);
        connect(changeShapeButton, &QPushButton::clicked, [item, view]() {
            item->changeShape();
            // アイテム自体の update() を呼び出さずに、ビュー全体のキャッシュをリセット
            // これにより、ビューはアイテムの変更を検知し、再描画を強制される
            // ただし、これは非効率的な方法であり、推奨されません
            view->resetCachedContent(); // 通常は item->update() で十分
            view->viewport()->update();
        });

        QVBoxLayout* layout = new QVBoxLayout(this);
        layout->addWidget(view);
        layout->addWidget(changeShapeButton);

        setLayout(layout);
        setWindowTitle("QGraphicsView::resetCachedContent() (カスタムアイテム)");
        resize(600, 500);

        // 2秒ごとに自動で形状を変更し、ビューを更新
        QTimer* timer = new QTimer(this);
        connect(timer, &QTimer::timeout, [item, view]() {
            item->changeShape();
            view->resetCachedContent(); // ここも非効率だが例として
            view->viewport()->update();
        });
        timer->start(2000);
    }
};

#include "main.moc"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.show();

    return a.exec();
}

解説

  • 注意
    この方法は、個々のアイテムの変更に対してresetCachedContent()を使用することは推奨されません。非常に非効率的であり、パフォーマンスの低下を招きます。あくまでresetCachedContent()がビュー全体のキャッシュをリセットする作用を示すための例です。
  • これは、ビューのキャッシュが、個々のアイテムのupdate()によって自動的に無効化されなかった場合に、ビュー全体を強制的に再描画させるためにresetCachedContent()を使用する「万が一のケース」を示しています。
  • しかし、この例では、item->update()を意図的に呼び出さず、代わりにボタンクリックやタイマーでview->resetCachedContent()を呼び出しています。
  • 通常、DynamicShapeItem::changeShape()を呼び出した後、item->prepareGeometryChange()(形状が変わる可能性があるので)とitem->update()を呼び出すべきです。これにより、Qtは変更されたアイテムの領域のみを効率的に再描画します。
  • DynamicShapeItemは、描画される形状を内部的に変更できるカスタムアイテムです。

多くの初心者開発者が陥りやすい間違いとして、シーン内のアイテムが変更されたときに無闇にresetCachedContent()を呼び出すことがあります。これはほとんどの場合、過剰な処理であり、パフォーマンスを損ないます。

// これは通常、間違った使い方です!
// (上記例のMainWindowのイベントハンドラを想像してください)

void on_item_moved_or_changed() {
    // ... アイテムの位置やプロパティを変更 ...

    // 間違いやすい使い方:
    // view->resetCachedContent(); // これは通常必要ありません!
    // view->viewport()->update(); // これも通常必要ありません!

    // 正しい使い方:
    // item->update(); // 変更されたアイテムの再描画を要求
    // または
    // scene->update(item->boundingRect()); // シーンの特定の領域の再描画を要求
}

QGraphicsView::resetCachedContent()は、QGraphicsViewが内部的に保持している描画キャッシュを強制的にリセットする機能です。これは、ビューの背景(特にカスタム描画している場合)や、ビューがアイテムの変更を正しく認識せず描画がおかしくなった特殊なケースにおいて、最後の手段として使用します。



以下に、resetCachedContent() の主な代替手段と、それぞれの使用例、そしてなぜそれらが推奨されるのかを説明します。

QGraphicsItem::update()

  • 使用例
    • アイテムの色やプロパティが変更されたとき。
    • アイテムがアニメーションで動いているとき(タイマーなどと組み合わせて)。
  • なぜ推奨されるか
    • 効率性
      変更されたアイテムの領域のみが再描画されるため、不要な描画処理を削減できます。
    • 粒度
      アイテムレベルでの更新が可能です。
  • 説明
    最も一般的で推奨される方法です。特定の QGraphicsItem の描画内容が変更された場合に、そのアイテムの描画領域のみを再描画するようにビューに要求します。
// 例: アイテムの色を変更し、再描画を要求する
class MyRectItem : public QGraphicsRectItem {
public:
    MyRectItem(const QRectF& rect) : QGraphicsRectItem(rect) {
        setBrush(Qt::red);
    }

    void changeColor() {
        if (brush().color() == Qt::red) {
            setBrush(Qt::blue);
        } else {
            setBrush(Qt::red);
        }
        update(); // このアイテムの描画領域を更新
    }
};

// ...
MyRectItem* item = new MyRectItem(QRectF(0, 0, 100, 100));
scene->addItem(item);

// ボタンクリックなどで色を変える
connect(button, &QPushButton::clicked, item, &MyRectItem::changeColor);

QGraphicsItem::prepareGeometryChange() + QGraphicsItem::update()

  • 使用例
    • アイテムのサイズが変更されたとき。
    • カスタムアイテムのboundingRect()shape()が変更される可能性があるとき。
  • なぜ推奨されるか
    • 正確性
      ジオメトリの変更によって生じる可能性のある描画の残像や欠落を防ぎます。
    • 効率性
      変更されたアイテムの領域に限定した再描画を維持します。
  • 説明
    QGraphicsItem のジオメトリ(形状やサイズ)が変更される可能性がある場合に、変更前にこのメソッドを呼び出します。これにより、Qtはアイテムの古いジオメトリと新しいジオメトリの両方を考慮し、描画キャッシュを適切に無効化して、アーティファクトが発生しないようにします。その後、update() を呼び出して再描画を要求します。
// 例: アイテムのサイズを変更する
class MyResizableRectItem : public QGraphicsRectItem {
public:
    MyResizableRectItem(const QRectF& rect) : QGraphicsRectItem(rect) {}

    void resizeTo(qreal newWidth, qreal newHeight) {
        prepareGeometryChange(); // ジオメトリが変更されることを通知
        setRect(0, 0, newWidth, newHeight);
        update(); // 新しいジオメトリで再描画を要求
    }
};

// ...
MyResizableRectItem* item = new MyResizableRectItem(QRectF(0, 0, 50, 50));
scene->addItem(item);

// ボタンクリックなどでサイズを変える
connect(button, &QPushButton::clicked, [item]() {
    item->resizeTo(100, 100);
});

QGraphicsScene::update()

  • 使用例
    • 複数のアイテムが同時に変更され、それらが互いに近い位置にある場合。
    • シーンの背景が、QGraphicsScene::drawBackground() をオーバーライドしていない方法で動的に変更された場合(ただし、QGraphicsView::drawBackground()をオーバーライドしている場合はresetCachedContent()の方が適切な場合もあります)。
    • グリッド線など、シーン全体にわたる描画内容が変更された場合。
  • なぜ推奨されるか
    • 柔軟性
      複数のアイテムが影響を受ける場合や、特定の背景領域を更新したい場合に便利です。
    • 効率性
      特定の領域を指定することで、無駄な再描画を避けることができます。
  • 説明
    シーン全体、またはシーン内の特定の矩形領域を再描画するように要求します。引数なしで呼び出すとシーン全体が更新されますが、QRectF を引数に渡すことで、その領域のみを更新するように指定できます。
// 例: シーンの特定の領域を更新する
// (ここでは、中心の四角形の領域を更新する例)
QGraphicsScene* scene = new QGraphicsScene();
scene->addRect(-50, -50, 100, 100, QPen(Qt::blue), Qt::cyan); // 中心に四角形

// ボタンクリックで中心の四角形を含む領域を更新
connect(button, &QPushButton::clicked, [scene]() {
    // 中心(0,0)からX方向Y方向それぞれ100の範囲を更新
    scene->update(QRectF(-100, -100, 200, 200));
});

// 例: シーン全体を更新する(あまり推奨されないが、必要に応じて)
// scene->update();

QGraphicsView::viewport()->update() または QGraphicsView::viewport()->repaint()

  • 使用例
    • ビューポート全体を更新する必要がある場合(例: ビューのサイズ変更イベントなど)。
    • resetCachedContent()を呼び出した後、すぐに描画を反映させたい場合(ただし、resetCachedContent()はキャッシュのリセットであり、update()repaint()と組み合わせて使うことが多いです)。
  • 説明
    QGraphicsView が持つビューポート(QWidget を継承したクラス)の再描画を要求します。update() はイベントループに再描画イベントをキューイングし、repaint() は即座に再描画を実行します。通常はupdate()が推奨されます。
// 例: ビューポート全体を更新
QGraphicsView* view = new QGraphicsView(scene);

connect(button, &QPushButton::clicked, [view]() {
    view->viewport()->update(); // ビューポートの再描画を要求
});
  • キャッシュモード
    QGraphicsView::setCacheMode() で設定されたキャッシュモードも、これらの更新メソッドの挙動に影響を与えます。例えば、QGraphicsView::CacheBackground が設定されている場合、背景を変更した際には resetCachedContent() が必要になることがあります。
  • パフォーマンス
    resetCachedContent() や引数なしの QGraphicsScene::update()viewport()->update() は描画コストが高くなる傾向があります。できるだけ粒度の細かい更新方法を選びましょう。
  • 変更の範囲
    複数のアイテムやシーンの一部が影響を受けるなら QGraphicsScene::update(QRectF)
  • 変更の粒度
    変更が特定のアイテムに限定されるなら QGraphicsItem::update()。ジオメトリも変わるなら prepareGeometryChange()