インタラクティブUIを実現!QML Item.statesプログラミング例

2025-06-06

簡単に言うと、Item.statesは、QMLのItem(矩形や画像、テキストなど、画面上のあらゆる要素の基底クラス)が持つ、異なる視覚的・動作的設定の集合を定義するプロパティです。これにより、UIの見た目や振る舞いを、現在の「状態」に応じて簡単に切り替えることができます。

具体的な説明

  1. 状態(State)とは何か?

    • UIコンポーネントが取りうる異なる設定のことです。例えば、ボタンが「通常の状態」「ホバーされた状態」「押された状態」を持つように、アイテムもさまざまな状態を持つことができます。
    • 各状態は、アイテムのプロパティ(色、サイズ、位置、表示・非表示など)を変更したり、スクリプトを実行したり、子アイテムの構成を変更したりする一連の変更を定義します。
  2. Item.statesプロパティ

    • QMLのすべてのItemには、statesというプロパティがあります。これはStateオブジェクトのリスト(list<State>)を保持します。
    • このリストに、アイテムが取りうる異なる状態をStateオブジェクトとして追加していきます。
  3. デフォルトの状態

    • すべてのItemには、デフォルトの状態(""という空文字列で表される)が常に存在します。
    • Itemが最初にロードされたとき、またはstateプロパティが空文字列に設定されたときに適用される状態です。statesプロパティで明示的に定義されていないプロパティ値は、このデフォルトの状態の値が使用されます。
  4. Stateオブジェクトの定義

    • Stateオブジェクトは、以下の主要なプロパティを持ちます。
      • name: その状態を識別するためのユニークな名前(例: "pressed", "active", "hidden")。この名前を使って、アイテムのstateプロパティを切り替えることで状態を変更します。
      • PropertyChanges: その状態になったときに変更されるプロパティとその値を定義します。例えば、Rectanglecolorを「赤」に変更するといった設定が可能です。
      • StateChangeScript: 状態が変更されたときに実行されるスクリプトを定義します。
      • ParentChange: アイテムの親を変更します。
      • AnchorChanges: アイテムのアンカーを変更します。
      • when: 真偽値の式を評価し、その式がtrueになったときに自動的にその状態に切り替わるように設定できます。式がfalseに戻ると、アイテムはデフォルトの状態に戻ります(またはwhen句を持たない他の状態に切り替わります)。
  5. 状態の変更方法

    • アイテムのstateプロパティに、定義したStatenameを設定することで、状態を切り替えることができます。
      Rectangle {
          id: myRect
          width: 100; height: 100
          color: "blue"
      
          states: [
              State {
                  name: "redState"
                  PropertyChanges { target: myRect; color: "red" }
              },
              State {
                  name: "greenState"
                  PropertyChanges { target: myRect; color: "green" }
              }
          ]
      
          MouseArea {
              anchors.fill: parent
              onClicked: {
                  // クリックするたびに状態を切り替える例
                  if (myRect.state === "redState") {
                      myRect.state = "greenState";
                  } else {
                      myRect.state = "redState";
                  }
              }
          }
      }
      
    • whenプロパティを使用すると、条件に基づいて自動的に状態が変更されます。
  6. Transition(遷移)との組み合わせ

    • 状態の切り替えは、通常、瞬時に行われますが、Transition要素と組み合わせることで、プロパティの変化をアニメーションさせ、より滑らかなUI表現を実現できます。
  • 再利用性: 状態定義をコンポーネント内でカプセル化できるため、コンポーネントの再利用性が高まります。
  • アニメーションとの統合: 状態間のスムーズなアニメーションを簡単に実現でき、ユーザー体験を向上させます。
  • 複雑なUIの管理: 複数のプロパティや要素が同時に変更されるような複雑なUIの状態管理を簡素化します。
  • UIロジックの分離: UIの見た目や振る舞いを、アプリケーションのロジックから分離し、宣言的に記述できます。これにより、コードの可読性と保守性が向上します。


