QML Flickable.movingプロパティ活用術:実践コード例で学ぶUI制御

2025-05-27

Flickableは、スマートフォンなどのタッチベースのUIでよく見られる、ドラッグやフリックによってコンテンツをスクロールさせる機能を提供するQML要素です。大きな画像の一部を表示したり、長いリストをスクロールさせたりする際に利用されます。

Flickable.movingプロパティはブール値 (true/false) をとり、以下のいずれかの理由でFlickable内のコンテンツが移動している場合にtrueになります。

  • flicking (フリック中)
    ユーザーが指を離した後に、慣性でコンテンツが自動的にスクロールし続けている状態です。
  • dragging (ドラッグ中)
    ユーザーがマウスボタンを押したまま、または指でタッチしたままFlickableを動かしている状態です。

つまり、Flickable.movingは、ユーザーが直接操作している「ドラッグ」と、その後の慣性による「フリック」の両方を含む、コンテンツの移動状態全般を表します。

このプロパティは、例えば、Flickableが動いている間に特定の視覚的な効果を適用したり、移動が停止したときに何かのアクションを実行したりする場合に役立ちます。

関連するプロパティとして、より具体的に移動の原因を示すものもあります。

  • Flickable.movingVertically: 垂直方向に移動しているかどうか。
  • Flickable.movingHorizontally: 水平方向に移動しているかどうか。
  • Flickable.flicking: 慣性でフリックしているかどうか。
  • Flickable.dragging: ユーザーがドラッグしているかどうか。


Flickableがまったく動かない、またはフリックできない

これは最も一般的な問題です。Flickable.movingが常にfalseである場合、以下の原因が考えられます。

  • interactiveプロパティがfalse
    意図せずにFlickable.interactiveプロパティがfalseに設定されている場合、ユーザー操作を受け付けなくなります。

    • トラブルシューティング
      Flickable { interactive: true }が設定されているか、またはデフォルトのtrueが維持されていることを確認します。
  • イベントの伝播問題(Event Propagation Issues)
    Flickable内にMouseAreaなどの他の入力ハンドラを持つ要素がある場合、それらの要素がフリックイベントを消費してしまい、Flickableにイベントが伝わらないことがあります。

    • トラブルシューティング
      • MouseAreapropagateComposedEventsプロパティをtrueに設定し、Flickableにもイベントが伝わるようにします。
      • または、MouseAreahoverEnabledacceptedButtonsなどのプロパティを適切に設定し、不要なイベントの消費を防ぎます。
      • 複雑なUIの場合、FlickableMouseAreaの階層構造を見直し、イベントハンドラの配置を検討します。
    Flickable {
        // ...
        Rectangle {
            // ...
            MouseArea {
                anchors.fill: parent
                // デフォルトではfalseの場合が多い
                propagateComposedEvents: true // これにより、Flickableにもイベントが伝播する
                onClicked: {
                    console.log("MouseArea Clicked");
                    mouse.accepted = false; // イベントをさらに上位へ伝播させる
                }
            }
        }
    }
    
  • コンテンツがFlickableより小さい
    Flickableがスクロール可能であるためには、そのcontentItem(または内部のコンテンツ)がFlickable自体のサイズよりも大きくなければなりません。例えば、Flickableの幅が200pxなのに、その中のRectangleの幅が100pxしかない場合、スクロールする余地がないためフリックできません。

    • トラブルシューティング
      FlickablecontentWidthcontentHeight、または内部のコンテンツのサイズがFlickable自体のサイズよりも大きいことを確認してください。デバッグのために、Flickableとコンテンツの境界を色などで可視化すると良いでしょう。
    Flickable {
        width: 200
        height: 200
        contentWidth: myContent.width // これがFlickable.widthより大きいことを確認
        contentHeight: myContent.height // これがFlickable.heightより大きいことを確認
    
        Rectangle {
            id: myContent
            width: 400 // Flickableの幅より大きい
            height: 400 // Flickableの高さより大きい
            color: "lightblue"
            // 他のコンテンツ
        }
    }
    

