Flickable.atXBeginningで何ができる?Qt QMLでのスクロールUI実装例

2025-05-27

Flickableとは何か?

まず、Flickableについて簡単に説明します。Flickableは、タッチデバイスなどでコンテンツを「フリック」してスクロールさせるUI要素を実装するためのQt Quickのコンポーネントです。大きな画像の一部を表示したり、長いリストをスクロールさせたりする際に使われます。ユーザーが指で画面をドラッグしたり、素早くフリックしたりすることで、表示されているコンテンツが移動し、隠れていた部分が見えるようになります。

Flickable.atXBeginningとは何か?

Flickable.atXBeginningは、ブール型(真偽値)のプロパティで、Flickableのコンテンツが水平方向(X軸方向)の先頭(一番左端)に位置しているかどうかを示します。

  • falseの場合
    コンテンツが水平方向のスクロール範囲の先頭にないことを意味します。まだ左にスクロールできるか、すでに右にスクロールされています。
  • trueの場合
    コンテンツが水平方向のスクロール範囲の先頭に達していることを意味します。つまり、それ以上左にスクロールできない状態です。

使用例と目的

このプロパティは、UIの状態を判断したり、それに基づいて何らかの動作をトリガーしたりするのに役立ちます。例えば、以下のようなシナリオで利用できます。

  • エッジ効果の実装
    先頭に達したときに、コンテンツが少し跳ね返るような視覚効果(バウンド効果)を実装する。
  • 特定のアクションの有効/無効化
    一番左までスクロールされたときに、「前のページへ」といったナビゲーションボタンを無効にする。
  • スクロールインジケーターの表示/非表示
    コンテンツが先頭にあるときに、水平スクロールバーや矢印を非表示にする。

関連するプロパティ

atXBeginningと同様に、以下のプロパティも存在します。

  • atYEnd: コンテンツが垂直方向の末尾(一番下)に位置しているかどうか。
  • atYBeginning: コンテンツが垂直方向の先頭(一番上)に位置しているかどうか。
  • atXEnd: コンテンツが水平方向の末尾(一番右端)に位置しているかどうか。


contentWidth または contentHeight の設定不足/誤り

問題
Flickable.atXBeginningが常にtrueになる、または期待通りにスクロールしない。 原因: Flickableは、表示するコンテンツの総幅 (contentWidth) と総高さ (contentHeight) を知る必要があります。これらのプロパティが正しく設定されていないと、Flickableはコンテンツがその境界内に収まっていると誤解し、スクロールを許可しなかったり、atXBeginningが常にtrueになったりします。 トラブルシューティング:

  • contentItem.childrenRect.width / heightを使用する
    Flickable内に複数の子要素があり、それらの合計サイズをコンテンツ幅/高さにしたい場合、contentItem.childrenRect.widthcontentItem.childrenRect.heightが役立つことがあります。ただし、これは子要素の原点(x, y)が考慮されるため、意図しない結果になる可能性もあります。
    Flickable {
        id: flickableArea
        width: 400
        height: 300
        contentWidth: contentItem.childrenRect.width
        contentHeight: contentItem.childrenRect.height
        clip: true
    
        Column { // contentItemの直接の子
            width: childrenRect.width // Column自身の幅を子要素に合わせて調整
            spacing: 10
            Rectangle { width: 200; height: 50; color: "red" }
            Rectangle { width: 300; height: 70; color: "green" }
            Rectangle { width: 500; height: 100; color: "blue" }
        }
    }
    
  • contentWidthとcontentHeightを明示的に設定する
    Flickable {
        width: 400
        height: 300
        contentWidth: myContent.width // または具体的な値
        contentHeight: myContent.height // または具体的な値
        clip: true // コンテンツがFlickableの境界外に出るのを防ぐ
        Item {
            id: myContent
            width: 800 // Flickableの幅より大きくする
            height: 600 // Flickableの高さより大きくする
            // ... コンテンツ ...
        }
    }
    

Flickable内にスクロール可能な別の要素がある