状態が切り替わらない、または意図しない状態になる

よくある原因

  • イベントハンドラ内での非同期的な状態変更
    • onClicked などのイベントハンドラ内で、すぐに状態が変更されないような非同期処理(例: Timertriggered)を行っている場合、ユーザーの操作とUIの同期がずれることがあります。
  • 状態がデフォルトに戻ってしまう
    • when プロパティを持つ状態の場合、その条件が false になると、QMLは自動的にデフォルトの状態 ("") に戻ろうとします。意図的にその状態を維持したい場合、別の制御方法(例えば、明示的な state プロパティの設定)が必要です。
  • State 定義内の PropertyChanges のターゲットミス
    • PropertyChangestarget プロパティが、変更したいアイテムではない別のアイテムを指している。
    • id のスコープの問題。例えば、Delegate 内で定義されたアイテムの id を、そのDelegate の外から直接参照しようとしている。
  • when 条件の誤り
    • Statewhen プロパティに指定した条件式が常に false になる、または意図したタイミングで true にならない。
    • 複数の when 条件を持つ状態があり、それらの評価順序や優先順位が複雑になっている。
  • state プロパティの設定ミス
    • Itemstate プロパティに、存在しない状態名を設定している。
    • タイプミスがある。
    • state プロパティが、意図しない場所で上書きされている。

トラブルシューティング

  • when 条件の単純化
    • 複雑な when 条件は、意図しない挙動の原因となりやすいです。可能であれば、より単純な条件に分割するか、複数の状態を組み合わせて実現することを検討します。
  • スコープの理解
    • id はその親要素内でのみ有効な識別子です。異なるスコープから直接 id を参照しようとしていないか確認します。必要であれば、プロパティエイリアス (property alias) や、親アイテムのプロパティを介してアクセスすることを検討します。
  • PropertyChanges の確認
    • PropertyChanges 内で設定しているプロパティ名と、そのプロパティが定義されているアイテムの id が正しいかを確認します。
  • QML Debugger の利用
    • Qt Creator の QML Debugger は、プロパティの現在の値や、バインディングの評価、状態の遷移などを視覚的に確認するのに非常に役立ちます。ブレークポイントを設定してステップ実行することも可能です。
  • デバッグ出力の活用
    • console.log() を使って、現在の state プロパティの値や、when 条件の評価結果を出力します。
    • onStateChanged シグナルを接続し、状態がいつ、何に変化したかを確認します。
      Item {
          id: myItem
          // ...
          states: [ /* ... */ ]
      
          onStateChanged: {
              console.log("State changed to: " + myItem.state);
          }
      }
      

アニメーションが期待通りに動作しない、またはカクつく

よくある原因

  • バインディングの連鎖
    • PropertyChanges が複数のプロパティを連鎖的に変更し、その結果として予期せぬパフォーマンスの低下を招いている。
  • パフォーマンスの問題
    • 一度に多数のプロパティが変更される、または複雑なグラフィック処理(シェーダー、ぼかし効果など)が含まれる状態遷移は、パフォーマンスに影響を与え、カクつきの原因となります。
    • 高解像度の画像や、CPU/GPUに負荷のかかる計算がQML内で頻繁に行われている。
    • PropertyChanges 内で、アニメーションに適さないプロパティ(例: visible)を直接変更している。opacityscale などを使った方がスムーズなアニメーションになりやすいです。
  • Transition の定義不足/誤り
    • 状態遷移時のアニメーションを定義する Transition が定義されていない。
    • Transitionfromto プロパティが適切に設定されていない(* で全ての状態遷移をカバーすることも可能)。
    • Transition 内の PropertyAnimation などのアニメーション要素が、変更されるべきプロパティを対象にしていない。

