Flickable.resizeContent()だけじゃない!Qt/QMLでのコンテンツ拡大縮小テクニック

2025-05-27

Flickable.resizeContent() とは

Flickable.resizeContent(real width, real height, QPointF center) は、Qt Quickの Flickable コンポーネントが内部に持つ「コンテンツ領域」のサイズを変更するためのメソッドです。

Flickable は、画像や長いテキストなど、表示領域よりも大きなコンテンツをスクロールして見せるためのQML要素です。ユーザーが画面をドラッグしたりフリックしたりすることで、この「コンテンツ領域」を移動させ、その一部を Flickable の表示領域(ビューポート)に表示します。

contentWidthcontentHeight プロパティもコンテンツのサイズを設定するために使われますが、resizeContent() メソッドは、それに加えてサイズ変更の基準となる中心点を指定できる点が特徴です。

パラメータ

  1. width: コンテンツの新しい幅(real型)。

  2. height: コンテンツの新しい高さ(real型)。

  3. center: サイズ変更の基準となる点(QPointF型)。この点が、新しいサイズに拡大または縮小された後も、コンテンツ内の同じ相対位置に留まるように調整されます。

    • たとえば、画像をピンチズームで拡大・縮小する際に、指で触れている箇所を中心にズームしたい場合にこの center パラメータが非常に役立ちます。マウスカーソルの位置や、複数のタッチ操作の中心点を center として渡すことで、ユーザーの意図に沿った自然なズーム動作を実現できます。
    • Qt.point(0,0) を指定すると、コンテンツの左上隅を基準にサイズ変更が行われます。

resizeContent() の主な用途

resizeContent() は主に以下のようなシナリオで利用されます。

  • 特定の基準点でのコンテンツの拡大・縮小: マウスホイールのスクロールイベントなどと組み合わせて、マウスカーソルが置かれている場所を中心にコンテンツを拡大・縮小するといった、よりインタラクティブな動作を実装する際に役立ちます。
  • 動的なコンテンツサイズの変更: Flickable 内のコンテンツが動的に追加・削除されたり、そのサイズがプログラムによって変更されたりする場合に、コンテンツ全体のサイズを適切に更新するために使われます。
  • ピンチズーム機能の実装: スマートフォンアプリなどで、画像をピンチ操作で拡大・縮小する際に、ユーザーが指を置いた位置を中心に画像を拡大・縮小し、かつその拡大・縮小されたコンテンツを Flickable 内で自由にスクロールできるようにするために使用されます。

contentWidth/contentHeight との違い

contentWidthcontentHeight プロパティは、コンテンツの論理的なサイズを直接設定します。これらのプロパティを変更すると、Flickable はコンテンツの新しいサイズに合わせて表示を調整しますが、デフォルトではコンテンツの左上隅(0,0)が基準となります。

一方、resizeContent() は、新しいサイズとともにどの点を中心にサイズ変更を行うかを指定できるため、より柔軟なコンテンツの変形と表示位置の調整が可能になります。

例えば、PinchArea と組み合わせてピンチズームを実装する際に resizeContent() を使うコードは以下のようになります(概念的な例です)。

import QtQuick 2.0

Flickable {
    id: flickable
    width: 400
    height: 300
    clip: true // Flickableの範囲外はクリップする

    // 大きな画像コンテンツ
    Image {
        id: image
        source: "bigImage.png"
        // contentWidth/contentHeightはImageの現在のサイズに連動させる
        // ただし、resizeContentが呼ばれた際に、contentWidth/contentHeightも更新される必要がある
    }

    PinchArea {
        anchors.fill: parent
        onPinchStarted: {
            // ピンチ開始時のコンテンツサイズを保存
            flickable.initialContentWidth = flickable.contentWidth
            flickable.initialContentHeight = flickable.contentHeight
        }
        onPinchUpdated: {
            // ピンチ操作の中心点を取得 (Flickableの座標系にマップ)
            var centerPoint = Qt.point(pinch.center.x - flickable.x, pinch.center.y - flickable.y);

            // 新しいコンテンツサイズを計算
            var newWidth = flickable.initialContentWidth * pinch.scale;
            var newHeight = flickable.initialContentHeight * pinch.scale;

            // resizeContentを呼び出し、指定した中心を中心にコンテンツサイズを変更
            flickable.resizeContent(newWidth, newHeight, centerPoint);
        }
        onPinchFinished: {
            // ピンチ終了後、コンテンツがビューポート内に収まるように調整する(オプション)
            flickable.returnToBounds();
        }
    }

    // ImageのサイズをFlickableのcontentWidth/contentHeightにバインド
    // これにより、resizeContent()が呼ばれた際にImageのサイズも自動的に変更される
    // (または、Image自体をcontentItemの子として直接resizeContentでサイズ変更する)
    Component.onCompleted: {
        // 初期コンテンツサイズを設定
        flickable.contentWidth = image.width
        flickable.contentHeight = image.height
    }
}