問題
Flickable内にListViewや別のFlickableなど、独自のスクロール機能を持つ要素を配置した場合、親のFlickableatXBeginningが期待通りに更新されない、またはフリックが正しく伝播しない。 原因: イベントの伝播(event propagation)の問題です。タッチイベントが子要素で消費され、親のFlickableに到達しないことがあります。 トラブルシューティング:

  • カスタムのタッチハンドリング
    複雑なインタラクションが必要な場合は、MultiPointTouchAreaなどを使用して、より低レベルでタッチイベントを処理し、独自のロジックでスクロールを制御することを検討します。
  • Flickableのinteractiveプロパティを制御する
    特定の状況で親または子のFlickableinteractiveプロパティをfalseに設定することで、フリックの競合を避けることができます。例えば、子要素がアクティブな間は親のフリックを無効にするなど。
  • MouseAreaのpropagateComposedEventsを使用する
    子要素がMouseAreaを使用している場合、MouseArea { propagateComposedEvents: true }を設定することで、イベントを親に伝播させることができます。ただし、これによりフリックの競合が発生する可能性があります。
  • Flickableのネストを避ける
    可能であれば、複数のスクロール領域をネストするのではなく、単一のFlickableで全てを管理できないか検討します。

コンテンツの動的な変更

問題
Flickableのコンテンツ(例えば、リストのアイテムなど)が動的に追加・削除された後、atXBeginningの状態が一時的に不安定になる、または更新が遅れる。 原因: コンテンツのサイズが変更された際に、Flickableがその新しいサイズを検知し、スクロール範囲を再計算するまでに若干の遅延が生じることがあります。特に、contentItem.childrenRectを使用している場合に顕著です。 トラブルシューティング:

  • 強制的な更新(非推奨): 通常は不要ですが、非常に稀なケースで、Flickable.forceLayout()のようなメソッド(QMLでは直接提供されていないが、C++からアクセスできる場合がある)を呼び出すことで、レイアウトの再計算を強制することができます。ただし、これはパフォーマンスに影響を与える可能性があります。
  • アニメーションとの連携
    コンテンツの追加・削除にアニメーションを使用している場合、アニメーションが完了するまでatXBeginningの状態に依存するロジックを遅延させることを検討します。
  • FlickableのonContentWidthChanged / onContentHeightChangedシグナルを監視する
    コンテンツの幅や高さが変更された際に、関連するロジック(例えば、ナビゲーションボタンの有効/無効化)を更新します。

Flickableの境界動作 (boundsBehavior) の理解不足

問題
atXBeginningtrueになるタイミングが、コンテンツが完全に端に到達した時と異なるように見える。 原因: FlickableboundsBehaviorプロパティは、コンテンツがスクロール可能な境界を超えて移動できるかどうかを制御します。デフォルトではFlickable.DragAndOvershootBoundsになっており、ユーザーがドラッグしたりフリックしたりした際にコンテンツが少しだけ境界をはみ出すことができます。この「はみ出し」があるため、見た目上は端に達していてもatXBeginningがまだfalseである可能性があります。 トラブルシューティング:

  • オーバーシュート量 (horizontalOvershoot, verticalOvershoot) を考慮する
    boundsBehaviorDragAndOvershootBoundsの場合、horizontalOvershootverticalOvershootプロパティで指定された量だけコンテンツがはみ出すことができます。これらの値が0でない限り、視覚的な端と論理的なatXBeginningが一致しない可能性があります。
  • boundsBehaviorを確認する
    Flickable.StopAtBoundsを設定すると、コンテンツは境界で正確に停止し、はみ出しがなくなります。これにより、atXBeginningがより直感的に動作するようになります。
    Flickable {
        // ...
        boundsBehavior: Flickable.StopAtBounds
        // ...
    }
    

レイアウトの問題とFlickableのサイズ

問題
Flickable自体が意図したサイズになっておらず、結果としてatXBeginningが正しく機能しない。 原因: Flickablewidthheight、またはanchorsの設定が間違っていると、Flickableの表示領域が正しく設定されず、コンテンツのスクロール範囲もおかしくなります。 トラブルシューティング:

  • anchorsの適切な使用
    親要素に対するanchors.fill: parentや、具体的なwidth/height設定が正しいことを確認します。
  • Flickableのサイズと位置をデバッグする
    RectangleなどでFlickableを囲み、border.colorなどを設定して、実際にFlickableがどのくらいの領域を占めているかを確認します。


例1: 先頭にいるときに左スクロール矢印を非表示にする

これは最も一般的なユースケースの一つです。コンテンツが既に一番左にある場合、それ以上左にスクロールする必要がないため、左方向へのスクロールを示すUI要素(例えば、矢印アイコン)を非表示にします。