トラブルシューティング

  • アニメーションの段階的な導入
    • まずアニメーションなしで状態遷移が正しく機能することを確認し、その後 Transition を追加していくと、問題の特定が容易になります。
  • 最適化の検討
    • visible: false を使って、非表示のアイテムはレンダリングされないようにします。
    • Loader コンポーネントを使って、必要な時にのみQMLコンポーネントをロードするようにします。
    • 不必要なバインディングを減らし、プロパティを直接設定できる場合はそうします。
    • 複雑なグラフィック効果が必要な場合は、QMLの機能だけでなく、OpenGL/Vulkan などでカスタム描画を行うことも検討します。
  • QML Profiler の利用
    • Qt Creator に付属している QML Profiler を使用して、アプリケーションのパフォーマンスボトルネックを特定します。特に、レンダリングループのどの部分で時間がかかっているか、CPUやGPUの使用状況などを分析できます。
  • Transition の確認
    • Transition ブロックが正しく配置され、対象となる状態遷移 (from, to) をカバーしているか確認します。
    • Animation 要素が、変更されるプロパティ名を正確に指定しているか確認します。

よくある原因

  • State 内でのstateプロパティの変更
    • State の定義内では、その target アイテムの state プロパティを直接変更することはできません。これは循環参照になるためです。もし状態の変更をトリガーしたい場合は、StateChangeScript や外部のロジックを使用する必要があります。
  • ネストされた states の問題
    • 複数のItemがそれぞれstatesを持つ場合、親アイテムの状態が子アイテムの状態に影響を与えることを理解していない。
    • 子アイテムの状態が親アイテムの状態にオーバーライドされたり、親アイテムの変更が子アイテムに伝播しなかったりすることがあります。
  • シンプルなテストケースで検証
    • 問題が複雑な状態の組み合わせで発生している場合、最小限のコードでその問題が再現できるか試してみます。これにより、原因の特定が容易になります。
  • 複雑な状態管理の設計
    • 複雑な状態ロジックを扱う場合は、StateMachine(Qt Quick Controls 2 にも関連クラスがあります)などのより高度な状態管理パターンを検討することも有効です。これは、より明確な状態遷移とロジックの分離を可能にします。
  • 状態階層の理解
    • QMLの状態は、アイテムの階層構造と密接に関連しています。親アイテムの状態は子アイテムに影響を与え、子アイテムは親の状態の一部をオーバーライドできます。この関係を明確に理解することが重要です。
  • Qt Creator のコンパイル出力/Issues パネル
    QMLの構文エラーや、Qtが検出した潜在的な問題は、Qt Creator のこれらのパネルに表示されます。
  • 公式ドキュメントの熟読
    Qt の公式ドキュメントは非常に充実しています。Item.statesTransition に関する詳細な説明や例を参照することで、多くの疑問が解決します。


基本的な状態変更 (プロパティの変更)

最も基本的な例は、アイテムのプロパティを状態に応じて変更することです。

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

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

    Rectangle {
        id: myRectangle
        width: 150
        height: 150
        x: 50
        y: 50
        color: "blue" // デフォルトの状態のRectangleの色

        // 状態を切り替えるためのMouseArea
        MouseArea {
            anchors.fill: parent
            onClicked: {
                // 現在の状態に応じて切り替える
                if (myRectangle.state === "redState") {
                    myRectangle.state = "greenState";
                } else if (myRectangle.state === "greenState") {
                    myRectangle.state = ""; // デフォルトの状態に戻す
                } else {
                    myRectangle.state = "redState";
                }
                console.log("Current state: " + myRectangle.state);
            }
        }

        // statesプロパティで状態を定義
        states: [
            State {
                name: "redState"
                // redStateになったときにmyRectangleの色を赤に変更
                PropertyChanges { target: myRectangle; color: "red" }
            },
            State {
                name: "greenState"
                // greenStateになったときにmyRectangleの色を緑に変更
                PropertyChanges { target: myRectangle; color: "green" }
            }
        ]
    }

    Text {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        text: "Rectangleをクリックして状態を切り替えてください"
        font.pointSize: 12
    }
}

