Qt QML Flickableのスクロール挙動をマスター!flickableDirectionからカスタム実装まで

2025-05-27

Flickableは、画面に表示しきれない大きなコンテンツを、ユーザーがドラッグしたりフリックしたりして閲覧できるようにするための要素です。このflickableDirectionプロパティを設定することで、フリック可能な方向を制限したり、自動的に判断させたりすることができます。

以下に設定可能な値とその説明を挙げます。

  • Flickable.HorizontalAndVerticalFlick

    • 水平方向と垂直方向の両方のフリックを許可します。
  • Flickable.VerticalFlick

    • 垂直方向のフリックのみを許可します。水平方向のフリックはできません。
  • Flickable.HorizontalFlick

    • 水平方向のフリックのみを許可します。垂直方向のフリックはできません。
  • Flickable.AutoFlickIfNeeded (QtQuick 2.7以降)

    • コンテンツの高さ(contentHeight)がFlickable自体の高さ(height)よりも大きい場合、垂直方向のフリックを許可します。
    • コンテンツの幅(contentWidth)がFlickable自体の幅(width)よりも大きい場合、水平方向のフリックを許可します。
    • AutoFlickDirectionとの違いは、コンテンツが実際に表示領域からはみ出している場合のみフリックを許可する点です。コンテンツがぴったり収まっている場合はフリックできません。
    • コンテンツの高さ(contentHeight)がFlickable自体の高さ(height)と異なる場合、垂直方向のフリックを許可します。
    • コンテンツの幅(contentWidth)がFlickable自体の幅(width)と異なる場合、水平方向のフリックを許可します。
    • つまり、コンテンツがFlickableの表示領域からはみ出している方向に自動的にフリックを許可します。

使用例

import QtQuick

Rectangle {
    width: 300
    height: 200
    color: "lightgray"

    Flickable {
        id: myFlickable
        width: parent.width
        height: parent.height
        // コンテンツの幅と高さをFlickableよりも大きく設定
        contentWidth: 500
        contentHeight: 400
        clip: true // コンテンツがFlickableの境界からはみ出さないようにクリップ

        // 垂直方向のみフリックを許可する例
        flickableDirection: Flickable.VerticalFlick

        Rectangle {
            width: 500
            height: 400
            color: "lightblue"
            Text {
                text: "これはフリック可能な大きなコンテンツです。\n垂直方向にのみスクロールできます。"
                anchors.centerIn: parent
                font.pixelSize: 20
                wrapMode: Text.WrapAtWordBoundaryOrAnywhere
                width: 450
                height: 350
            }
        }
    }
}

この例では、myFlickableflickableDirectionFlickable.VerticalFlickに設定しているため、ユーザーは縦方向にのみコンテンツをフリックしてスクロールできます。横方向にフリックしようとしても動きません。



フリックできない、または意図しない方向にしかフリックできない