Flickable.movingの状態が期待通りに変化しない

特定の条件でFlickable.movingtrueにならない、またはfalseに戻らない場合があります。

  • 慣性(Flick)が短い
    フリックの速度が速すぎたり遅すぎたりすると、慣性スクロールがすぐに停止し、Flickable.movingtrueになる期間が非常に短くなることがあります。
    • トラブルシューティング
      • flickDecelerationプロパティを調整して、フリックの減速を制御できます。値を小さくすると、慣性スクロールが長く続きます。
      • デバッグログ(onMovingChanged: console.log("Moving: " + Flickable.moving);)を入れて、状態の変化を詳しく確認します。
  • プログラムによる移動
    Flickable.contentXFlickable.contentYを直接設定してコンテンツを移動させた場合、Flickable.movingtrueになりません。Flickable.movingはユーザーのジェスチャーによる移動(ドラッグ、フリック)を反映するためのものです。
    • トラブルシューティング
      プログラム的に移動している場合は、Flickable.movingを使用するのではなく、移動が完了したかどうかを別の方法で判断する必要があります(例: contentXcontentYが目標値に到達したか)。

Flickable.movingを使用した際のパフォーマンス問題

onMovingChangedシグナルハンドラ内で重い処理を実行すると、スクロールのパフォーマンスに影響を与えることがあります。

  • 頻繁な状態変化
    Flickable.movingは、ドラッグ中やフリック中には頻繁に状態が変化する可能性があります。このシグナルハンドラ内で複雑な計算やUIの更新を行うと、フレームレートが低下する可能性があります。
    • トラブルシューティング
      • onMovingChanged内で実行する処理を最小限に抑えます。
      • パフォーマンスが問題になる場合は、Flickable.draggingFlickable.flickingなど、より具体的な状態変化を監視し、必要な場合にのみ処理を実行するようにします。
      • JavaScriptのsetTimeoutなどを利用して、処理を遅延させたり、一定時間ごとにバッチ処理したりすることも検討します。

Flickableの境界外へのスクロール

Flickableのコンテンツがその境界を超えて表示されてしまうことがあります。

  • clipプロパティの不足
    Flickableclipプロパティがfalse(デフォルト)の場合、コンテンツはFlickableの境界を超えても表示されます。
    • トラブルシューティング
      Flickable { clip: true } を設定して、コンテンツがFlickableの可視領域外には表示されないようにします。

全体的なトラブルシューティングのヒント

  • シンプルな例で切り分け
    問題が複雑なUIの一部で発生している場合、最小限のQMLコードでFlickableだけを構成し、問題が再現するかどうかを確認します。これにより、問題の原因がFlickable自体にあるのか、他の要素との相互作用にあるのかを特定しやすくなります。
  • デバッグメッセージの活用
    console.log()を積極的に使用し、Flickable.movingFlickable.draggingFlickable.flickingcontentXcontentYcontentWidthcontentHeightなどのプロパティの値が期待通りに変化しているかを確認します。


Flickable.movingの状態を表示する

最も基本的な例として、Flickableが現在移動中であるかどうかをテキストで表示します。

// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 400
    height: 400
    visible: true
    title: "Flickable.moving State"

    Flickable {
        id: myFlickable
        anchors.fill: parent
        clip: true // コンテンツがFlickableの境界外にはみ出さないようにクリップ

        // スクロール可能なコンテンツのサイズを定義
        // Flickableのサイズよりも大きくないとスクロールできません
        contentWidth: myContent.width
        contentHeight: myContent.height

        // Flickableが動いているかどうかを表示するテキスト
        Text {
            id: movingStateText
            anchors.top: parent.top
            anchors.horizontalCenter: parent.horizontalCenter
            text: myFlickable.moving ? "Moving..." : "Stopped"
            font.pixelSize: 24
            color: myFlickable.moving ? "red" : "green"
            z: 2 // コンテンツの上に表示されるようにする
        }

        // スクロールされるコンテンツ
        Rectangle {
            id: myContent
            width: 800 // Flickableの幅(400)より大きい
            height: 800 // Flickableの高さ(400)より大きい
            color: "lightgray"

            // コンテンツ内の要素
            Column {
                anchors.centerIn: parent
                spacing: 20
                Repeater {
                    model: 10
                    Text {
                        text: "Item " + (index + 1)
                        font.pixelSize: 30
                    }
                }
            }
        }

        // Flickableの移動状態が変化したときにログを出力
        onMovingChanged: {
            console.log("Flickable.moving changed to: " + myFlickable.moving);
        }
        onDraggingChanged: {
            console.log("Flickable.dragging changed to: " + myFlickable.dragging);
        }
        onFlickingChanged: {
            console.log("Flickable.flicking changed to: " + myFlickable.flicking);
        }
    }
}

解説

  • onMovingChangedonDraggingChangedonFlickingChangedシグナルハンドラを使用して、コンソールに状態の変化を出力し、内部動作を理解するのに役立てます。
  • movingStateTextは、myFlickable.movingプロパティの真偽値に基づいてテキストと色を動的に変更します。
  • FlickablecontentWidthcontentHeightは、内部のmyContentのサイズに設定されており、Flickable自体のサイズ (400x400) よりも大きいため、スクロール可能です。

Flickableが停止したときにアクションを実行する

Flickableが移動を停止したときに、特定の処理を実行する例です。例えば、ユーザーがスクロールを終えた後に、現在の表示位置に基づいてデータを更新するといったシナリオが考えられます。

// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 400
    height: 400
    visible: true
    title: "Action on Flickable Stop"

    Flickable {
        id: myFlickable
        anchors.fill: parent
        clip: true
        contentWidth: 800
        contentHeight: 800

        // スクロールされるコンテンツ
        Rectangle {
            width: 800
            height: 800
            color: "lightgreen"
            Text {
                text: "Scroll me!"
                anchors.centerIn: parent
                font.pixelSize: 40
            }
        }

        // 停止したときに表示するメッセージ
        Text {
            id: stopMessage
            anchors.bottom: parent.bottom
            anchors.horizontalCenter: parent.horizontalCenter
            text: ""
            font.pixelSize: 20
            color: "blue"
            visible: text !== "" // テキストがある場合のみ表示
            z: 2
        }

        // Flickableが移動を停止したときに実行
        // onMovingChangedを監視し、movingがfalseになったときに処理を行う
        onMovingChanged: {
            if (!myFlickable.moving) {
                // Flickableが停止した!
                console.log("Flickable has stopped moving. Current X:", myFlickable.contentX);
                console.log("Current Y:", myFlickable.contentY);

                // 停止メッセージを表示
                stopMessage.text = "Stopped at X: " + Math.round(myFlickable.contentX) + ", Y: " + Math.round(myFlickable.contentY);

                // メッセージを2秒後に消す
                resetMessageTimer.restart();
            } else {
                // 移動開始時にメッセージをクリア
                stopMessage.text = "";
            }
        }

        Timer {
            id: resetMessageTimer
            interval: 2000 // 2秒
            running: false
            onTriggered: {
                stopMessage.text = ""; // メッセージをクリア
            }
        }
    }
}

解説

  • Timerを使って、メッセージが一定時間表示された後に自動的に消えるようにしています。
  • onMovingChangedシグナルハンドラ内で、myFlickable.movingfalseになった(つまり移動が停止した)ときに、現在のcontentXcontentYをコンソールに出力し、メッセージをUIに表示します。

Flickableが移動している間、何らかの視覚的な効果を適用する例です。ここでは、スクロール中にコンテンツの不透明度を少し下げることで、移動中であることを示します。

// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 400
    height: 400
    visible: true
    title: "Visual Feedback on Moving"

    Flickable {
        id: myFlickable
        anchors.fill: parent
        clip: true
        contentWidth: 800
        contentHeight: 800

        // スクロールされるコンテンツ
        Rectangle {
            id: scrollableContent
            width: 800
            height: 800
            color: "orange"

            // Flickableが動いている間、不透明度を調整
            opacity: myFlickable.moving ? 0.7 : 1.0

            // opacityの変化をアニメーションさせる
            Behavior on opacity {
                NumberAnimation { duration: 150 } // 150msでスムーズに変化
            }

            Text {
                text: "Scroll me to see the effect!"
                anchors.centerIn: parent
                font.pixelSize: 30
                color: "black"
            }
        }
    }
}
  • Behavior on opacityを使用することで、不透明度の変化が即座に行われるのではなく、150msかけてスムーズにアニメーションするようにしています。これにより、より洗練されたユーザー体験を提供できます。
  • scrollableContentopacityプロパティをmyFlickable.movingに基づいて設定しています。movingtrueの間は不透明度が0.7になり、停止すると1.0に戻ります。


ここでは、Flickable.movingの代替となる、あるいは関連する状態を判断するためのプログラミング手法について説明します。

Flickable.dragging と Flickable.flicking を個別に利用する

Flickable.movingは、Flickable.dragging(ユーザーが直接ドラッグしている状態)とFlickable.flicking(ユーザーが指を離した後に慣性でスクロールしている状態)の両方を含む合成プロパティです。

特定の状況で、これらの状態を区別して処理したい場合は、それぞれのプロパティを直接監視するのがより正確な方法です。

用途

  • フリックが完全に停止した後にのみデータをロードしたい(フリック中はまだ目的地が確定していない可能性があるため)。
  • ドラッグ中にのみ特定のUIフィードバックを与えたい。


Flickable {
    id: myFlickable
    // ... (Flickableの設定)

    Text {
        anchors.centerIn: parent
        text: {
            if (myFlickable.dragging) return "Dragging...";
            if (myFlickable.flicking) return "Flicking...";
            return "Stopped";
        }
        color: "blue"
        font.pixelSize: 24
    }

    onDraggingChanged: {
        console.log("Dragging: " + myFlickable.dragging);
        if (myFlickable.dragging) {
            // ドラッグ開始時の処理
        } else {
            // ドラッグ終了時の処理
        }
    }

    onFlickingChanged: {
        console.log("Flicking: " + myFlickable.flicking);
        if (myFlickable.flicking) {
            // フリック開始時の処理
        } else {
            // フリック終了時の処理(慣性スクロール停止時)
        }
    }
}

contentX / contentY の変化を監視する

Flickable.contentXFlickable.contentYは、Flickable内のコンテンツの現在のスクロール位置を示します。これらのプロパティの変化を監視することで、Flickableがスクロールしていることを間接的に判断できます。

用途

  • スクロール位置が特定の閾値を超えたときに何かをしたい場合。
  • プログラムによるスクロールとユーザーによるスクロールの両方に対応したい場合。Flickable.movingはユーザー操作に限定されるため、contentX/Yを直接変更した場合はtrueになりません。

注意点

  • スクロールが停止したかどうかを判断するには、現在の値と少し前の値を比較して変化がなくなったことを確認する、またはTimerを使って一定時間変化がないことを確認するなどのロジックが必要になります。
  • contentX/contentYはスクロール中に非常に頻繁に更新されます。onContentXChangedonContentYChangedで重い処理を実行すると、パフォーマンスの問題を引き起こす可能性があります。