解説

  • myRectangle.state = ""とすることで、デフォルトの状態(statesリストで明示的に定義されていない、初期設定の値)に戻すことができます。
  • MouseAreaonClickedハンドラで、myRectangle.stateプロパティの値を変更することで、状態を明示的に切り替えています。
  • PropertyChanges要素を使って、その状態になったときに変更したいプロパティとその値を定義します。targetプロパティで、どのアイテムのプロパティを変更するかを指定します。
  • Stateにはユニークなname(例: "redState", "greenState")があります。
  • statesプロパティに、Stateオブジェクトのリストを定義しています。
  • myRectangleというRectangleアイテムがあります。

when プロパティによる自動状態遷移

特定の条件がtrueになったときに自動的に状態が切り替わるように設定できます。

// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15 // CheckBoxを使用するため

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

    Column {
        anchors.centerIn: parent
        spacing: 20

        CheckBox {
            id: myCheckBox
            text: "有効にする"
        }

        Rectangle {
            id: statusIndicator
            width: 100
            height: 100
            radius: 50 // 円形にする
            color: "gray" // デフォルト(無効)の状態

            Text {
                anchors.centerIn: parent
                text: "無効"
                color: "white"
                font.pointSize: 14
            }

            states: [
                State {
                    name: "enabledState"
                    // myCheckBox.checked が true のときにこの状態になる
                    when: myCheckBox.checked

                    PropertyChanges { target: statusIndicator; color: "lightgreen" }
                    PropertyChanges { target: statusIndicator.children[0]; text: "有効" } // 子Textのテキストを変更
                }
            ]
        }
    }
}

解説

  • チェックが外れる(checked: falseになる)と、when条件がfalseになり、statusIndicatorは自動的にデフォルトの状態(color: "gray", text: "無効")に戻ります。
  • myCheckBoxがチェックされる(checked: trueになる)と、when条件がtrueになり、statusIndicatorは自動的に"enabledState"に切り替わります。
  • この状態のwhenプロパティはmyCheckBox.checkedにバインドされています。
  • statusIndicatorstatesプロパティに"enabledState"を定義しています。
  • myCheckBoxというCheckBoxと、statusIndicatorという円形のRectangleがあります。

Transition との組み合わせ (アニメーション)

状態の切り替えをアニメーションさせたい場合は、transitionsプロパティにTransitionを定義します。

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

Window {
    visible: true
    width: 600
    height: 400
    title: "States with Transitions Example"

    Rectangle {
        id: myButton
        width: 120
        height: 60
        x: 50
        y: 50
        color: "lightblue"
        radius: 10

        Text {
            anchors.centerIn: parent
            text: "通常"
            font.pointSize: 16
        }

        MouseArea {
            anchors.fill: parent
            onPressed: {
                myButton.state = "pressed"; // 押された状態
            }
            onReleased: {
                myButton.state = ""; // デフォルトの状態に戻す
            }
            onEntered: {
                if (myButton.state !== "pressed") {
                    myButton.state = "hovered"; // ホバーされた状態 (押されていない場合のみ)
                }
            }
            onExited: {
                if (myButton.state !== "pressed") {
                    myButton.state = ""; // デフォルトの状態に戻す (押されていない場合のみ)
                }
            }
        }

        states: [
            State {
                name: "hovered"
                PropertyChanges { target: myButton; color: "lightsteelblue"; scale: 1.1 }
                PropertyChanges { target: myButton.children[0]; text: "ホバー" }
            },
            State {
                name: "pressed"
                PropertyChanges { target: myButton; color: "cornflowerblue"; scale: 0.95 }
                PropertyChanges { target: myButton.children[0]; text: "押された" }
            }
        ]

        // 状態遷移のアニメーションを定義
        transitions: [
            Transition {
                // どの状態からどの状態への遷移でも適用 (*)
                // プロパティの変化をアニメーションさせる
                NumberAnimation { properties: "x,y,width,height,scale,radius"; duration: 150 }
                ColorAnimation { properties: "color"; duration: 150 }
                // Textのプロパティはアニメーションできないので、瞬時に変わる
                // PropertyAction { properties: "text"; duration: 0 } // これでも良いが、デフォルトで瞬時に変わる
            }
        ]
    }
}