コンテンツが期待通りにズーム/移動しない

  • 原因と解決策:
    • 座標系の理解不足: resizeContent()center 引数は、Flickable のコンテンツ座標系での点を期待します。これは、Flickable 自体のローカル座標系や、親要素の座標系とは異なる場合がほとんどです。特に PinchArea などからピンチの中心点を取得する場合、その点は PinchArea のローカル座標なので、FlickablecontentXcontentY を考慮してコンテンツ座標に変換する必要があります。
      // PinchArea の中心点を Flickable のコンテンツ座標に変換する例
      var centerPoint = Qt.point(pinch.center.x - flickable.x + flickable.contentX,
                                 pinch.center.y - flickable.y + flickable.contentY);
      flickable.resizeContent(newWidth, newHeight, centerPoint);
      
      flickable.xflickable.y は、Flickable 自体が親の中でどこに配置されているかを示すプロパティです。これを引いて、flickable.contentXflickable.contentY を足すことで、Flickable のコンテンツ領域の原点からの相対的な位置に変換できます。
    • clip: true の設定忘れ: Flickable はデフォルトでは clip プロパティが false です。この場合、コンテンツが Flickable の表示領域をはみ出してもクリップされずに表示されてしまいます。ズームによってコンテンツが巨大化する場合、clip: true を設定しないと見た目が崩れることがあります。
      Flickable {
          id: flickable
          clip: true // これを忘れずに
          // ...
      }
      
    • contentWidth/contentHeight との競合: resizeContent() を使う場合、通常は contentWidthcontentHeight プロパティを直接バインドするのではなく、resizeContent() メソッド内でそれらが適切に更新されるように設計します。例えば、Imagewidth/heightFlickablecontentWidth/contentHeight をバインドしていると、resizeContent() でサイズを変更した際に、Imageの暗黙的なサイズ変更により予期せぬ挙動になることがあります。代わりに、resizeContent() を呼び出す際に、コンテンツのサイズを直接設定するか、コンテンツの scale プロパティなどを操作することを検討してください。
  • 問題: resizeContent() を呼び出しても、コンテンツが期待通りの位置を中心に拡大・縮小されなかったり、Flickable の範囲外に飛び出してしまったりする。

コンテンツがバウンドしない、またはバウンドがおかしい

  • 原因と解決策:
    • returnToBounds() の呼び出し忘れ: resizeContent() でコンテンツサイズを変更した後、Flickable がその新しいコンテンツサイズに合わせて現在の表示位置を調整し、境界内に収めるために returnToBounds() を呼び出すことが推奨されます。特にズームアウトしてコンテンツが小さくなった場合、ビューポート内に収まるように自動調整されます。
      onPinchUpdated: {
          // ...
          flickable.resizeContent(newWidth, newHeight, centerPoint);
          flickable.returnToBounds(); // これを追加
      }
      
    • boundsBehavior の設定: FlickableboundsBehavior プロパティが、コンテンツが境界を超えて移動できるかどうかを制御します。デフォルトは Flickable.DragAndOvershootBounds です。コンテンツを常に境界内に留めたい場合は Flickable.StopAtBounds を設定することを検討してください。
      Flickable {
          boundsBehavior: Flickable.StopAtBounds
          // ...
      }
      
  • 問題: resizeContent() でサイズを変更した後、フリックしてもコンテンツがFlickableの端で止まらずに無限にスクロールしたり、逆にすぐに止まってしまったりする。

複数のズーム操作で位置がずれる

  • 原因と解決策:
    • 浮動小数点数の精度問題: resizeContent() で計算されるサイズや位置は浮動小数点数 (real) であり、連続する計算によってわずかな誤差が累積し、見た目のずれとして現れることがあります。
    • 対策:
      • 可能な限り、基準となるコンテンツのサイズや位置を整数に丸める (Math.round())。
      • ズーム操作の開始時に、現在のコンテンツの正確な状態(幅、高さ、contentX, contentY)を保存し、それらを基準に新しい計算を行うことで、累積誤差を減らすことができます。
  • 問題: ズームイン/アウトを繰り返すと、ズームの中心点が徐々にずれていく。