よくある原因と解決策

  • flickableDirection の設定ミス

    • 原因
      意図しない方向にflickableDirectionが設定されている。例えば、垂直方向のフリックを期待しているのにFlickable.HorizontalFlickに設定しているなど。
    • 解決策
      目的のフリック方向に合わせてflickableDirectionを正確に設定してください。
  • ネストされたFlickableやMouseAreaとの競合

    • 原因
      複数のFlickableをネストしたり、Flickableの子要素にMouseAreaなどを配置したりすると、イベントの伝播で競合が発生し、意図しない要素がフリック操作を受け取ってしまうことがあります。
    • 解決策
      • イベントの伝播を考慮し、適切なMouseAreapropagateComposedEventsを使用したり、FlickableonPressAndHoldなどで特定のインタラクションを制御したりします。
      • 本当にネストされたFlickableが必要かどうかを再検討します。多くの場合、単一のFlickableと適切に配置されたコンテンツで実現できます。
      • Flickable内にListViewGridViewを配置する場合、通常、ListViewGridView自体がスクロール機能を持っているため、その親にFlickableを置く必要はありません。もし置く場合は、ListViewGridViewのスクロールとFlickableのフリックが競合しないように注意が必要です。
  • interactive プロパティの false 設定

    • 原因
      Flickableinteractiveプロパティがfalseに設定されていると、ユーザーとのインタラクション(フリック、ドラッグなど)が全て無効になります。
    • 解決策
      interactive: true に設定するか、デフォルトのままにしてください(デフォルトはtrue)。
  • clip: false の設定

    • 原因
      Flickableはデフォルトでclip: falseです。これは、コンテンツがFlickableの境界からはみ出して表示されることを意味します。フリック動作自体には影響しませんが、見た目上、コンテンツが画面外に隠れていないため、フリックの必要性を感じにくい場合があります。
    • 解決策
      コンテンツをFlickableの境界内でクリップしたい場合は、clip: trueを設定してください。これにより、フリックの必要性が視覚的に明確になります。
    Flickable {
        width: 300
        height: 200
        contentWidth: 500
        contentHeight: 400
        clip: true // これにより、Flickableの境界外のコンテンツが非表示になる
        // ...
    }
    
    • 原因
      Flickableがコンテンツをフリック可能にするには、contentWidthFlickablewidthより大きいか、contentHeightFlickableheightより大きい必要があります。特にAutoFlickDirectionAutoFlickIfNeededを使用している場合、この条件が満たされないとフリックは発生しません。
    • 解決策
      コンテンツがFlickableの表示領域からはみ出すように、contentWidthまたはcontentHeightを適切に設定してください。
    Flickable {
        width: 300
        height: 200
        // コンテンツがFlickableの表示領域からはみ出るように設定
        contentWidth: 500
        contentHeight: 400
        // flickableDirection: Flickable.AutoFlickDirection // デフォルトでも可
    
        // 中のコンテンツ
        Rectangle {
            width: 500
            height: 400
            color: "lightblue"
        }
    }
    

スクロールがスムーズでない、またはぎこちない

よくある原因と解決策

  • interactive プロパティの false 設定(一時的な問題)

    • 原因
      アニメーション中など、一時的にinteractivefalseに設定し、その後trueに戻す際に、タイミングによってはフリックがスムーズでないと感じられることがあります。
    • 解決策
      interactiveプロパティの変更を慎重に行い、ユーザーがフリックしようとするタイミングと重ならないようにします。
  • 過剰なレンダリング

    • 原因
      Flickable内のコンテンツが非常に複雑であったり、大量の要素を含んでいたりすると、レンダリング負荷が高まり、フリックがぎこちなく感じられることがあります。特に、Repeaterなどで大量のアイテムを生成している場合。
    • 解決策
      • ListViewGridViewなど、ビューポート外のアイテムを効率的に破棄・再利用するQML要素の使用を検討してください。これらはFlickableの内部的な最適化を多く含んでいます。
      • 複雑なグラフィック要素やアニメーションを最適化します。
      • 不必要なBindingPropertyAnimationがないか確認します。
      • OpenGLレンダリングが有効になっているか確認します(通常はデフォルトで有効)。

Flickableの初期位置がずれる

よくある原因と解決策

  • レイアウトの依存関係

    • 原因
      Flickableのサイズや位置が、他の要素のレイアウトに依存している場合、その依存関係が複雑になると、初期表示がずれることがあります。
    • 解決策
      レイアウトの依存関係をシンプルにし、各要素のサイズと位置が明確になるように設定を見直してください。anchorsを適切に使用し、循環参照にならないように注意します。
  • contentX, contentY の初期設定

    • 原因
      FlickablecontentXcontentYプロパティは、コンテンツの表示開始位置を制御します。これらが意図しない値に設定されていると、初期位置がずれることがあります。
    • 解決策
      アプリケーションの要件に合わせて、contentXcontentYを適切に初期化してください(例: contentX: 0, contentY: 0)。