解説

  • transitionsプロパティにTransitionを定義します。
    • from: "*", to: "*"(デフォルト)は、どの状態からどの状態への遷移でもこのアニメーションが適用されることを意味します。
    • NumberAnimationColorAnimationを使って、対応するプロパティ(scale, colorなど)の変化をアニメーションさせます。durationでアニメーションの時間を指定します。
  • MouseAreaを使って、マウスのイベント(押す、離す、入る、出る)に応じてmyButton.stateを切り替えます。
  • "hovered""pressed"という2つの状態を定義し、それぞれ色やスケール、テキストを変更するようにします。
  • myButtonというカスタムボタンのようなRectangleを作成します。

StateChangeScript によるスクリプト実行

状態が変更されたときに特定のJavaScriptコードを実行したい場合に使用します。

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

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

    Rectangle {
        id: myBox
        width: 100
        height: 100
        color: "orange"
        x: 50
        y: 50

        MouseArea {
            anchors.fill: parent
            onClicked: {
                if (myBox.state === "expanded") {
                    myBox.state = "";
                } else {
                    myBox.state = "expanded";
                }
            }
        }

        states: [
            State {
                name: "expanded"
                PropertyChanges { target: myBox; width: 200; height: 200; color: "purple" }

                // expanded状態になったときに実行されるスクリプト
                StateChangeScript {
                    script: {
                        console.log("Box expanded! Current size: " + myBox.width + "x" + myBox.height);
                    }
                }
            }
        ]

        transitions: [
            Transition {
                NumberAnimation { properties: "width,height"; duration: 200 }
                ColorAnimation { properties: "color"; duration: 200 }
            }
        ]
    }

    Text {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        text: "Boxをクリックして拡大・縮小してください"
        font.pointSize: 12
    }
}

解説

  • 注意
    StateChangeScript内で、そのStateが属するアイテムのstateプロパティを直接変更することはできません。これは循環参照になるためです。
  • ここではコンソールにログを出力していますが、より複雑なロジックや外部関数呼び出しなども可能です。
  • myBox"expanded"状態に遷移した際に、StateChangeScript内のscriptブロックが実行されます。

状態に応じてアイテムの親を変更したり、アンカーを変更したりすることもできます。

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

Window {
    visible: true
    width: 600
    height: 400
    title: "Parent and Anchor Changes Example"

    Rectangle {
        id: container1
        width: 250
        height: 250
        x: 20
        y: 20
        color: "lightgray"
        border.color: "black"
        border.width: 2

        Text { anchors.centerIn: parent; text: "Container 1"; font.pointSize: 14 }
    }

    Rectangle {
        id: container2
        width: 250
        height: 250
        x: 300
        y: 20
        color: "lightgray"
        border.color: "black"
        border.width: 2

        Text { anchors.centerIn: parent; text: "Container 2"; font.pointSize: 14 }
    }

    Rectangle {
        id: movableBox
        width: 80
        height: 80
        color: "red"
        // 初期状態はcontainer1の子として中央に配置
        parent: container1
        anchors.centerIn: parent

        MouseArea {
            anchors.fill: parent
            onClicked: {
                if (movableBox.state === "movedToContainer2") {
                    movableBox.state = ""; // デフォルト(Container1)に戻す
                } else {
                    movableBox.state = "movedToContainer2"; // Container2へ移動
                }
            }
        }

        states: [
            State {
                name: "movedToContainer2"
                // 親をcontainer2に変更
                ParentChange { target: movableBox; parent: container2 }
                // 新しい親の中央にアンカーを変更
                AnchorChanges { target: movableBox; anchors.centerIn: parent }
                // その他のプロパティも変更可能
                PropertyChanges { target: movableBox; color: "blue"; rotation: 360 }
            }
        ]

        transitions: [
            Transition {
                // 親の変更はアニメーションしないが、それ以外のプロパティはアニメーション可能
                NumberAnimation { properties: "x,y,rotation"; duration: 500 } // x, yは親が変更されても追従する
                ColorAnimation { properties: "color"; duration: 500 }
            }
        ]
    }

    Text {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        text: "赤いボックスをクリックして親を切り替えてください"
        font.pointSize: 12
    }
}
  • ParentChangeはアニメーション対象外ですが、x, yrotationcolorなどのプロパティはTransitionによってスムーズにアニメーションします。
  • MouseAreaのクリックで状態を切り替えることで、movableBoxcontainer1container2の間を移動し、見た目も変化します。
  • "movedToContainer2"という状態を定義し、その中で以下の変更を行います。
    • ParentChange: movableBoxparentcontainer2に変更します。
    • AnchorChanges: 親が変わった後のmovableBoxのアンカーをparentcenterInに設定します。
    • PropertyChanges: 色や回転などのプロパティも変更します。
  • movableBoxというRectanglecontainer1の中に配置されています。