Flickable 内のアイテムがアンカーできない

  • 原因と解決策:
    • Flickable の内部構造: Flickable の子アイテムは、直接 Flickable の親ではなく、Flickable の内部にある contentItem という非表示のアイテムの子として配置されます。したがって、Flickable の子アイテムを Flickable の領域にアンカーしたい場合は、parent ではなく FlickablecontentItem を参照する必要があります。
      Flickable {
          id: flickable
          width: 400
          height: 300
      
          // contentItem の子として配置される
          Rectangle {
              width: 1000
              height: 1000
              color: "lightblue"
              // Flickable のコンテンツ領域にアンカーしたい場合
              // anchors.fill: flickable.contentItem // または parent
          }
      }
      
      resizeContent() と直接的な関連は少ないですが、Flickable のコンテンツを正しくレイアウトする上で重要な点です。
  • 問題: Flickable の子アイテムを Flickable 自体に anchors.fill: parent のようにアンカーしようとすると、期待通りに動作しない。

画像のロード完了前に resizeContent() を呼び出す

  • 原因と解決策:
    • 画像のロード状態: Image アイテムは非同期で画像をロードします。画像がロードされる前にそのサイズにアクセスすると、デフォルト値(通常は0)が返されます。
    • Image.onStatusChanged の利用: Imagestatus プロパティが Image.Ready になったことを検出してから、画像のサイズにアクセスして resizeContent() を呼び出すようにします。
      Flickable {
          id: flickable
          // ...
          Image {
              id: image
              source: "bigImage.png"
              onStatusChanged: {
                  if (status === Image.Ready) {
                      // 画像がロードされた後にコンテンツサイズを設定する
                      flickable.resizeContent(image.width, image.height, Qt.point(0,0));
                      flickable.returnToBounds();
                  }
              }
          }
      }
      
  • 問題: Imagesource を設定した直後に resizeContent() を呼び出すと、画像がまだロードされておらず、image.widthimage.height が0になっているため、期待通りにサイズ変更されない。
  • clip: true の確認: コンテンツがFlickableの表示領域からはみ出してしまう場合は、clip: true が設定されているかを確認してください。
  • returnToBounds() の適用: resizeContent() を呼び出した後は、ほとんどの場合 returnToBounds() を呼び出すことで、コンテンツがFlickableの境界内に適切に収まるように調整されます。
  • console.log() を活用: デバッグ時には、width, height, center の各パラメータが期待通りの値になっているかを console.log() で出力して確認することが非常に有効です。flickable.contentXflickable.contentY の値も合わせて確認すると、コンテンツの現在の位置関係を把握しやすくなります。
  • 座標系を明確に: resizeContent() に渡す center の座標系は、Flickableのコンテンツ座標系であることを常に意識してください。他の要素(MouseArea, PinchArea など)から取得した座標は、適切に変換する必要があります。


マウスホイールによる画像ズームの例

この例では、Flickable 内の画像をマウスホイールでズームイン/アウトします。ズームはマウスカーソルの位置を中心に実行されます。

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 800
    height: 600
    visible: true
    title: "Flickable Image Zoom with Mouse Wheel"

    Flickable {
        id: flickable
        anchors.fill: parent
        clip: true // コンテンツがFlickableの範囲外に出ないようにクリップする
        flickableDirection: Flickable.HorizontalAndVerticalFlick // 縦横フリックを許可

        // 初期ズームレベル
        property real currentZoom: 1.0
        // 最大・最小ズームレベル
        property real maxZoom: 5.0
        property real minZoom: 0.2

        // Flickable内に表示する画像
        Image {
            id: image
            source: "https://picsum.photos/1200/800" // 大きめの画像URLを適宜変更してください
            // 画像がロードされたら、Flickableのコンテンツサイズを初期化
            onStatusChanged: {
                if (status === Image.Ready) {
                    // 画像の元々のサイズをコンテンツサイズとする
                    flickable.contentWidth = image.implicitWidth
                    flickable.contentHeight = image.implicitHeight
                    // Imageのサイズをコンテンツサイズに合わせる (ズーム時にはImageのscaleプロパティを使うため、ここは基本サイズ)
                    width: image.implicitWidth
                    height: image.implicitHeight
                    // 初期表示位置を中央に
                    flickable.contentX = (flickable.contentWidth - flickable.width) / 2
                    flickable.contentY = (flickable.contentHeight - flickable.height) / 2
                }
            }
            // 画像のスケールはFlickableのcurrentZoomに連動させる
            scale: flickable.currentZoom
            // ズームの中心をImageの中心にする(FlickableのresizeContentで位置調整するため)
            transformOrigin: Item.TopLeft
        }

        MouseArea {
            anchors.fill: parent
            hoverEnabled: true // マウスホイールイベントのために必要
            onWheel: (mouse) => {
                // Ctrlキーが押されている場合にズーム
                if (mouse.modifiers & Qt.ControlModifier) {
                    var oldZoom = flickable.currentZoom;
                    var zoomFactor = 1.1; // ズーム倍率

                    if (mouse.angleDelta.y > 0) {
                        // ホイール上方向(ズームイン)
                        flickable.currentZoom *= zoomFactor;
                    } else {
                        // ホイール下方向(ズームアウト)
                        flickable.currentZoom /= zoomFactor;
                    }

                    // ズームレベルを制限
                    flickable.currentZoom = Math.max(flickable.minZoom, Math.min(flickable.maxZoom, flickable.currentZoom));

                    // 新しいコンテンツサイズを計算
                    var newContentWidth = image.implicitWidth * flickable.currentZoom;
                    var newContentHeight = image.implicitHeight * flickable.currentZoom;

                    // マウスカーソル位置をFlickableのコンテンツ座標に変換
                    // mouse.x, mouse.y はMouseAreaのローカル座標
                    // flickable.contentX, flickable.contentY はFlickableのコンテンツが左上隅からどれだけオフセットしているか
                    var mouseContentX = mouse.x + flickable.contentX;
                    var mouseContentY = mouse.y + flickable.contentY;

                    // resizeContentを呼び出して、マウスカーソルを中心にコンテンツサイズを変更
                    // このメソッドがコンテンツのオフセット(contentX/contentY)も調整してくれる
                    flickable.resizeContent(newContentWidth, newContentHeight, Qt.point(mouseContentX, mouseContentY));

                    // ズーム後にコンテンツが境界内に収まるように調整
                    flickable.returnToBounds();

                    // ホイールスクロールを無効にする(ズーム操作に専念させるため)
                    mouse.accepted = true;
                }
            }
        }
    }
}

