Image.paintedHeightだけじゃない?Qt/QML画像表示の代替テクニック

2025-05-31

Qtプログラミングにおける Image.paintedHeight は、QMLの Image 要素が実際に描画される高さを表す読み取り専用のプロパティです。

通常、Image 要素の height プロパティを設定すると、その要素の高さが指定され、画像はそのサイズに合わせてスケーリングされます。しかし、fillMode プロパティを使って画像の表示方法を調整した場合、height で指定した要素の高さと、実際に画面に描画される画像の高さが異なることがあります。

例えば、fillMode: Image.PreserveAspectFit(アスペクト比を維持してフィットさせる)を設定した場合、Image 要素の height が画像の本来のアスペクト比と合わない場合、画像は要素の高さに合わせて最大限に表示されますが、余白が生じることがあります。このとき、paintedHeight は余白を含まない、画像そのものが実際に描画されている高さを返します。

  • 読み取り専用
    このプロパティは値を設定することはできず、現在の描画状態に基づいてQtによって自動的に計算されます。
  • レイアウト調整
    fillMode を使用して画像のアスペクト比を維持しつつ表示する場合、画像の周りに余白(またはクロップされた部分)が生じることがあります。paintedHeight を使うことで、その余白のサイズなどを計算し、他のUI要素の配置を正確に調整するのに役立ちます。
  • 実際の描画サイズ
    Image 要素の widthheight プロパティで指定されたサイズではなく、fillMode などによって実際に描画されている画像のピクセル単位の高さを取得できます。


paintedHeight が期待通りに更新されない

これは最も一般的な問題の一つです。Image.paintedHeight は画像のロードや fillMode などのプロパティ変更後に自動的に更新されますが、特定の条件下ではすぐに反映されないことがあります。

原因

  • タイミングの問題
    QMLのバインディングやコンポーネントのライフサイクルによっては、paintedHeight の値が必要なタイミングでまだ確定していないことがあります。
  • キャッシュ
    Qt の画像キャッシュメカニズムにより、画像の内容が変更されても、source プロパティが同じままだと再ロードされず、paintedHeight が古い値を返すことがあります。
  • 画像の非同期ロード
    ネットワークからの画像や大きなローカル画像は非同期でロードされるため、Image 要素が作成された直後には paintedHeight がまだ正確な値を持っていない場合があります。

トラブルシューティング

  • QQuickImageProvider の利用
    C++ 側で QQuickImageProvider を実装している場合、画像が変更されたときにプロバイダ側で適切なキャッシュ無効化や更新通知を行う必要があります。
  • cache: false の設定
    Image 要素の cache プロパティを false に設定すると、キャッシュの使用を無効にできます。これは、頻繁に更新される画像に対して有効ですが、パフォーマンスに影響を与える可能性があります。
    Image {
        id: myImage
        source: "path/to/my/image.png"
        cache: false // キャッシュを無効にする
    }
    
  • source の変更を強制する (キャッシュの問題)
    画像の元データが変更されたのに paintedHeight が更新されない場合、QML エンジンに画像を再ロードさせるために、source プロパティを一時的に空文字列などに設定し、その後正しいパスに戻す方法があります。
    Image {
        id: myImage
        source: "path/to/my/image.png"
        property string currentSource: source // source を保持するプロパティ
    
        function reloadImage() {
            var oldSource = currentSource;
            currentSource = ""; // 一時的に空にする
            currentSource = oldSource; // 元に戻して再ロードをトリガー
        }
    
        onStatusChanged: {
            if (myImage.status === Image.Ready) {
                console.log("Painted Height after reload:", myImage.paintedHeight);
            }
        }
    }
    
    // 必要に応じて myImage.reloadImage() を呼び出す
    
    より確実な方法としては、source URL にタイムスタンプやバージョン番号のようなユニークなクエリパラメータを追加して、URL を毎回異なるものにすることです。
    Image {
        id: myImage
        property int imageVersion: 0 // バージョン管理用
        source: `path/to/my/image.png?v=${imageVersion}` // クエリパラメータを追加
    
        function updateImage() {
            imageVersion++; // バージョンを更新して再ロードを強制
        }
    }
    
  • onStatusChanged シグナルハンドラの使用
    画像が完全にロードされたことを確認するために、Image.Ready ステータスをチェックします。
    Image {
        id: myImage
        source: "path/to/my/image.png"
        onStatusChanged: {
            if (myImage.status === Image.Ready) {
                console.log("Painted Height:", myImage.paintedHeight);
                // ここで paintedHeight を使用したロジックを実行
            }
        }
    }
    