開発環境でのみ動作し、実機で動作しない

よくある原因と解決策

  • タッチイベントの認識

    • 原因
      開発環境(デスクトップ)ではマウスでフリック操作をシミュレートできますが、実機ではタッチイベントが正しく認識されないことがあります。
    • 解決策
      デバイスドライバやQtのバージョンが適切か確認します。タッチスクリーンが正しくキャリブレーションされているか確認します。
  • シンプルなコードで再現
    問題が発生した場合、最小限のQMLコードでその現象を再現できるか試してみてください。これにより、問題の切り分けが容易になります。
  • デバッグ出力の活用
    console.log()を使用して、Flickablewidth, height, contentWidth, contentHeight, flickableDirection, contentX, contentYなどのプロパティの値を常に監視し、期待通りの値になっているか確認してください。


準備: 基本となるFlickable構造

以下の各例では、共通のFlickableと、その中に配置される大きなRectangleを使用します。これにより、Flickableの表示領域からはみ出すコンテンツを用意し、フリック動作を確認できるようにします。

// main.qml

import QtQuick
import QtQuick.Window

Window {
    width: 400
    height: 300
    visible: true
    title: "Flickable.flickableDirection Examples"

    // フリック可能な領域
    Flickable {
        id: myFlickable
        width: 300 // Flickable自身の幅
        height: 200 // Flickable自身の高さ
        anchors.centerIn: parent // ウィンドウ中央に配置
        clip: true // コンテンツをFlickableの境界内でクリップする

        // フリックされるコンテンツ
        // Flickableのサイズ (300x200) よりも大きくする
        contentWidth: 600 // 左右にフリックできるように幅を大きくする
        contentHeight: 400 // 上下にフリックできるように高さを大きくする

        Rectangle {
            width: 600
            height: 400
            color: "lightblue"
            Text {
                id: contentText
                text: "フリック可能なコンテンツです。\nこのテキストと背景はFlickableの表示領域よりも大きいです。"
                anchors.centerIn: parent
                font.pixelSize: 20
                color: "black"
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
                wrapMode: Text.WordWrap
            }
        }

        // ここに flickableDirection プロパティを設定します
        // flickableDirection: Flickable.AutoFlickDirection // 各例で設定

        // 状態表示用のText (オプション)
        Text {
            anchors.top: myFlickable.bottom
            anchors.horizontalCenter: myFlickable.horizontalCenter
            y: 10
            font.pixelSize: 14
            color: "darkblue"
            text: "Flickable Direction: " + (function() {
                if (myFlickable.flickableDirection === Flickable.AutoFlickDirection) return "AutoFlickDirection";
                if (myFlickable.flickableDirection === Flickable.AutoFlickIfNeeded) return "AutoFlickIfNeeded";
                if (myFlickable.flickableDirection === Flickable.HorizontalFlick) return "HorizontalFlick";
                if (myFlickable.flickableDirection === Flickable.VerticalFlick) return "VerticalFlick";
                if (myFlickable.flickableDirection === Flickable.HorizontalAndVerticalFlick) return "HorizontalAndVerticalFlick";
                return "Unknown";
            })()
        }
    }
}

上記のmain.qmlをベースとして、flickableDirectionプロパティを変更していきます。

Flickable.AutoFlickDirection (デフォルト)

説明
これがFlickableのデフォルトの動作です。コンテンツの幅がFlickableの幅と異なる場合(contentWidth != width)、水平方向のフリックを許可します。同様に、コンテンツの高さがFlickableの高さと異なる場合(contentHeight != height)、垂直方向のフリックを許可します。

コード例

// main.qml の Flickable 内に記述
flickableDirection: Flickable.AutoFlickDirection
// もしくは、flickableDirectionプロパティを省略(デフォルト動作のため)

動作
上記の基本構造では、contentWidth (600) はwidth (300) よりも大きく、contentHeight (400) はheight (200) よりも大きいため、水平方向と垂直方向の両方にフリックできるようになります。