解説

  1. Flickable の設定: clip: true でコンテンツがはみ出さないようにし、flickableDirection で縦横フリックを有効にします。
  2. Image: Flickable の子として配置され、その scale プロパティを flickable.currentZoom にバインドします。transformOriginItem.TopLeft に設定することで、resizeContent() が位置調整に集中できるようにします。
  3. MouseArea: Flickable 全体を覆い、マウスホイールイベント (onWheel) を捕捉します。
  4. ズームロジック:
    • Ctrl キーが押されている場合にズーム操作を有効にします。
    • currentZoom プロパティを更新し、ズームレベルを制限します。
    • resizeContent() のための座標変換: mouse.xmouse.yMouseArea のローカル座標です。これを Flickable のコンテンツ座標系に変換するために、現在の flickable.contentXflickable.contentY を加算します。この変換された点が resizeContent()center 引数に渡されます。
    • flickable.resizeContent(newContentWidth, newContentHeight, Qt.point(mouseContentX, mouseContentY)) を呼び出し、コンテンツの新しいサイズとズームの中心点を指定します。
    • flickable.returnToBounds() は、サイズ変更後にコンテンツがFlickableの表示領域内に収まるように調整する重要なメソッドです。

ピンチズーム(タッチデバイス向け)の例

タッチデバイスでのピンチジェスチャーを使ったズームの例です。

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15 // コンソール出力のために使用

