QGraphicsScene stickyFocus の落とし穴!回避策とベストプラクティス

2025-04-26

基本的な概念

  • スティッキーフォーカス (Sticky Focus)
    スティッキーフォーカスが有効な場合、あるアイテムがフォーカスを得ると、そのアイテムが明示的にフォーカスを失うまで、または別のアイテムが強制的にフォーカスを得るまで、フォーカスを保持し続けます。つまり、マウスで別のアイテムをクリックしても、フォーカスを持っていたアイテムがフォーカスを失いません。
  • フォーカス (Focus)
    GUI アプリケーションにおいて、キーボード入力などを受け付ける状態を指します。グラフィックスシーンでは、特定の QGraphicsItem がフォーカスを持つことができます。

詳細な説明

QGraphicsScene::stickyFocus プロパティは、ブール値 (true または false) を持ちます。

  • stickyFocus が false の場合 (デフォルト)
    • あるアイテムがフォーカスを得ると、ユーザーが別のアイテムをクリックしたり、シーン外をクリックしたりすると、そのアイテムはフォーカスを失います。
    • 標準的なGUIアプリケーションのフォーカス動作に近いです。
  • stickyFocus が true の場合
    • あるアイテムがフォーカスを得ると、そのアイテムは、ユーザーが別のアイテムをクリックしたり、シーン外をクリックしたりしても、フォーカスを保持します。
    • フォーカスを失うためには、QGraphicsItem::clearFocus() を明示的に呼び出すか、別のアイテムが QGraphicsItem::setFocus() を呼び出して強制的にフォーカスを得る必要があります。
    • これにより、ユーザーが特定のアイテムに集中して作業を続ける必要がある場合に便利です。

使用例

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>

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

  QGraphicsScene scene;
  scene.setSceneRect(0, 0, 400, 300);

  QGraphicsRectItem *rect1 = scene.addRect(50, 50, 100, 50);
  rect1->setFlag(QGraphicsItem::ItemIsFocusable);
  rect1->setFocus();

  QGraphicsRectItem *rect2 = scene.addRect(200, 100, 100, 50);
  rect2->setFlag(QGraphicsItem::ItemIsFocusable);

  QGraphicsView view(&scene);
  view.show();

  // スティッキーフォーカスを有効にする
  scene.setStickyFocus(true);

  return app.exec();
}

上記の例では、scene.setStickyFocus(true); を呼び出すことでスティッキーフォーカスを有効にしています。この状態で、rect1 が初期状態でフォーカスを持っています。rect2 をクリックしても、rect1 はフォーカスを保持します。



一般的なエラーとトラブルシューティング

  1. 意図しないフォーカス保持
    • 原因
      stickyFocustrue に設定されているにもかかわらず、アイテムが意図せずフォーカスを保持し続ける。
    • トラブルシューティング
      • QGraphicsItem::clearFocus() が必要な場所で適切に呼び出されているか確認します。
      • 他のアイテムが誤って QGraphicsItem::setFocus() を呼び出していないか確認します。
      • イベントハンドラー (mousePressEvent など) でフォーカスが予期せず設定されていないか確認します。
      • QGraphicsScene::focusItem()を用いて、現在どのアイテムがフォーカスを持っているか確認します。
  2. フォーカスが移動しない
    • 原因
      stickyFocustrue に設定されており、意図的にフォーカスを移動させようとしても移動しない。
    • トラブルシューティング
      • フォーカスを移動させたいアイテムで QGraphicsItem::setFocus() が呼び出されているか確認します。
      • 他のアイテムがフォーカスをクリアする前に、再度フォーカスを設定していないか確認します。
      • フォーカスを受け取るアイテムの QGraphicsItem::ItemIsFocusable フラグが設定されていることを確認します。
  3. フォーカス関連のイベントが期待通りに発生しない
    • 原因
      フォーカスが変更された際に発生する focusInEventfocusOutEvent が期待通りに発生しない。
    • トラブルシューティング
      • QGraphicsItem::setFlag(QGraphicsItem::ItemIsFocusable) が設定されているか確認します。
      • イベントハンドラーが正しく実装されているか確認します。
      • シーンまたはビューのイベントフィルターがフォーカスイベントを妨害していないか確認します。
  4. 複数のアイテムが同時にフォーカスを持つ
    • 原因
      通常、QGraphicsScene では同時に複数のアイテムがフォーカスを持つことはありませんが、誤った実装により発生する可能性があります。
    • トラブルシューティング
      • フォーカスの設定とクリアを管理するコードを確認し、競合がないか確認します。
      • カスタムのフォーカス管理ロジックが意図しない動作を引き起こしていないか確認します。
  5. ビューのフォーカスとの競合
    • 原因
      QGraphicsView 自体もフォーカスを持つことがあり、シーンのフォーカスと競合する可能性があります。
    • トラブルシューティング
      • ビューのフォーカス動作 (setFocusPolicy など) を確認し、シーンのフォーカスと適切に連携するように調整します。
      • ビューのフォーカスが不要な場合は、Qt::NoFocus などを設定します。
  6. デバッグのヒント
    • QGraphicsScene::focusItem()
      現在フォーカスを持っているアイテムを特定するために使用します。
    • qDebug()
      フォーカス関連のイベントや変数の値をログ出力して、動作を追跡します。
    • イベントフィルター
      シーンまたはビューにイベントフィルターを設定して、フォーカス関連のイベントを監視し、デバッグします。