// FlickableAtXBeginningExample1.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    width: 600
    height: 400
    visible: true
    title: "Flickable.atXBeginning Example 1"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20

        Text {
            text: "水平スクロール可能なコンテンツ"
            font.pixelSize: 20
            Layout.alignment: Qt.AlignHCenter
        }

        // Flickable
        Flickable {
            id: myFlickable
            Layout.fillWidth: true
            Layout.preferredHeight: 200
            clip: true // Flickableの境界外に出るコンテンツをクリップする
            flickableDirection: Flickable.HorizontalFlick // 水平方向のみフリック可能

            // コンテンツの幅を設定することが重要!
            // この例では、子要素の合計幅を使う
            contentWidth: contentRow.width
            contentHeight: contentRow.height // 垂直スクロールしないので、子要素の高さに合わせる

            Row {
                id: contentRow
                spacing: 10
                // 複数の長方形を並べて、Flickableの幅よりも広くする
                Repeater {
                    model: 10 // 10個のアイテム
                    delegate: Rectangle {
                        width: 150
                        height: 150
                        color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
                        border.color: "black"
                        border.width: 1
                        Text {
                            text: index + 1
                            anchors.centerIn: parent
                            font.pixelSize: 30
                            color: "white"
                        }
                    }
                }
            }

            // スクロール位置の表示 (デバッグ用)
            Text {
                id: debugText
                anchors.top: parent.bottom
                anchors.left: parent.left
                anchors.leftMargin: 5
                text: "X: " + Math.round(myFlickable.contentX) + " atXBeginning: " + myFlickable.atXBeginning
                color: "black"
                font.pixelSize: 14
            }
        }

        RowLayout {
            Layout.fillWidth: true
            Layout.preferredHeight: 50
            Layout.alignment: Qt.AlignHCenter

            // 左矢印ボタン
            Button {
                text: "<"
                font.pixelSize: 24
                // Flickableが先頭にいる場合に非表示にする
                visible: !myFlickable.atXBeginning
                // ボタンが非表示になったときもスペースを占有しないようにする
                Layout.preferredWidth: visible ? 50 : 0
                Layout.preferredHeight: 50

                onClicked: {
                    myFlickable.contentX = Math.max(0, myFlickable.contentX - 100); // 100px左にスクロール
                }
            }

            // 右矢印ボタン (おまけ: Flickable.atXEnd の例)
            Button {
                text: ">"
                font.pixelSize: 24
                // Flickableが末尾にいる場合に非表示にする
                visible: !myFlickable.atXEnd
                Layout.preferredWidth: visible ? 50 : 0
                Layout.preferredHeight: 50

                onClicked: {
                    myFlickable.contentX = Math.min(myFlickable.contentWidth - myFlickable.width, myFlickable.contentX + 100); // 100px右にスクロール
                }
            }
        }
    }
}

解説

  1. myFlickable.flickableDirection: Flickable.HorizontalFlick で、水平方向のみスクロールできるようにしています。
  2. myFlickable.contentWidth: contentRow.width で、Flickableがスクロールすべき総幅を、内部のRow要素の幅に設定しています。これが正しく設定されていないと、Flickableはスクロール可能であると認識せず、atXBeginningも常にtrueのままになる可能性があります。
  3. 左矢印のButtonvisibleプロパティを !myFlickable.atXBeginning にバインドしています。これにより、Flickableが一番左端に位置すると、ボタンが非表示になります。
  4. Layout.preferredWidth: visible ? 50 : 0 を設定することで、visible: false になった際にボタンが占有するレイアウトスペースも0になり、他の要素がそのスペースを埋めることができます。

例2: atXBeginning の状態変化に基づいてアニメーションをトリガーする

atXBeginningプロパティはonAtXBeginningChangedシグナルも持っています。このシグナルを監視することで、状態の変化に応じて特定のアニメーションや動作をトリガーすることができます。