Flickable.AutoFlickIfNeeded (QtQuick 2.7以降)

説明
AutoFlickDirectionと似ていますが、より厳密な条件でフリックを許可します。コンテンツの幅がFlickableの幅よりも大きい場合(contentWidth > width)、水平方向のフリックを許可します。コンテンツの高さがFlickableの高さよりも大きい場合(contentHeight > height)、垂直方向のフリックを許可します。コンテンツがぴったりフィットしている場合 (contentWidth === width など) はフリックを許可しません。

コード例

// main.qml の Flickable 内に記述
flickableDirection: Flickable.AutoFlickIfNeeded

動作
基本構造では、contentWidth > width (600 > 300) かつ contentHeight > height (400 > 200) なので、この場合も水平方向と垂直方向の両方にフリックできるようになります。 もし、contentWidth: 300 と設定した場合、水平方向のフリックはできなくなります。

Flickable.HorizontalFlick

説明
水平方向のフリックのみを明示的に許可します。垂直方向のフリックは完全に無効になります。

コード例

// main.qml の Flickable 内に記述
flickableDirection: Flickable.HorizontalFlick

動作
この設定では、左右にコンテンツをフリックできますが、上下にフリックしようとしても動きません。

Flickable.VerticalFlick

説明
垂直方向のフリックのみを明示的に許可します。水平方向のフリックは完全に無効になります。

コード例

// main.qml の Flickable 内に記述
flickableDirection: Flickable.VerticalFlick

動作
この設定では、上下にコンテンツをフリックできますが、左右にフリックしようとしても動きません。

説明
水平方向と垂直方向の両方のフリックを明示的に許可します。contentWidth/contentHeightwidth/heightの比較は行われません。設定された方向のフリックは常に許可されます。

コード例

// main.qml の Flickable 内に記述
flickableDirection: Flickable.HorizontalAndVerticalFlick

動作
この設定では、コンテンツがFlickableの領域に収まっていても、理論的には両方向にフリック操作が可能です(ただし、コンテンツがはみ出していない場合は動きが見えません)。基本構造では、コンテンツがはみ出しているため、両方向にフリックできます。

Flickable.flickableDirectionプロパティは、ユーザーがコンテンツをどのように操作できるかを細かく制御するための重要なツールです。特にモバイルアプリケーションなど、タッチ操作が主流の環境では、意図しないフリック方向を制限することで、より直感的で快適なユーザーエクスペリエンスを提供できます。

  • HorizontalAndVerticalFlick
    常に両方向にフリックを許可したい場合に使用します。
  • HorizontalFlick / VerticalFlick
    特定の方向にしかフリックさせたくない場合に明示的に設定します。
  • AutoFlickDirection / AutoFlickIfNeeded
    コンテンツのサイズに応じて自動でフリック方向を決定したい場合に便利です。


FlickableのinteractiveプロパティとcontentX/Yの直接操作

説明
Flickableinteractiveプロパティをfalseに設定することで、ユーザーによる通常のフリック操作を無効にできます。その上で、FlickablecontentXcontentYプロパティをQMLのコードやJavaScript関数から直接変更することで、プログラム的にスクロールを制御できます。これは、スクロールバーのドラッグやボタンクリックによるスクロール、特定のイベント(例: データのロード完了)に応じたスクロールなど、カスタムのスクロールトリガーを実装する際に有用です。

メリット

  • スクロール位置を非常に細かく制御できます。
  • Flickableの内部的な物理シミュレーション(慣性スクロールなど)の一部は利用できます。

デメリット

  • interactive: falseにすると、ユーザーがフリックで操作できなくなるため、別途UI要素(スクロールバーなど)を提供する必要があります。
  • フリックジェスチャー自体を自分で実装する必要がある場合、複雑になります。

コード例

import QtQuick
import QtQuick.Window