プロパティの直接変更 (Property Bindings & Imperative Code)

Item.states を使わずに、QMLのプロパティバインディングやJavaScriptによる直接的なプロパティ変更で状態を表現する方法です。

特徴

  • 命令的 (Imperative)
    状態の遷移やプロパティの変更を、コードで明示的に指示します。
  • シンプル
    非常に単純なUIの変更であれば、states を使うよりも直接コードを書く方が早い場合があります。


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

Window {
    visible: true
    width: 400
    height: 300
    title: "Direct Property Change Example"

    Rectangle {
        id: myRectangle
        width: 150
        height: 150
        x: 50
        y: 50
        color: "blue"

        MouseArea {
            anchors.fill: parent
            onClicked: {
                if (myRectangle.color === "blue") {
                    myRectangle.color = "red";
                    myRectangle.width = 200;
                    myRectangle.height = 200;
                } else if (myRectangle.color === "red") {
                    myRectangle.color = "green";
                    myRectangle.width = 100;
                    myRectangle.height = 100;
                } else {
                    myRectangle.color = "blue";
                    myRectangle.width = 150;
                    myRectangle.height = 150;
                }
                console.log("Current color: " + myRectangle.color);
            }
        }
    }
}

メリット

  • 非常に小規模な変更や、状態の数が少ない場合にコード量が少なくなることがあります。

デメリット

  • 状態の宣言的な管理ができない
    各状態がどのように定義されているか一目でわかりにくくなります。
  • アニメーションとの統合が手間
    アニメーションを適用するには、PropertyAnimation などを手動でトリガーする必要があります。statestransitions のように自動的に連携しません。
  • コードが複雑化しやすい
    状態の数が増えたり、変更するプロパティが増えたりすると、if/else if の連鎖が長くなり、コードが読みにくく、保守しにくくなります。

Loader コンポーネントによる動的なコンポーネントのロード

UIコンポーネント全体を、特定の状態に応じて動的にロード/アンロードする方法です。これは、UIの状態が大きく異なる場合(例: 異なる画面の表示、複雑なダイアログの表示など)に特に有効です。

特徴

  • リソースの節約
    必要ないコンポーネントはロードされないため、メモリやCPUリソースの節約になります。
  • UIの切り替え
    コンポーネント全体を切り替えるのに適しています。


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

Window {
    visible: true
    width: 600
    height: 400
    title: "Loader Example"

    property string currentPage: "page1" // "page1", "page2"

    Column {
        anchors.top: parent.top
        anchors.horizontalCenter: parent.horizontalCenter
        spacing: 10
        Button {
            text: "Page 1"
            onClicked: parent.parent.currentPage = "page1"
        }
        Button {
            text: "Page 2"
            onClicked: parent.parent.currentPage = "page2"
        }
    }

    // currentPageプロパティに応じてコンポーネントをロード
    Loader {
        id: pageLoader
        anchors.fill: parent
        anchors.topMargin: 60 // ボタンの高さ分
        source: {
            if (currentPage === "page1") {
                return "Page1.qml";
            } else if (currentPage === "page2") {
                return "Page2.qml";
            }
            return ""; // 何もロードしない
        }

        // 遷移アニメーション
        transitions: Transition {
            to: "*"
            OpacityAnimator { duration: 500 }
            ScaleAnimator { duration: 500 }
        }
    }
}
// Page1.qml
import QtQuick 2.15
import QtQuick.Controls 2.15