paintedHeight が 0 または無効な値を返す

原因

  • C++との連携の問題
    QQuickImageProvider を使用している場合、C++側で画像データが正しくQMLに渡されていない。
  • 画像の読み込みエラー
    画像ファイルが破損している。
  • リソースパスの誤り
    qrc: プレフィックスを使用している場合、リソースファイル (.qrc) へのパス設定が間違っている。特に、QMLでは qrc:/ プレフィックスが必要です(ウィジェットでは : / の場合がある)。
  • 画像形式の非対応
    Qt がサポートしていない画像形式(例: 特定のエンコーディングのPNG、破損したJPEGなど)。
  • 画像のパスが不正
    指定された source パスに画像ファイルが存在しないか、アクセス権がない。

トラブルシューティング

  • QQuickImageProvider のデバッグ
    C++の QQuickImageProvider を使用している場合、QImageQPixmap が有効な状態 (isNull()false を返す) であるか、適切なサイズ (width()height()0 でない) であるかを確認します。
  • 別の簡単な画像でテスト
    問題の画像ではなく、Qt が確実にロードできるシンプルな画像(例: 小さなPNG)でテストし、問題が画像自体にあるのか、コードや環境設定にあるのかを切り分けます。
  • 画像ファイルの検証
    別の画像ビューアで画像ファイルを開いてみて、破損していないか、互換性のある形式かを確認します。
  • onStatusChanged でエラーを捕捉
    Image.Error ステータスをチェックして、画像ロードのエラーをハンドリングします。
    Image {
        id: myImage
        source: "invalid/path/to/image.png"
        onStatusChanged: {
            if (myImage.status === Image.Error) {
                console.error("Image loading error:", myImage.statusMessage);
            }
        }
    }
    
  • コンソールエラーの確認
    Qt Creator のアプリケーション出力 (Application Output) や、ブラウザでQMLをデバッグしている場合はブラウザのコンソールログを確認します。画像ロードに関するエラーメッセージ(例: qt.qml.image: Cannot open file ...)が表示されることがあります。
  • パスの確認
    • 絶対パスを使用していることを確認する。
    • qrc: プレフィックスを正しく使用しているか確認する (例: source: "qrc:/images/myimage.png")。
    • 開発環境で画像ファイルが存在し、アクセス可能であることを確認する。

paintedHeight はあくまで「実際に描画された」高さを表すため、Image 要素自体の height プロパティとは異なる意味を持ちます。

原因

  • Image.heightImage 要素が占める領域の高さを定義しますが、Image.paintedHeightfillMode の影響を受けて実際に描画される画像の高さを表します。Image.PreserveAspectFitImage.PreserveAspectCrop を使用した場合、この2つの値が異なることがあります。


例1: 画像のロード後に実際の描画高さをログ出力する

この例は、画像がロードされ、レンダリング準備ができた後に paintedHeight の値を取得して出力する方法を示しています。

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

Window {
    width: 640
    height: 480
    visible: true
    title: "PaintedHeight Example 1"

    Column {
        anchors.centerIn: parent
        spacing: 10

        Image {
            id: myImage
            source: "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png" // 例としてPNG画像をロード
            width: 300 // Image要素の幅を設定
            height: 300 // Image要素の高さを設定
            fillMode: Image.PreserveAspectFit // アスペクト比を維持してフィットさせる

            // 画像のステータスが変更されたときに呼び出される
            onStatusChanged: {
                if (myImage.status === Image.Ready) {
                    console.log("Image loaded successfully!");
                    console.log("Image.width:", myImage.width);
                    console.log("Image.height:", myImage.height);
                    console.log("Image.paintedWidth:", myImage.paintedWidth); // 実際の描画幅
                    console.log("Image.paintedHeight:", myImage.paintedHeight); // 実際の描画高さ

                    // paintedHeightを使って別のUI要素を配置する例
                    statusText.text = `画像がロードされました。\n` +
                                      `要素サイズ: ${myImage.width}x${myImage.height}\n` +
                                      `描画サイズ: ${myImage.paintedWidth}x${myImage.paintedHeight}`;
                } else if (myImage.status === Image.Error) {
                    console.error("Image loading error:", myImage.statusMessage);
                    statusText.text = `画像ロードエラー: ${myImage.statusMessage}`;
                }
            }
        }

        Text {
            id: statusText
            text: "画像をロード中..."
            font.pixelSize: 16
            color: "black"
        }
    }
}