Window {
    width: 400
    height: 300
    visible: true
    title: "Custom Scroll with Buttons"

    Flickable {
        id: customFlickable
        width: 300
        height: 200
        anchors.centerIn: parent
        clip: true

        contentWidth: 600 // 横に長いコンテンツ
        contentHeight: 400 // 縦に長いコンテンツ

        interactive: false // ユーザーによる直接フリックを無効化

        Rectangle {
            width: 600
            height: 400
            color: "lightgreen"
            Text {
                anchors.centerIn: parent
                text: "このコンテンツはボタンでスクロールします。\nFlickableの慣性スクロールは有効です。"
                font.pixelSize: 18
                wrapMode: Text.WordWrap
            }
        }

        // 垂直スクロールボタン
        Button {
            id: scrollDownButton
            text: "下にスクロール"
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.top: parent.bottom
            y: 10
            onClicked: {
                // contentYを直接操作してスクロール
                customFlickable.contentY = Math.min(customFlickable.contentY + 50,
                                                  customFlickable.contentHeight - customFlickable.height);
            }
        }

        Button {
            id: scrollUpButton
            text: "上にスクロール"
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.top: scrollDownButton.bottom
            y: 5
            onClicked: {
                customFlickable.contentY = Math.max(customFlickable.contentY - 50, 0);
            }
        }

        // 水平スクロールボタン
        Button {
            id: scrollRightButton
            text: "右にスクロール"
            anchors.left: parent.right
            anchors.verticalCenter: parent.verticalCenter
            x: 10
            onClicked: {
                customFlickable.contentX = Math.min(customFlickable.contentX + 50,
                                                  customFlickable.contentWidth - customFlickable.width);
            }
        }

        Button {
            id: scrollLeftButton
            text: "左にスクロール"
            anchors.left: scrollRightButton.right
            anchors.verticalCenter: parent.verticalCenter
            x: 5
            onClicked: {
                customFlickable.contentX = Math.max(customFlickable.contentX - 50, 0);
            }
        }
    }
}

ScrollView (Qt Quick Controls 2) の利用

説明
ScrollViewは、内部的にFlickableを使用しており、プラットフォームネイティブなスクロールバーを提供します。ScrollViewは通常、flickableDirectionを自動的に決定しますが、ScrollBar.horizontal.policyScrollBar.vertical.policyプロパティを使ってスクロールバーの表示ポリシーを制御し、結果としてフリックの有効/無効に間接的に影響を与えることができます。例えば、ScrollBar.horizontal.policy: ScrollBar.AlwaysOffとすれば、水平スクロールバーは表示されず、水平方向のフリックもユーザーには推奨されない動きになります(ただし、完全には無効化されない場合もあります)。

メリット

  • 複雑なデータリストを表示する際に、ListViewGridViewを内包して使用することが推奨されます。
  • Flickableの基本的な機能に加え、スクロールバーのポリシー制御が可能です。
  • プラットフォームに合わせたスクロールバーが自動的に提供されます。

デメリット

  • Flickableよりも抽象化されているため、低レベルな制御はしにくい場合があります。
  • flickableDirectionのような直接的なフリック方向制御は提供されません。

コード例

import QtQuick
import QtQuick.Window
import QtQuick.Controls

Window {
    width: 400
    height: 300
    visible: true
    title: "ScrollView Example"

    ScrollView {
        width: 300
        height: 200
        anchors.centerIn: parent

        // 垂直スクロールバーのみを表示し、水平スクロールバーを無効にする例
        // これにより、実質的に垂直フリックのみを推奨するUIになる
        ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
        ScrollBar.vertical.policy: ScrollBar.AsNeeded // 必要に応じて表示

        Rectangle {
            width: 500 // 横に長いコンテンツ
            height: 400 // 縦に長いコンテンツ
            color: "lightcoral"
            Text {
                anchors.centerIn: parent
                text: "ScrollViewは、コンテンツがはみ出す方向にスクロールバーを表示します。\n水平スクロールバーは無効です。"
                font.pixelSize: 18
                wrapMode: Text.WordWrap
            }
        }
    }
}