Rectangle {
    width: 200; height: 200
    color: "lightgreen"
    radius: 10
    anchors.centerIn: parent

    Text {
        anchors.centerIn: parent
        text: "これはページ1です!"
        font.pointSize: 20
        color: "darkgreen"
    }
}
// Page2.qml
import QtQuick 2.15
import QtQuick.Controls 2.15

Rectangle {
    width: 250; height: 150
    color: "lightblue"
    radius: 10
    anchors.centerIn: parent

    Text {
        anchors.centerIn: parent
        text: "これはページ2です!"
        font.pointSize: 20
        color: "darkblue"
    }
}

メリット

  • 動的なコンポーネント生成
    実行時にQMLファイルを動的にロードできます。
  • リソース管理
    必要に応じてコンポーネントをロード・アンロードするため、リソースの効率的な利用が可能です。
  • 完全に異なるUIの切り替え
    コンポーネントの内容が大きく異なる場合に、コードの分離性が高まります。

デメリット

  • アニメーションの制御が複雑になる場合がある
    Loadertransitions を使うことで、ロード/アンロード時のアニメーションは可能ですが、個々のプロパティのアニメーションは、ロードされるコンポーネント内で別途定義する必要があります。
  • 状態の概念が分散
    Item.states のように単一のアイテム内でプロパティの集合として状態を管理するわけではないため、概念的に異なります。

C++ モデル/ロジックからの制御

QMLはUI層を担当し、複雑な状態管理やビジネスロジックはC++バックエンドで行うというアーキテクチャです。C++側で状態を管理し、QML側はその状態をプロパティバインディングやシグナル/スロットで受け取ってUIを更新します。

特徴

  • パフォーマンス
    処理負荷の高いロジックはC++で行うことでパフォーマンスを最適化できます。
  • テスト容易性
    C++のコードはQMLよりもテストしやすいです。
  • 強力なロジック管理
    複雑なビジネスロジックや、複数のUI要素にまたがる状態管理に適しています。

例 (概念)

C++側 (MyStateController.h)

#ifndef MYSTATECONTROLLER_H
#define MYSTATECONTROLLER_H

#include <QObject>
#include <QString>

class MyStateController : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString currentState READ currentState NOTIFY currentStateChanged)

public:
    explicit MyStateController(QObject *parent = nullptr);

    QString currentState() const;

public slots:
    void transitionToNextState(); // QMLから呼び出すスロット

signals:
    void currentStateChanged();

private:
    QString m_currentState;
};

#endif // MYSTATECONTROLLER_H

C++側 (MyStateController.cpp)

#include "MyStateController.h"

MyStateController::MyStateController(QObject *parent)
    : QObject(parent), m_currentState("default")
{
}

QString MyStateController::currentState() const
{
    return m_currentState;
}

void MyStateController::transitionToNextState()
{
    if (m_currentState == "default") {
        m_currentState = "active";
    } else if (m_currentState == "active") {
        m_currentState = "finished";
    } else {
        m_currentState = "default";
    }
    emit currentStateChanged(); // QMLに状態変更を通知
}

QML側 (main.qml)

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