解説

  • この例では、fillMode: Image.PreserveAspectFit を設定しているため、元の画像のアスペクト比に応じて paintedWidth または paintedHeightwidth または height と異なる値になる可能性があります。例えば、正方形の領域に横長の画像をフィットさせると、paintedHeightheight よりも小さくなります。
  • widthheightImage 要素が占める領域のサイズですが、paintedWidthpaintedHeightfillMode によって実際に画像が描画されるサイズ(余白やクロップを除いた部分)を示します。
  • Image.Ready ステータスチェック: Image 要素の onStatusChanged シグナルハンドラ内で、myImage.status === Image.Ready をチェックしています。これにより、画像が完全にロードされ、描画準備が整ったことを確認してから paintedHeight を参照できます。非同期で画像をロードする場合に非常に重要です。

例2: paintedHeight を使って他の要素を正確に配置する

画像の下に別のUI要素(例えばテキストやボタン)を配置したい場合、Image 要素の height を基準にするのではなく、paintedHeight を使うことで画像の実際のフッターに要素を合わせることができます。

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

Window {
    width: 400
    height: 600
    visible: true
    title: "PaintedHeight Layout Example"
    color: "lightgray"

    Rectangle {
        id: imageContainer
        width: 300
        height: 400
        anchors.centerIn: parent
        color: "white"
        border.color: "blue"
        border.width: 2

        Image {
            id: photo
            source: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/A_Black_Cat_%283120610014%29.jpg/800px-A_Black_Cat_%283120610014%29.jpg" // 横長の猫の画像
            width: parent.width // コンテナの幅に合わせる
            height: parent.height // コンテナの高さに合わせる
            fillMode: Image.PreserveAspectFit // アスペクト比を維持してフィット

            // paintedHeight が変わるたびにログ出力 (オプション)
            onPaintedHeightChanged: {
                console.log("photo.paintedHeight changed to:", photo.paintedHeight);
            }
        }

        // 画像のキャプションを、画像の「実際に描画された部分」の直下に配置
        Text {
            id: captionText
            text: "かわいい猫の画像です。"
            font.pixelSize: 18
            color: "darkblue"
            horizontalAlignment: Text.AlignHCenter
            wrapMode: Text.Wrap // テキストが長い場合に折り返す

            // ここがポイント: photo.paintedHeight を使ってY座標を決定
            anchors.top: photo.y + photo.paintedHeight + 10 // 画像の実際の描画下端から10px下に配置
            width: parent.width - 20 // 親の幅に合わせて左右に余白を持たせる
            anchors.horizontalCenter: parent.horizontalCenter
        }

        // paintedHeight を表示する情報テキスト
        Text {
            text: `Image height: ${photo.height}\nPainted height: ${photo.paintedHeight.toFixed(0)}`
            font.pixelSize: 14
            color: "gray"
            anchors.top: captionText.bottom + 5
            anchors.horizontalCenter: parent.horizontalCenter
        }
    }
}