MouseAreaとcontentX/Yの組み合わせ (カスタムスクロール実装)

説明
Flickableを使用せず、より低レベルなタッチ/マウスジェスチャーを自分で処理して、コンテンツのx, yプロパティを直接操作する方法です。MouseAreaを使ってドラッグ操作を検出し、その移動量に基づいてコンテンツの位置を更新します。フリック(慣性スクロール)のシミュレーションも、自分で速度計算とタイマーを使ったアニメーションを実装する必要があります。

メリット

  • 非常に特殊なインタラクション(例: 円形スクロール、無限スクロールなど)を実装できます。
  • スクロール動作を完全に自由にカスタマイズできます(フリックの減速、バウンドエフェクトなど)。

デメリット

  • パフォーマンスの最適化が難しい場合があります。
  • 慣性スクロールや境界バウンドなどの複雑な物理シミュレーションを自力で実装する必要があり、開発コストが高いです。

コード例

import QtQuick
import QtQuick.Window

Window {
    width: 400
    height: 300
    visible: true
    title: "Pure Custom Scroll"

    Item {
        id: viewport
        width: 300
        height: 200
        anchors.centerIn: parent
        clip: true // 必須: これがないとコンテンツがはみ出して表示される

        property real contentX: 0
        property real contentY: 0

        // スクロールされるコンテンツ
        Rectangle {
            id: content
            width: 600
            height: 400
            color: "lightskyblue"
            x: viewport.contentX
            y: viewport.contentY
            Text {
                anchors.centerIn: parent
                text: "これはMouseAreaで制御されるカスタムスクロールです。\n慣性スクロールはありません。"
                font.pixelSize: 18
                wrapMode: Text.WordWrap
            }
        }

        MouseArea {
            anchors.fill: parent
            property real lastX: 0
            property real lastY: 0

            // ドラッグ開始時の処理
            onPressed: (mouse) => {
                lastX = mouse.x;
                lastY = mouse.y;
            }

            // ドラッグ中の処理
            onPositionChanged: (mouse) => {
                // contentXとcontentYをMouseAreaのドラッグ量に応じて更新
                // xをドラッグするとcontentXが変化
                viewport.contentX += mouse.x - lastX;
                // yをドラッグするとcontentYが変化
                viewport.contentY += mouse.y - lastY;

                // 境界チェック (FlickableのboundsBehaviorに相当するものを手動で実装)
                // 左端と上端
                if (viewport.contentX > 0) viewport.contentX = 0;
                if (viewport.contentY > 0) viewport.contentY = 0;
                // 右端と下端
                if (viewport.contentX < viewport.width - content.width)
                    viewport.contentX = viewport.width - content.width;
                if (viewport.contentY < viewport.height - content.height)
                    viewport.contentY = viewport.height - content.height;

                lastX = mouse.x;
                lastY = mouse.y;
            }
        }
    }
}


上記の例では単純なドラッグ移動のみを実装しており、フリックのような慣性スクロールは含まれていません。慣性スクロールを実装するには、onReleased時にマウスの速度を計算し、NumberAnimationBehaviorなどを用いてcontentX/contentYをアニメーションさせる必要があります。これは非常に複雑になるため、通常はFlickableを使用することが強く推奨されます。

  • Flickableの挙動では実現できない、非常に特殊なスクロールやジェスチャーインタラクションが必要な場合
    interactive: falsecontentX/Yの直接操作、またはMouseAreaを使った完全なカスタム実装を検討します。ただし、これらは開発コストが高く、慎重なパフォーマンス最適化が必要です。
  • カスタムスクロールバーが必要な場合や、ListView/GridViewを扱う場合
    ScrollViewを検討してください。
  • ほとんどの場合、Flickable.flickableDirectionが最適です。 Qtが提供する優れたフリック物理とパフォーマンスを享受できます。