Window {
    width: 800
    height: 600
    visible: true
    title: "Flickable Pinch Zoom"

    Flickable {
        id: flickable
        anchors.fill: parent
        clip: true
        flickableDirection: Flickable.HorizontalAndVerticalFlick

        property real initialContentWidth: image.implicitWidth
        property real initialContentHeight: image.implicitHeight

        Image {
            id: image
            source: "https://picsum.photos/1200/800" // 大きめの画像URLを適宜変更してください
            // ImageのサイズはFlickableのコンテンツサイズと連動させる
            // resizeContentがcontentWidth/contentHeightを更新すると、Imageのサイズも自動的に変更される
            width: flickable.contentWidth
            height: flickable.contentHeight
            antialiasing: true // ズーム時の画像の品質向上
        }

        PinchArea {
            anchors.fill: parent
            // ピンチ開始時のコンテンツサイズを保存
            property real storedContentWidth: 0
            property real storedContentHeight: 0

            onPinchStarted: {
                storedContentWidth = flickable.contentWidth
                storedContentHeight = flickable.contentHeight
                console.log("Pinch Started - Initial Content Size:", storedContentWidth, "x", storedContentHeight);
            }

            onPinchUpdated: {
                // ピンチのスケールファクタに基づいて新しいコンテンツサイズを計算
                var newWidth = storedContentWidth * pinch.scale;
                var newHeight = storedContentHeight * pinch.scale;

                // ズームレベルの制限(オプション)
                // 例: オリジナルサイズの0.5倍から5倍まで
                var minZoom = image.implicitWidth * 0.5;
                var maxZoom = image.implicitWidth * 5.0;
                newWidth = Math.max(minZoom, Math.min(maxZoom, newWidth));
                newHeight = newWidth * (image.implicitHeight / image.implicitWidth); // アスペクト比を維持

                // ピンチの中心点 (Flickableのコンテンツ座標系に変換)
                // pinch.center は PinchArea のローカル座標
                // flickable.contentX, flickable.contentY は Flickable のコンテンツオフセット
                var pinchContentX = pinch.center.x + flickable.contentX;
                var pinchContentY = pinch.center.y + flickable.contentY;

                // resizeContentを呼び出して、ピンチの中心を中心にコンテンツサイズを変更
                flickable.resizeContent(newWidth, newHeight, Qt.point(pinchContentX, pinchContentY));

                // ズーム後にコンテンツが境界内に収まるように調整
                flickable.returnToBounds();

                // ログ出力 (デバッグ用)
                console.log("Pinch Updated - New Size:", newWidth, "x", newHeight, "Center:", pinchContentX, ",", pinchContentY);
            }

            onPinchFinished: {
                // ピンチ終了後も境界内に収める
                flickable.returnToBounds();
                console.log("Pinch Finished");
            }
        }

        // 初期ロード時にImageのサイズに合わせてFlickableのコンテンツサイズを設定
        Component.onCompleted: {
            if (image.status === Image.Ready) {
                flickable.contentWidth = image.implicitWidth;
                flickable.contentHeight = image.implicitHeight;
                // 初期表示位置を中央に
                flickable.contentX = (flickable.contentWidth - flickable.width) / 2;
                flickable.contentY = (flickable.contentHeight - flickable.height) / 2;
            } else {
                // 画像がまだロードされていない場合は、ロード完了イベントを待つ
                image.onStatusChanged.connect(function() {
                    if (image.status === Image.Ready) {
                        flickable.contentWidth = image.implicitWidth;
                        flickable.contentHeight = image.implicitHeight;
                        flickable.contentX = (flickable.contentWidth - flickable.width) / 2;
                        flickable.contentY = (flickable.contentHeight - flickable.height) / 2;
                    }
                });
            }
        }
    }
}

解説

  1. Image のサイズ: Imagewidthheightflickable.contentWidthflickable.contentHeight にバインドしています。これにより、resizeContent()Flickable のコンテンツサイズを変更すると、Image の表示サイズも自動的に更新されます。
  2. PinchArea: MultiPointTouchArea の一種で、ピンチジェスチャーを検出します。
  3. onPinchStarted: ピンチ操作が開始されたときに、現在の Flickable のコンテンツサイズを storedContentWidth/Height に保存します。これにより、ピンチ中のスケール計算の基準となります。
  4. onPinchUpdated: ピンチ操作中に継続的に呼び出されます。
    • pinch.scale (現在のピンチのスケールファクタ) を利用して新しいコンテンツサイズを計算します。
    • pinch.center (ピンチ操作の中心点) を Flickable のコンテンツ座標系に変換します。この変換は、mouse.x の例と同様に flickable.contentXflickable.contentY を加算することで行われます。
    • flickable.resizeContent() を呼び出し、新しいサイズと中心点を指定します。
    • flickable.returnToBounds() で境界内に収めます。

この例では、ボタンをクリックすると、Flickable のコンテンツが特定の座標(例: コンテンツの中心)を中心にズームイン/アウトします。

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15