解説

  • これにより、画像の周りに生じる余白を考慮せず、常に画像の実際の描画部分の直下にキャプションを配置できます。onPaintedHeightChanged シグナルは、paintedHeight が変化したときに通知を受け取るために使用できます。
  • captionText (Text 要素) は、画像の実際の描画領域の下に配置したいと考えています。ここで anchors.top: photo.y + photo.paintedHeight + 10 が重要になります。
    • photo.y: Image 要素のY座標(コンテナ内での位置)。
    • photo.paintedHeight: 画像が実際に描画されている高さ。
    • photo.y + photo.paintedHeight: これは画像の実際の描画領域の下端のY座標を示します。
    • + 10: その下端からさらに10ピクセル下に captionText を配置します。
  • しかし、fillMode: Image.PreserveAspectFit が設定されているため、横長の猫の画像は imageContainer の高さ全体を埋めるのではなく、アスペクト比を維持してコンテナ内に収まります。この結果、画像の上下に余白ができます。
  • photo という Image 要素は、imageContainer の幅と高さ全体を占めるように設定されています (width: parent.width, height: parent.height)。

この例では、スライダーを使って Image 要素の width を動的に変更し、それに伴う paintedHeight の変化を観察します。

// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15 // Sliderを使うために必要

Window {
    width: 800
    height: 600
    visible: true
    title: "Dynamic PaintedHeight Example"

    Column {
        anchors.fill: parent
        spacing: 20
        padding: 20

        Text {
            text: "画像幅を調整:"
            font.pixelSize: 18
        }

        Slider {
            id: widthSlider
            from: 50
            to: 700
            value: 300 // 初期値
            width: parent.width - 40 // 親の幅からパディング分を引く
            onValueChanged: {
                myDynamicImage.width = value;
            }
        }

        Rectangle {
            width: myDynamicImage.width // Imageの幅に合わせる
            height: myDynamicImage.height // Imageの高さに合わせる
            color: "lightgray"
            border.color: "darkgray"
            border.width: 1
            clip: true // コンテナのサイズを超えた部分をクリップ

            Image {
                id: myDynamicImage
                source: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_of_a_beautiful_landscape.JPG/800px-Image_of_a_beautiful_landscape.JPG" // 高解像度の風景画像
                width: widthSlider.value // スライダーの値にバインド
                height: 400 // Image要素の高さは固定
                fillMode: Image.PreserveAspectFit // アスペクト比を維持してフィット

                // paintedHeight が変化するたびにログ出力
                onPaintedHeightChanged: {
                    console.log(`Dynamic Image - Width: ${myDynamicImage.width}, Painted Height: ${myDynamicImage.paintedHeight}`);
                    // UIに表示を更新
                    infoText.text = `要素の幅: ${myDynamicImage.width.toFixed(0)}\n` +
                                    `要素の高さ: ${myDynamicImage.height.toFixed(0)}\n` +
                                    `描画幅: ${myDynamicImage.paintedWidth.toFixed(0)}\n` +
                                    `描画高さ: ${myDynamicImage.paintedHeight.toFixed(0)}`;
                }
                onStatusChanged: {
                    if (status === Image.Ready) {
                         // 初回ロード時に情報更新
                        infoText.text = `要素の幅: ${myDynamicImage.width.toFixed(0)}\n` +
                                        `要素の高さ: ${myDynamicImage.height.toFixed(0)}\n` +
                                        `描画幅: ${myDynamicImage.paintedWidth.toFixed(0)}\n` +
                                        `描画高さ: ${myDynamicImage.paintedHeight.toFixed(0)}`;
                    }
                }
            }
        }

        Text {
            id: infoText
            font.pixelSize: 16
            color: "black"
            text: "画像をロード中..."
        }
    }
}
  • onPaintedHeightChanged シグナルハンドラは、myDynamicImagepaintedHeight プロパティが変更されるたびにトリガーされます。これにより、paintedHeight がリアルタイムでどのように変化しているかを確認できます。
  • fillMode: Image.PreserveAspectFit のため、width が変化すると、それに合わせて画像の縦横比が保たれるように paintedWidthpaintedHeight が調整されます。
  • myDynamicImageheight: 400 で高さが固定されていますが、width はスライダーによって変わります。
  • Slider を操作することで myDynamicImage.width の値が変化します。


以下に、Image.paintedHeight の代替となるプログラミング手法をいくつか説明します。

Image.sourceSize プロパティは、ロードされた画像の元のピクセルサイズ(スケーリング前のアスペクト比と解像度)を提供します。sourceSize.height は元の画像の高さを返します。