// FlickableAtXBeginningExample2.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    width: 600
    height: 400
    visible: true
    title: "Flickable.atXBeginning Example 2"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20

        Text {
            text: "先頭に到達すると、フリック可能エリアが点滅"
            font.pixelSize: 20
            Layout.alignment: Qt.AlignHCenter
        }

        Flickable {
            id: animatedFlickable
            Layout.fillWidth: true
            Layout.preferredHeight: 200
            clip: true
            flickableDirection: Flickable.HorizontalFlick
            contentWidth: contentContainer.width
            contentHeight: contentContainer.height

            Rectangle {
                id: contentContainer
                width: 1000 // Flickableの幅より広くする
                height: parent.height
                color: "lightgray"
                border.color: "darkgray"
                border.width: 2

                Text {
                    text: "スクロールしてください"
                    anchors.centerIn: parent
                    font.pixelSize: 40
                    color: "darkblue"
                }
            }

            // `atXBeginning` が変化したときに実行されるロジック
            onAtXBeginningChanged: {
                if (atXBeginning) {
                    console.log("Flickableが先頭に到達しました!");
                    // Rectangleの境界色をアニメーションで点滅させる
                    borderAnimation.start();
                } else {
                    console.log("Flickableが先頭から離れました。");
                    borderAnimation.stop(); // アニメーションを停止
                    contentContainer.border.color = "darkgray"; // 色を元に戻す
                }
            }

            ColorAnimation on border.color {
                id: borderAnimation
                target: contentContainer
                from: "darkgray"
                to: "red"
                duration: 500
                loops: Animation.Infinite // 無限ループ
                running: false // デフォルトでは停止
            }
        }
    }
}

解説

  1. FlickableonAtXBeginningChangedシグナルハンドラを使用しています。
  2. atXBeginningtrueになった場合(コンテンツが先頭に到達した場合)、borderAnimation.start()を呼び出して、contentContainerの境界色を点滅させるアニメーションを開始します。
  3. atXBeginningfalseになった場合(コンテンツが先頭から離れた場合)、borderAnimation.stop()を呼び出し、境界色を元のdarkgrayに戻しています。

例3: atXBeginningの状態に応じて異なるコンポーネントを表示する

LoaderStateと組み合わせて、atXBeginningの状態に基づいてUIの一部を切り替えることも可能です。

// FlickableAtXBeginningExample3.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    width: 600
    height: 400
    visible: true
    title: "Flickable.atXBeginning Example 3"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20

        Text {
            text: "先頭にいると異なるメッセージを表示"
            font.pixelSize: 20
            Layout.alignment: Qt.AlignHCenter
        }

        Flickable {
            id: messageFlickable
            Layout.fillWidth: true
            Layout.preferredHeight: 200
            clip: true
            flickableDirection: Flickable.HorizontalFlick
            contentWidth: largeContent.width
            contentHeight: largeContent.height

            Rectangle {
                id: largeContent
                width: 1200 // 十分広くする
                height: parent.height
                color: "lightcyan"

                Row {
                    anchors.fill: parent
                    spacing: 20
                    Repeater {
                        model: 10
                        delegate: Rectangle {
                            width: 100
                            height: parent.height - 20
                            y: 10
                            color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
                            border.color: "black"
                            border.width: 1
                            Text {
                                text: "Item " + (index + 1)
                                anchors.centerIn: parent
                                font.pixelSize: 18
                                color: "black"
                            }
                        }
                    }
                }
            }
        }

        // Flickableの状態に応じて表示を切り替えるテキスト
        Text {
            id: statusMessage
            Layout.fillWidth: true
            Layout.preferredHeight: 50
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
            font.pixelSize: 24
            color: "darkblue"

            // messageFlickable.atXBeginning の状態に基づいてテキストを切り替える
            text: messageFlickable.atXBeginning ? "あなたはコンテンツの先頭にいます!" : "右にスクロールしてコンテンツを見てください。"

            // テキストの切り替えを滑らかにするためのColorAnimation
            ColorAnimation on color {
                from: "darkblue"
                to: messageFlickable.atXBeginning ? "green" : "darkblue"
                duration: 300
            }
        }
    }
}
  1. statusMessagetextプロパティを三項演算子 (? :) を使ってmessageFlickable.atXBeginningの値にバインドしています。これにより、atXBeginningtrueなら「あなたはコンテンツの先頭にいます!」、falseなら「右にスクロールしてコンテンツを見てください。」と表示されます。
  2. ColorAnimationを使って、テキストのcoloratXBeginningの状態に応じてスムーズに変化させています。


Flickable.atXBeginningは、FlickablecontentXプロパティがゼロ(またはoriginX)に等しいかどうかを内部的にチェックしています。したがって、このプロパティの代わりに、contentXoriginXを直接比較することで同様のロジックを実装できます。

Flickable.contentX と Flickable.originX を直接比較する

なぜ代替となるか
atXBeginningは、まさにこの比較を行っているラッパープロパティです。これを直接使用することで、より低レベルな制御が可能になります。特に、スクロールの「しきい値」をカスタマイズしたい場合や、わずかなオーバーシュートを許容したい場合に有用です。