Window {
    width: 800
    height: 600
    visible: true
    title: "Flickable Zoom Button Example"

    Flickable {
        id: flickable
        anchors.fill: parent
        clip: true
        flickableDirection: Flickable.HorizontalAndVerticalFlick

        property real currentZoom: 1.0
        property real zoomStep: 0.2 // ズームの段階

        Image {
            id: image
            source: "https://picsum.photos/1200/800"
            width: flickable.contentWidth
            height: flickable.contentHeight
            antialiasing: true
        }

        // 初期ロード時の設定
        Component.onCompleted: {
            if (image.status === Image.Ready) {
                flickable.contentWidth = image.implicitWidth
                flickable.contentHeight = image.implicitHeight
                // 初期表示位置を中央に
                flickable.contentX = (flickable.contentWidth - flickable.width) / 2
                flickable.contentY = (flickable.contentHeight - flickable.height) / 2
            } else {
                image.onStatusChanged.connect(function() {
                    if (image.status === Image.Ready) {
                        flickable.contentWidth = image.implicitWidth;
                        flickable.contentHeight = image.implicitHeight;
                        flickable.contentX = (flickable.contentWidth - flickable.width) / 2;
                        flickable.contentY = (flickable.contentHeight - flickable.height) / 2;
                    }
                });
            }
        }
    }

    // ズームインボタン
    Button {
        text: "Zoom In"
        anchors.bottom: parent.bottom
        anchors.left: parent.left
        width: 100
        height: 40
        onClicked: {
            var oldZoom = flickable.currentZoom;
            flickable.currentZoom += flickable.zoomStep;
            flickable.currentZoom = Math.min(flickable.currentZoom, 3.0); // 最大ズームを3.0に制限

            var newContentWidth = image.implicitWidth * flickable.currentZoom;
            var newContentHeight = image.implicitHeight * flickable.currentZoom;

            // ズームの中心をFlickableのビューポート中央に設定
            var centerX = flickable.contentX + flickable.width / 2;
            var centerY = flickable.contentY + flickable.height / 2;

            flickable.resizeContent(newContentWidth, newContentHeight, Qt.point(centerX, centerY));
            flickable.returnToBounds();
        }
    }

    // ズームアウトボタン
    Button {
        text: "Zoom Out"
        anchors.bottom: parent.bottom
        anchors.left: parent.left
        anchors.leftMargin: 110
        width: 100
        height: 40
        onClicked: {
            var oldZoom = flickable.currentZoom;
            flickable.currentZoom -= flickable.zoomStep;
            flickable.currentZoom = Math.max(flickable.currentZoom, 0.5); // 最小ズームを0.5に制限

            var newContentWidth = image.implicitWidth * flickable.currentZoom;
            var newContentHeight = image.implicitHeight * flickable.currentZoom;

            // ズームの中心をFlickableのビューポート中央に設定
            var centerX = flickable.contentX + flickable.width / 2;
            var centerY = flickable.contentY + flickable.height / 2;

            flickable.resizeContent(newContentWidth, newContentHeight, Qt.point(centerX, centerY));
            flickable.returnToBounds();
        }
    }
}
  1. ズームボタン: Button を2つ配置し、クリックイベントでズーム操作を実行します。
  2. ズームロジック:
    • currentZoomzoomStep を使って、ズームレベルを段階的に増減させます。
    • この例では、ズームの中心を Flickableビューポートの中央に設定しています。これは、flickable.contentX + flickable.width / 2flickable.contentY + flickable.height / 2 で計算できます。この点を resizeContent()center 引数に渡すことで、常にビューポートの中央を中心にズームが行われます。
    • もちろん、特定の場所(例: 画像の左上隅 Qt.point(0,0))をズームの中心にすることも可能です。


resizeContent() の主な機能は、「コンテンツのサイズ変更」と「そのサイズ変更に合わせてコンテンツの表示位置を調整する」ことです。この2つの要素を個別に、または異なる方法で実現するのが代替手法です。

contentWidth / contentHeight プロパティと contentX / contentY プロパティを個別に操作する

これは最も直接的な代替方法です。Flickable のコンテンツの幅と高さを contentWidthcontentHeight プロパティで設定し、同時にコンテンツの表示位置を contentXcontentY プロパティで調整します。

ズームイン/アウトの例

コンテンツがズームされる際、そのサイズは contentWidthcontentHeight に設定され、同時に contentXcontentY を調整して、ズームの中心点がビューポート内の同じ相対位置に留まるようにします。

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 800
    height: 600
    visible: true
    title: "Flickable Manual Zoom"

    Flickable {
        id: flickable
        anchors.fill: parent
        clip: true
        flickableDirection: Flickable.HorizontalAndVerticalFlick

        property real currentZoom: 1.0 // 現在のズームレベル
        property real zoomFactor: 1.1 // ズーム倍率

        Image {
            id: image
            source: "https://picsum.photos/1200/800"
            // ImageのサイズはFlickableのコンテンツサイズにバインド
            width: flickable.contentWidth
            height: flickable.contentHeight
            antialiasing: true

            onStatusChanged: {
                if (status === Image.Ready) {
                    // 画像ロード時にFlickableのコンテンツサイズを初期化
                    flickable.contentWidth = image.implicitWidth;
                    flickable.contentHeight = image.implicitHeight;
                    // 初期表示位置を中央に
                    flickable.contentX = (flickable.contentWidth - flickable.width) / 2;
                    flickable.contentY = (flickable.contentHeight - flickable.height) / 2;
                }
            }
        }

        MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onWheel: (mouse) => {
                if (mouse.modifiers & Qt.ControlModifier) {
                    var oldContentWidth = flickable.contentWidth;
                    var oldContentHeight = flickable.contentHeight;

                    var newZoom;
                    if (mouse.angleDelta.y > 0) {
                        newZoom = flickable.currentZoom * flickable.zoomFactor; // ズームイン
                    } else {
                        newZoom = flickable.currentZoom / flickable.zoomFactor; // ズームアウト
                    }

                    // ズームレベルを制限
                    newZoom = Math.max(0.2, Math.min(5.0, newZoom));

                    // 新しいコンテンツサイズを計算
                    var newContentWidth = image.implicitWidth * newZoom;
                    var newContentHeight = image.implicitHeight * newZoom;

                    // ズームの中心となるコンテンツ座標 (マウス位置を基準)
                    var zoomCenterX = mouse.x + flickable.contentX;
                    var zoomCenterY = mouse.y + flickable.contentY;

                    // contentX/contentY を調整して、ズーム中心が同じ相対位置に留まるようにする
                    flickable.contentX = zoomCenterX - (zoomCenterX / oldContentWidth) * newContentWidth;
                    flickable.contentY = zoomCenterY - (zoomCenterY / oldContentHeight) * newContentHeight;

                    // Flickableのコンテンツサイズを更新 (Imageのwidth/heightも連動して更新される)
                    flickable.contentWidth = newContentWidth;
                    flickable.contentHeight = newContentHeight;

                    flickable.currentZoom = newZoom; // ズームレベルを更新

                    flickable.returnToBounds(); // 境界内に収める
                    mouse.accepted = true;
                }
            }
        }
    }
}