Window {
    visible: true
    width: 400
    height: 300
    title: "C++ State Control Example"

    // C++オブジェクトをQMLに公開
    // main.cppで qmlRegisterSingletonType<MyStateController>("com.example.app", 1, 0, "MyStateController", myStateControllerProvider);
    // のように登録しておく
    property var stateController: MyStateController // 登録したMyStateControllerのインスタンス

    Rectangle {
        id: myRectangle
        width: 150
        height: 150
        x: 50
        y: 50
        // C++のcurrentStateプロパティにバインド
        color: stateController.currentState === "default" ? "blue" :
               stateController.currentState === "active" ? "green" :
               stateController.currentState === "finished" ? "red" : "gray"

        Text {
            anchors.centerIn: parent
            text: stateController.currentState // C++の状態を表示
            font.pointSize: 16
            color: "white"
        }

        MouseArea {
            anchors.fill: parent
            onClicked: {
                stateController.transitionToNextState(); // C++のメソッドを呼び出す
            }
        }
    }
}

メリット

  • パフォーマンス
    処理の重い部分をC++で実装することで、高いパフォーマンスが期待できます。
  • 複雑なロジックの管理
    状態間の複雑な依存関係や、外部データとの連携など、高度な状態管理に適しています。
  • 分離と再利用性
    UIとビジネスロジックが明確に分離され、それぞれの再利用性が高まります。

デメリット

  • 単純なUIにはオーバーキル
    非常にシンプルな状態管理には、C++を持ち出すのは大げさです。
  • 設定が複雑
    QMLとC++の連携設定(qmlRegisterType, Q_PROPERTY, Q_INVOKABLEなど)が必要で、学習コストがかかります。

これらのコントロールは、内部的に複数の「ページ」や「タブ」を持ち、それぞれが独立した状態のUIとして機能します。これは特定の種類の状態管理(ページ遷移、タブ切り替え)に特化した、高レベルな代替手段と言えます。

特徴

  • 組み込みのアニメーション
    スワイプやタブ切り替えのアニメーションが組み込まれています。
  • 特定のUIパターンに特化
    ページングやタブ切り替えなどの一般的なUIパターンを簡単に実装できます。


// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

Window {
    visible: true
    width: 600
    height: 400
    title: "SwipeView / TabBar Example"

    ColumnLayout {
        anchors.fill: parent

        TabBar {
            id: tabBar
            Layout.fillWidth: true
            currentIndex: swipeView.currentIndex // TabBarとSwipeViewを同期

            TabButton { text: "写真" }
            TabButton { text: "ビデオ" }
            TabButton { text: "設定" }
        }

        SwipeView {
            id: swipeView
            Layout.fillWidth: true
            Layout.fillHeight: true
            currentIndex: tabBar.currentIndex // SwipeViewとTabBarを同期

            // 各ページは独立したItem (状態) として機能
            Rectangle {
                color: "lightgreen"
                Text { anchors.centerIn: parent; text: "ギャラリー写真"; font.pointSize: 24 }
            }
            Rectangle {
                color: "lightblue"
                Text { anchors.centerIn: parent; text: "ギャラリービデオ"; font.pointSize: 24 }
            }
            Rectangle {
                color: "lightcoral"
                Text { anchors.centerIn: parent; text: "設定ページ"; font.pointSize: 24 }
            }
        }
    }
}

メリット

  • 優れたUX
    スワイプジェスチャーやタブ切り替えなどの自然なUIインタラクションが提供されます。
  • 迅速な開発
    一般的なUIパターンを非常に少ないコードで実装できます。

デメリット

  • 汎用性の欠如
    これらは特定のUIパターンに特化しており、一般的な状態管理の解決策ではありません。

Item.states はQMLにおける宣言的な状態管理の標準的な方法であり、ほとんどのUIの状態変化に適しています。しかし、上記のような代替方法も、特定の要件や複雑度に応じて適切な選択肢となり得ます。

  • SwipeView/TabBar
    特定のUIパターン(ページング、タブ切り替え)を効率的に実装したい場合。
  • C++との連携
    複雑なビジネスロジック、複数のUI要素にまたがる状態、高いパフォーマンスが求められる場合。
  • Loader
    UI全体が大きく異なる、またはリソースを動的に管理したい場合。
  • 単純なプロパティ変更
    非常に小さな変更のみで、状態という概念を導入するほどではない場合。