コード例

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    width: 600
    height: 400
    visible: true
    title: "Flickable.contentX Manual Check"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20

        Text {
            text: "contentX を手動でチェック"
            font.pixelSize: 20
            Layout.alignment: Qt.AlignHCenter
        }

        Flickable {
            id: myFlickable
            Layout.fillWidth: true
            Layout.preferredHeight: 200
            clip: true
            flickableDirection: Flickable.HorizontalFlick
            contentWidth: contentRow.width
            contentHeight: contentRow.height

            Row {
                id: contentRow
                spacing: 10
                Repeater {
                    model: 10
                    delegate: Rectangle {
                        width: 150
                        height: 150
                        color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
                        border.color: "black"
                        border.width: 1
                        Text {
                            text: index + 1
                            anchors.centerIn: parent
                            font.pixelSize: 30
                            color: "white"
                        }
                    }
                }
            }

            // atXBeginning の代わりに contentX を使用
            property bool isAtBeginningManually: {
                // わずかな浮動小数点の誤差を考慮して Math.abs() を使う
                // Math.abs(myFlickable.contentX - myFlickable.originX) < 1.0 のように閾値を設けることも可能
                return myFlickable.contentX <= myFlickable.originX + 1; // わずかな誤差を許容
            }

            Text {
                id: debugText
                anchors.top: parent.bottom
                anchors.left: parent.left
                anchors.leftMargin: 5
                text: "contentX: " + Math.round(myFlickable.contentX) +
                      " originX: " + Math.round(myFlickable.originX) +
                      " isAtBeginningManually: " + myFlickable.isAtBeginningManually
                color: "black"
                font.pixelSize: 14
            }
        }

        RowLayout {
            Layout.fillWidth: true
            Layout.preferredHeight: 50
            Layout.alignment: Qt.AlignHCenter

            Button {
                text: "<"
                font.pixelSize: 24
                visible: !myFlickable.isAtBeginningManually // 手動チェックプロパティを使用
                Layout.preferredWidth: visible ? 50 : 0
                Layout.preferredHeight: 50
                onClicked: {
                    myFlickable.contentX = Math.max(0, myFlickable.contentX - 100);
                }
            }

            Button {
                text: ">"
                font.pixelSize: 24
                visible: myFlickable.contentX + myFlickable.width < myFlickable.contentWidth - 1 // 手動で atXEnd に相当するチェック
                Layout.preferredWidth: visible ? 50 : 0
                Layout.preferredHeight: 50
                onClicked: {
                    myFlickable.contentX = Math.min(myFlickable.contentWidth - myFlickable.width, myFlickable.contentX + 100);
                }
            }
        }
    }
}

利点

  • ListViewGridViewのようにoriginXが動的に変わる可能性があるコンテキストでも、正確な「先頭」判定ができる。
  • 必要に応じて、厳密な0ではなく、微小な誤差範囲(例: contentX < 5など)を「先頭」と見なすようなカスタマイズが可能。
  • atXBeginningが内部的に行っていることと全く同じことを理解できる。

欠点

  • 浮動小数点数の比較には注意が必要(=ではなく、Math.abs(a - b) < epsilonのような比較が推奨される)。
  • atXBeginningプロパティを使う方が、コードが簡潔で意図が明確になることが多い。

onContentXChanged シグナルハンドラ内でロジックを実装する

なぜ代替となるか
atXBeginningはあくまで現在の状態を示すプロパティであり、その変化を検知するにはonAtXBeginningChangedシグナルを使用します。onContentXChangedを使うことで、atXBeginningプロパティが提供するよりも細かい粒度でスクロールイベントを監視し、特定のスクロール位置に到達した際の処理を実装できます。