利点

  • ズームの中心計算を自分で定義できるため、柔軟性が高いです。
  • resizeContent() が内部で何をしているかをより詳細に制御できます。

欠点

  • ズーム時の contentX/contentY の計算が複雑になる傾向があります。特に、中心を正確に維持するための数学的な計算が必要になります。resizeContent() はこの計算を抽象化してくれます。

コンテンツアイテム自体を scale プロパティで変形させる

Flickable の子アイテム(コンテンツ本体)に scale プロパティを設定し、その transformOrigin を調整することで、見た目のズームを実現できます。この場合、FlickablecontentWidthcontentHeight は、スケーリングされていない元のコンテンツのサイズを設定し、contentXcontentY を使ってスケーリングされたコンテンツのビューポートを調整します。

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 800
    height: 600
    visible: true
    title: "Flickable Scale Transformation"

    Flickable {
        id: flickable
        anchors.fill: parent
        clip: true
        flickableDirection: Flickable.HorizontalAndVerticalFlick

        // Flickableのコンテンツサイズは、スケーリングされていないImageのimplicitWidth/Heightに設定
        // contentWidth/contentHeight はフリック可能な"仮想空間"のサイズを表す
        contentWidth: image.implicitWidth
        contentHeight: image.implicitHeight

        Image {
            id: image
            source: "https://picsum.photos/1200/800"
            // Image自体のサイズはimplicitWidth/Heightのまま
            // スケーリングは transform.scale を使用
            antialiasing: true

            // スケール変換を適用
            transform: Scale {
                id: imageScale
                // ズームの中心点を設定
                // ここではFlickableのビューポートの中心がImage座標系でどこにあたるかを計算
                origin.x: (flickable.contentX + flickable.width / 2) / flickable.contentWidth * image.implicitWidth
                origin.y: (flickable.contentY + flickable.height / 2) / flickable.contentHeight * image.implicitHeight
                // scaleの値はFlickableのプロパティにバインド
                xScale: flickable.currentScale
                yScale: flickable.currentScale
            }

            onStatusChanged: {
                if (status === Image.Ready) {
                    // 画像ロード時にFlickableのcontentWidth/Heightを初期化
                    flickable.contentWidth = image.implicitWidth
                    flickable.contentHeight = image.implicitHeight
                    // 初期表示位置を中央に
                    flickable.contentX = (flickable.contentWidth * flickable.currentScale - flickable.width) / 2
                    flickable.contentY = (flickable.contentHeight * flickable.currentScale - flickable.height) / 2
                }
            }
        }

        property real currentScale: 1.0 // 現在のスケール
        property real scaleStep: 0.1 // ズーム段階

        MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onWheel: (mouse) => {
                if (mouse.modifiers & Qt.ControlModifier) {
                    var oldScale = flickable.currentScale;

                    if (mouse.angleDelta.y > 0) {
                        flickable.currentScale += flickable.scaleStep; // ズームイン
                    } else {
                        flickable.currentScale -= flickable.scaleStep; // ズームアウト
                    }

                    // スケールを制限
                    flickable.currentScale = Math.max(0.2, Math.min(5.0, flickable.currentScale));

                    // スケール変更による contentX/contentY の調整
                    // マウス位置を基準点として、ズーム前のコンテンツ上の相対位置を維持
                    var mouseXInContent = mouse.x + flickable.contentX;
                    var mouseYInContent = mouse.y + flickable.contentY;

                    var newContentX = mouseXInContent * (flickable.currentScale / oldScale) - mouse.x;
                    var newContentY = mouseYInContent * (flickable.currentScale / oldScale) - mouse.y;

                    flickable.contentX = newContentX;
                    flickable.contentY = newContentY;

                    flickable.returnToBounds();
                    mouse.accepted = true;
                }
            }
        }
    }
}