// 例:意図しないフォーカス保持のデバッグ
void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
  QGraphicsItem::mousePressEvent(event);
  setFocus(); // ここで予期せずフォーカスを設定している可能性がある

  // デバッグ用ログ
  qDebug() << "Item clicked, focus set.";

  // ...
}

// 例:フォーカスが移動しない場合のデバッグ
void MyScene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
  QGraphicsScene::mousePressEvent(event);

  QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
  if (item && item->isFocusable()) {
    item->setFocus();
    qDebug() << "Focus set to item:" << item;
  }
  qDebug() << "current focus item:" << focusItem();
}


例1: スティッキーフォーカスを有効にする基本的な例

この例では、QGraphicsScene 上に2つの矩形アイテムを作成し、stickyFocus を有効にして、最初の矩形がフォーカスを保持する様子を示します。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QDebug>

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

    QGraphicsScene scene;
    scene.setSceneRect(0, 0, 400, 300);

    QGraphicsRectItem *rect1 = scene.addRect(50, 50, 100, 50);
    rect1->setFlag(QGraphicsItem::ItemIsFocusable);
    rect1->setFocus(); // 最初の矩形に初期フォーカスを設定

    QGraphicsRectItem *rect2 = scene.addRect(200, 100, 100, 50);
    rect2->setFlag(QGraphicsItem::ItemIsFocusable);

    QGraphicsView view(&scene);
    view.show();

    // スティッキーフォーカスを有効にする
    scene.setStickyFocus(true);

    // デバッグ用:現在のフォーカスアイテムを表示
    qDebug() << "Initial focus item:" << scene.focusItem();

    return app.exec();
}

説明

  1. 2つの QGraphicsRectItem を作成し、ItemIsFocusable フラグを設定します。
  2. rect1setFocus() を呼び出して初期フォーカスを設定します。
  3. scene.setStickyFocus(true) を呼び出して、スティッキーフォーカスを有効にします。
  4. qDebug() を使用して、初期フォーカスアイテムを表示します。
  5. この状態でアプリケーションを実行すると、rect1 がフォーカスを持ち続け、rect2 をクリックしてもフォーカスは移動しません。

例2: スティッキーフォーカスを無効にし、フォーカスを明示的に切り替える例

この例では、スティッキーフォーカスを無効にし、マウスをクリックしたアイテムにフォーカスを切り替えるようにします。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QGraphicsSceneMouseEvent>
#include <QDebug>

class MyScene : public QGraphicsScene {
public:
    MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {}

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
        QGraphicsScene::mousePressEvent(event);

        QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
        if (item && item->isFocusable()) {
            item->setFocus();
            qDebug() << "Focus set to item:" << item;
        }
        qDebug() << "current focus item:" << focusItem();
    }
};

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

    MyScene scene; // カスタムシーンを使用
    scene.setSceneRect(0, 0, 400, 300);

    QGraphicsRectItem *rect1 = scene.addRect(50, 50, 100, 50);
    rect1->setFlag(QGraphicsItem::ItemIsFocusable);
    rect1->setFocus();

    QGraphicsRectItem *rect2 = scene.addRect(200, 100, 100, 50);
    rect2->setFlag(QGraphicsItem::ItemIsFocusable);

    QGraphicsView view(&scene);
    view.show();

    // スティッキーフォーカスを無効にする
    scene.setStickyFocus(false);

    return app.exec();
}

説明

  1. QGraphicsScene を継承した MyScene クラスを作成し、mousePressEvent をオーバーライドします。
  2. mousePressEvent 内で、クリックされたアイテムがフォーカス可能であれば、そのアイテムに setFocus() を呼び出します。
  3. scene.setStickyFocus(false) を呼び出して、スティッキーフォーカスを無効にします。
  4. この状態でアプリケーションを実行すると、クリックした矩形にフォーカスが移動します。

例3: clearFocus() を使用してフォーカスを明示的にクリアする例

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QPushButton>
#include <QGraphicsProxyWidget>
#include <QVBoxLayout>
#include <QWidget>

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

    QGraphicsScene scene;
    scene.setSceneRect(0, 0, 400, 300);

    QGraphicsRectItem *rect = scene.addRect(50, 50, 100, 50);
    rect->setFlag(QGraphicsItem::ItemIsFocusable);
    rect->setFocus();

    QWidget *widget = new QWidget();
    QVBoxLayout *layout = new QVBoxLayout(widget);
    QPushButton *clearButton = new QPushButton("Clear Focus");
    layout->addWidget(clearButton);

    QGraphicsProxyWidget *proxy = scene.addWidget(widget);
    proxy->setPos(200, 100);

    QGraphicsView view(&scene);
    view.show();

    scene.setStickyFocus(true);

    QObject::connect(clearButton, &QPushButton::clicked, [rect]() {
        rect->clearFocus();
    });

    return app.exec();
}
  1. QGraphicsRectItemQPushButton を作成し、シーンに追加します。
  2. stickyFocustrue に設定します。
  3. QPushButtonclicked シグナルをラムダ式に接続し、rect->clearFocus() を呼び出して矩形のフォーカスをクリアします。
  4. ボタンをクリックすると、矩形のフォーカスがクリアされます。