Flickable {
    id: myFlickable
    // ... (Flickableの設定)

    property var lastContentX: myFlickable.contentX
    property var lastContentY: myFlickable.contentY
    property bool isReallyMoving: false // カスタムの「移動中」状態

    onContentXChanged: {
        // X座標が変化した
        if (myFlickable.contentX !== lastContentX) {
            isReallyMoving = true;
            stopDetectionTimer.restart(); // 移動検知タイマーをリスタート
        }
        lastContentX = myFlickable.contentX;
    }

    onContentYChanged: {
        // Y座標が変化した
        if (myFlickable.contentY !== lastContentY) {
            isReallyMoving = true;
            stopDetectionTimer.restart(); // 移動検知タイマーをリスタート
        }
        lastContentY = myFlickable.contentY;
    }

    // スクロールが停止したことを検知するためのタイマー
    Timer {
        id: stopDetectionTimer
        interval: 100 // 100ms間変化がなければ停止とみなす
        running: false
        onTriggered: {
            // 100ms間 contentX/Y の変化がなかった場合
            isReallyMoving = false;
            console.log("Flickable has stopped (via contentX/Y monitoring)");
        }
    }

    Text {
        anchors.centerIn: parent
        text: isReallyMoving ? "Content is moving!" : "Content is stopped."
        color: "purple"
        font.pixelSize: 20
    }
}

解説
この例では、contentXcontentYが変化するたびにカスタムプロパティisReallyMovingtrueにし、タイマーをリスタートします。タイマーがトリガーされる(つまり、一定時間contentX/contentYに変化がなかった)と、isReallyMovingfalseに戻します。これにより、プログラムによるスクロールも含めたコンテンツの移動状態をより柔軟に判断できます。

Qt 6では、FlickableonMovementStartedonMovementEndedという新しいシグナルが追加されました。これらはFlickable.movingの状態変化と密接に関連しており、特に移動の開始と終了のイベントに特化して処理を記述したい場合に非常に便利です。

用途

  • 移動が完全に終了したときにのみ実行する処理(例: スナップ処理、データロード)。
  • 移動が開始されたときにのみ実行する処理(例: 高解像度画像を低解像度プレビューに切り替える)。

例 (Qt 6)

import QtQuick 6.0
import QtQuick.Window 6.0

Window {
    width: 400
    height: 400
    visible: true
    title: "Flickable Movement Signals (Qt 6)"

    Flickable {
        id: myFlickable
        anchors.fill: parent
        clip: true
        contentWidth: 800
        contentHeight: 800

        Text {
            id: statusText
            anchors.top: parent.top
            anchors.horizontalCenter: parent.horizontalCenter
            text: "Ready to scroll"
            font.pixelSize: 24
            color: "darkblue"
            z: 2
        }

        Rectangle {
            width: 800
            height: 800
            color: "lightcoral"
            Text {
                text: "Scroll me in Qt 6!"
                anchors.centerIn: parent
                font.pixelSize: 40
            }
        }

        // 移動開始時に呼び出される
        onMovementStarted: {
            console.log("Flickable movement started!");
            statusText.text = "Movement Started!";
            statusText.color = "red";
        }

        // 移動終了時に呼び出される
        onMovementEnded: {
            console.log("Flickable movement ended!");
            statusText.text = "Movement Ended!";
            statusText.color = "green";
            // ここで、スクロール後の最終処理を行う
            // 例: contentX, contentY の値を基に何かをする
            console.log("Final position: X=" + myFlickable.contentX + ", Y=" + myFlickable.contentY);
        }
    }
}

解説
onMovementStartedonMovementEndedは、Flickable.movingtrueになった直後、およびfalseになった直後にトリガーされるシグナルです。これらはonMovingChangedよりも意図が明確で、イベントドリブンなプログラミングに適しています。

  • onMovementStarted / onMovementEnded (Qt 6以降)
    移動の開始と終了に特化したイベント処理が必要な場合に最も推奨される。
  • contentX / contentY の監視
    プログラムによるスクロールも含め、すべてのコンテンツ移動を検出したい場合に有効。ただし、スクロール停止の検知には追加のロジックが必要。
  • Flickable.dragging / Flickable.flicking
    移動の原因(直接操作か慣性か)を区別したい場合に最適。
  • Flickable.moving
    ユーザー操作による「移動中」(ドラッグまたはフリック)の最も一般的な判断に最適。