利点

  • アニメーションやトランジションが容易に適用できる。
  • Image や他の Itemscale プロパティを活用できる。
  • 視覚的なスケーリングが直接的で分かりやすい。

欠点

  • contentX/contentY の調整が resizeContent() を使うよりも複雑になる可能性があります。特にズームの中心を正確に維持しようとすると、計算が複雑になります。
  • FlickablecontentWidth / contentHeight と、実際のコンテンツの「表示上の」サイズとの間に乖離が生じるため、混乱しやすい場合があります。contentWidth/contentHeight はあくまでフリック可能な「仮想キャンバス」のサイズであり、実際に表示されるコンテンツのサイズは scale プロパティに依存します。

ScrollView を使用する(QtQuick.Controls 2.x)

Flickable の代わりに ScrollView を使用することもできます。ScrollView は内部的に Flickable を利用しており、自動的にスクロールバーなどを提供してくれます。ズーム機能は Flickable と同様に実装する必要がありますが、ScrollView の子アイテムのサイズが直接コンテンツサイズとして扱われるため、contentWidth/contentHeight を明示的に設定する必要がない場合があります。

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15 // ScrollViewのために必要

Window {
    width: 800
    height: 600
    visible: true
    title: "ScrollView with Zoom"

    ScrollView {
        id: scrollView
        anchors.fill: parent

        // ScrollViewの子が直接コンテンツとなる
        Image {
            id: image
            source: "https://picsum.photos/1200/800"
            // 画像の実際の幅と高さを制御するためにプロパティを持つ
            property real currentWidth: implicitWidth
            property real currentHeight: implicitHeight
            width: currentWidth // Imageのwidth/heightを直接操作
            height: currentHeight
            antialiasing: true

            onStatusChanged: {
                if (status === Image.Ready) {
                    // 初期サイズを設定
                    image.currentWidth = image.implicitWidth;
                    image.currentHeight = image.implicitHeight;
                }
            }
        }

        MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onWheel: (mouse) => {
                if (mouse.modifiers & Qt.ControlModifier) {
                    var oldWidth = image.currentWidth;
                    var oldHeight = image.currentHeight;

                    var zoomFactor = 1.1;
                    if (mouse.angleDelta.y > 0) {
                        image.currentWidth *= zoomFactor;
                        image.currentHeight *= zoomFactor;
                    } else {
                        image.currentWidth /= zoomFactor;
                        image.currentHeight /= zoomFactor;
                    }

                    // ズームレベルを制限 (ImageのimplicitWidth基準)
                    image.currentWidth = Math.max(image.implicitWidth * 0.2, Math.min(image.implicitWidth * 5.0, image.currentWidth));
                    image.currentHeight = image.currentWidth * (image.implicitHeight / image.implicitWidth); // アスペクト比維持

                    // ScrollViewのcontentX/contentYを調整
                    // scrollView.flickableItem は ScrollViewが内部で使うFlickableインスタンス
                    var scrollFlickable = scrollView.flickableItem;
                    if (scrollFlickable) {
                        var mouseContentX = mouse.x + scrollFlickable.contentX;
                        var mouseContentY = mouse.y + scrollFlickable.contentY;

                        scrollFlickable.contentX = mouseContentX - (mouseContentX / oldWidth) * image.currentWidth;
                        scrollFlickable.contentY = mouseContentY - (mouseContentY / oldHeight) * image.currentHeight;

                        scrollFlickable.returnToBounds();
                    }
                    mouse.accepted = true;
                }
            }
        }
    }
}

利点

  • ScrollView の子アイテムのサイズが直接コンテンツサイズになるため、Flickable.contentWidth/contentHeight を明示的に設定する手間が省ける場合があります。
  • スクロールバーが自動的に表示されるため、デスクトップアプリケーションなどでより標準的なUIを提供できます。

欠点

  • resizeContent() のような便利なメソッドは ScrollView 自体には提供されていないため、ズームロジックは手動で実装する必要があります。
  • 内部の Flickable インスタンス (flickableItem) にアクセスして contentX/contentY を操作する必要があるため、少し間接的になります。

Flickable.resizeContent() は、コンテンツのサイズと位置の調整を連携して行うための非常に便利なメソッドです。特にズーム操作で中心点を維持したい場合に、その複雑な計算を抽象化してくれる点が強みです。

代替手法は以下の状況で検討する価値があります。

  • 標準的なスクロールバーが必要な場合: ScrollView を使用して、自動的なスクロールバーの表示とその内部の Flickable を操作したい場合。
  • 視覚的なスケーリングを優先する場合: コンテンツアイテム自体の scale プロパティを使って視覚的な変形を表現したい場合。
  • 簡単なサイズ変更と位置調整の場合: contentWidth/contentHeightcontentX/contentY の直接操作で十分な場合。