コード例

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    width: 600
    height: 400
    visible: true
    title: "Flickable.onContentXChanged Example"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20

        Text {
            text: "onContentXChanged でイベントを監視"
            font.pixelSize: 20
            Layout.alignment: Qt.AlignHCenter
        }

        Flickable {
            id: eventFlickable
            Layout.fillWidth: true
            Layout.preferredHeight: 200
            clip: true
            flickableDirection: Flickable.HorizontalFlick
            contentWidth: contentGrid.width
            contentHeight: contentGrid.height

            Grid {
                id: contentGrid
                columns: 5
                spacing: 5
                Repeater {
                    model: 20 // 20個のアイテム
                    delegate: Rectangle {
                        width: 100
                        height: 100
                        color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
                        border.color: "black"
                        border.width: 1
                        Text {
                            text: index + 1
                            anchors.centerIn: parent
                            font.pixelSize: 25
                            color: "white"
                        }
                    }
                }
            }

            // スクロール位置が変更されるたびに呼び出される
            onContentXChanged: {
                if (contentX <= originX + 1) { // ほぼ先頭にいる
                    statusLabel.text = "コンテンツが先頭にあります!";
                    statusLabel.color = "green";
                } else if (contentX >= contentWidth - width - 1) { // ほぼ末尾にいる
                    statusLabel.text = "コンテンツが末尾にあります!";
                    statusLabel.color = "red";
                } else {
                    statusLabel.text = "スクロール中...";
                    statusLabel.color = "darkblue";
                }
            }
        }

        Text {
            id: statusLabel
            Layout.fillWidth: true
            Layout.preferredHeight: 50
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
            font.pixelSize: 24
            color: "darkblue"
            text: "スクロールしてください" // 初期値
        }
    }
}

利点

  • スクロールの進行状況を示すプログレスバーのようなUIを実装する際に、contentXから直接計算できる。
  • atXBeginningtrue/falseという単純な状態だけでなく、特定のスクロール位置に到達したときなど、より複雑な条件に基づいてアクションをトリガーできる。
  • スクロール位置の変化に即座に反応できる。

欠点

  • atXBeginningのような単純な真偽値が必要なだけなら、オーバーキルになる場合がある。
  • contentXは非常に頻繁に更新される可能性があるため、重い計算をonContentXChanged内で行うとパフォーマンスに影響を与える可能性がある。

Flickable.visibleArea を使用する (より高度な場合)

なぜ代替となるか
atXBeginningは「先頭にいるかどうか」という単純なチェックですが、visibleArea.xPositionは0から1までの正規化された値で現在の水平スクロール位置を示します。コンテンツが先頭にいる場合、xPositionは0に近くなります。

コード例

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    width: 600
    height: 400
    visible: true
    title: "Flickable.visibleArea.xPosition Example"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20

        Text {
            text: "visibleArea.xPosition でスクロール位置を視覚化"
            font.pixelSize: 20
            Layout.alignment: Qt.AlignHCenter
        }

        Flickable {
            id: ratioFlickable
            Layout.fillWidth: true
            Layout.preferredHeight: 200
            clip: true
            flickableDirection: Flickable.HorizontalFlick
            contentWidth: largeImage.width // 例として大きな画像
            contentHeight: largeImage.height

            Image {
                id: largeImage
                source: "https://via.placeholder.com/1200x400/FF5733/FFFFFF?text=Large+Image+Content" // ダミー画像
                width: 1200
                height: 400
            }

            // visibleArea.xPosition は 0.0 (先頭) から 1.0 (末尾) まで
            // 実際は contentWidth - width の範囲なので、最後の部分は見えない
            property bool isAtBeginningViaRatio: ratioFlickable.visibleArea.xPosition < 0.01 // 1%の閾値

            Text {
                anchors.top: parent.bottom
                anchors.left: parent.left
                anchors.leftMargin: 5
                text: "xPosition: " + ratioFlickable.visibleArea.xPosition.toFixed(2) +
                      " isAtBeginningViaRatio: " + ratioFlickable.isAtBeginningViaRatio
                color: "black"
                font.pixelSize: 14
            }
        }

        // スクロールバーの代替としてのプログレスバー
        ProgressBar {
            id: scrollProgress
            Layout.fillWidth: true
            Layout.preferredHeight: 20
            value: ratioFlickable.visibleArea.xPosition // xPosition をプログレスバーの値にバインド
            from: 0
            to: 1
            contentItem: Rectangle {
                implicitWidth: parent.width * parent.value
                implicitHeight: parent.height
                color: "blue"
            }
            background: Rectangle {
                implicitWidth: parent.width
                implicitHeight: parent.height
                color: "lightgray"
                border.color: "darkgray"
                border.width: 1
            }
        }

        Text {
            Layout.fillWidth: true
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
            font.pixelSize: 20
            color: "darkgreen"
            text: ratioFlickable.isAtBeginningViaRatio ? "コンテンツの先頭です!" : "スクロール中..."
        }
    }
}

利点

  • widthRatioheightRatioを使って、表示されているコンテンツの割合を把握できる。
  • 正規化された値 (0.0から1.0) でスクロール位置を把握できるため、プログレスバーのようなUIに直接バインドしやすい。