カスタムフォーカス管理

stickyFocus を使用せず、独自のフォーカス管理ロジックを実装する方法です。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QGraphicsSceneMouseEvent>
#include <QDebug>

class MyScene : public QGraphicsScene {
public:
    MyScene(QObject *parent = nullptr) : QGraphicsScene(parent), currentFocusItem(nullptr) {}

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
        QGraphicsScene::mousePressEvent(event);

        QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
        if (item && item->isFocusable()) {
            setCustomFocus(item);
        }
    }

    void setCustomFocus(QGraphicsItem *item) {
        if (currentFocusItem != item) {
            if (currentFocusItem) {
                // 前のアイテムのフォーカスをクリア(必要に応じて)
                // currentFocusItem->clearFocus();
                qDebug() << "Focus lost from item:" << currentFocusItem;
            }
            currentFocusItem = item;
            item->setFocus();
            qDebug() << "Focus set to item:" << item;
        }
    }

private:
    QGraphicsItem *currentFocusItem;
};

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

    MyScene scene;
    scene.setSceneRect(0, 0, 400, 300);

    QGraphicsRectItem *rect1 = scene.addRect(50, 50, 100, 50);
    rect1->setFlag(QGraphicsItem::ItemIsFocusable);
    scene.setCustomFocus(rect1); // 初期フォーカスを設定

    QGraphicsRectItem *rect2 = scene.addRect(200, 100, 100, 50);
    rect2->setFlag(QGraphicsItem::ItemIsFocusable);

    QGraphicsView view(&scene);
    view.show();

    return app.exec();
}

説明

  1. MyScene クラスで currentFocusItem を保持し、現在のフォーカスアイテムを追跡します。
  2. setCustomFocus() メソッドで、フォーカスを切り替えるロジックを実装します。
  3. mousePressEvent() で、クリックされたアイテムに setCustomFocus() を呼び出します。
  4. これにより、stickyFocus を使用せずに、独自のフォーカス制御を実現できます。

イベントフィルターの使用

イベントフィルターを使用して、フォーカス関連のイベントを監視し、特定の条件でフォーカスを制御する方法です。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QEvent>
#include <QDebug>

class FocusFilter : public QObject {
public:
    FocusFilter(QGraphicsScene *scene) : scene(scene) {}

protected:
    bool eventFilter(QObject *obj, QEvent *event) override {
        if (event->type() == QEvent::GraphicsSceneMousePress) {
            QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
            QGraphicsItem *item = scene->itemAt(mouseEvent->scenePos(), QTransform());
            if (item && item->isFocusable()) {
                item->setFocus();
                qDebug() << "Focus set to item:" << item;
                return true; // イベントを処理済みとして扱う
            }
        }
        return QObject::eventFilter(obj, event);
    }

private:
    QGraphicsScene *scene;
};

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

    QGraphicsScene scene;
    scene.setSceneRect(0, 0, 400, 300);

    QGraphicsRectItem *rect1 = scene.addRect(50, 50, 100, 50);
    rect1->setFlag(QGraphicsItem::ItemIsFocusable);
    rect1->setFocus();

    QGraphicsRectItem *rect2 = scene.addRect(200, 100, 100, 50);
    rect2->setFlag(QGraphicsItem::ItemIsFocusable);

    QGraphicsView view(&scene);
    view.show();

    FocusFilter *filter = new FocusFilter(&scene);
    scene.installEventFilter(filter);

    return app.exec();
}

説明

  1. FocusFilter クラスを作成し、eventFilter() をオーバーライドします。
  2. eventFilter() で、QEvent::GraphicsSceneMousePress イベントを監視し、クリックされたアイテムにフォーカスを設定します。
  3. scene.installEventFilter(filter) で、シーンにイベントフィルターをインストールします。
  4. これにより、マウスイベントを監視してフォーカスを制御できます。

ステートマシンを使用

複雑なフォーカス制御が必要な場合、ステートマシンを使用してフォーカスの状態を管理する方法があります。

// (ステートマシンライブラリを使用するなど、複雑なためコードは省略)
  1. フォーカスの状態を定義します(例:NO_FOCUS, ITEM1_FOCUSED, ITEM2_FOCUSED)。
  2. 状態遷移を定義します(例:アイテムクリックで状態遷移)。
  3. 状態に応じてフォーカスを設定・クリアします。
  4. 複雑なフォーカス制御に適していますが、実装が複雑になる可能性があります。